/* #1244, the word-search beat's card (#307's one-file-per-beat split).

   Nothing here lives in `app/static/study.css` or in
   `app/static/shared/beat_card.css`: this card uses the shared beat shell
   (`.beat`, `.advance`, `.beat-dismiss`) exactly as it is and adds only the
   grid and the clock, neither of which any other card has. Since #2333 it
   adds no control of its own at all -- the card's one button IS the shared
   `.advance.beat-dismiss` pill, relabelled between "check" and "continue"
   in place (see the tombstone below, and `word_search.js`).

   Tokens (`--ink`, `--line`, `--muted`, `--accent`, `--surface`, `--ground`,
   `--ok`, `--ok-soft`, `--no`, `--no-soft`, `--sans-he`, `--serif`, `--mono`,
   `--milim-text-scale`) all come from the host page's own sheet -- the
   contract every `beats/*.css` file here follows, so this card paints the same
   on `/study` and on the practice hub. */

/* ---- the source line (#2342) -------------------------------------------- */

/* "Is it possible to guarantee we just saw the word that it is asking us to
   find?" -- his own words, first time he met this card. The guarantee was
   already there (`word_search.py`'s `offer()` draws only from the reading
   window); nothing on the card SAID so. This one line is the saying.

   Small, muted and above the clue rather than folded into it: the clue
   itself is untouched (`.ws-clue`'s exact text is asserted by
   `tests/js/test_beat_word_search.mjs` in both languages), so this is a
   second, independent sentence rather than a rewrite of the first. Uppercase
   and letter-spaced the way a section eyebrow reads -- a label about the
   card, not part of the question the card is asking -- so it does not
   compete with `.ws-gloss`/`.ws-clue-he` for the one thing on this 30-second
   card that is actually variable. English on both clue kinds, including the
   Hebrew antonym card: this sentence frames the GAME, not the clue, and the
   antonym clue's own header already argues against adding an English lead-in
   to the negated phrase itself -- that argument does not extend to a
   sentence sitting above it in its own line. */
.ws-source { margin:0 0 4px; color:var(--muted); font-family:var(--serif);
             font-size:11px; line-height:1.3; text-align:center;
             text-transform:uppercase; letter-spacing:.06em; }

/* ---- the clue ---------------------------------------------------------- */

/* "the word for love" -- his own phrasing. The lead-in is muted and the gloss
   is not, because the gloss is the whole question and the four words in front
   of it are grammar. `--serif` and 15.5px are `.opt`'s own body values
   (shared/beat_card.css), so the clue reads as this card's sentence rather
   than as a heading. */
.ws-clue { margin:0 0 2px; color:var(--muted); font-family:var(--serif);
           font-size:15.5px; line-height:1.4; text-align:center; }
.ws-gloss { color:var(--ink); font-weight:700; }

/* #1604's second clue kind: `לא שבור` -- Hebrew, so `--sans-he` and not
   `--serif`. The rule above is written for an English sentence in the reader's
   serif face and is wrong for Hebrew twice over: `--serif` is not this app's
   Hebrew face (`--sans-he` resolves to the self-hosted "Noto Sans Hebrew
   Subset", shared/fonts.css) and 15.5px is a body-copy size for a phrase that
   IS the question.

   `--ink` rather than `--muted`, and 19px x `--milim-text-scale`, because the
   English card puts the variable half of its clue in `.ws-gloss` at full
   contrast and this card is all variable half -- there is no grammar in front
   of it to mute. 19px is `.cloze-cue`/`.pick-cue`/`.numgen-cue`'s own Hebrew
   body size (every other card in this package that paints Hebrew prose), and
   `--milim-text-scale` is applied for the same reason they apply it: #1637's
   cap is on the multiplier, not on which elements honour it, and a Hebrew
   phrase that ignored the reader's text-size setting would be the one line on
   the card that did.

   `direction:rtl` is set here as well as by `word_search.js`'s `dir="rtl"`
   attribute, which is this file's own existing arrangement one rule down:
   `.ws-grid` carries both too. The attribute is what an assistive technology
   and the jsdom test read; the property is what the layout engine reads; and
   a card whose Hebrew depends on a host page's inherited direction is a card
   that paints differently on `/study` and on the practice hub. */
.ws-clue-he { color:var(--ink); font-family:var(--sans-he); direction:rtl;
              font-size:calc(19px * var(--milim-text-scale)); line-height:1.6; }

/* ---- the clock --------------------------------------------------------- */

