:root{
  --page:#141310; --card:#1e1c18; --line:#33302a; --ink:#f4f1ea; --ink2:#cfc9bd;
  --ink3:#8f897d; --brand:#e8b53a; --brand-ink:#20180a; --coral:#EF5A3C; --coral-ink:#B23218;
  /* Positive moments render GREEN to match the app (app green #13713F); these are
     its dark-theme adaptation — a light green accent + green-tinted chip fill. */
  --pos:#54d98c; --pos-bg:#16301f; --pos-line:#255238;
  /* Coral-tinted counterpart of the --pos triplet, for cautionary notices. */
  --warn:#f5a08c; --warn-bg:#301a16; --warn-line:#5c2f24;
}
*{box-sizing:border-box}
[hidden]{display:none!important}
html,body{margin:0;background:var(--page);color:var(--ink);
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  -webkit-font-smoothing:antialiased}
#app{max-width:520px;margin:0 auto;min-height:100dvh;display:flex;flex-direction:column;
  padding:max(12px,env(safe-area-inset-top)) 14px max(16px,env(safe-area-inset-bottom))}

/* state */
.state{margin:auto;text-align:center;color:var(--ink2);display:flex;flex-direction:column;
  align-items:center;gap:14px;padding:40px 8px}
/* The `gap` above provides the rhythm; the browser's default paragraph margins
   were stacking on top of it. */
.state p{margin:0}
.spinner{width:34px;height:34px;border-radius:50%;border:3px solid #ffffff22;
  border-top-color:var(--brand);animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* viewer */
#viewer{display:flex;flex-direction:column;gap:11px}

/* orientation header — brand wordmark + who the feedback is from */
.vhdr{display:flex;flex-direction:column;gap:2px;padding:2px 2px 0}
.vhdr .wordmark{font-size:10.5px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--brand)}
.vhdr .vctx{font-size:14px;font-weight:600;color:var(--ink)}
.vhdr .vctx b{color:var(--brand)}
/* .stagewrap only exists on the authoring surface (review.html) — it's what
   lets the tool rails stand outside .stage's own clipped box. See the markup
   comment in review.html. The read-only viewer (index.html) has no rails and
   skips the wrapper entirely; .stage there is still a direct flex child of
   #viewer, which is why it keeps `align-self:center` — a plain block child of
   .stagewrap doesn't need it (`margin:0 auto` centers it there instead), but
   .stage is shared markup between both surfaces and align-self on a non-flex
   parent is simply inert, not wrong, so one rule serves both. */
