/* #1375, the jumble beat's card -- a notebook page, a jumbled word bank, an
   OK button, and one verdict node.

   ## `direction:rtl` on `.jumble-line` and `.jumble-bank` is LOAD-BEARING

   Hebrew assembles right to left: the first word of the sentence belongs at
   the RIGHT of the line. `jumble.js` places a word by appending it, so the
   first word placed is the line's first child -- and it is THESE TWO
   DECLARATIONS, and nothing else, that make a first child the rightmost
   thing on screen. Under `direction:rtl` a `flex-direction:row` container's
   main-start is the right edge, and `flex-wrap:wrap` starts each new row at
   the right again, which is exactly what a wrapped Hebrew line does.

   Delete either declaration and the card silently renders Hebrew backwards
   with every test still green on the DOM order -- so they are pinned
   directly, by `tests/test_beat_jumble.py`, rather than left as a
   reading of two files. The owner's own description says "left, right or up
   down", which is the natural English phrasing and is not the geometry.

   `dir="rtl"` is also on the elements in `jumble.js`'s markup. That is not a
   duplicate of this: the attribute sets the BIDI base direction (which is
   what makes a mixed Hebrew/Latin chip lay out correctly and what a screen
   reader reads), while `direction` here is what the flex algorithm consults.
   In practice the attribute implies the property via the UA stylesheet -- so
   this rule is the belt to the markup's braces, and it is the half a
   stylesheet-only change can break.

   ## The notebook

   "there should be lines like you would see on the line of a high school
   notebook. Thin grey lines." -- one `repeating-linear-gradient`, `--line`
   at 1px, so the rules are the CARD's furniture rather than a border on each
   word. They are there before anything is placed, and placing a word neither
   adds nor moves one, because the gradient repeats at exactly the height a
   placed row occupies (`--jumble-rule`) and the box is `--jumble-rules` of
   them tall.

   `--line` is the same token `study.css` gives the pace rail and
   `button.pick-word`'s underline: this app already has a "thin grey line"
   and this is it, not a new grey.

   ## The heights, which are measured rather than chosen

   #1352 is open because study cards already overflow at 393x852, and this is
   the tallest card in the app -- a bank AND a filled notebook AND, when the
   answer is wrong, the correct Hebrew as well.

   **Everything below is measured in a real browser at a real 393x852
   viewport** (CDP device metrics, `innerWidth === 393` asserted, real
   webfonts loaded), against these exact stylesheet bytes, over **all 3,475
   cards this beat can actually produce on the owned corpus**. The height
   model was validated against real renders at a residual of 0.000px. The
   harness is in this ticket's PR body. They are stated here because a number
   nobody can reproduce is worse than none: the first version of this comment
   carried an ESTIMATED table and every figure was wrong, including the SHAPE
   -- it claimed the card grows when answered, and the card is tallest before
   it is answered, because chips leaving the bank shrink it more than the
   verdict adds.

   Budget: **631px** (852, less 107 of rail and pace slider, less the 58px
   fixed bottom nav, less `.stage`'s own 56 of padding). Worst card, at
   `MAX_WORDS = 8` with five distractors:

       chip size   worst audio   worst english   cards over budget
          19px       659.9px        636.2px          143 / 125
          17px       605.9px        582.2px            0 / 0
          16px       605.9px        582.2px            0 / 0

   **What bounds this card's height is how many rows the WORD BANK wraps to,
   not how many words the sentence has.** At 6, 7 or 8 words the worst card is
   identical, so capping words buys nothing; every card that has ever gone
   over budget was a five-bank-row card. The two dials that do move it are the
   chip size above and the distractor count -- and the owner named the first
   as the one to spend, which is why the fifth distractor survived at the
   time.

   The visibly empty space left on a played card is therefore the BANK, which
   reserves the rows it started with rather than the rows it ends with. That
   is the reserve doing its job (see `jumble.js`), and it is where the pixels
   are if anyone ever needs them back -- not the notebook, which since the
   line reserve was narrowed to the answer's own words draws exactly the rows
   the sentence occupies.

   ## #1794 spent the OTHER dial, and re-measured this one

   The owner played the five-distractor card and asked for "1-3 detractors
   ... It's really hard w five", so the count is now `1-3 scaled to the
   line's length` (`JumbleBeat.distractors_for`, whose docstring carries the
   table that chose the shape). That takes the bank from a median 11 chips to
   8 and, more to the point here, off three rows and onto two:

       distractor rule       bank rows (ask)          tallest    median card
       flat 5 (was)          {2: 46, 3: 228, 4: 19}   520.19px    412.59px
       1-3 scaled (now)      {2: 276, 3: 18}          466.19px    358.59px

   **No four-row bank survives**, which matters because the sentence above --
   "every card that has ever gone over budget was a five-bank-row card" --
   says the tall cards are made of bank rows and nothing else.

   So the obvious question is whether `--jumble-chip-size` can go back up.
   **It can, and it is deliberately not being taken.** Re-measured over the
   same 294-card population, scaled distractors, font-gated:

       chip size   bank rows (ask)      line rows (filled)     tallest   over budget
          17px     {2: 276, 3: 18}      {1: 109, 2: 185}       466.19px      0 / 588
          18px     {2: 265, 3: 29}      {1:  96, 2: 198}       466.19px      0 / 588
          19px     {2: 249, 3: 45}      {1:  88, 2: 204, 3: 2} 479.94px      0 / 588

   19px -- `.match-he`/`.pick-cue`'s size, which this card was a sibling of
   until #1352 -- now fits with 151px to spare, where it used to put 143
   cards over. But it spends a good part of what the count cut just bought:
   three-row banks go 18 -> 45, and two cards grow a third notebook row. That
   is a legibility-versus-uniformity trade and it is the OWNER's, not the
   builder's -- he named the font size as his own lever ("if it makes sense
   the hebrew for the puzzle can be a smaller font size"), so raising it
   again is a thing to put to him rather than to take because it fits. Left
   at 17px; the table is here so the question is answered rather than
   reopened from scratch.

   **Measurement caveat, and it is not a small one.** The #1794 figures come
   from a harness that refuses to report unless `document.fonts.status` is
   `"loaded"`. That gate exists because it was needed: `document.fonts.ready`
   resolves BEFORE a script-painted card has demanded the Hebrew face, so an
   ungated run measures chips ~7.8% narrow (a fixed eight-letter string:
   87.48px fallback against 94.34px loaded) and reports fewer bank rows than
   the phone draws. It was caught by a run at a LARGER font reporting FEWER
   rows than one at a smaller. Any future re-measurement of this file's
   numbers must carry the same gate or it will quietly disagree.

   `--jumble-chip-size` is what was spent to fit, and it was set from the
   measurement rather than chosen.

   **#2069/#2067, re-measured -- and one thing above was wrong before this
   paragraph was corrected.** #2069 removes `.jumble-prompt`'s whole line
   (the audio variant's caption) from every audio card unconditionally.
   #2067 adds `.jumble-reveal` (below) carrying `.beat-reserve` (#1283):
   that class is `visibility:hidden` (shared/beat_card.css), which KEEPS the
   box, so the reveal's space is spent from FIRST PAINT -- the UNANSWERED
   state -- not "only after the card is answered" as an earlier version of
   this paragraph claimed. It adds to exactly the state this file's own
   header calls tallest.

   Re-run with a fresh `jumble_height_probe.py` (scratch, not committed --
   same shape as this section's own `jumble_probe.py`): a real LocalServer
   over a temp copy of `demo.db`, CDP `Emulation.setDeviceMetricsOverride`
   at 393x852, `document.fonts.status === "loaded"` gated exactly as this
   header requires, driving `jumble.js`'s own `html`/`wire` with REAL cards
   pulled from the real `/api/v1/practice/jumble/round` endpoint (not
   synthesised). 3,911 distinct real `(cue, chip-set)` audio-with-English
   draws -- every audio-eligible cue this beat can offer, at every
   distractor combination 8,000 requests turned up, plateaued identically
   at 500/6,000/8,000 requests, so this is the ceiling and not a thin
   sample:

       population                          worst height   over 631px budget
       BEFORE (this ticket's parent)         455.94px          0 / 3,911
       AFTER  (with the reveal, no prompt)   492.00px          0 / 3,911

   **No card exceeds budget, before or after** -- headroom goes ~175px to
   ~139px in the worst real case, nowhere near the 631px ceiling. One
   correction the same run forces: **the "599.94px"/"605.9px" figures
   elsewhere in this file predate #1794's distractor-count fix** (flat 5
   distractors, superseded by the 1-3 scaled rule) and are stale against
   the shipped stylesheet -- #1794's OWN re-measurement (466.19px, this
   file's own #1794 section) is the right ballpark and is what this run
   reproduces (455.94px, 2% off, plausibly sampling/version drift rather
   than disagreement). **Every eligible audio cue in `demo.db` already
   carries a translation** (this file's own #1373 note: "306 of 306 owned
   cues"), so the "audio card with no reveal" case this ticket also
   guarded is not reachable on the owned corpus today -- 0 of 8,000
   requests drew one -- and only matters for a freshly-ingested,
   untranslated film. Because nothing here approaches budget, `.jumble-
   reveal` keeps the plain `.beat-reserve` shape (cloze.js's #2061
   precedent) rather than a defensive redesign (no-reserve, an ellipsis
   clamp, sharing the verdict's own reserve) that the numbers do not call
   for. */