/* His "a timer counts down from 30", as a number and a bar.

   Both, not one: the number is the only exact reading (a bar cannot say
   "seven"), and the bar is the only one that can be read without looking
   away from the grid, which is where the learner's eyes are for the whole
   thirty seconds. `--mono` on the number so the face does not jitter as the
   digits change width -- a proportional "11" is narrower than "30" and the
   bar beside it would visibly shift once a second. */
.ws-clock { display:flex; align-items:center; gap:10px; width:100%;
            max-width:19rem; margin:0 0 2px; }
.ws-seconds { font-family:var(--mono); font-size:13px; color:var(--muted);
              min-width:2.2ch; text-align:right; font-variant-numeric:tabular-nums; }
.ws-bar { flex:1; height:4px; border-radius:2px; background:var(--line);
          overflow:hidden; }
/* `--ws-left` is the fraction of the round still to run, written by
   `word_search.js`'s `paintClock` on every tick. `transform:scaleX` rather
   than a `width`, so a repaint four times a second is a compositor job and
   never a layout one -- the same reasoning #1049 recorded when 32 animated
   coins dropped frames on the owner's phone.

   `transform-origin` is LOGICAL (`right` in this RTL card is the inline start,
   but this bar is not text and its direction is the clock's, not the
   language's) -- so it is written physically as `left` and the bar drains
   leftward regardless of the grid's `dir`. A clock that ran backwards in an
   RTL card would be a second, invisible RTL decision hiding inside the one
   this ticket is about. */
.ws-bar-fill { display:block; height:100%; background:var(--accent);
               transform-origin:left center; transform:scaleX(var(--ws-left, 1));
               transition:transform .12s linear; }
/* The last stretch, turned red. `.ws-low` is added by `word_search.js`'s
   `paintClock` under `LOW_MS`; the threshold lives there rather than here
   because a stylesheet cannot read a clock, and a `@container`/`@supports`
   trick that faked it would be a second place the number lives.

   `--no` at full strength, the same red every wrong state in this app uses.
   Not a soft tint: this is the one moment on the card where the learner needs
   to be told something they are not already looking at, and a wash would read
   as one more decorative band. It is deliberately the SAME red as a wrong
   accept even though it means something different -- the two never appear
   together (a wrong accept clears in 420ms and the bar is a separate element),
   and giving "urgent" a third colour of its own would put a fourth signal
   colour on a card that already carries accent, ok and no. */
.ws-clock.ws-low .ws-bar-fill { background:var(--no); }
.ws-clock.ws-low .ws-seconds { color:var(--no); }

/* ---- the grid ---------------------------------------------------------- */

/* **The RTL decision, and it is made here.**

   `direction:rtl` plus `grid-auto-flow:row` means the inline axis runs right
   to left, so the first cell in DOM order paints in the TOP-RIGHT corner and
   the row fills leftward -- which is how Hebrew reads. `word_search.js` emits
   cells in plain row-major order and reverses nothing; this declaration is the
   whole of the mirroring. See that file's header for the argument and
   `tests/test_beat_word_search.py::test_the_grid_declares_rtl` for the pin.

   The `dir="rtl"` attribute is on the element as well. Belt and braces on
   purpose: the attribute is what a jsdom test can read (jsdom computes no
   grid layout at all), and the declaration is what a browser lays out from.
   Neither alone would let both halves of the claim be tested.

   `aspect-ratio:1` on the cell rather than a fixed size, and `--ws-size` from
   the markup rather than a literal `repeat(5, ...)`: the server sends the grid
   size with the payload, and a hard 5 here would misgrid the first card that
   ever sends anything else.

   #2062: `touch-action:none`, on the GRID only -- his ask was "hold down and
   swipe", and without this a drag across the grid is a page scroll on any
   touch device: nothing about this element's default `touch-action` already
   suppresses one the way `.ws-cell`'s own `cursor:pointer` might suggest.
   Scoped to this one element rather than the card or the stage, so a swipe
   that starts on the clue, the clock or the controls still scrolls the page
   normally -- the same "no wider than it has to be" rule `.paceline` states
   for its own `touch-action:pan-y` in study.css. */
.ws-grid { display:grid; direction:rtl; touch-action:none;
           grid-template-columns:repeat(var(--ws-size, 5), 1fr);
           gap:6px; width:100%; max-width:19rem; margin:2px 0; }

