/* #1692, the picture round's card (#307's one-file-per-beat split).

   The card is a drawing, the frame `מה זה?` under it, and four Hebrew nouns.
   Almost every value here is one already shipped on a sibling surface, reached
   for deliberately rather than invented -- #1239: *"the cards need to be
   constant as much as possible so the eye doesn't jump around"*. What is
   genuinely this card's own is `.pic-art`, because no other beat in this app
   renders a picture at all.

   ## The one rule in this file that is load-bearing, and it is a colour

   `.pic-art { color:var(--ink) }`. The drawings are `stroke="currentColor"`
   throughout, which is what lets one file serve both themes with no second
   copy and no asset pipeline -- so the ink is inherited from this rule, and a
   hardcoded stroke anywhere in the SVGs or a missing `color` here breaks the
   dark theme silently and nothing in the test suite can see it.
   `picture.py::_load_pictures` refuses to start on a hardcoded stroke; this
   rule is the other half, and both themes were screenshotted on #1692 rather
   than reasoned about.

   It is also why the markup is INLINE rather than `<img src=…>`: an `<img>`
   renders the SVG in its own document, where `currentColor` resolves against
   the image's own initial `color` (black) and never against this rule. See
   `picture.py`'s header.

   ## What is NOT here, and that is the point of the file

   **No option-button rule of this beat's own, and no `.wrong` selector.** The
   buttons are `<button class="opt pic-opt">`: `.opt` is the shared class from
   `shared/beat_card.css`, so `.opt.wrong` -- which is already in all three
   copies of #1586's selector list (the ring, the light-theme `--wrong-sheen`
   override, and the reduced-motion escape) -- matches these buttons by
   construction. `.pic-opt` only re-faces them for Hebrew.

   That is a deliberate choice against `numeral_gender`'s shape. Its captions
   are Hebrew too, and it took the other road: a private `.numgen-opt` with its
   own `.correct`/`.wrong` tints, which then has to be hand-added to all three
   shared lists -- exactly the "ninth judged surface that tints and forgets to
   opt in" that `tests/test_beat_wrong_pulse.py` exists to catch. Riding on
   `.opt` and overriding two properties gets the same look with nothing to
   forget, and `shared/beat_card.css` is untouched by this ticket.

   No rule in this file lives in `app/static/study.css` or in
   `shared/beat_card.css`; both sheets are untouched. */

/* #1760: this whole beat's markup wraps itself in `<div class="pic">`
   (`picture.js`'s `html()`) -- a new root, not `.beat` itself. `.beat` is the
   shared shell in `shared/beat_card.css`; it renders one child per beat and
   this file cannot reach into it from here, so a beat that needs its own
   ancestor selector (`.pic.right …`, below) has to bring its own wrapper.
   The layout the wrapper carries is `.beat`'s own -- centred column, 18px
   gap -- restated here rather than inherited, because `.pic` is now the
   flex parent of the plate/prompt/options/dismiss stack and `.beat`'s rule
   only reaches its own direct child (`.pic` itself).

   `align-self:stretch` (#1789) -- found while measuring this beat for that
   ticket, not asked for by it, and it is what makes the fix below possible
   rather than cosmetic. `.beat`'s own `align-items:center` (shared/
   beat_card.css) governs how IT sizes `.pic`, its one flex child: `center`
   means shrink-to-fit, not stretch, so `.pic`'s width was set by its own
   widest child's intrinsic size -- `.pic-plate`, fixed at 168px -- and
   `.pic-options`'s `width:100%` (below) was resolving against THAT 168px,
   not against the stage's real content width. Measured on a real 393px
   viewport (`.stage`'s own 24px side padding, study.css): a `.pic-opt`
   button rendered 80x47.59px, capped by the plate, decades short of the
   22rem the comment below says it may grow to. This is the identical
   shrink-to-fit trap `shared/warm_up.css`'s own `.warmup-screen` rule hit
   and named (#1580, "a percentage width on a descendant … resolves as if
   it were auto") -- same fix, one line, on this file's own wrapper. */
.pic { display:flex; flex-direction:column; align-items:center; gap:18px;
       align-self:stretch; }

/* The plate (#1760): the art floated on the ground with nowhere to sit. A
   168px rounded square (radius 28) in `--raise`, reading the same `--shadow`
   token `.opt` does -- a soft double drop in light, a 1px `--line` ring plus
   a blur in dark, because a shadow alone is invisible on a near-black ground
   (see that token's own comment in study.css). This is the thing that takes
   the verdict below; the drawing itself never changes shape or position. */