/* The rule pitch, and it is 44px because MEASUREMENT said so.

   It was 38px, on the reasoning that a rule is a writing line rather than a
   touch target. That was wrong twice over, and the render at 393x852 showed
   both: `.jumble-chip`'s own `min-height:44px` (the touch floor, which a
   placed word still needs -- it is tapped to take it back) beats a 38px
   `height` on the same element, so rows landed 44px apart while the gradient
   repeated every 38px and **the words floated visibly off the lines**. And
   `min-height:114px` for three 38px rules then measured taller than the two
   44px rows inside it, leaving a third rule that never filled.

   44 is the one number that makes the pitch, the row height and the touch
   floor the same thing, so a word cannot drift off a rule by construction.
   The chip rule below sets `min-height:0` and takes its height from here for
   that reason -- delete that line and the 38px bug comes back at 44. */
/* #1400: the notebook and its replay button now share one row --
   `.jumble-line-row`, this card's own `.cuerow` (study.css). It carries the
   width/max-width this group used to give `.jumble-line` directly (below);
   `.jumble-line` itself no longer needs either; see its own `flex`/
   `min-width` for why. */
.jumble-line-row, .jumble-bank { width:100%; max-width:22rem; }
/* `align-items:flex-start` rather than the default `stretch`: the button
   already pins itself to the top via `.cuereplay`'s own `align-self`
   (unoverridden here for the first time, see `.jumble-line-row
   .jumble-replay` below), so this is belt-and-braces against a moment
   `.jumble-line` is shorter than the button (nothing today, but nothing
   should have to reason about `stretch` giving it a taller box than its own
   content to stay safe). */
.jumble-line-row { display:flex; align-items:flex-start; }

.jumble-line {
  --jumble-rule:54px;
  --jumble-rules:2;
  /* The line FILLS its row rather than being told a width -- THE
     READING-EDGE CONTRACT's clause 1 (study.css), inherited rather than
     re-derived: with the button `flex:0 0 40px` (`.cuereplay`'s own basis,
     unoverridden -- see below) and the line `flex:1 1 auto`, flexbox hands
     the line whatever is left, which is 100% of `.jumble-line-row`'s own
     22rem when the English variant ships no button at all. No manual
     `calc(22rem - 48px)`: that would be the right number with a button
     present and the wrong one without, and flexbox already knows which case
     it is. `min-width:0` is the usual flex-item companion -- a Hebrew line
     with no natural minimum content width must still be ALLOWED to be
     narrower than its content's own intrinsic size, which is what lets it
     wrap in the first place. */
  flex:1 1 auto; min-width:0;
  direction:rtl;
  display:flex; flex-wrap:wrap; align-content:flex-start;
  justify-content:flex-start;
  /* Row gap 0 so rows stack exactly on the rule pitch and a chip's bottom
     edge is always a ruled line; a column gap so two placed words read as
     two words rather than one long tile.

     #1412: measured on the SERVED page (`jumble_probe.py`, this PR body),
     this was already a literal 4px constant -- not a function of
     `--milim-text-scale` or of the cue, so #1416's "the gap must be
     dynamic" did not hold under measurement (see #1416's own commit). What
     was real is this ticket's own complaint that 4px reads as chips still
     touching ("they bump together and it feels tight"). Bumped to 6px,
     checked against #1352's marginal budget rather than assumed free: over
     the 255 real cues this beat can draw from (`demo.db`, all five sources,
     DISTRACTORS_MAX distractors drawn), 22 of them cross from two notebook
     rows to three at 6px -- but the population's tallest card is 599.94px at
     4px AND at 6px (the existing worst bank-driven cards already sit there),
     so the budget's headroom (631px minus 599.94px = 31.06px) is unmoved.
     Not the exhaustive worst-of-300-bank-orders sweep the original
     3,475-card measurement used, so read this as a real, non-adversarial
     sample rather than a proof for every possible draw. */
  gap:0 6px;
  min-height:calc(var(--jumble-rule) * var(--jumble-rules));
  background-image:repeating-linear-gradient(
    to bottom,
    transparent 0,
    transparent calc(var(--jumble-rule) - 1px),
    var(--line) calc(var(--jumble-rule) - 1px),
    var(--line) var(--jumble-rule));
  /* The rules must not slide when the box grows past its minimum, which it
     can if a learner places more chips than fit three rows -- the gradient is
     anchored to the top, so row four is drawn in the same rhythm. */
  background-position:top left;
  background-repeat:repeat;
}

/* The prompt, at `.beat-text`'s own scale and receded like `.pick-prompt`:
   this card's Hebrew is the puzzle, and the English above it is a caption
   for it rather than a second thing to read. */
.jumble-prompt { color:var(--muted); font-size:calc(17px * var(--milim-text-scale)); }

/* #1375's second variant: the English sentence as the prompt.

   `--ink` and the reading serif, at the size `.beat-text` uses -- not
   `--muted` like `.jumble-prompt` above. That difference is the point rather
   than an inconsistency: on the audio card the English line is a caption
   ("Listen, then put the line back together") and on this one it IS the
   question, the only thing on screen saying what to build. A receded
   question is a question the learner has to hunt for.

   `dir="ltr"` is on the element; nothing here overrides the page's own
   direction, because this is the one string on the card that is English. */
.jumble-english { margin:0; font-family:var(--serif);
                  font-size:calc(19px * var(--milim-text-scale)); line-height:1.4;
                  color:var(--ink); max-width:22rem; }

/* A word ON the line is STILL A CHIP. The owner, having played it:

     "I would expect the words to be put onto the line in the bubbles rather
      than cemented onto the line as if it was always text. Its important to
      feel like they are like shapes you can move around. so just as the
      distractors that are left are in different pieces, I want that too on
      the lines."

   The first version rendered a placed word as flat text on the rule, on a
   notebook-page argument. He is right that it was wrong, and the reason is
   sharper than taste: the card's whole premise is that these are objects you
   move, and flattening one the instant it is placed says the opposite at
   exactly the wrong moment. It also contradicts the drag-to-reorder he asked
   for (#1377) -- a thing you can drag has to look like a thing.

   So the difference between "in the bank" and "on the line" is POSITION, plus
   a state tint, and never whether it looks like an object. `--raise` against
   the bank's `--surface` is that tint: a half-step of elevation in both
   themes (lighter in dark, darker in light), with a firmer border. Enough to
   read at a glance which words are still to be used, without either state
   stopping being a chip. */