/* 44px is the floor, not a design -- the minimum comfortable touch target, the
   same number `word_match.css` records as the one its tiles must never go back
   under. At 393px wide the card's column gives this grid 19rem = 304px, so a
   5-wide grid with 6px gaps deals 56.8px cells: comfortably over the floor,
   with `min-height` there so a narrower host can shrink the cell's WIDTH
   before it shrinks the thumb target. */
/* #1798, Phase 4 of the Lantern rollout
   (docs/design/2026-09-05-fable/05-handoff-roadmap.md): radius 8 -> 14 and
   `box-shadow:var(--shadow)`, the same two declarations `.opt` carries in
   `shared/beat_card.css`. Phase 1 put Lantern's TOKENS on the host page's
   `:root`, so this cell already read Lantern's `--surface`/`--line`/`--ok`
   from the day that landed -- and kept its own pre-Lantern flat SHAPE,
   because the shape is declared right here. A learner playing this card
   after a `.opt` card saw two tiles built out of the same colours to two
   different recipes, which is #1764's finding on `tense.css` restated on a
   25-cell grid.

   `box-shadow` joins the transition list rather than being left off it --
   #1777 in full: every other property on this rule eases over .12s, and a
   shadow that snaps under a smoothly-easing border is the defect the owner
   reported as things "feeling broken" without being able to name it.

   `--rest-shadow` is what `beatWrongPulse` (shared/beat_card.css) holds
   underneath its ring while a wrong tap pulses. Without it this cell's new
   elevation would VANISH for the pulse's 420ms and snap back -- the exact
   regression #1777 found on the three surfaces #1759/#1761 had already
   given a resting shadow to. `.ws-cell.wrong` is in that keyframe's
   selector list, so this cell needs the declaration; see that block's own
   comment for the mechanism. */
.ws-cell { display:flex; align-items:center; justify-content:center;
           position:relative; aspect-ratio:1; min-height:44px; padding:0;
           font-family:var(--sans-he); font-size:calc(20px * var(--milim-text-scale));
           line-height:1; color:var(--ink); background:var(--surface);
           border:1px solid var(--line); border-radius:14px; cursor:pointer;
           box-shadow:var(--shadow); --rest-shadow:var(--shadow);
           transition:background .12s ease, border-color .12s ease,
                      box-shadow .12s ease; }
.ws-cell:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.ws-cell[disabled] { cursor:default; }

/* Selected. `--accent`'s outline, not a fill -- #429's finding on the
   word-match board applies exactly: the eye should be able to land on what is
   still UNPICKED, and a filled cell in a grid of twenty-five competes with the
   letters. The `box-shadow` rings the cell 1px outside its own 1px border so
   the outline reads at a full 2px without a border-width change reflowing a
   25-cell grid.

   `--accent` rather than `--ok`: on this card a picked cell is not yet right
   about anything. Green arrives only at `.found`. */
/* #1798: `var(--shadow)` is appended rather than replaced. `box-shadow` is a
   single property, so the ring alone would have DELETED the resting
   elevation the base rule above now declares -- a picked cell would drop
   flat while every cell around it floated, which is the same
   "elevation vanishes on a state change" defect #1777 fixed on the wrong
   pulse. Ring first so it paints on top of the drop, the order
   `beatWrongPulse` states its own reason for. */
.ws-cell.picked { border-color:var(--accent);
                  box-shadow:0 0 0 1px var(--accent), var(--shadow);
                  background:var(--surface); }

/* A wrong accept, for `WRONG_MS` (word_search.js) and then gone.

   The RING is not here. `.ws-cell.wrong` is in the shared selector list in
   `shared/beat_card.css`, which is where every judged surface's wrong-answer
   treatment now lives (#1586) -- this card was written while that unification
   was still in flight and briefly carried its own `wsWrongPulse` copy, pinned
   by test to word_match's count. That is exactly the eight-disagreeing-copies
   shape #1586 removed, so it joined the shared rule instead of being the
   ninth. `tests/test_beat_wrong_pulse.py` fails if a private one comes back.

   What IS this card's own business is the settled colour underneath: the
   cell's colours never move during the pulse, so the letter is exactly as
   legible mid-ring as at rest -- `.wrong` sets them once and only
   `box-shadow` animates, which is the shared rule's construction too. */
.ws-cell.wrong { border-color:var(--no); background:var(--no-soft); color:var(--ink); }

/* Found it. The answer's cells, at the end of a round the learner won.
   `--ok-soft` fill with an `--ok` border and the letter left at `--ink`: the
   settled-green pairing every other beat uses, and the `color:var(--ink)` is
   the same measured trap `cloze.css`, `tense.css` and `word_match.css` each
   record -- dimming this text to `--muted` measures 4.46:1 against the
   composited green, under the 4.5:1 floor, on the one word the learner most
   wants to read. */