.pic-plate { width:168px; height:168px; border-radius:28px; background:var(--raise);
             box-shadow:var(--shadow); display:flex; align-items:center;
             justify-content:center;
             transition:background var(--dur-bloom) var(--ease-out); }

/* #3139 (owner, on /study, "a bit of space between the card and what is above it"): the plate is the one
   beat card that is itself the first thing under the pace line, so it sat exactly `.stage`'s 8px padding
   below it (measured 390x844: pace bottom 107, plate top 115). A beat that opens with TEXT gets a little
   more for free: `.beat-text` is 30px at line-height 1.35, so its first glyph sits ~5px inside its box.
   12px here puts the plate's edge 20px under the pace line (measured). Scoped `#stage.stage` (/study's
   own, as build_it.css does): /skills has a `#stage` too, under a heading, and its plate is not touched.
   The board's first row is its Hebrew word (text), which already has that room. */
#stage.stage .pic-plate { margin-top:12px; }
/* The drawing. A 100x100 viewBox, so a square box and no aspect juggling.

   128px is the art's size INSIDE the 168px plate (#1760) -- large enough
   that the elephant's trunk wrinkles and the newspaper's column rules
   survive at 1.68/1.33 stroke weights, small enough that the plate keeps a
   visible margin around the drawing rather than the art touching its own
   frame. It does NOT scale with `--milim-text-scale`: that token sizes
   TEXT, and a learner who has turned it up is asking for bigger words, not
   a bigger picture. */
.pic-art { color:var(--ink); width:128px; height:128px; }
.pic-art svg { width:100%; height:100%; display:block; }

/* `.beat-text` already gives the frame its 30px Hebrew face -- this is the one
   big line on the card and it is the point of the card, which is what that
   class is for. Only the direction is added, because `מה זה?` is Hebrew and
   the `?` has to land on the left. */
.pic-prompt { direction:rtl; }

/* The English under the frame, muted -- `.tense-prompt`/`.pick-prompt`/
   `.numgen-prompt`'s own register, the line naming the question rather than
   being the point of the card. Smaller than the Hebrew on purpose: #1692's
   argument is that a question word is acquired as a frame that repeats until
   it is transparent, so the Hebrew is what the eye should land on and this is
   the crutch a learner stops reading. */
.pic-prompt-en { margin:0; color:var(--muted); font-size:calc(13px * var(--milim-text-scale));
                 line-height:1.4; }

/* A 2-column grid, not a stacked column (#1760, #1220's shape for short
   answers): these are single nouns, and the grid saves ~110px over the
   four-row stack `.opt`'s `width:100%` used to give -- which is what keeps
   the plate and the options on one screen at text scale 1.15. The 22rem cap
   is `.pron-options`', `.numgen-options`' and `.numgen-cue`'s; the 8px gap
   now runs both axes, the same value the stacked column used between rows.

   `grid-template-rows:repeat(var(--rows, 2), 1fr)` (#1789, widened by #2586):
   his `equal-everywhere` choice on the same worksheet answer that asked for
   this grid to be bigger (also landing on `.ask`'s own two-column case, out
   of this file). `--rows` comes from the markup (`picture.js`) rather than a
   literal `2` baked in here, the same reason `word_match.css`'s own `--rows`
   is markup-driven: Sharp eye rung 3 (#2586) makes the option count itself
   per-card, four or five, so a fixed `repeat(2, …)` would misgrid a
   five-option card's third (single-tile) row into whatever `auto` happened
   to give it. `N` `fr` rows with no definite grid height still equalise
   however many there are: the CSS Grid sizing algorithm computes one
   "hypothetical fr size" from the taller row's own max-content contribution
   and gives every flexible track that same size (spec 11.7.1) -- so if a
   longer word ever wraps to two lines in one cell, EVERY row grows to match
   it rather than only the row that needed it. Explicit rather than left to
   `grid-auto-rows`'s default `auto`, which does not carry that guarantee. */
.pic-options { display:grid; grid-template-columns:repeat(6, 1fr); grid-template-rows:repeat(var(--rows, 2), 1fr);
               gap:8px; width:100%; max-width:22rem; }