.jumble-line .jumble-chip {
  background:var(--raise); border-color:var(--muted);
  /* #1923: a wrapped line's two rows touched -- his own words, "the word
     boxes don't touch vertically ... not a lot of space but just a bit" --
     because `.jumble-line`'s row-gap is 0 (see that rule's own comment,
     #1412) and a chip is exactly one rule pitch tall, so a chip on row N's
     border sits flush against row N+1's, 0px measured between them
     (`gutter_probe.py`, this PR body: row-1 bottom 272.9375, row-2 top
     272.9375 at 393px, source 8 cue 5, the owner's own reproduction).

     `.jumble-line`'s row-gap stays 0 -- turning it on would grow the total
     line height by (rows-1)*gap and misalign the ruled-line background,
     which repeats at a fixed `--jumble-rule` pitch tied to the UNGAPPED row
     height (#1412's own comment: "rows stack exactly on the rule pitch").
     Instead the chip gives up 6px of its own height to a symmetric
     `margin-block`, so the flex line's cross size -- 44px, a chip's outer
     MARGIN box -- is unchanged: same row pitch, same ruled-line alignment,
     same total card height (#1352's 631px budget, untouched), and the same
     44px spacing #1879's `insertionTarget`/`ROW_STICK` measures between row
     tops (a uniform +3px offset on every row, not a change in the distance
     between them). Re-run after this change: `.jumble-line` row gaps went
     from a measured 0px to 6px, and
     `tests/js/test_beat_jumble.mjs`'s two `#1879: ...` tests (row-boundary
     hysteresis, and the bank-and-back placement) still pass unedited.

     6px, not a new number: the same value `.jumble-line`'s own column gap
     already settled on for this exact "chips still touching" complaint
     (#1412), and the same figure `.ws-grid`'s row gap (word_search.css) and
     `.ex-table`/`.ex-schematic`'s row gap (explainer.css) already ship at --
     picked for consistency with what is already in the app rather than a
     fresh guess.

     **The margin box is not the hit box**, and an earlier version of this
     comment conflated them: `margin-block` sets the flex LINE's cross size,
     but margin never receives pointer events, so the chip's own border box
     is the tap target. At the old 44px pitch the chip was 44 - 6 = 38px and
     crossed #482's floor invisibly ("44 is the touch floor and this must
     never go under it", from his own "I don't want the user feeling cramped
     with clicking on the items"), which is why an `::after` overlay had to
     put the 44px band back.

     **At a 54px pitch that overlay is gone, and removing it is the point of
     the new number rather than a side effect.** The chip is 54 - 10 = 44px
     in its own border box, so it clears #482 unaided: nothing generated,
     nothing to keep in sync, and no `pointer-events` assumption upstream
     that could silently reopen a short tap target. `position:relative` went
     with it; it existed only to anchor the overlay.

     **54 and 10 are HIS numbers, off a reference he chose.** He reported the
     shipped 6px gap as not looking different on his phone, then showed a
     second app doing the same job. Both were measured on his own device,
     same screen, same method:

       this app, shipped:   6.0pt between rows, 44pt pitch, chip bottom
                            2.0pt above its ruled line
       the reference app:  10.5pt between rows, 54pt pitch, chip bottom
                            3.0pt above its ruled line

     The gap was 57% of the reference's and the whole row 10pt shorter.
     Rendered side by side at 393px and re-measured in the browser
     (`docs/probes/2026-09-07-jumble-gutter/`), 54/10 reproduces it almost
     exactly: gap 10.0, pitch 54.0, chip 44.0.

     Worth knowing before anyone "fixes" the 2pt float this file already has:
     **the reference app's chips also sit ABOVE their ruled line** (3.0pt),
     so floating is the convention rather than a defect. That is also the
     open question on the 2026-09-07 decisions page, and this is evidence
     for its "leave it floating" option.

     **Cost, checked rather than assumed: +20px of card height** (2 rows x
     10). #1352's budget is 631px against this beat's measured 599.94px, so
     the headroom goes 31.06 -> 11.06px. Three-row lines are ~1% of cues and
     already overflow the two reserved rules; unchanged either way. */
  min-height:0; height:calc(var(--jumble-rule) - 10px); margin-block:5px;
  /* Tighter than a bank tile (12px): eight chips on a 343px line pay their
     horizontal padding eight times, and it is what decides whether the line
     wraps to two rows or three. Measured, not chosen -- see the header. */
  padding:0 8px;
  align-self:flex-start;
}
@media (hover: hover) {
  .jumble-line .jumble-chip:not([disabled]):hover { border-color:var(--accent); }
}

/* ---- #1596: every animated movement on this card, enumerated -------------
   The owner played a build that already had #1572's deceleration and STILL
   asked for easing "instead of constant speed" (`git merge-base
   --is-ancestor fe2c1d6 55ccd63` is true -- fe2c1d6 predates his report by
   about 7 hours). So the fix is not "add a curve", it is "find the one
   motion that still snaps." Here is every one, as actually declared:

     1. THE LIFT -- picking a chip up or putting it down. `.dragging` below,
        `transition:transform .1s ease, box-shadow .1s ease`. A SIZE change
        (`scale(1.06)`), not a position -- and the one riding a plain `ease`
        rather than the organic curve #3 uses.
     2. THE FOLLOW -- a dragged chip tracking the finger. `jumble.js`'s
        `follow()` writes an inline `translate` on every `pointermove`, no
        transition at all. None is wanted here: a chip that lagged the
        finger would not be a reduced animation, it would be a broken
        control (the reduced-motion block below leaves this one alone by
        name, for exactly that reason).
     3. THE SETTLE -- `.jumble-settling` below,
        `transition:translate <duration> cubic-bezier(.2,.7,.3,1)`, already
        decelerating since #1572/fe2c1d6. (That duration was a flat .2s when
        this enumeration was written; #1777 made it a function of the distance
        travelled, and #1851 made that the distance the FURTHEST chip in the
        gesture travels, shared by all of them -- see the rule's own comment.
        The curve, and this entry's point about it, are unchanged.)
        `jumble.js`'s `playToHome()` hangs
        this ONE class on TWO different moments: the chip a drag just
        released sliding its last few pixels home (`endDrag`), and every
        OTHER chip a placement displaced sliding into the room the move
        opened for it (`animateReorder`'s FLIP release). These read as two
        candidates from the ticket's own text ("the settling chip" versus
        "the displaced chips' FLIP inversion release") but are ONE rule in
        the code -- same class, same curve, same duration -- which is why
        neither can be what he still means: he had already played both.
     4. Nothing in the reveal or the verdict animates -- `.jumble-answer`
        below carries no `transition` at all. And no separate "board slide"
        exists as its own mechanism: #1572's own commit language for what #3
        does to the OTHER chips is "the board slides" -- it is #3, not a
        fifth thing.

   Of the four, #2 must stay un-eased by design and #3 already carries
   exactly the curve #1596 asks for. #1 is the one movement left on a plain
   curve -- eased below to match #3, so picking a chip up, dragging it, and
   watching the board make room and settle now reads as one continuous
   organic system instead of a generic pop ahead of a bespoke glide. This is
   a judgement call, not a device measurement like the rest of this file's
   numbers -- flagged in the PR body for the owner to confirm it reads right,
   since a curve is "cheap to change and hard to judge from a diff" (the
   ticket's own words). */

/* #1377: the chip currently being dragged. `jumble.js` toggles this class
   between the pointer crossing `DRAG_THRESHOLD` and the drag ending, and
   never while the chip is merely tapped -- see its own header.

   `transform:scale` and `box-shadow` only: neither affects layout, so a
   chip picking itself up and being reordered among its own siblings cannot
   change `.jumble-line`'s height, which is already reserved at wire time
   for the largest this puzzle can hold (see jumble.js). #1375's three
   reflow defects have nothing to come back from here. `position:relative`
   plus `z-index` keeps the lifted chip drawn over its neighbours while it
   passes them; it does not remove the chip from flow, so it still occupies
   its own flex slot throughout. */
/* #1572 widens the selector to any chip (a bank chip drags now too) and
   leaves the declarations alone -- `transform` stays the lift's property, and
   the follow is written to the INDIVIDUAL `translate` property instead, which
   composes with `transform` rather than replacing it. That is what lets the
   reduced-motion block below still neutralise the lift by name while a chip
   under a finger keeps following it, and it is also why the translation is
   not itself multiplied by 1.06: the individual properties are applied
   before `transform`, so the two do not interfere. */