/* #1798: and this is the beat's `.correct` class, so it takes the
   correct-answer bloom (03-motion-system.md §1) -- 2px `--ok` ring, 6px
   halo, 22px outer glow, arriving once over `--dur-bloom` and settling on
   exactly the values declared here, so there is no jump at the frame the
   animation ends. The keyframe is `verdictBloom` in
   `shared/beat_card.css`, referenced by name and not redeclared: keyframes
   are document-global and both host pages load that sheet, which is the
   same call `cloze.css` makes for `.cloze-option.correct`.

   The class name is `.found`, not `.correct`, and that is why this rule is
   here rather than covered by the shared selector list -- the list in
   `beat_card.css` is the WRONG-answer list; the roadmap's instruction is to
   mirror it per beat on each beat's own correct class.

   Contiguous cells: a found word is adjacent cells in a 6px-gap grid, so
   neighbouring halos meet. That was checked in the browser before it was
   kept -- the merged halo reads as one lit word rather than as five
   colliding rings, which is what the state actually means. */
.ws-cell.found { border-color:var(--ok); background:var(--ok-soft); color:var(--ink);
                 box-shadow:0 0 0 2px var(--ok), 0 0 0 6px var(--ok-halo),
                            0 0 22px var(--ok-bloom);
                 animation:verdictBloom var(--dur-bloom) var(--ease-out) 1; }

/* Where it was, at the end of a round the clock ended. Deliberately NOT
   `.found`'s green: the learner did not find it, and painting the reveal in
   the success colour would be the card congratulating them for running out of
   time. `--accent` is the colour this app already uses for "here is the thing
   being taught" (the reader's session word, `.tense-target`,
   `.numgen-counted`), which is exactly what these cells are now. */
.ws-cell.revealed { border-color:var(--accent); background:var(--surface);
                    /* #1798: elevation kept under the ring -- see `.picked`
                       above for why appending rather than replacing is the
                       whole of the change here. */
                    box-shadow:0 0 0 1px var(--accent), var(--shadow);
                    color:var(--accent); }

/* #2063: his own ask -- "indicate the correct order on an incorrect
   answer". `word_search.js`'s `revealAnswer()` writes `data-order` as
   `1..N` in the answer's true reading order (see that function's own
   comment for why it can never be the reversed one). A small numbered
   badge rather than anything that reflows the cell: `position:absolute`
   inside `.ws-cell`'s own `position:relative` (declared on the base rule
   above) touches no cell's box and nothing downstream of the grid, so
   `.beat-dismiss` never moves (#1283).

   Physical `top`/`right`, not logical `inset-inline-*`, for the same
   reason the clock bar above is physical: this badge is a sequence number,
   not text, and its corner is cosmetic rather than a thing the language's
   direction decides. */
.ws-cell.revealed::after { content:attr(data-order); position:absolute;
                    top:2px; right:2px; min-width:14px; height:14px;
                    padding:0 2px; border-radius:999px; display:flex;
                    align-items:center; justify-content:center;
                    font-family:var(--mono); font-size:9px; font-weight:700;
                    line-height:1; background:var(--accent); color:var(--surface);
                    pointer-events:none; }

/* ---- the accept control: GONE, folded into the one button (#2333) ------

   This file used to style a second, primary-looking pill -- `.ws-accept`
   inside a `.ws-controls` row -- that sat directly above this card's
   Continue. #1778 is the ticket that made it a labelled pill rather than a
   check-mark circle, and its `--control` fill, its 48px height and its 600
   weight all existed for ONE reason, stated in the rule they lived in: "the
   two pills stacked under the grid read as 'the one that judges' and 'the
   one that leaves' ... the FILL is the only thing saying which is which."

   #2333 removed the stack. The owner, from the beta: the Check and the
   Continue should be "one button in one place". There is one node now
   (`word_search.js`'s `html()`), it is the shared `.advance.beat-dismiss`
   pill, and it changes its label between "check" and "continue" in place --
   so there is no second control left for a fill to distinguish this one
   from. The primary/secondary treatment lost its SUBJECT, not its argument;
   if this card ever shows two controls again, that reasoning is above and
   is still right.

   #1778's surviving half is in `word_search.js` rather than here: the
   control still carries a WORD and the word is still "check"
   (`LABEL_ACCEPT`, whose comment holds that argument in full), and it is
   still a real `<button>` with no `aria-label` fighting its caption.
   `.advance` (shared/beat_card.css) supplies everything this file used to
   restate -- the `--mono`/13px/.08em/lowercase type recipe those rules were
   copied from in the first place, the 44px tap floor (#1973), #1571's drop
   shadow and sheen, and #1682b's `.right`/`.wrong` verdict fill, which is
   what the owner asked this button to carry.

   The disabled treatment went with it, and that is not a regression to
   re-add: there is no disabled state left to paint. The old Check was
   `[disabled]` with nothing selected because "accepting an empty selection
   would post a `beat_answers` row with an empty `chosen`"; the one button
   now simply reads "continue" in that state and leaves, which is the same
   refusal expressed as a label rather than as a dimmed pill. #723's
   exemption, already retired by #1778, stays retired -- nothing here fades
   a caption any more. */