/* #3191, a first pass for the owner's eye ("the five answer buttons look bad ... interesting and intuitive, maybe a bit
   smaller and laid out differently"). The two-column grid above gave five options as 2 + 2 + 1 with the last one stranded
   on the left under two full-width rows. The grid is now SIX columns and each pill spans two, so three sit side by side
   and the wrap is the one a reader expects: 5 options are 3 + 2 with the second row centred (the 4th pill starts at
   column 2, the 5th follows it), 6 are 3 + 3, and 4 are still 2 + 2 (each spanning three). `picture.js` writes
   `data-n` and the matching `--rows`. The rows stay `1fr`, so every pill is still the same height (the #1789 mechanism
   above is unchanged); only the column count moved. Pills are `.pic-opt`'s own, below: 52px tall instead of 64, a full
   radius, and a slightly larger Hebrew face, because three across leaves the word as the one thing in each pill. */
.pic-opt { grid-column:span 2; }
.pic-options[data-n="2"] .pic-opt, .pic-options[data-n="4"] .pic-opt { grid-column:span 3; }
.pic-options[data-n="5"] .pic-opt:nth-child(4) { grid-column:2 / span 2; }
.pic-options[data-n="7"] .pic-opt:nth-child(7) { grid-column:3 / span 2; }

/* Hebrew words, so `--sans-he` and centred -- NOT `.opt`'s `--serif`,
   left-aligned, 15.5px pill, which is right there only because the study
   card's answers and `pronoun`'s captions are English phrases ("you, to a
   man"). These say `בית` and `עוגה`. Same 16px as `.numgen-opt`, which is the
   other card in this app whose buttons are bare Hebrew nouns.

   `direction:rtl` as well as the face: a Hebrew word in an LTR box renders the
   same on its own, but a `?` or a nikkud-free clitic added to this table later
   would not, and the box should be right the day that happens rather than the
   day someone notices.

   `min-height` was `44px` (the shared floor every other `.opt` subclass
   restates, for the reason below) and is now `64px` (#1789, his own
   worksheet instruction: "make the four options … biggeer … the same size
   as the icons in 'flip and match'"). 64px is not picked to look right --
   it is `shared/warm_up.css`'s own `.warmup-card { min-height:64px }`,
   MEASURED as what that card actually renders at on a real 393px viewport
   inside this same `.stage` (75.75 x 64px: `(345 - 3*14) / 4` wide by
   `min-height`'s own floor, since the card's `aspect-ratio:29/24` would
   otherwise ask for only 62.68px -- see that file's own header for the
   arithmetic this borrows). The WIDTH is deliberately not copied the same
   way: that 75.75px is what a FOUR-column track gives an 8-card board, and
   copying it onto a TWO-column, four-option grid would shrink these
   buttons into a narrow island with empty stage on either side -- the
   "literal constant lifted across" the ticket itself warns against. Width
   here stays derived from the grid track instead (now the stage's own
   content width, `.pic`'s `align-self:stretch` fix above), which is the
   same "derive from the track, borrow only the floor" shape `.warmup-card`
   itself uses for its own height.

   `display:flex` + `align-items:center` (#1789) -- new. At the old 44px
   floor a single line of `.opt`'s own `padding:12px 14px` already nearly
   filled the box (measured ~47.6px, barely over the floor), so top-aligned
   text read as centred by accident. 64px leaves real slack below a single
   line, and `.opt` itself has no flex/centring of its own (every sibling
   pill -- cloze, numeral_gender, tense -- stays close enough to its
   content height that the gap was never visible); this beat is the one
   whose floor now clears its content by a wide margin, so the centring
   has to be stated here rather than inherited. `justify-content:center`
   keeps the horizontal centring `text-align:center` already gave a single
   line; both are kept because a wrapped two-line word still wants
   `text-align:center` between its own lines.

   `min-height` is restated here (not left to `.opt`'s own padding/
   line-height) for the same reason `cloze.css`, `numeral_gender.css` and
   `tense.css` all restate it on their own option class:
   `tests/test_welcome_settings_pages.py`'s sibling checks (and the
   equivalent per-beat 44px-now-64px guards) read a literal `min-height`
   off THIS class's own rule, not off `.opt`'s cascade -- and a 2x2 grid,
   which can compress a button's box in a way a full-width stacked row
   never could, is exactly where that distinction bites.

   Everything else -- the border, the radius, the transition, the focus
   ring, the `.correct` green, the `.wrong` red and its three-pulse ring --
   is inherited from `.opt` and deliberately not restated. */