.jumble-line .jumble-chip.dragging,
.jumble-bank .jumble-chip.dragging {
  position:relative; z-index:2;
  transform:scale(1.06);
  box-shadow:0 4px 10px rgba(0,0,0,.28);
  /* #1596: was `transform .1s ease, box-shadow .1s ease` -- see the
     enumeration above. Same `cubic-bezier(.2,.7,.3,1)` `.jumble-settling`
     already uses, and the duration is nudged from .1s to .16s with it: a
     decelerating curve over .1s barely has time to show its own shape. */
  transition:transform .16s cubic-bezier(.2,.7,.3,1),
             box-shadow .16s cubic-bezier(.2,.7,.3,1);
}

/* #1572: the chips that are NOT under the finger, sliding to the room the
   dragged one just asked for. `jumble.js` puts this class on for exactly as
   long as one FLIP takes and takes it off again, so a resting board carries
   neither the class nor an inline `translate` and measures precisely what it
   measured before this ticket.

   `translate` only, never `all`: this class lands on a chip that may be
   changing containers in the same frame (bank padding is 12px, a placed
   chip's is 8px), and transitioning `width`/`background` through that would
   animate the chip's own box rather than its position -- which is #1375's
   reflow family wearing a new hat.

   200ms is a shade under the 250ms that reads as sluggish under a moving
   thumb and well over the ~100ms at which a slide is indistinguishable from
   the snap it replaces. `cubic-bezier(.2,.7,.3,1)` is a decelerating curve:
   the chip leaves quickly and arrives gently, which is what makes a row of
   them read as making room rather than as being pushed.

   ---- #1777/#1794: the duration is no longer a constant, and why ----------

   (The measurements here stand and the sqrt law and 480ms ceiling are back in
   force, but **"per chip" below was superseded by #1851** -- the duration is
   now decided once per gesture. Read this section for how the distance table
   was arrived at, then the #1851 section under it for what consumes it.)

   The owner, 2026-09-06, his THIRD report on this card's motion: *"the moving
   around words from line to line still jumps around a lot ... Things
   shouldn't feel like they just jump around but be more fluid and
   predictable. It feels good when moving side to side on the same line."*

   That last sentence is the measurement instruction. Same-line motion is
   already right; line-to-line is not. **Both were running the same 200ms,
   and they are not the same distance.** Measured in a real Chrome at
   393x852, font-gated, over every reorder and every removal on all 294 cards
   the owned corpus produces -- FLIP travel per displaced chip, classified by
   whether it stayed on its row:

       what moves                          median travel   speed at a flat 200ms
       same row, side to side                  56.5px            283 px/s
       across a bank<->line boundary          170.9px            854 px/s
       across a wrapped ROW boundary          225.9px          1,146 px/s
       the worst of them                      305.6px          1,528 px/s

   **A chip that changes rows travels four times as far in the same time, so
   it moves four times as fast.** The motion he likes and the motion he calls
   "jumping" are one rule with one duration; what differs is only how far it
   has to go, and a fixed duration turns distance into speed. That is the
   whole of "not predictable" -- nothing on the board moves at a rate the eye
   can learn.

   So the duration became a function of the distance, written by `jumble.js`
   into `--jumble-settle-ms` -- per chip as #1777 shipped it, per GESTURE since
   #1851. The law is `200ms * sqrt(distance / 56px)`, clamped:

     * **56px and 200ms are the measured same-line move** -- the one he says
       already feels good. It is the anchor, so THAT motion is unchanged to
       the millisecond and this ticket cannot regress what it was told was
       right.
     * **sqrt, not linear.** True constant speed is what "predictable" asks
       for literally, and it is unaffordable: 226px at 283px/s is 799ms, well
       past the 250ms this rule's own paragraph above measures as sluggish.
       The square root is the standard compromise (it is what Material's
       "dynamic duration" is). The table below is READ BACK off the shipped
       code -- the harness reports the `--jumble-settle-ms` actually written
       to each chip, and every one of the 11,370 chip displacements it
       exercises carried one:

           travel      duration was -> now      speed was -> now
            56.5px       200ms -> 201ms          283 ->   282 px/s
           170.9px       200ms -> 349ms          854 ->   489 px/s
           225.9px       200ms -> 402ms        1,146 ->   562 px/s
           305.6px       200ms -> 467ms        1,528 ->   654 px/s

       **The spread between the fastest thing on the board and the ordinary
       same-line nudge goes from 5.40x to 2.32x, and the same-line nudge
       itself is unmoved** (283 -> 282 px/s, one px/s of rounding). That is
       the whole claim: the motion he said feels good is untouched, and the
       motion he said jumps has been brought toward it.

     * **The clamps are rails, not policy.** 160ms floors travels under ~36px
       (the corpus's smallest same-row nudge is 34.7px) so a tiny shuffle
       cannot drop toward the ~100ms at which a slide stops being one. 480ms
       is above anything this corpus produces -- the longest travel measured,
       305.6px, computes to 467ms -- so it never binds here and exists for a
       wider phone or a longer line.

   The custom property is read through `var()` with the old constant as its
   fallback, so a page that somehow gets this stylesheet without the script
   still animates exactly as it did before.

   ---- #1833, then #1851: the quantity above is the WRONG one -------------

   #1833 was the owner reporting the residual speed spread: "the first three
   ... move around with a nice ease - but the others jump more immediately and
   when they jump to a new line it is very jarring." #1841 answered it by
   raising `settleMs()`'s exponent from .5 (sqrt) to .75 -- speed spread 2.32x
   down to about 1.53x, at the cost of the worst case running 714ms instead of
   467ms, with `SETTLE_MAX_MS` going 480 -> 730 to leave that unclamped.

   **He reported again on a build containing that, and #1851 is the verdict:**

     "If I put on two words onto a line and start moving them around it feels
     good (it felt good before), even three feels good ... but sometimes four
     or five -- and all of a sudden the ones to the left start jumping
     immediately ... And if it fills all the way to two lines then it is
     spastic and jumps super fast. **So the slow down wasn't likely needed,
     but the fix for this speedup based on how much the line grows is what the
     problem is.**"

   **The optimised quantity was wrong, not merely under-tuned.** Speed was
   never what he could see; ARRIVAL was. A per-chip duration makes the chips in
   one gesture stop at different times, and the eye reads whichever stopped
   first as having snapped -- so the harder each chip's speed was equalised,
   the further apart they landed. Measured for #1851 in a real Chrome against
   these shipped files, one `animateReorder` at a time, on a 304px line (this
   rule's own `max-width:22rem` less the replay button, i.e. his phone):

       words on the line   rows   chips a reorder moves   their travels
         2                   1      1                      46px
         3                   1      1                      46px
         5 (short words)     1      1-2                    46px
         6 and 7             2      1-6                    46px x4, 221.9px, 4.1px

   Within a row, every displaced chip moves by exactly the dragged chip's own
   width plus the gap -- ONE number, so two or three words cannot produce a
   spread at all. That is why he says they feel good, and felt good before any
   of this. The spread arrives with the WRAP: taking a chip out of row 0 lets
   row 1's first chip come up to join it, and that one travels a row's width
   while its neighbours nudge a slot. In the six-chip reorder above, under
   #1841's law: 160, 172, 172, 172, 172 and **562ms** -- six chips leaving
   together and stopping 402ms apart. Under #1777's sqrt they stopped 217ms
   apart. **#1841 nearly doubled the arrival spread while halving the speed
   spread.** "the ones to the LEFT" confirms it: the line is RTL, so left is
   the far end -- the shortest travels, the first to stop.

   So `jumble.js` now sizes **one duration per reorder** from that gesture's
   own furthest travel, and gives it to every chip the gesture displaced. The
   board leaves and lands as one motion; a chip with 46px to cover simply
   crosses it slowly. The maximum rather than a high percentile, for two
   measured reasons: a reorder displaces one to six chips, so a percentile over
   that many samples is the maximum or its neighbour; and the outlier IS the
   wrapping chip, the biggest and most visible motion on the board and the one
   he calls jarring -- excluding it would clamp 222px to ~172ms, i.e. 1,290px/s,
   which is the "spastic" being reported, deliberately reintroduced.

   **The exponent goes back to .5 and `SETTLE_MAX_MS` back to 480.** #1841
   raised both purely to narrow the within-gesture speed spread, which sharing
   the duration now removes by construction. Sharing also inverts who pays for
   a long duration: not the far traveller any more, but the 46px chip crawling
   beside it. At ^.75 the measured wrapping reorder would hold the whole board
   for 562ms (46px at 82px/s); at ^.5 it is 398ms (46px at 116px/s), against
   the ~250ms this rule's own paragraph above measures as sluggish for a single
   chip. And a continuous drag fires reorders a few pointer-frames apart
   (measured: steps 88, 100 and 112 of one 120-step drag), with `clearTween`
   truncating whatever is still in flight -- so most of a long duration is
   never seen anyway.

       gesture max travel        shared duration (^.5)   the 46px chip's speed
        46px  (2-5 words)          181ms                   254 px/s
       221.9px (a wrap)            398ms                   116 px/s
       261.4px (a tap flight)      432ms                   106 px/s
       305.6px (corpus worst)      467ms                    99 px/s

   480 clears the corpus's own worst case by 13ms and never binds -- the margin
   #1777 sized it for. 730 existed only as headroom for a per-chip long tail
   that no longer exists.

   The anchor is untouched throughout: a two- or three-word gesture's maximum
   travel IS the same-line nudge, so those are the gestures none of this can
   disturb. Stated exactly rather than as "unchanged", because reverting the
   exponent does move it a little -- read back off the shipped code in Chrome,
   the measured 46px nudge goes **172ms -> 181ms**, 9ms back TOWARD the flat
   200ms it was running when he first said it felt good.

   Read back off the shipped code, the same wrapping reorder that produced
   160/172/172/172/172/562ms before this change now produces **398ms on all
   six chips**, sized from its own 221.9px maximum; a reorder that stays inside
   one row still produces 181ms.

   ---- #1869: the truncation above is real, and it is not what he sees -----

   His fifth report, on a build containing #1851: *"the more words and more
   lines the faster it moves ... the more 'flippy' and erratic it can feel."*
   #1869 read it as the truncation this comment already records -- a 398ms
   glide replaced after 80ms, so most of it never plays. Measured
   (`scripts/jumble_motion_probe.py`, real drags in a real Chrome), that is
   true, it is not visible, and the remedy it suggests is backwards.

   **The defect was in `clearTween`, in jumble.js, and this rule is what made
   it invisible.** Removing `.jumble-settling` removes the transition declared
   here; it does not remove the transition already RUNNING, and Chrome keeps
   applying a cancelled transition's value until the next frame's animation
   update. So `animateReorder`'s "layout" measurement was a paint box whenever
   a chip was mid-glide, its FLIP inversion overshot by the leftover translate,
   and the chip was teleported backwards by exactly that much before re-running
   the whole travel. Worst single frame measured: **133px**. `clearTween` now
   cancels the running transition outright; its own comment carries the
   before/after rects.

   **What that means for the curve here, because it is counter-intuitive.**
   `cubic-bezier(.2,.7,.3,1)` covers 68% of its distance in the first 25% of
   its time, so it opens at about 3.5x its own mean speed. The peak px/s on a
   wrapping reorder is therefore set by this curve, not by the duration -- and
   shortening the mid-drag duration, which is what "more of each glide is seen"
   asks for, would RAISE the number he is complaining about. Measured after the
   fix, a wrap peaks at ~1,950px/s at 398ms. Halving the duration doubles that.
   The curve and the duration law are both left exactly as #1596 and #1851 set
   them, and this paragraph is why.

       board                  worst 1-frame jump    peak px/s      reorders
       3 words, one row         0 ->   0px            921 -> 1,008     2
       5 words, one row         0 ->   0px            947 ->   949     4
       7 words, two rows       73 ->   0px          4,453 -> 2,016   7 -> 8
       9 words, two rows      134 ->   0px          8,339 -> 1,977    11
       9 words, within row 0    0 ->   0px            932 ->   906     2

   The last row is the control and it is the finding: **the word count is not
   the variable, the row crossing is.** Nine words dragged without leaving row
   0 behave exactly like three, before and after. His own "more words and more
   lines" says both; only the second half is load-bearing.

   ---- the second mechanism that was investigated and IS NOT REAL ----------

   Recorded because it is the obvious next suspicion and it costs a rule to
   re-derive. A chip travelling from the bank to the line is a `.jumble-line`
   descendant while it flies, and `.jumble-line-row` precedes `.jumble-bank`
   in the markup -- so it LOOKS like it must paint behind the bank chips
   whose rows it crosses, which would make it visibly vanish mid-flight and
   would be a fine explanation of "jumps around". `.jumble-settling` was
   given `position:relative; z-index:1` on that reasoning, and the reasoning
   does not survive being measured.

   Measured: every bank->line flight on all 294 cards, walked in 10 steps,
   hit-testing the centre of each real overlap between the flying chip and a
   live bank chip. **531 of 1,786 flights (29.7%) do genuinely overlap one,
   and in 0 of them is the flying chip painted underneath** -- with the
   stacking rule and, as a control forcing `position:static; z-index:auto`,
   without it. Identical, 0 and 0, and the control was verified to have
   actually applied (the harness reads `.jumble-settling`'s computed
   `position`/`z-index` back on every run for exactly that reason). So the
   rule was removed rather than shipped: it fixed nothing.

   The first version of that measurement DID report a defect -- 369 of 1,779
   flights, 20.7%, on 76.5% of cards -- and every one of those was an
   artefact of the metric. It counted an overlap of two bounding boxes at 2px
   in each axis; a chip is `border-radius:10px` and `elementFromPoint`
   respects the rounded shape, so a 2-4px CORNER clip hit-tests as "not the
   flying chip" whatever the stacking is. The threshold is 16px now, clear of
   the radius on both axes. Written down because the artefact was convincing:
   it had a plausible mechanism, a big number, and it moved in the right
   direction when the distractor count came down. */