.stagewrap{position:relative;width:100%}
.stage{position:relative;width:100%;background:#0d0c0a;border-radius:14px;overflow:hidden;
  align-self:center;margin:0 auto;box-shadow:0 8px 30px #0007;touch-action:manipulation}
.stage video{display:block;width:100%;height:100%;background:#0d0c0a}

/* ⤢ frame-size toggle — authoring-only. Sits ON the frame (not the transport)
   because it's the frame's own ceiling it's changing. z-index:3 clears both
   #draw (no z-index, but sibling order alone would do it) AND #objects.editing
   (z-index:2 — see that rule below), so it stays reachable and on top
   regardless of which is active when it's tapped. */
.stagebtn{position:absolute;top:8px;right:8px;z-index:3;width:30px;height:30px;border-radius:8px;
  background:#00000066;border:1px solid #ffffff33;color:#fff;font-size:15px;cursor:pointer;
  display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px)}
.stagebtn:active{background:#000000aa}

/* Buffering spinner — a later sibling of the video inside .stage (see
   `createBufferSpinner` in player.js, which builds this rather than hand-
   duplicating it into both index.html and review.html). Shown by default
   (there's no frame to see until the first one decodes) and toggled off
   `playing`/`canplay`, back on for `waiting`/a seek into unbuffered territory
   — replacing what used to just be the stage's own black background with
   nothing on it, which read as broken rather than loading. `pointer-events:
   none` so it never intercepts a tap meant for the rail or the frame
   underneath once content IS there but a later stall re-shows it. */
.stagespin{position:absolute;inset:0;z-index:2;display:flex;align-items:center;
  justify-content:center;pointer-events:none}
.stagespin .spinner{width:30px;height:30px}
#overlay{position:absolute;inset:0;pointer-events:none;
  opacity:0;transition:opacity .4s ease}
#overlay.show{opacity:1}
.objects{position:absolute;inset:0;pointer-events:none;
  opacity:0;transition:opacity .4s ease}
.objects.show{opacity:1}
/* `width:max-content` is load-bearing, not tidying. These are absolutely
   positioned with `left` set and `width:auto`, which shrink-to-fits into the
   space from `left` to the layer's RIGHT EDGE — the centring `translate(-50%)`
   is a paint-time transform and does not feed back into that. So a wrapping
   plate silently got narrower the further right it was placed: at x=0.85 the
   same note came out 40px wide and six clipped lines tall, while at x=0.2 it
   was 212px and fitted. Content-driven width bounded by `max-width` below makes
   the box the same size wherever it sits, which is also what the Flutter side
   does (`Align` hands the plate the whole frame to measure against). */
.objects .obj{position:absolute;transform:translate(-50%,-50%);width:max-content;
  filter:drop-shadow(0 1px 3px #000a)}
/* A text note on the frame. It WRAPS. It used to be `white-space:nowrap` with a
   `max-width:80%`, which is a contradiction: the max-width bounded the white
   plate while nowrap kept the line at its full single-line length, so a note
   past ~25 characters ran its coral text straight out of the plate and across
   the dancer. Three bounds now, in order of which one you want to hit:
     1. it wraps, so the box grows down instead of overflowing sideways;
     2. `plateFontSize()` in player.js shrinks the type as the note lengthens,
        so those extra lines cost less of the frame (and stay legible);
     3. max-height is the backstop the first two are meant to keep you off — at
        the authoring cap the ladder can now exceed it (200 chars, raised from
        120 once this stopped being a dead end — see .plate.expanded below).
        While composing it becomes a scroll instead of a clip (see
        .compose .obj.composing).
   The ceiling is FOUR LINES, expressed in em so it means the same thing at every
   step of the ladder — `4 × 1.25 line-height`, plus the 9px of padding+border
   that `box-sizing:border-box` folds in. Four because that is what Flutter's
   `PlacedTextMetrics.maxLines` says, and a note written here replays there.
   No font-size here: player.js sets it per note. 14px is the ladder's top step,
   kept as the value a plate rendered without it would have had.

   `.plate` itself is a NON-clipping wrapper — only `.platebody` (its one
   child, built by placedObjectEl in player.js) carries the visual chrome and
   the `overflow:hidden` clip. That split exists solely so the two corner
   badges below can hang OFF the label's edge instead of overlapping its text:
   both badges are positioned children of `.plate`, and a badge positioned
   outside `.platebody`'s box would previously get cut off by that same
   `overflow:hidden` if it lived on the clipped element itself. `max-width`
   stays on `.plate` (not `.platebody`) deliberately — `.plate` is the element
   actually doing the `width:max-content` shrink-to-fit (inherited from
   `.objects .obj` above), so it's the one whose containing block is the
   FRAME, not another shrink-to-fit box. Putting the 80% on `.platebody`
   instead would resolve it against `.plate`'s own width, which is what
   `.platebody`'s `width:100%` is simultaneously trying to fill — a
   containing-block cycle browsers break by ignoring the percentage entirely
   during that sizing pass, silently reopening the pre-wrap bug the comment
   above this describes. `.platebody` just fills whatever width `.plate`
   settles on.

   The visual chrome (background/border/padding/type) is written once as a
   selector LIST shared with `.compose .obj.composing` further down — the
   in-progress editor is a raw `.obj.plate` with no `.platebody` inside it
   (it's the contentEditable itself, not a wrapper around one; see that rule's
   own comment for why), so without this list it would have silently gone
   back to plain unstyled text the moment `.objects .plate` stopped carrying
   that chrome directly. */
.objects .plate{cursor:pointer;max-width:80%}
.objects .plate .platebody,
.compose .obj.composing{background:#fff;color:var(--coral-ink);
  border:1.5px solid var(--coral-ink);border-radius:7px;padding:3px 8px;font-weight:800;
  font-size:14px;line-height:1.25;text-align:center;white-space:pre-wrap;overflow-wrap:anywhere;
  box-shadow:0 1px 4px #0009}
.objects .plate .platebody{display:block;width:100%;max-height:calc(5em + 9px);overflow:hidden}
/* Tap toggles this — see the click handler in placedObjectEl (player.js) and
   the tap-vs-drag split in makeDraggable (review.js). Lifts only the ceiling;
   the wrap and the shrinking type (the two bounds that keep a plate compact in
   the first place) are untouched, so an expanded plate still reads as the
   same label, just all of it. Targets `.platebody` now, not `.plate` itself
   — that's the element the ceiling actually lives on. */
.objects .plate.expanded .platebody{max-height:none;overflow:visible}
/* The "there's more" affordance a clipped plate had none of — `cursor:pointer`
   is invisible on a touchscreen, so without this a truncated note just looked
   like a broken hard crop instead of a deliberate, tappable one. `.clipped` is
   set in JS (renderGroup, player.js) off a real scrollHeight/clientHeight
   comparison against `.platebody` (the clipped element), so the badge only
   ever shows on a plate that is ACTUALLY cut off — never on a short note that
   merely wraps to a couple of lines. A pseudo-element of `.plate` (the
   non-clipping wrapper), hung half off the label's own bottom edge rather
   than sitting inside its box — that's what stops it from overlapping the
   last visible line of text the way the old inside-the-clip badge did. Dark
   and blurred like `.stagebtn`/the other boxed icons on this same frame,
   which reads better hanging over the video than the old solid-coral pill did
   (and doesn't compete with the plate's own coral/white palette). Hidden
   again once `.expanded`, matching the ceiling it's paired with above. */
.objects .plate.clipped:not(.expanded)::after{content:'⌄';position:absolute;
  left:50%;bottom:-9px;transform:translateX(-50%);background:#00000099;
  border:1px solid #ffffff40;backdrop-filter:blur(2px);color:#fff;font-size:11px;
  font-weight:800;line-height:1;padding:2px 8px;border-radius:999px}
/* The other half of that pair — expanding a plate had no way back except
   knowing to tap it again, with nothing on screen saying so. Same badge, same
   spot, opposite glyph: `.clipped` stays true after expanding (nothing
   re-measures it until the next repaint, see the click handler in
   placedObjectEl), so this only ever shows on a plate that actually grew —
   never on a short note a stray tap happened to toggle `.expanded` on with no
   visible effect. */
.objects .plate.clipped.expanded::after{content:'⌃';position:absolute;
  left:50%;bottom:-9px;transform:translateX(-50%);background:#00000099;
  border:1px solid #ffffff40;backdrop-filter:blur(2px);color:#fff;font-size:11px;
  font-weight:800;line-height:1;padding:2px 8px;border-radius:999px}
/* The corner badge that reopens an already-placed note for editing —
   authoring-only, appended in review.js's `buildObject` (not the shared
   `placedObjectEl`, so the read-only viewer never gets one). Hung off the
   top-right corner of `.plate` (the non-clipping wrapper — see above), centred
   on the corner point rather than inset inside it, so it sits over the video
   background beside the label instead of on top of its first line. Same dark
   boxed-icon treatment as `.stagebtn`: a solid coral circle here read as one
   more competing colour block next to the coral text/border it was overlapping. */
.objects .plate .plateedit{position:absolute;top:-9px;right:-9px;width:18px;height:18px;
  border-radius:50%;background:#00000099;border:1px solid #ffffff40;backdrop-filter:blur(2px);
  color:#fff;padding:0;font-size:9px;line-height:1;display:flex;align-items:center;
  justify-content:center;cursor:pointer}
.objects .plate .plateedit:active{background:#000000cc}

/* ── transport ────────────────────────────────────────────────────────────
   A row BELOW the stage, not an overlay on it. It used to be an
   absolutely-positioned bar inside .stage, which cost two things:

   - On the authoring surface the bottom band of the frame was not drawable. The
     bar is a later sibling than #draw with no z-index on either, so it won above
     it in both paint and hit order and swallowed every pointerdown there — and
     on a portrait dance clip that band is the feet.
   - It put chrome on top of the dance. These controls are primary tools here,
     not chrome to be minimized out of the way, so they get their own row and
     stay legible instead of fading over the frame.

   There is also no centre play button any more. It lived exactly where someone
   wants to draw, and it toggled on two independent conditions (paused, and
   whether a tool was armed), which read as flicker. One play/pause, bottom left.

   TWO rows, not one: play + readout on top, the scrubber spanning the full width
   underneath (it wraps there via `flex:1 0 100%`, so the markup order is
   unchanged). Sharing a row cost the scrubber everything the other controls
   took — on a portrait clip the bar came out around 90px wide, and the moment
   markers are 11px each, so a few notes seconds apart merged into one blob. The
   markers ARE the point of this bar; their separability scales with its width.

   For the same reason the row is no longer sized to the frame: fitStage() used
   to pin it to the stage's width so the scrubber's ends lined up with the
   video's edges, but a portrait clip is height-capped to roughly half the
   column, which threw away half the timeline's resolution to buy an alignment.
   Full column width, centred under the frame. */
.transport{display:flex;flex-wrap:wrap;align-items:center;column-gap:11px;row-gap:3px;
  width:100%;align-self:center;padding:1px 0}
.ctl{background:none;border:none;color:var(--ink);cursor:pointer;padding:0;
  width:34px;height:34px;flex:0 0 auto;border-radius:50%;
  display:flex;align-items:center;justify-content:center;transition:background .12s ease}
.ctl:active{background:#ffffff1a}
.ctl svg{width:24px;height:24px;display:block}

/* ±1s step buttons — authoring-only (index.html's read-only viewer has no
   #stepBack/#stepFwd, so player.js just never wires these). A pill, not a
   round icon button like play/pause, since they carry text. */
.ctl.step{width:auto;height:28px;border-radius:8px;padding:0 8px;font-size:12px;
  font-weight:700;color:var(--ink2);background:var(--card);border:1px solid var(--line);
  font-variant-numeric:tabular-nums}
.ctl.step:active{background:var(--brand);border-color:var(--brand);color:var(--brand-ink)}

/* Elapsed / total. Tabular figures so the row doesn't jitter as digits change,
   and the elapsed half is the emphasized one — it's the value you read.
   `margin-left:auto` parks it at the far end of the top row, opposite play —
   the layout every video player has trained people on, and it uses the space
   the scrubber vacated instead of leaving it dead. */
.time{flex:0 0 auto;margin-left:auto;font-size:12px;color:var(--ink3);
  font-variant-numeric:tabular-nums}
.time b{color:var(--ink);font-weight:700}
/* Hundredths (paused-only, see fmtClockTime in player.js) rendered smaller and
   dimmer than the whole-second readout it's attached to — otherwise `0:12.00`
   sits right next to a bare `1:24` and the mismatched precision reads as a
   typo rather than the deliberate step-precision bump it is. */
.time .frac{font-size:.75em;opacity:.65;font-weight:400}

/* scrubber — its own full-width row (see .transport). Taller than it looks: the
   30px box is the touch target, the visible track is the 3px ::before inside it. */
.progress{position:relative;flex:1 0 100%;height:30px;display:flex;align-items:center;
  cursor:pointer;touch-action:none}
/* The track spans the full element. It used to be inset `left:13px;right:13px`,
   stale coupling to the old overlay bar's horizontal padding — which left the
   grey track misaligned with the coral fill and with the moment markers, both of
   which are positioned from 0. */
.progress::before{content:"";position:absolute;left:0;right:0;height:3px;border-radius:2px;background:#ffffff33}
.progress i{position:relative;display:block;height:3px;border-radius:2px;background:var(--brand);width:0%}
/* Playhead knob. Anchored to the fill's right edge, so it tracks the playhead
   for free with no second position to keep in sync. Deliberately --ink (white)
   rather than the brand colour: the green/coral moment markers share this track,
   and the one thing that must never be mistaken for a marker is the playhead.
   The z-index lifts it above those markers, which are a later sibling. */
.progress i::after{content:"";position:absolute;right:0;top:50%;z-index:2;
  width:13px;height:13px;margin-right:-6.5px;border-radius:50%;background:var(--ink);
  transform:translateY(-50%);box-shadow:0 1px 4px #000b;transition:transform .12s ease}
.progress.scrubbing i::after{transform:translateY(-50%) scale(1.3)}
/* Moment markers on the scrubber — same coordinate origin as the fill (full
   width, 0% = left edge) so a marker sits where the playhead reaches for that
   time. Constructive notes use the coral accent, matching the take pills. */
.progress .markers{position:absolute;left:0;right:0;top:0;bottom:0;pointer-events:none}
.progress .marker{position:absolute;top:50%;width:11px;height:11px;padding:0;border-radius:50%;
  background:var(--pos);border:2px solid var(--page);transform:translate(-50%,-50%);
  box-shadow:0 1px 3px #000a;pointer-events:auto;cursor:pointer;transition:transform .15s ease}
.progress .marker.con{background:#ff8f73}
.progress .marker.active{transform:translate(-50%,-50%) scale(1.4)}

/* moment navigator (chip strip above the take) */
.moments .mhd{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:0 2px 8px}
.moments .mlabel{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--ink3)}
.moments .mnav{display:flex;align-items:center;gap:6px}
.moments .mcount{font-size:11px;color:var(--ink3);font-variant-numeric:tabular-nums;min-width:34px;text-align:center}
.mstep{width:28px;height:28px;border-radius:50%;border:1px solid var(--line);background:var(--card);
  color:var(--ink);font-size:17px;line-height:1;cursor:pointer;padding:0;
  display:flex;align-items:center;justify-content:center;transition:opacity .15s ease,background .12s ease}
.mstep:disabled{opacity:.3;cursor:default}
.mstep:not(:disabled):active{background:var(--brand);border-color:var(--brand);color:var(--brand-ink)}
/* padding leaves room for the active chip's outline (2px + 1px offset), which
   the horizontal scroll's implicit overflow-y would otherwise clip at the top. */
.mchips{display:flex;gap:8px;overflow-x:auto;padding:4px 3px;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.mchips::-webkit-scrollbar{display:none}
.mchip{flex:0 0 auto;display:inline-flex;align-items:center;gap:6px;background:var(--pos-bg);color:var(--pos);
  border:1px solid var(--pos-line);border-radius:20px;padding:7px 12px;font:inherit;font-size:13px;font-weight:700;
  cursor:pointer;white-space:nowrap}
.mchip.con{background:#3a201a;color:#ff8f73;border-color:#5c2f24}
.mchip .mt{opacity:.65;font-variant-numeric:tabular-nums}
.mchip .mn{max-width:180px;overflow:hidden;text-overflow:ellipsis;font-weight:500;opacity:.9}
.mchip.active{outline:2px solid currentColor;outline-offset:1px}

/* take */
.take{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:13px 14px}
.take .who{display:flex;align-items:center;gap:8px;margin-bottom:7px}
.take .who .av{width:24px;height:24px;border-radius:50%;background:var(--brand);color:var(--brand-ink);
  font-weight:800;font-size:12px;display:flex;align-items:center;justify-content:center}
.take .who b{font-size:13px}
.take .prose{font-size:14px;line-height:1.55;color:var(--ink2)}
.refpill{display:inline-flex;align-items:center;gap:3px;background:var(--pos-bg);color:var(--pos);
  border:1px solid var(--pos-line);border-radius:20px;padding:0 7px;font-size:12px;font-weight:700;
  cursor:pointer;white-space:nowrap}
.refpill.con{background:#3a201a;color:#ff8f73;border-color:#5c2f24}

/* cta */
.cta{display:flex;align-items:center;justify-content:center;
  text-decoration:none;background:var(--brand);color:var(--brand-ink);font-weight:800;font-size:15px;
  border-radius:14px;padding:15px;box-shadow:0 4px 14px #e8b53a44}

/* value prop — one line telling first-time viewers what the app is, with an
   inline "See more" link to the marketing site (sits just above the CTA). */
.valueprop{text-align:center;font-size:12px;line-height:1.5;color:var(--ink2);margin:0;padding:0 4px}
.valueprop b{color:var(--ink)}
.valueprop a{color:var(--brand);font-weight:700;text-decoration:underline;text-underline-offset:2px;white-space:nowrap}

/* expiry note — muted footer treatment, pinned to the very bottom. Informs
   without alarming (shown only for a claimable review subject). */
.expiry{text-align:center;color:var(--ink3);font-size:11.5px;line-height:1.4;margin:2px 0 0;padding:0 10px}

/* ── collection viewer (collection.html / collection.js) ──────────────────
   A public collection is a roster grid + tap-to-open, sharing the tokens and
   .cta/.wordmark/.valueprop styles above. A tile is a real link to that
   move's own `/m/<clipId>` page — playback lives there, not in this file. */
.colhero{padding:2px 2px 0}
.colhero h1{margin:0;font-size:22px;line-height:1.2;font-weight:800;color:var(--ink)}
.colhero .colmeta{margin:6px 0 0;font-size:12.5px;color:var(--ink3);font-weight:600}
.colhero .colmeta .save{color:var(--brand)}
.colhero .coldesc{margin:8px 0 0;font-size:13.5px;line-height:1.5;color:var(--ink2)}
.rlabel{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--ink3);margin:4px 2px -2px}
/* roster grid: two-up poster tiles, portrait-leaning (dance clips are usually
   vertical), tap to open. */
.roster{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.tile{position:relative;display:block;border:none;padding:0;margin:0;cursor:pointer;
  background:#0d0c0a;border-radius:12px;overflow:hidden;aspect-ratio:3/4;text-align:left;
  text-decoration:none;color:inherit;box-shadow:0 4px 16px #0006}
.tile img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#0d0c0a}
.tile .badge{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:44px;height:44px;border-radius:50%;background:#000000aa;color:#fff;
  display:flex;align-items:center;justify-content:center}
.tile .badge svg{width:22px;height:22px;margin-left:2px}
.tile .yt{position:absolute;right:7px;top:7px;font-size:9.5px;font-weight:800;letter-spacing:.04em;
  text-transform:uppercase;color:#fff;background:#0009;border-radius:5px;padding:2px 5px}
.tile .tt{position:absolute;left:0;right:0;bottom:0;padding:16px 9px 8px;font-size:12px;
  font-weight:700;color:#fff;line-height:1.25;
  background:linear-gradient(transparent,#000000d8);
  overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}

/* ── review authoring (review.html / review.js) ───────────────────────────
   The inverse of the read-only viewer above: same stage, same renderer, but a
   capture layer and a tool rail on top. Reuses .stage/.transport/.mchips/.cta
   verbatim — only what's genuinely new is defined here. */

/* The same column treatment #viewer gets. Its absence was drift, not a choice:
   the authoring surface has strictly MORE stacked parts than the viewer (rail,
   hint, take box, send) and every one of them was flush against the next, which
   read as one undifferentiated slab and made the hint look like a caption of the
   tool rail rather than a line of its own. */
#review{display:flex;flex-direction:column;gap:11px}
/* The status region is an always-present live region (see review.html), so it is
   usually an empty div — which would otherwise still spend a gap on each side. */
#review > #sendStatus:empty{display:none}

/* ── send status ──────────────────────────────────────────────────────────
   The second message channel, and the whole reason .toolhint could go back to
   being a hint. Everything CONSEQUENTIAL — validation, sign-in progress, and
   every recoverable send failure — lands here instead: 14px rather than 11.5,
   in a bordered card rather than centred grey, and sitting directly above the
   button that produced it rather than three elements upstream of it.

   Two things that look cosmetic and are not:
   - It is a sibling of #send, not of the tool rail, so `setTool()` can no longer
     clobber it. A reviewer's first instinct after a failure is to reach for a
     tool, and that used to erase the explanation mid-read.
   - The `.ic` glyph is what carries severity to someone who doesn't parse
     colour; the palette alone was doing that job. */
.status{display:flex;align-items:flex-start;gap:9px;border-radius:11px;padding:11px 12px;
  font-size:14px;line-height:1.4;background:var(--warn-bg);border:1px solid var(--warn-line);color:var(--warn)}
.status .ic{flex:0 0 auto;line-height:1.35}
/* Guidance rather than failure — "add a summary", "ready, tap again". Same size
   and position, neutral palette, no warning glyph. */
.status.info{background:var(--card);border-color:var(--line);color:var(--ink2)}
.status b{color:var(--ink)}

/* Who the review will be signed as. Best-effort and quiet by design: it only
   appears once the auth SDK happens to be warm AND a session is already
   restored, because the thesis of this page is that no account stands between
   arriving and drawing. But when we DO know, staying silent is worse — a shared
   phone otherwise attributes someone's work to whoever signed in last, with the
   page's own copy ("so they know who it's from") never saying who. */
.identity{margin:0;padding:0 4px;text-align:center;font-size:12.5px;line-height:1.4;color:var(--ink3)}
.identity b{color:var(--ink2);font-weight:700}

/* Post-send checkmark */
.donemark{width:56px;height:56px;border-radius:50%;background:var(--pos-bg);color:var(--pos);
  border:2px solid var(--pos-line);font-size:28px;font-weight:800;
  display:flex;align-items:center;justify-content:center}
/* For an outcome that ENDED the attempt without delivering anything: already
   reviewed, link closed, video gone. The green mark is reserved for a review
   that actually landed — wearing it for a non-delivery congratulates someone
   whose work just went nowhere. Nothing went wrong here either, so it is not a
   red X: it is simply over. */
.donemark.neutral{background:#2a2723;color:var(--ink3);border-color:#3f3b34}
/* Tucked under the headline rather than reading as a second one — it is the
   detail (what was sent / what happened to the draft), not the news. */
.state .donesub{margin-top:-6px;font-size:13.5px;line-height:1.45;color:var(--ink3);max-width:34ch}

/* Live capture layer. Sits above the replay canvas, but only intercepts pointer
   events while a tool is armed — otherwise it would swallow the drags meant for
   repositioning a placed emoji or text note underneath it. */
.draw{position:absolute;inset:0;touch-action:none;pointer-events:none}
.draw.armed{pointer-events:auto;cursor:crosshair}

/* The owner's ask — "what do you want them to look at", carried from the send
   sheet. Brand-tinted rather than reusing .draftnote's green/coral: those two
   are notices ABOUT this session (your draft survived; this browser may not
   send), while this is content FROM the person asking, and it should read as
   theirs. Quoted-looking left rule for the same reason. */
.ask{border-left:3px solid var(--brand);background:#ffffff08;border-radius:0 10px 10px 0;
  padding:9px 12px}
.ask .asklab{display:block;font-size:10px;font-weight:800;letter-spacing:.07em;
  text-transform:uppercase;color:var(--brand)}
.ask p{margin:3px 0 0;font-size:14.5px;font-weight:600;color:var(--ink);line-height:1.4}
.ask .askgoals{font-size:12.5px;font-weight:400;color:var(--ink2);margin-top:5px}

/* Restored-draft notice */
.draftnote{display:flex;align-items:center;justify-content:space-between;gap:10px;
  background:var(--pos-bg);border:1px solid var(--pos-line);border-radius:11px;
  padding:9px 12px;font-size:12.5px;color:var(--pos)}
/* Same shape, cautionary palette — used for the in-app-browser warning, which is
   advice rather than the good news a restored draft is. */
.draftnote.warn{background:var(--warn-bg);border-color:var(--warn-line);color:var(--warn)}
.linkbtn{background:none;border:none;padding:0;font:inherit;font-size:12.5px;font-weight:700;
  color:inherit;cursor:pointer;flex:0 0 auto}

/* "Have the app? Continue there" hand-off (review.html only) — shown ONLY
   while the canvas is still empty (see wireReviewCta in review.js): a tap
   here opens the native app, which would otherwise abandon anything drawn
   only in this tab. Neutral card, not the brand/positive/warn palettes above
   — this is a fork in the road, not a status about the current session. */
.appcta{display:flex;align-items:center;justify-content:space-between;gap:10px;
  background:var(--card);border:1px solid var(--line);border-radius:11px;
  padding:10px 12px;color:var(--ink);text-decoration:none;font-size:13px;font-weight:700}
.appcta .arr{color:var(--brand);font-weight:800;flex:0 0 auto}

/* Tool rails — two floor-anchored vertical columns OVER the frame, mirroring
   the app's AnnotationToolRail (right, "what you add") / AnnotationEditRail
   (left, "what you take back") — lib/widgets/annotation_tool_rails.dart. A
   horizontal bar below the stage cost a portrait clip (most of what this
   surface sees) its full width for six tools; these cost it nothing, and the
   sizing below matches VideoConstants' single source of truth
   (railBoxedButtonSize=44, railIconSize→20px glyphs, railSpacing=8,
   railEdgeInset=4) so the web surface reads as the same control set as the
   app rather than a redesign of it.

   Bottom-anchored, not centred or top-anchored: `#sizeToggle` already owns
   the top-right corner (see .stagebtn), and the app's own rails are
   floor-anchored above their transport for the same reason a scrubber's
   controls read top-to-bottom as "primary action nearest your thumb".

   Positioned relative to #stageWrap, not .stage — `right`/`left` below are
   the on-video fallback (.stage fills the wrap, no letterbox). review.js's
   positionRails() overwrites both with an inline style once it can measure
   an actual gap between the two, parking the rail in the pillarbox margin
   instead when one exists. */
.rail{position:absolute;left:auto;right:auto;bottom:8px;top:8px;display:flex;
  flex-direction:column;justify-content:flex-end;gap:8px;z-index:3;pointer-events:none}
.rail-right{right:4px}
.rail-left{left:4px}
/* The container spans the frame's height so it can bottom-align its children,
   but only the buttons themselves should catch taps — otherwise the empty
   space above a short rail would shield #draw from the pen. */
.rail .tool,.rail .emojiwrap{pointer-events:auto}
.toolhint{margin:0;padding:0 4px;font-size:11.5px;line-height:1.4;color:var(--ink3);text-align:center}

/* Every rail control (including .echoice in the emoji popover below) blocks
   the platform's own long-press gesture. Without this, holding the emoji
   button long enough to trigger OUR picker ALSO trips the browser's native
   text-selection/callout timer on the same gesture — on iOS Safari that wins
   and shows Copy/Look Up/Search instead of (or on top of) the picker, because
   nothing here was telling it the glyph inside a <button> isn't selectable
   text. */
.tool,.echoice{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}

.tool{flex:0 0 auto;border:1px solid transparent;background:none;color:var(--ink);
  font-size:17px;line-height:1;cursor:pointer;padding:0;
  display:flex;align-items:center;justify-content:center;transition:background .12s ease}
.tool:disabled{opacity:.3;cursor:default}
.tool.active{background:var(--coral);border-color:var(--coral);color:#fff}

/* The boxed variant every rail button uses — a visible plate (glass over
   video, matching #sizeToggle's existing treatment) rather than a ghost
   glyph, so the box itself is the tap target. */
.tool.boxedtool{width:44px;height:44px;border-radius:11px;
  background:#00000066;border:1px solid #ffffff33;backdrop-filter:blur(2px)}
.tool.boxedtool svg{width:20px;height:20px;display:block}

/* Text — the one tool that can carry an instruction, so it keeps the gold
   (primary) fill everywhere else the app marks a primary action, even as an
   icon-only box. */
.tool.boxedtool.primary{background:var(--brand);border-color:var(--brand);color:var(--brand-ink)}
/* .active (composing) must win over the resting gold fill above — same
   specificity, so without this the cascade order would decide it instead,
   and which one that is is an accident of where each rule sits in the file. */
.tool.boxedtool.primary.active{background:var(--coral);border-color:var(--coral);color:#fff}

/* Emoji default box: shows the current default (EmojiReactBar's `_selected`);
   the caret badge is a picker affordance layered in its corner, not a
   separate tap target — see the caret-vs-glyph split in review.js's click
   handler. .emojiwrap (not the button) is the popover's positioning context —
   see the button-in-button note on the markup in review.html. */
.emojiwrap{position:relative;display:inline-flex;flex:0 0 auto}
.tool.boxedtool.emojidefault .edefault{font-size:22px;line-height:1}
.tool.emojidefault .caret{position:absolute;right:-3px;bottom:-3px;width:13px;height:13px;
  border-radius:50%;background:var(--card);border:1px solid var(--line);color:var(--ink3);
  font-size:9px;line-height:1;display:flex;align-items:center;justify-content:center}

/* The picker popover — mirrors Flutter's _EmojiPopover (white pill, one
   accent tint for the selected choice) rather than the page's dark theme,
   since it is a direct port of that widget's chrome. `position:fixed` with no
   static top/left/right: openEmojiPicker() in review.js sets both inline,
   after measuring the popover against the viewport. It used to be `absolute`
   or `right:calc(100% + 8px)` off the button, but the button sits close to
   the frame's own right edge AND .stage clips overflow — so on a narrow phone
   the popover ran out of room and got clipped by .stage before it ever
   reached the screen edge. Fixed positioning escapes both problems: it's
   viewport-relative, not stage-relative, so JS can clamp it to stay fully
   on-screen regardless of how little margin the button has around it. */
.emojipop{position:fixed;display:flex;align-items:center;gap:2px;background:#fff;
  border:1px solid var(--line);border-radius:999px;padding:4px 6px;
  box-shadow:0 8px 24px rgba(0,0,0,.4);z-index:5}
.echoice{width:30px;height:30px;flex:0 0 auto;border:none;border-radius:999px;background:transparent;
  font-size:17px;line-height:1;padding:0;cursor:pointer;display:flex;align-items:center;justify-content:center}
/* Light-surface counterpart of --pos-bg: that token is tuned for the dark
   page background and would read as a near-black blob on this white pill. */
.echoice.selected{background:#e3f7ea}

/* The one destructive control in the rail — visually distinct from the seven
   constructive/neutral tools next to it, not a peer of them. */
.tool.danger:not(:disabled):hover,.tool.danger:not(:disabled):active{
  background:#3a201a;border-color:#5c2f24;color:#ff8f73}

/* Draggable placed object (emoji / text) while authoring — the read-only
   .objects .obj is pointer-inert, these are not.
   The hit target must be the OBJECT, never the container: `.objects` is
   `inset:0`, so making it pointer-reactive turns the whole frame into a
   transparent shield over #draw (a z-index:auto sibling) — and `.editing` is on
   from the moment the first note lands near the playhead, which killed drawing
   outright. `pointer-events:none` here with `auto` on the child is the standard
   pattern: the container stays transparent to hit-testing while the drag targets
   opt back in.
   The z-index has to stay on the CONTAINER rather than the child: `.objects` has
   an opacity transition, which makes it its own stacking context, so a z-index on
   the child could never escape it to clear a sibling anyway. */
.objects.editing{pointer-events:none;z-index:2}
.objects.editing .obj.drag{pointer-events:auto}
.objects .obj.drag{cursor:grab;touch-action:none}
/* Same opt-back-in as `.obj.drag` above, but unconditional rather than gated
   on `.editing`: the read-only viewer (index.html/app.js) never sets
   `editing` on its player — nothing there is draggable — so a plate's tap-to-
   expand (placedObjectEl's click handler, player.js) would otherwise be
   unreachable there, the container's `pointer-events:none` swallowing it
   before it ever reached the element. Harmless on the authoring surface: a
   plate is only ever in the DOM while its moment is the active one, which is
   exactly when `.editing` is already present too. */
.objects .plate{pointer-events:auto}
.objects .obj.sel{outline:2px dashed var(--brand);outline-offset:4px;border-radius:6px}

/* ── on-frame text composer ───────────────────────────────────────────────
   The layer a text note is typed on, before it becomes a moment. It reuses
   `.objects` for the `.obj` positioning rules but is always shown (no opacity
   transition to wait on) and sits above both the replay layer and #draw. Same
   pointer-events split as .editing above: the layer is `inset:0`, so it must stay
   transparent to hit-testing or it shields the whole frame from the pen. */
.objects.compose{opacity:1;z-index:3;pointer-events:none}
.compose .obj{pointer-events:auto}

/* The editor IS the `.obj.plate` the note will replay as — that is the point, it
   makes authoring a literal preview — plus a caret and an outline saying "this is
   live". min-width so an empty box is still a visible target. Its visual chrome
   (background/border/padding/type) comes from the selector list on
   `.objects .plate .platebody` above, which this rule is deliberately listed
   alongside — a committed note gets that chrome via its `.platebody` child, but
   the editor has no such child (it's the contentEditable itself), so it needs
   the same declarations applied directly.

   The one place it deliberately differs from the replayed plate: same
   max-height, but overflows by SCROLLING rather than clipping. Hiding what
   someone is in the middle of typing is not a bound, it's a trap — they'd keep
   going with no idea the words were landing off-box. */
.compose .obj.composing{min-width:46px;cursor:text;caret-color:var(--coral-ink);
  outline:2px solid var(--brand);outline-offset:3px;max-height:calc(5em + 9px);
  overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}
.compose .obj.composing:empty::before{content:attr(data-placeholder);opacity:.4}

/* Composed take */
.takelabel{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.05em;
  color:var(--ink3);margin-bottom:7px}
.takeinput{width:100%;background:var(--page);color:var(--ink);border:1px solid var(--line);
  border-radius:9px;padding:10px 11px;font:inherit;font-size:14px;line-height:1.5;resize:vertical}
.takeinput::placeholder{color:var(--ink3)}
.takeinput:focus{outline:none;border-color:var(--brand)}

/* Take card (authoring surface) — a summary + door, not the editor itself.
   `.take .prose` above already styles the pills; these are just the card's
   own chrome (header row, empty-state prompt, the 4-line clamp). */
.takehd{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}
.takehd .takelabel{margin-bottom:0}
.takeempty{display:block;width:100%;text-align:left;background:none;border:none;padding:0;
  font:inherit;font-size:14px;line-height:1.5;color:var(--ink3);cursor:pointer}
.takeempty b{color:var(--coral);font-weight:700}
.prose.clamp{display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}

/* Take sheet — a bottom sheet, not a modal dialog: it edits the SAME value
   the card previews (see syncTakeCard/persist in review.js), so closing it
   — Save or tapping the backdrop — is always "done for now", never a
   discard. `max-width` matches #app so it doesn't span an ultra-wide
   viewport edge-to-edge. */
.sheetbackdrop{position:fixed;inset:0;background:#000000aa;z-index:30}
.sheet{position:fixed;left:0;right:0;bottom:0;z-index:31;background:var(--page);
  border:1px solid var(--line);border-bottom:none;border-radius:18px 18px 0 0;
  max-width:520px;margin:0 auto;padding:16px 16px max(16px,env(safe-area-inset-bottom));
  box-shadow:0 -8px 30px #0007}
.sheettitle{margin:0 0 2px;font-size:16px;font-weight:800;color:var(--ink)}
.sheethint{margin:0 0 8px;font-size:12px;color:var(--ink3)}
.takechips{margin-bottom:10px}
/* Auto-grown by review.js's fitTakeSheetText; max-height is a CSS-only
   backstop at the same ceiling in case JS hasn't run yet on first paint. */
.sheettext{resize:none;overflow-y:auto;max-height:220px;margin-bottom:12px}

/* Send button busy state */
.cta[disabled]{opacity:.55;pointer-events:none}

/* ── single-clip viewer (clip.html / clip.js) ──────────────────────────────
   Reuses .stage/.transport/.ctl/.progress/.time/.mchip/.cta/.wordmark/.vctx
   verbatim; only what's genuinely new for a bare, un-annotated move lives
   here. */

/* Height-capped stage (the fold budget), unlike .stage's JS-driven exact fit
   elsewhere: no canvas overlay here to keep pixel-exact, so a CSS cap +
   object-fit:contain is enough. A blurred copy of the poster fills the
   letterbox — "contain, never cover": cropping a dance clip cuts the feet,
   the thing being studied. */
.mstage{height:min(470px,62dvh)}
.mstage .stagebg{position:absolute;inset:-12%;background-size:cover;background-position:center;
  filter:blur(24px) brightness(.45);z-index:0}
.mstage video,.mstage iframe{position:relative;z-index:1;width:100%;height:100%;border:none;background:transparent}
.mstage video{object-fit:contain}

/* Credit byline — above the stage (in .vhdr's flow, styled off .vctx), whole
   row tappable. Monochrome glyph on purpose: no gradient tile where an
   avatar would go, this is attribution, not a profile card. */
.credit{display:flex;align-items:center;gap:7px;text-decoration:none;color:var(--ink2)}
.credit .igicon{width:16px;height:16px;flex:0 0 auto;color:var(--ink3)}
.credit b{color:var(--ink);font-weight:700}

/* Speed row — reuses .mchips/.mchip (moment-navigator chips) for a plain
   labelled control row instead of a moment strip. */
.speedrow{align-items:center}
.speedlabel{flex:0 0 auto;font-size:12px;color:var(--ink3);margin-right:2px}

/* Move title — the IG caption verbatim (often a full sentence, sometimes with
   emoji), not a curated short name like a collection's. Deliberately smaller
   and lighter than .colhero h1, which is sized for 2-4 word program names. */
.mtitle{padding:2px 2px 0}
.mtitle h1{margin:0;font-size:15px;line-height:1.4;font-weight:700;color:var(--ink)}

/* Sticky CTA — pinned above the fold's bottom edge, faded into the page
   behind it so content doesn't clip abruptly underneath. */
.stickycta{position:sticky;bottom:0;display:flex;flex-direction:column;gap:7px;
  padding-top:10px;margin-top:2px;
  background:linear-gradient(to bottom, transparent, var(--page) 22%)}

/* Featured-in — the whole block (label included) is omitted client-side when
   the payload's list is empty; see clip.js's renderFeatured. */
.featuredin{display:flex;flex-direction:column;gap:6px}
.featuredrow{display:flex;flex-direction:column;gap:2px;text-decoration:none;
  background:var(--card);border:1px solid var(--line);border-radius:11px;padding:10px 12px}
.featuredrow .ftitle{font-size:13.5px;font-weight:700;color:var(--ink)}
.featuredrow .fmeta{font-size:11.5px;color:var(--ink3)}

.contribline{margin:0;padding:0 4px;text-align:center;font-size:11.5px;line-height:1.5;color:var(--ink3)}
.reportline{margin:0;text-align:center}
.reportline .linkbtn{font-size:11.5px;color:var(--ink3);text-decoration:underline;text-underline-offset:2px}

/* Failure state — a primary link out (Instagram, if derivable) rather than
   an install CTA: the page just broke, offering to install the app is the
   wrong ask. Reuses .state's centred column (incl. `.state p{margin:0}`
   above), no spinner. */
.failsub{margin:0;color:var(--ink3);font-size:11.5px}