@media (hover: hover) {
  .ws-cell:not([disabled]):hover { border-color:var(--accent); }
}

/* ---- #3048: the guided first grid ---------------------------------------

   The learner's first grid ever tells them the answer: its cells wear the same
   numbered badge `.revealed` uses for a wrong accept (reading order, 1..N) and
   the next one to tap pulses. `.ws-guide` is the line that walks them through
   it. Colours are the card's own (`--accent`, `--muted`, `--ink`), so no new
   pair is introduced for the contrast sweep. */
.ws-guide { margin:0 0 8px; color:var(--ink); font-family:var(--serif);
            font-size:15px; line-height:1.4; text-align:center; min-height:4.2em; }
.ws-guide-word { font-family:var(--sans-he); font-size:calc(19px * var(--milim-text-scale)); }
.ws-cell.guide { color:var(--accent); }
.ws-cell.guide:not(.found)::after { content:attr(data-order); position:absolute;
                    top:2px; right:2px; min-width:14px; height:14px;
                    padding:0 2px; border-radius:999px; display:flex;
                    align-items:center; justify-content:center;
                    font-family:var(--mono); font-size:9px; font-weight:700;
                    line-height:1; background:var(--accent); color:var(--surface);
                    pointer-events:none; }
.ws-cell.guide-next, .advance.guide-press { animation:wsGuidePulse 1.2s ease-in-out infinite; }
.ws-cell.guide-next { border-color:var(--accent); box-shadow:0 0 0 2px var(--accent); }
@keyframes wsGuidePulse {
  0%, 100% { transform:scale(1); }
  50% { transform:scale(1.06); }
}

/* ---- keyframes --------------------------------------------------------- */


/* The file's ONE `prefers-reduced-motion` block, stated rather than left to be
   noticed -- #1036's finding, which `word_match.css` records in full: the
   guard tests that read a file's reduced-motion rules slice the FIRST such
   block, and a second block added below this one would be invisible to every
   one of them. Whatever this card gates next joins THIS block.

   `animation:none` rather than a shortened duration, for the reason study.css
   records for `.hooray`: a `.01ms` run leaves the outcome to
   `animation-fill-mode`, a property this rule would then depend on without
   saying so. Naming `none` means the ring never plays at all and the cell is
   at `.wrong`'s settled red from the first frame the class lands.

   The clock bar's `transition` is also dropped here. It is a 120ms linear
   nudge and not an animation, but it is the one thing on this card that moves
   continuously for thirty seconds, which is exactly what a learner who asked
   for less motion is asking to be spared. The bar still shrinks -- it just
   steps to each new value instead of sliding. Nothing else is removed: the
   red still appears, the green still settles, the reveal still lights. */
@media (prefers-reduced-motion: reduce) {
  .ws-bar-fill { transition:none; }
  .ws-cell.guide-next, .advance.guide-press { animation:none; }
  /* #1798: the correct-answer bloom, parked in THIS block rather than in a
     second one -- the paragraph above is the reason why. `animation:none`,
     never a shortened duration: the ring, the halo and the outer glow are
     all declared on `.ws-cell.found`'s own base rule and not inside
     `@keyframes verdictBloom`, so a learner who asked for less motion still
     gets the full verdict shape the instant the word is found and simply
     never sees it arrive over 540ms. This is the same escape
     `shared/beat_card.css` writes for `.opt.correct` and `cloze.css` for
     `.cloze-option.correct`, per file, which is the granularity
     `tests/test_reduced_motion_escape.py` asks the question at. */
  .ws-cell.found { animation:none; }
}