.jumble-chip.jumble-settling {
  transition:translate var(--jumble-settle-ms, .2s) cubic-bezier(.2,.7,.3,1);
}

/* `prefers-reduced-motion`, degrading the drag to a static state -- the
   owner's own stylesheet convention (word_match.css, safe_cracker.css: one
   block per file, `reduced_motion_blocks` in tests/_reduced_motion.py reads
   every one rather than only the first). The lift itself is dropped, not
   merely its transition: an un-transitioned `scale(1.06)` would still be a
   size CHANGE the instant the class lands, which is motion by another name.
   The soft shadow is left as a still, non-animating "this one moved" cue.

   #1572 adds the reorder's own tween to the same block, and the important
   half is what is NOT here: **dragging is not disabled.** `jumble.js`
   releases each displaced chip's inversion whether or not a transition is
   declared, so with motion reduced the chip is simply AT its new position on
   the next frame. The move happens; only the travel is dropped. The dragged
   chip's own follow is an inline `translate` and is deliberately untouched --
   a chip that stopped tracking the finger would not be a reduced animation,
   it would be a broken control. */
@media (prefers-reduced-motion: reduce) {
  .jumble-line .jumble-chip.dragging,
  .jumble-bank .jumble-chip.dragging { transform:none; transition:none; }
  /* #1777 gave the rule above a `--jumble-settle-ms`, written as an INLINE
     custom property by `jumble.js` -- per chip then, one per gesture since
     #1851. This still parks it, and needs no knowledge of either:
     `transition:none` sets `transition-property:none`, so there is no property
     left for any duration to apply to, whatever the script wrote. */
  .jumble-chip.jumble-settling { transition:none; }
  /* #1595: parks the check/continue button's own press-and-sheen the same
     way `.advance`'s own escape does (shared/beat_card.css) -- zero duration
     is also what `jumbleSheenMs()` in jumble.js reads as "nothing to wait
     for," so this same cascade removes the CONTINUE press's wait too, with
     no separate branch in the script. */
  .jumble-check { transition:none; --jumble-check-sheen-ms:0; }
  .jumble-check:active { translate:none; }
  .jumble-check.jumble-check-lit::after { animation:none; }
}

/* The bank. `dir`/`direction` for the same reason the line has them -- a
   jumble of Hebrew words filling left-to-right is a small constant lie about
   the language, and it costs one declaration not to tell it. */