.pic-opt { font-family:var(--sans-he); font-size:calc(17px * var(--milim-text-scale));
           text-align:center; direction:rtl; min-height:52px; padding:10px 8px; border-radius:26px;
           display:flex; align-items:center; justify-content:center; }

/* #1760: the drawing redraws itself in green on a right answer -- the one
   place in this app where a stroke draw-on is free, because the art already
   *is* paths. `revealOnRight` (pick_in_cue.js, shared by every judged beat)
   only ever touches the tapped option and the dismiss button; this beat adds
   `.right` to its OWN `.pic` wrapper itself (picture.js's `wire()`), since
   `.pic` is the one ancestor this file can reach that pick_in_cue.js does
   not own and #1760 is scoped away from touching.

   The plate blooms the shared way (`verdictBloom`, declared once in
   `shared/beat_card.css` and referenced by name here exactly as
   `cloze.css`'s `.cloze-option.correct` does) -- 2px `--ok` ring, 6px halo,
   22px outer glow, over `--dur-bloom` (540ms) -- while the art itself tints
   `--ok` and every path draws on from its own measured length to 0 over
   700ms. The length is `--len`, a custom property `picture.js`'s `wire()`
   writes once per path with `getTotalLength()` (inside `try/catch`: jsdom
   has none, so the tests see only the class change); `400` is a fallback
   guess for the one path that measurement failed on, not a value anything
   ships with intentionally. A wrong answer touches none of this -- the art
   does nothing, and the tapped option's own three-pulse ring (`.opt.wrong`,
   `shared/beat_card.css`) is the entire signal, which is deliberate: a
   drawing that shook on a miss would be the "toy app" note the design brief
   forbids. */
.pic.right .pic-plate { background:var(--ok-soft);
  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; }
.pic.right .pic-art { color:var(--ok); }
.pic.right .pic-art path { stroke-dasharray:var(--len,400); stroke-dashoffset:var(--len,400);
  animation:drawOn 700ms var(--ease-out) forwards; }
@keyframes drawOn { to { stroke-dashoffset:0; } }

/* #161: the board -- one Hebrew word, four (or, since #2586, five) drawings
   as the options.

   `.pic-word` is the card's one big line (`.beat-text`, as `מה זה?` is on
   the single card), right-to-left because it is Hebrew. It sits FIRST, above
   the grid, so on a short phone the word is what stays on screen.

   `.pic-board` is the single card's 2-column grid with pictures in the
   cells: the same 8px gap both ways, two `1fr` columns, and a cap narrower
   than `.pic-options`' 22rem -- 20rem (320px) makes each tile ~156px square,
   which with the word, the hint and Continue fits a 393x660 visible area
   with room over (measured on #161's PR). A tile is `aspect-ratio:1`, so
   its HEIGHT follows the TRACK WIDTH, not the row -- unlike `.pic-options`'
   text pills, a third row of tiles needs no `--rows`/`1fr`-row trick to stay
   square and equal to the first two: column width alone decides it, and
   that never changes when Sharp eye rung 3 (#2586) adds a fifth tile in a
   third (single-tile) row.

   Measured at 375px (#2586, a real headless Chrome render of this beat's
   own `html()`, no hand-mirrored markup): the whole card grows from 476.7px
   to 640.7px (+164px, exactly one tile row plus its gap) and no horizontal
   overflow appears. Against #161's own 660px reference visible-stage height
   that leaves ~19px of headroom, down from ~183px at four tiles -- still
   fits, but the margin the four-tile board had is largely gone, and a
   shorter phone's stage will need to scroll (`.stage`'s own `overflow-y:auto`
   already handles that gracefully for other tall cards; nothing here is
   new). Flagged on #2586 as a real, narrow margin rather than a defect.

   `.pic-tile` rides on `.opt` for the same reason `.pic-opt` does (the
   `.correct` green, the `.wrong` ring and its reduced-motion escape arrive
   by construction); it only undoes `.opt`'s text-pill padding and centres
   the drawing. `.pic-tile-art` carries the load-bearing `color:var(--ink)`
   for `currentColor` -- the same rule `.pic-art` states above, restated on
   the board's own class because the single card's `.pic-art` rules also
   carry the draw-on, and on the board that must reach ONE tile, not four (or
   five). */