/* `min-height` is written by `jumble.js` at wire time, from the bank's own
   measured height while it is still full. Not a constant here, because the
   right value is "however tall THIS card's bank is" -- a 4-word puzzle and a
   10-word one field different numbers of chips. Without it the card visibly
   reflows when the last word is placed: measured at 393x852, ten words
   leaving the bank drops it from 206px to 98px and everything below (the
   replay, OK, the verdict, Continue) jumps 108px up the screen, which is
   exactly the movement #1283 was filed about.

   #1416 asks 2 and 3 make this reservation close to redundant rather than
   wrong: since every chip that leaves is immediately replaced by a same-size
   placeholder (`.jumble-slot` below), the bank's occupied footprint no longer
   changes at all after wire time, only which particular chips are visible
   within it. `min-height` is kept anyway as the same webfont-race guard its
   own comment already argues for. */
.jumble-bank {
  direction:rtl;
  display:flex; flex-wrap:wrap; justify-content:center; align-content:flex-start;
  gap:10px;
}

/* #1416 ask 3: a bank slot belongs to its word for the life of the card. The
   owner: "if you remove them from the line to put them back into the word
   bank they should go to the same position they started in." `jumble.js`
   swaps a real chip for one of these the moment it is placed, and swaps it
   back on removal -- see that file's header for why a placeholder rather
   than a second data model.

   It is a `.jumble-chip` (same class, same text) so it claims the identical
   footprint an irregular-width Hebrew word needs -- a fixed generic size
   would not -- and it subsumes #1416 ask 2 for free: nothing else in the
   bank ever moves, because every OTHER chip's own slot is holding its own
   ground the same way.

   #1459, and why `visibility` became `opacity`. The owner: "the space from
   where the card was taken should be a filled shadow of where it was. Will
   make it easier for the eye to keep things spatially consistent when
   removing items from the choices." `visibility:hidden` reserved the box
   but showed nothing -- a shadow has to be SEEN. `opacity` is the one
   property that keeps the geometry `visibility` already gave (the box
   #1416 asked for first, and this must not change it) while making the
   fill visible; `display` was never a candidate, it drops the box from flow
   entirely.

   `.45` is not a new number -- it is this app's own standing signal for
   "cannot be tapped right now" (`.moved-claim[disabled]` in study.css,
   `.cracker-key:disabled` in safe_cracker.css, both stated in those files'
   own comments as exactly that), reused rather than invented so a faded
   chip reads the same way here as everywhere else in the app. It answers
   all three of #1459's constraints at once: a real chip faded to `.45`
   does not read as TAPPABLE (the same fade the rest of the app already
   spends on "disabled"); it does not read as a word STILL TO PLACE (every
   live bank chip is full opacity, so the two are never confusable at a
   glance); and it carries no verdict colour of its own -- a slot is never
   inside `.jumble-line`, so `.jumble-line.right/.wrong .jumble-chip`
   cannot reach it, and it stays furniture on both outcomes, the same as
   the ruled line (#1418).

   `pointer-events:none` is now doing real work rather than riding on
   `visibility:hidden`'s own effect (a hidden box was never hit-testable to
   begin with); `[disabled]` still drops the cursor and the tab order (see
   `.jumble-chip[disabled]` below). A VISIBLE node is no longer dropped from
   the accessibility tree by the UA the way a `visibility:hidden` one was --
   `jumble.js` now marks it `aria-hidden="true"` at creation for that reason,
   since its text would otherwise duplicate the real chip already announced
   on the line. */
.jumble-slot { opacity:.45; pointer-events:none; }

/* A chip IN the bank: a real tile, sized like `.match-he` (#482's floor --
   "I don't want the user feeling cramped with clicking on the items"). 44px
   rather than that board's 60px because a chip is one word rather than a
   two-line gloss, and because fifteen of them have to fit; 44 is the touch
   floor and this must never go under it. */
/* `--jumble-chip-size` is the one dial for the puzzle's Hebrew, and **17px is
   measured, not chosen.** The owner offered exactly this lever -- "if it makes
   sense the hebrew for the puzzle can be a smaller font size" -- when making a
   placed word a chip pushed the card back over budget.

   It was 19px (`.match-he`/`.pick-cue`'s size, this card being their sibling
   rather than a smaller-print variant). At 19px a boxed chip costs 18px of
   chrome that flat text did not, and 143 of the 3,475 real cards went over the
   631px budget. At 17px, zero do -- with all five distractors kept, all 255
   cues kept and `MAX_WORDS` still 8. 16px was measured too and buys nothing
   further (identical worst heights), so there is no reason to go below 17.

   This dial deliberately does NOT reach `.jumble-answer` or `.jumble-english`:
   the red correct-answer line and the English question are things to READ, not
   chips to fit, and they stay at 19px. */
.jumble-line, .jumble-bank { --jumble-chip-size:17px; }
.jumble-chip {
  font-family:var(--sans-he); font-size:calc(var(--jumble-chip-size) * var(--milim-text-scale));
  line-height:1.2; color:var(--ink);
  background:var(--surface); border:1px solid var(--line); border-radius:10px;
  padding:0 12px; min-height:44px; cursor:pointer;
  /* #1377: a chip is draggable, and `touch-action:none` is what makes that
     possible on a touch screen at ALL. `.stage` carries
     `touch-action:manipulation`, and without an override here iOS claims a
     drag begun on this chip for its own scroll/pan gesture and the
     `pointermove` stream stops arriving mid-drag -- the exact failure
     study.css's `.paceline` comment records for the pace pip (#388), and the
     same neighbourhood #1134's open double-tap-zoom bug lives in.
     `user-select:none` stops a slow drag from selecting the word's own text
     instead of moving it.

     **#1572 widened this from `.jumble-line .jumble-chip` to every chip**,
     because a bank chip is no longer tap-only: it drags onto the line, into
     the position the gap opened at. The cost is real and is stated rather
     than waved at -- a swipe that STARTS on a chip no longer scrolls the
     page. It is still per-chip and not per-card: the prompt, the ruled line
     itself, the bank's own gaps, the Check button and everything around them
     keep their native gestures, so the card is still scrollable by anything
     but a word. That trade is the one this beat's whole geometry is built
     for anyway (the header's height budget exists so this card FITS a
     393x852 phone without scrolling), and the alternative -- a bank word
     that cannot be picked up -- is the report this ticket is answering. */
  touch-action:none; -webkit-user-select:none; user-select:none;
}
.jumble-chip:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.jumble-chip[disabled] { cursor:default; }

/* The verdict, on the CHIPS ONLY -- #1418 part 1 settles a question this
   comment used to leave open.

   The first version coloured BOTH the ruled line and the chips: measurement
   showed the chips alone already occlude **82% of the first rule and 47% of
   the second** on the worst card (a chip's own 1px bottom border lands
   exactly on the 1px the gradient paints), so colouring the rule too was
   reasoned as filling the gaps the chips leave. #1418 asks a different
   question -- not "does the colour show", but whether the RULE should carry
   one at all -- and the owner's report is explicit: "those lines shouldn't
   get the color of being correct or wrong. They should remain grey for
   where the words go." His standing principle, stated on this same ticket:
   "if they really don't have meaning they don't have to have any colour --
   it's meaningless." A ruled line marks where a word GOES, not what the
   learner wrote there, so it carries no verdict of its own and stays the one
   grey `--line` paints above, regardless of outcome. There is deliberately
   no `.jumble-line.right`/`.jumble-line.wrong` rule here any more to repaint
   it back.

   So the chips carry the WHOLE verdict now, and `pick_in_cue`'s
   `button.pick-word.correct` is the precedent copied rather than re-derived
   -- soft tint, coloured border, and **the text stays `--ink`**. Dimming it
   to `--muted` is the trap that beat's own comment records: measured at
   4.46:1 against the composited green, under the 4.5:1 bar, on the one word
   a wrong answer most needs the learner to be able to read.

   No geometry changes here -- border WIDTH and padding are untouched, so the
   card's measured height is unaffected by which way the answer went. */
.jumble-line.right .jumble-chip { border-color:var(--ok); background:var(--ok-soft); }
/* The tint only -- #1586/#1411 declares the three-pulse red ring once, in
   `shared/beat_card.css`, whose list names `.jumble-line.wrong .jumble-chip`.
   No animation property belongs in this file
   (`tests/test_beat_wrong_pulse.py` fails if one reappears), and the shared
   3px spread is right here: the chips sit in a `10px` gap flex line, so the
   rings clear each other, and a `box-shadow` is outside the border box, so
   the "no geometry changes here" sentence above still holds exactly -- the
   card's measured height is unaffected by which way the answer went, ring or
   no ring. Every chip on the line pulses together, in sync, because they are
   one verdict on one line and not several. */