.pic-word { direction:rtl; margin:0; }
.pic-board { display:grid; grid-template-columns:repeat(6, 1fr); gap:8px; width:100%;
             max-width:20rem; }
/* #3191 (the owner's phone, 2026-10-06: five tiles in two columns are three rows, the fifth alone, and after a right
   answer Continue sits under the bottom bar) and #2852 (level 5 deals SEVEN tiles, four rows in two columns, 804px):
   the board is SIX columns and each tile spans two, so five or more tiles sit THREE across, the same wrap the word
   pills use. 4 tiles are still 2 x 2 (each spanning three); 5 are 3 + 2 with the second row centred (the fourth tile
   starts at column 2); 6 are 3 + 3; 7 are 3 + 3 + 1 with the last one centred (column 3). A tile is `aspect-ratio:1`,
   so the width decides the height: ~101px at 375, ~88px at 320, never under the 44px tap floor, and the board is
   two rows (five, six) or three (seven) instead of three or four. */
.pic-board .pic-tile { grid-column:span 2; }
.pic-board[data-n="2"] .pic-tile, .pic-board[data-n="4"] .pic-tile { grid-column:span 3; }
.pic-board[data-n="5"] .pic-tile:nth-child(4) { grid-column:2 / span 2; }
.pic-board[data-n="7"] .pic-tile:nth-child(7) { grid-column:3 / span 2; }
.pic-tile { aspect-ratio:1; min-height:44px; padding:10px; display:flex;
            align-items:center; justify-content:center; }
.pic-tile-art { color:var(--ink); width:84%; height:84%; display:block;
                pointer-events:none; }
.pic-tile-art svg { width:100%; height:100%; display:block; }
.pic-tile.correct .pic-tile-art { color:var(--ok); }
.pic.right .pic-tile.correct .pic-tile-art path {
  stroke-dasharray:var(--len,400); stroke-dashoffset:var(--len,400);
  animation:drawOn 700ms var(--ease-out) forwards; }

/* #2591, Ear rung 3 ("Sound first"): the board's own copy of `.cuereplay`
   (`picture.js`'s `SOUND_FIRST_REPLAY_HTML`), the same shared control
   `.tense-replay`/`.jumble-replay`/`.spk-replay` already ride on -- see
   `tense.css`'s own comment on its copy for the full account of why a
   second rule is needed at all.

   `[hidden]` needs its own rule for the reason that comment gives:
   `.cuereplay`'s own `display:flex` (`shared/beat_card.css`) is an author
   declaration and outranks the UA stylesheet's `[hidden] { display:none }`
   on ORIGIN regardless of specificity, so without this the button would
   keep painting -- and stay tappable -- while `picture.js` believes it has
   hidden it. `.pic-word[hidden]` needs no equivalent: that paragraph has no
   `display` rule of its own anywhere in this file, so the UA stylesheet's
   own `[hidden]` rule already wins.

   No `margin-top` of any kind, in particular no NEGATIVE one -- #1300's own
   invariant, which `tests/js/test_study_first_line_anchor.mjs` sweeps every
   sheet for: once `#stage` top-anchors its first line, nothing in flow may
   pull a box above the position that anchor gave it, and a negative
   block-start margin is exactly that pull. It is not needed here either:
   `.pic-word[hidden]` renders nothing (the UA `[hidden] { display:none }`
   rule, undisturbed -- see above), so `.pic`'s own `gap:18px` never opens a
   slot for it, and `.pic-replay` sits flush under `.pic`'s own top edge on
   its own, the same as any other flex child that happens to be first.

   `align-self:center` overrides `.cuereplay`'s own `align-self:flex-start`
   -- that default reads right beside a line of TEXT (`.tense-cue-row`'s own
   shape, "the start of the line"), which in `.pic`'s COLUMN flex means the
   left edge of the card rather than under the (hidden) word. Measured in a
   real headless-Chrome render (#2591's own scratch probe) before this
   override: the glyph sat pinned to the top-left corner of the card,
   nowhere near the word it replays. */
.pic-replay { align-self:center; }
.pic-replay[hidden] { display:none; }