.jumble-line.wrong .jumble-chip { border-color:var(--no); background:var(--no-soft); }

/* `Check`, which becomes `Continue` -- ONE control, not two (#1478). A pill,
   unlike `.advance` (study.css) -- and that difference is the point rather
   than an inconsistency with #706: this button is a decision the learner
   makes and can put off, right up until it stops being one and becomes the
   only way forward. It used to sit beside a second, `.advance`-shaped
   button revealed on the same press; the owner's own words, 2026-09-02: "we
   don't double up the UI here with two buttons ... just change OK to Check
   and then when pressed it becomes the Continue button." So `jumble.js`
   relabels and rewires THIS element in `finish()` rather than un-reserving a
   second one -- there is no other button here any more.

   No `[disabled]` rule: unlike the two-button version, this element is
   never `.disabled` -- disabling it once it becomes Continue would make the
   one way forward untappable, so what freezes on a verdict is the chips
   (see `jumble.js`'s `finish()`), not this button. Its `min-width` is set
   inline, at wire time, to the wider of "Check"/"Continue" -- see
   `jumble.js`'s own comment on why that has to be measured rather than
   guessed at in a stylesheet that does not know the running font. */
.jumble-check {
  min-height:44px; padding:0 24px;
  font-family:var(--mono); font-size:11px; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink);
  background:var(--raise); border:1px solid var(--line); border-radius:22px;
  cursor:pointer;
  /* #1595, the owner's own report and Q8 answer: "the ok button should be
     the same size as what will become the continue button. Both should have
     the drop shadow effect and the sheen." #1571's treatment D (the drop
     shadow, the sheen) landed on `.advance` (shared/beat_card.css) -- but
     this button is not `.advance`, on purpose, per this rule's own header
     above, and it must not become it now: `.advance` is also the ONE
     selector shared/advance.js's document-level click listener watches, and
     giving this button that class would make its FIRST press -- the
     grading one -- wait ~420ms for a sweep before it even judges the
     answer, which is backwards for a control that has to fail the instant
     a wrong tap lands (see the ticket's own "stop and report" escape for
     exactly this trap).

     So the LOOK is reproduced here, unconditionally, directly on
     `.jumble-check` -- never behind a class jumble.js toggles with the
     label -- which is what makes "the ok button... the same size... [with]
     the drop shadow effect and the sheen" true BY CONSTRUCTION: this is one
     persistent element, and this rule never stops applying to it, whichever
     word it wears. The WAIT is applied separately, by hand, in jumble.js,
     only inside the closure `finish()` installs once this control stops
     being a decision and becomes the only way forward -- see that
     function's own comment and `jumbleSheenMs()` above it.

     `box-shadow`/`position`/`overflow` below are `.advance`'s own mechanism,
     read here rather than shared (this file owns no rule in
     shared/beat_card.css and was told not to add one) -- offset 3px
     down-right in `var(--line)`, non-inset so it costs no layout, exactly
     `.advance`'s comment there argues. */
  position:relative; overflow:hidden;
  box-shadow:3px 3px 0 var(--line);
  --jumble-check-sheen-ms:420;
  transition:translate .22s cubic-bezier(.2,1.5,.4,1), box-shadow .22s ease;
}
/* The press: flattens the drop shadow and travels onto the edge it was
   resting above -- `.advance:active`'s identical mechanic (#1571), read here
   rather than shared since this button does not carry that class.

   #1700/#1731: the flattened shadow is `0 0 0 transparent` rather than
   `var(--line)` -- zero blur and zero spread paint nothing regardless of
   colour, so this is a documentation change, not a visual one (see
   `.advance:active`'s identical comment, shared/beat_card.css). The added
   second shadow is the press highlight `.advance:active` also gained: a
   16px, 50%-mixed blurred glow in this button's own verdict hue -- `--line`
   unconditionally here (an ungraded Check press), `--ok`/`--no` on the
   `.right`/`.wrong` overrides below, mirroring `.advance`'s pair exactly so
   the two controls keep reading as one button (#1595's own point). No more
   `--accent` yellow on this control either. */
.jumble-check:active {
  translate:3px 3px;
  box-shadow:0 0 0 transparent,
             0 0 16px 1px color-mix(in srgb, var(--line) 50%, transparent);
  transition-duration:0s, 0s;
}
.jumble-check.right:active {
  box-shadow:0 0 0 transparent,
             0 0 16px 1px color-mix(in srgb, var(--ok) 50%, transparent);
}
.jumble-check.wrong:active {
  box-shadow:0 0 0 transparent,
             0 0 16px 1px color-mix(in srgb, var(--no) 50%, transparent);
}
/* The sheen. Named `jumble-check-lit` rather than reusing `.advance-lit`:
   that class means something on a DIFFERENT element (shared/beat_card.css),
   and a page that ever loaded both stylesheets must not have two rules
   answering to one class name -- the same reasoning that class's own name
   already gives for not being called `.lit` (study.css's `.stage.lit`).
   `jumble.js` lights it on `pointerup` (both the grading press and the
   continue press -- unconditional, so both look like the same button) and
   on a keyboard press with no pointer event; it comes off on this
   element's own `animationend`. */
/* #1700 retired `var(--glow)` (the accent's yellow halo) from this sweep
   the same way it did on `.advance::after` -- toned to the button's own
   verdict hue instead, `--line` unconditionally so an ungraded Check press
   still sheens. `.right`/`.wrong` below override the middle stop. */
.jumble-check::after {
  content:""; position:absolute; inset:0; pointer-events:none;
  border-radius:inherit;
  background:linear-gradient(105deg, transparent 35%,
             color-mix(in srgb, var(--line) 50%, transparent) 50%, transparent 65%);
  translate:-120% 0; opacity:0;
}
.jumble-check.right::after {
  background:linear-gradient(105deg, transparent 35%,
             color-mix(in srgb, var(--ok) 50%, transparent) 50%, transparent 65%);
}
.jumble-check.wrong::after {
  background:linear-gradient(105deg, transparent 35%,
             color-mix(in srgb, var(--no) 50%, transparent) 50%, transparent 65%);
}
.jumble-check.jumble-check-lit::after { animation:jumbleCheckSheen .42s ease-out; }
@keyframes jumbleCheckSheen { from { translate:-120% 0; opacity:1; } to { translate:120% 0; opacity:0; } }
.jumble-check:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }

/* #1700/#1731, the owner's settled q3 spec: "everything should match ..
   if its correct green shadow and if its incorrect then red". Added by
   `jumble.js`'s `finish()` the instant this control relabels itself
   Continue -- mirroring `.advance.right`/`.advance.wrong` (shared/
   beat_card.css) BY HAND, because this element cannot wear that class at
   all (see this file's own #1595 comment, above, on why).

   `background`/`color` are now IDENTICAL between `.right` and `.wrong` --
   `--raise`/`--ink`, this button's own resting fill, unchanged from the
   unanswered "Check" state above -- so grading no longer recolours the
   button's fill at all; only `border-color` and `box-shadow` carry the
   verdict, one token swapped for its pair, mirroring `.advance`'s own pair
   exactly. `box-shadow` is toned to the same token as `border-color` for
   the reason `.advance.right`'s header measures: a shadow toned to its own
   edge reads as depth, a shadow toned to `--ground` (or orphaned from a
   verdict-coloured edge) does not.

   `#1731`: this box-shadow pair is what
   `test_the_check_button_carries_advances_look_unconditionally`
   (tests/test_beat_jumble.py) now compares against `.advance.right`'s and
   `.advance.wrong`'s own, in addition to the base pair it already checked
   -- see that test's docstring for the seam decision.

   `.jumble-check.right::before` is UNCHANGED and stays: the non-colour
   signal this ticket owes in the tick's place, now doing more work than
   before since the fill itself no longer tells right from wrong. Kept
   exactly as #1595 shipped it -- `aria-hidden` in effect (generated
   content carries no accessible text by default), decorative only, the
   button's real accessible name stays "Continue" either way. `.wrong` gets
   no glyph of its own, matching `.advance.wrong`'s own comment: the
   revealed correct answer (`.jumble-answer.shown`, below, already red) is
   this side's non-colour content. */