/* #1760: this file's own `prefers-reduced-motion` escape --
   `tests/test_reduced_motion_escape.py` asks the question per FILE, so a
   sheet loaded on its own page cannot borrow a sibling's. The bloom parks
   the same way `.opt.correct`'s does (`animation:none`, never a shortened
   duration -- the ring/halo/glow stay, declared on the base rule above, not
   inside `@keyframes verdictBloom`). The draw-on is different in kind, not
   just parked: `animation:none` alone would leave every path sitting at its
   own full `--len` offset -- INVISIBLE, since `stroke-dashoffset` at the
   path's own length is an empty line -- so `stroke-dashoffset:0` is
   restated here to land the drawing fully present at t=0, exactly the
   ticket's own done-when. */
@media (prefers-reduced-motion: reduce) {
  .pic-plate { transition:none; }
  .pic.right .pic-plate { animation:none; }
  .pic.right .pic-art path { animation:none; stroke-dashoffset:0; }
  .pic.right .pic-tile.correct .pic-tile-art path { animation:none; stroke-dashoffset:0; }
}

/* #2898, the isometric word pictures go live (README: docs/demos/2026-09-30-iso-pictures,
   section 5, the owner's answer "right/wrong: A", 2026-09-30).

   A baked iso picture (`app/static/pictures/iso/<slug>.svg`, `class="iso-pic"`) is a
   FILLED drawing in its own colours, so the three flat-line rules above do the wrong
   thing to it: the draw-on would re-draw the ink over a finished fill, and the green
   tint has nothing to tint. Both are switched off for it, and the verdict is carried by
   the plinth instead of a glow round the outline.

   RIGHT: the plinth turns green, a check badge sits where the path puts its padlock, and
   the object pops. WRONG: the plinth turns amber, a cross badge appears, and the object
   greys. Three surfaces, one rule each side:
     right = `.pic.right .pic-art` (the card), `.pic-tile.correct .pic-tile-art` (the
             board, which also lights the real answer after a miss), and
             `.warmup-card-pic.matched .warmup-card-face` (Flip and match);
     wrong = the card's art when a `.pic-opt` was marked wrong, `.pic-tile.wrong
             .pic-tile-art`, and `.warmup-card-pic.wrong .warmup-card-face`.

   THE GREY IS ON THE OUTER <svg>, NEVER ON AN INNER ELEMENT. WebKit ignores a CSS filter
   on an element inside an SVG (the path's locked-node rule in study.css says the same),
   so a filter on `.iso-obj` would grey on Chrome and not on an iPhone. On the outer
   <svg> it works on both. The price is that the plinth is greyed with the object; the
   amber is picked vivid enough to still read, and the cross badge (HTML, outside the
   filter) says "wrong" on its own. The pop is a transform on `.iso-obj`, which WebKit
   does honour.

   THE BADGE is a pair of pseudo-elements, no script and no markup: a box exactly as big
   as the drawing's `viewBox="30 2 140 168"` area (height:100%, aspect-ratio 140/168,
   centred, which is what `preserveAspectRatio` "meet" gives the <svg> in any container),
   so the circle at (150,34) r15 -- the path's padlock position -- lands on the same spot
   on a square card, a square board tile and Flip and match's wide tile alike. `::before`
   is the disc (a mask painted in the verdict colour), `::after` is the white glyph. Only
   a container that holds an `.iso-pic` gets one (`:has`); a flat drawing never does. */
:root {
  --iso-g-t:#4FD488; --iso-g-l:#2FA263; --iso-g-r:#217A49; --iso-g-ink:#14432A;
  --iso-a-l:#E39A12;
}
@media (prefers-color-scheme: light) {
  :root {
    --iso-g-t:#3FB877; --iso-g-l:#248F58; --iso-g-r:#1B6F44; --iso-g-ink:#0F4A2B;
    --iso-a-l:#DD8C0A;
  }
}
:root[data-theme="dark"] {
  --iso-g-t:#4FD488; --iso-g-l:#2FA263; --iso-g-r:#217A49; --iso-g-ink:#14432A;
  --iso-a-l:#E39A12;
}
:root[data-theme="light"] {
  --iso-g-t:#3FB877; --iso-g-l:#248F58; --iso-g-r:#1B6F44; --iso-g-ink:#0F4A2B;
  --iso-a-l:#DD8C0A;
}

/* The card's plate keeps its own bloom for a flat drawing; for an iso picture the plinth
   is the verdict, so the green ring/halo/glow round the plate goes (the owner asked for
   the glow on the drawing to become something else). */
.pic.right .pic-plate:has(.iso-pic) { background:var(--raise); box-shadow:var(--shadow);
  animation:none; }
/* Switch off the flat draw-on for filled art, on both surfaces that carry it. */
.pic.right .pic-art svg.iso-pic path, .pic.right .pic-tile.correct .pic-tile-art svg.iso-pic path {
  animation:none; stroke-dasharray:none; stroke-dashoffset:0; }

.pic-art, .pic-tile-art { position:relative; }
.iso-pic { overflow:visible; }
.iso-pic .pl-t, .iso-pic .pl-l, .iso-pic .pl-r, .iso-pic .pl-shadow {
  transition:fill .28s, stroke .28s, opacity .28s; }

/* The plinth, green. */
:is(.pic.right .pic-art, .pic-tile.correct .pic-tile-art, .warmup-card-pic.matched .warmup-card-face) .iso-pic .pl-t { fill:var(--iso-g-t); stroke:var(--iso-g-ink); }
:is(.pic.right .pic-art, .pic-tile.correct .pic-tile-art, .warmup-card-pic.matched .warmup-card-face) .iso-pic .pl-l { fill:var(--iso-g-l); stroke:var(--iso-g-ink); }
:is(.pic.right .pic-art, .pic-tile.correct .pic-tile-art, .warmup-card-pic.matched .warmup-card-face) .iso-pic .pl-r { fill:var(--iso-g-r); stroke:var(--iso-g-ink); }
:is(.pic.right .pic-art, .pic-tile.correct .pic-tile-art, .warmup-card-pic.matched .warmup-card-face) .iso-pic .pl-shadow { fill:var(--iso-g-l); opacity:.7; }
/* The plinth, amber. NOT recoloured inside the <svg>: the grey filter below sits on the outer
   <svg> and would wash the amber to tan (measured: grayscale(.8) leaves a beige slab, even from
   #FF8F00). So the svg's own plinth faces are cleared and the amber plinth is painted UNDER the
   svg as the container's background -- the same three polygons the bake writes (identical in all
   eight files), in the same `viewBox="30 2 140 168"`, fitted with `contain` so it lands on the
   drawing's own plinth on any container (the card's square, the board's tile, Flip and match's
   wide, padded face -- `background-origin:content-box` there). The object stays above it and
   takes the grey; the plinth does not. (`--no`, the app's red, is a literal swap in the data URI
   if it reads better than amber.) */
:is(.pic:has(.pic-opt.wrong) .pic-art, .pic-tile.wrong .pic-tile-art, .warmup-card-pic.wrong .warmup-card-face) .iso-pic :is(.pl-t, .pl-l, .pl-r) { fill:none; stroke:none; }
:is(.pic:has(.pic-opt.wrong) .pic-art, .pic-tile.wrong .pic-tile-art, .warmup-card-pic.wrong .warmup-card-face):has(.iso-pic) {
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='30 2 140 168' stroke-linejoin='round' stroke-width='1.1'%3E%3Cpolygon points='34.2,128 100,166 100,150 34.2,112' fill='%23D8890F' stroke='%235A3A08'/%3E%3Cpolygon points='165.8,128 100,166 100,150 165.8,112' fill='%23B06A05' stroke='%235A3A08'/%3E%3Cpolygon points='100,74 165.8,112 100,150 34.2,112' fill='%23FFB52E' stroke='%235A3A08'/%3E%3C/svg%3E"), none;
  background-repeat:no-repeat; background-position:center; background-size:contain;
  /* two layers so the face's own colour (the second, `none` image) keeps its border-box paint */
  background-origin:content-box, padding-box; background-clip:content-box, border-box; }
/* The object greys: the OUTER <svg> (see above), never an inner element. */
:is(.pic:has(.pic-opt.wrong) .pic-art, .pic-tile.wrong .pic-tile-art, .warmup-card-pic.wrong .warmup-card-face) svg.iso-pic { filter:grayscale(.8) brightness(.96); }
/* The object pops, from its base. */
:is(.pic.right .pic-art, .pic-tile.correct .pic-tile-art, .warmup-card-pic.matched .warmup-card-face) .iso-pic .iso-obj {
  transform-box:fill-box; transform-origin:50% 100%;
  animation:isoPop .38s cubic-bezier(.2,1.5,.4,1) 1; }
@keyframes isoPop { 0% { transform:scale(1); } 40% { transform:scale(1.08); } 100% { transform:scale(1); } }