/* #1759, out of this ticket's own file scope: `test_the_check_button_carries_advances_look_unconditionally`
   pins this box-shadow byte-identical to `shared/beat_card.css`'s
   `.advance.right`/`.wrong`, which Phase 1 of the Lantern rollout gave an
   18px glow (`--ok-bloom`/`--no-halo`). Mirrored here as the same one-line
   value match that test already requires -- not a restyle of this file's
   own tiles/keyframes, which stays Phase 4's. */
.jumble-check.right {
  border-color:var(--ok); background:var(--raise); color:var(--ink);
  box-shadow:3px 3px 0 var(--ok), 0 0 18px var(--ok-bloom);
}
.jumble-check.right::before { content:"✓\00A0"; }
.jumble-check.wrong {
  border-color:var(--no); background:var(--raise); color:var(--ink);
  box-shadow:3px 3px 0 var(--no), 0 0 18px var(--no-halo);
}

/* One node, two outcomes, one height (#1283: nothing enters or leaves flow
   when the card is answered).

   This `min-height` is only the FLOOR -- one line. `jumble.js` overwrites it
   at wire time with the height this card's own cue actually needs, because
   the real value is a property of the sentence and no stylesheet can know
   it: measured at 393x852, a one-line reserve under a two-line cue leaves
   the card 27px short and everything below the verdict jumps by that much
   the moment OK is pressed. Two lines here would fix that cue and lose the
   next one that wraps to three. See jumble.js. */
.jumble-answer {
  margin:0; min-height:calc(30px * var(--milim-text-scale));
  font-family:var(--sans-he); font-size:calc(19px * var(--milim-text-scale));
  line-height:1.5; max-width:22rem;
}
/* `--no`, the same red `study.css` measures at 4.10:1 dark / 4.29:1 light and
   reserves for large text -- and this IS large text: 19px Hebrew at the
   card's scale clears the 3.0:1 bar that applies to it. The owner asked for
   the correct line "in red", and it is the same red the rest of the app
   already means "wrong" with. */
.jumble-answer.shown { color:var(--no); }
/* #1682b retired the standalone tick this rule used to draw
   (`.jumble-answer.tick`, "✓" at the encouragement's own size): the owner's
   later, more specific instruction moved that signal onto `.jumble-check`
   itself (above) rather than a second element beside it. `jumble.js` no
   longer ever adds a `tick` class to this node; nothing here answers to one
   any more. */

/* #1860: "I'm scanning the text to see where I made the mistake" -- the
   words `jumble.js`'s `jumbleWrongMask` picked out of the reveal, wrapped in
   `<mark>`. Reset to plain inline text first: the UA default (a yellow
   background, black text) is a SECOND colour scheme fighting the reveal's
   own red, which is exactly the "colour alone" shape this ticket and the
   owner both rule out -- so this rule removes the highlight colour rather
   than picking a different one, and carries the "this one was wrong" signal
   in WEIGHT and an UNDERLINE instead, neither of which colour vision
   affects. `text-underline-offset` clears Hebrew's own descenders
   (final-form letters run below the baseline) so the rule reads as its own
   line rather than colliding with the letterform. */
.jumble-answer mark {
  background:none; color:inherit; font-weight:700;
  text-decoration:underline; text-underline-offset:0.2em;
}

/* The replay button, landed at #1400's placement. `.cuereplay` (study.css)
   still sizes and colours it -- square, filled, bevelled, clause 6 of THE
   READING-EDGE CONTRACT stated there -- unchanged by this file.

   ---- history: why this rule USED to override the row-shaped defaults ----

   Until #1400, `jumble.js` built this button inside `.beat`'s CENTRED
   COLUMN, and `.cuereplay`'s own defaults (`flex:0 0 40px`, a width basis;
   `align-self:flex-start`) are written for a ROW. Two overrides used to sit
   here to correct that: `flex:0 0 auto` (a width basis is meaningless as a
   column's cross-axis size) and `align-self:center` (uncorrected,
   `flex-start` pinned the glyph to the column's own LEFT EDGE -- a lone icon
   in the margin, measured and seen in the render, rather than a control
   belonging to the puzzle).

   #1400 wraps `#jumbleLine` and `#jumbleReplay` in `.jumble-line-row`
   (jumble.js), which finally IS a row -- the same shape `.cuerow` already
   is. There is no column left to correct for, so both overrides are GONE
   rather than renamed: `.cuereplay`'s own `flex:0 0 40px` and
   `align-self:flex-start` now apply unmodified, exactly as they do on the
   cue row.

   ---- what THIS rule adds: the two things `.cuereplay` cannot know --------

   `margin-inline-start:8px` is contract clause 3's gap, copied at its
   amended value (`.cuerow .cuereplay` in study.css) rather than re-measured
   -- nothing about this card's geometry gives a reason for a second number.

   `margin-block-start:2px` is clause 3's own vertical-centring arithmetic
   (half of the leftover between the row's first-line height and the
   button's fixed 40px), carried over at the CONSTANT it resolves to here
   rather than restated as the cue row's `calc()`: `.cuerow .cuereplay`
   solves for a line-height that scales with the text-size slider, and
   `--jumble-rule` does not -- it is a literal 44px (see this file's own
   header on why 44 is the one number that makes the pitch, the row height
   and the touch floor agree, at every scale). So the leftover here is
   always `44px - 40px = 4px`, half of which is a constant 2px rather than a
   `calc()` solving for a variable that never varies.

   Scoped to `.jumble-line-row` rather than left bare, for the identical
   reason `.cuerow .cuereplay` is scoped rather than declared on `.cuereplay`
   itself: a bare margin would nudge every OTHER copy of this control, and
   `tests/js/test_study_cue_reading_edge.mjs` already guards that the bare
   class stays margin-free.

   ---- the width question #1400 was left open on, now measured -----------

   `.jumble-line` no longer has a fixed width to give up: it is `flex:1 1
   auto` (above), so flexbox -- not a subtracted constant -- decides how much
   room the button and its gap take from it, and it takes exactly 48px
   (40px + 8px) only on cards that actually carry a button (the audio
   variant; the English variant's line still gets the full row). Measured on
   demo.db (`jumble_probe.py`, this ticket's scratchpad, not committed) over
   every cue this beat can currently draw a REAL card from (real
   `cue_sightings`, no demo override -- a stricter, smaller population than
   the 255-cue sample the standing finding above used): 19 audio-variant
   cards, all narrowed to a 297px line as designed, all still 2 notebook
   rows -- **0 of 19 reach a third row, let alone a fourth** -- and the
   tallest of them sits at 497.94px against the 631px budget. Smaller sample,
   same conclusion as the standing finding:
   `docs/demos/2026-09-01-1418-jumble-round-two/README.md` carries the full
   table. */
.jumble-line-row .jumble-replay { margin-inline-start:8px; margin-block-start:2px; }

/* #2067: the line's own English, revealed once the card is answered --
   shape and rule copied from `cloze.css`'s own `#2061` reveal
   (`.cloze-reveal`/`.cloze-gloss`), itself borrowed from `tense.css`'s
   `.tense-reveal`/`.tense-gloss`, rather than re-derived: same dashed
   divider, same reading-face paragraph, same `.beat-reserve` contract
   (#1283) that makes revealing it cost no reflow.

   `jumble.js` builds this node only when there is a translation to show on
   the audio variant (`content.english` on a card whose `prompt !== "
   english"`), so an untranslated cue never reserves an empty, bordered box
   -- the same guard `.cloze-reveal` is built under. `dir="ltr"` is on the
   paragraph itself, matching `.jumble-english` two rules up: this is the
   one string on the card that reads left to right. */
.jumble-reveal { width:100%; max-width:22rem; border-top:1px dashed var(--line);
                 padding-top:12px; margin-top:4px; display:flex; flex-direction:column;
                 align-items:center; }
.jumble-reveal-english { margin:0; font-family:var(--serif);
                          font-size:calc(15px * var(--milim-text-scale)); line-height:1.4;
                          color:var(--ink); text-align:center; max-width:22rem; }