/* The badge: geometry shared by both pseudo-elements, only where an iso picture is. */
:is(.pic.right .pic-art, .pic-tile.correct .pic-tile-art, .pic:has(.pic-opt.wrong) .pic-art, .pic-tile.wrong .pic-tile-art):has(.iso-pic)::before,
:is(.pic.right .pic-art, .pic-tile.correct .pic-tile-art, .pic:has(.pic-opt.wrong) .pic-art, .pic-tile.wrong .pic-tile-art):has(.iso-pic)::after {
  content:""; position:absolute; top:0; left:50%; height:100%; aspect-ratio:140 / 168;
  translate:-50% 0; pointer-events:none; transform-origin:85.7% 19%; }
:is(.warmup-card-pic.matched, .warmup-card-pic.wrong) .warmup-card-face:has(.iso-pic)::before,
:is(.warmup-card-pic.matched, .warmup-card-pic.wrong) .warmup-card-face:has(.iso-pic)::after {
  content:""; position:absolute; top:8px; left:50%; height:calc(100% - 16px); aspect-ratio:140 / 168;
  translate:-50% 0; pointer-events:none; transform-origin:85.7% 19%; }
/* Only the RIGHT badge springs in. A wrong answer's motion is the shared pulse in
   shared/beat_card.css (#1586), never a private animation here, so the cross simply appears. */
.pic.right .pic-art:has(.iso-pic)::before, .pic.right .pic-art:has(.iso-pic)::after,
.pic-tile.correct .pic-tile-art:has(.iso-pic)::before, .pic-tile.correct .pic-tile-art:has(.iso-pic)::after,
.warmup-card-pic.matched .warmup-card-face:has(.iso-pic)::before, .warmup-card-pic.matched .warmup-card-face:has(.iso-pic)::after {
  animation:isoBadge .34s cubic-bezier(.2,1.6,.4,1) 1; }
@keyframes isoBadge { 0% { scale:0; } 70% { scale:1.18; } 100% { scale:1; } }
/* ::before = the disc, a mask of one circle painted in the verdict colour. */
:is(.pic.right .pic-art, .pic-tile.correct .pic-tile-art, .warmup-card-pic.matched .warmup-card-face):has(.iso-pic)::before { background:var(--iso-g-l); }
:is(.pic:has(.pic-opt.wrong) .pic-art, .pic-tile.wrong .pic-tile-art, .warmup-card-pic.wrong .warmup-card-face):has(.iso-pic)::before { background:var(--iso-a-l); }
:is(.pic.right .pic-art, .pic-tile.correct .pic-tile-art, .pic:has(.pic-opt.wrong) .pic-art, .pic-tile.wrong .pic-tile-art, .warmup-card-pic:is(.matched, .wrong) .warmup-card-face):has(.iso-pic)::before {
  -webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='30 2 140 168'%3E%3Ccircle cx='150' cy='34' r='15'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
  mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='30 2 140 168'%3E%3Ccircle cx='150' cy='34' r='15'/%3E%3C/svg%3E") center / 100% 100% no-repeat; }
/* ::after = the glyph, white: a check on a right answer, a cross on a wrong one. */
:is(.pic.right .pic-art, .pic-tile.correct .pic-tile-art, .warmup-card-pic.matched .warmup-card-face):has(.iso-pic)::after {
  background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='30 2 140 168'%3E%3Cpath transform='translate(150 34)' d='M-6,0.5 l4.5,4.5 l8.5,-9.5' fill='none' stroke='%23fff' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat; }
:is(.pic:has(.pic-opt.wrong) .pic-art, .pic-tile.wrong .pic-tile-art, .warmup-card-pic.wrong .warmup-card-face):has(.iso-pic)::after {
  background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='30 2 140 168'%3E%3Cpath transform='translate(150 34)' d='M-5,-5 l10,10 M5,-5 l-10,10' fill='none' stroke='%23fff' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat; }

/* Reduced motion drops the pop (and the badge's entrance); the colours and the badge
   are still there at once. */
@media (prefers-reduced-motion: reduce) {
  .iso-pic .pl-t, .iso-pic .pl-l, .iso-pic .pl-r, .iso-pic .pl-shadow { transition:none; }
  .iso-pic .iso-obj { animation:none !important; }
  .pic-art::before, .pic-art::after, .pic-tile-art::before, .pic-tile-art::after,
  .warmup-card-face::before, .warmup-card-face::after { animation:none !important; }
}
