/* bubbleup.gg/demo — the floating stream card.
 *
 * This file is the LOOK half of `pip.js` and nothing else. It is an addendum to
 * `demo.css` the way `demo.css` is one to `style.css`: every colour, hairline
 * and shadow here is read out of the page's own tokens, so the card cannot
 * drift into being its own little design.
 *
 * NOTHING here applies until `pip.js` has put `data-pip="on"` on `#stage`. That
 * is deliberate and it is the whole safety story of this lane: a browser with
 * JavaScript off, a viewport under the 1200px desktop threshold, or a page that
 * has not been restructured yet (no `#stage`) gets `demo.css`'s layout exactly
 * as it is written, untouched, because every selector below is gated on an
 * attribute only the module sets.
 *
 * ---------- the model ----------
 *
 * The card is the iPhone app's floating thumbnail
 * (`ios/Packages/BubbleupChat/…/ThumbnailFloat.swift`,
 * `ios/App/ThumbnailCardView.swift`), ported rather than re-invented:
 *
 *   - 10px corner radius — `ThumbnailCardView`'s
 *     `RoundedRectangle(cornerRadius: 10)`, and the one number that carries over
 *     from the phone unscaled, because a corner radius is a fact about the eye
 *     rather than about the screen.
 *   - a 1px hairline — the phone strokes `.white.opacity(0.10)`; on this page the
 *     equivalent is `--line`, which IS the page's hairline and is what every
 *     other framed thing here is drawn with.
 *   - the page's drop shadow — the same pair `style.css` puts under a raised
 *     card, so the thing that floats over the field looks lifted by the same
 *     light as everything else on the site.
 *   - and, since finding 145, no handle bar at all. The port briefly added one
 *     — a slim bar along the top edge — on the argument that a pointer needs
 *     somewhere to grab that is NOT the player's own controls. Kevin's answer
 *     ("can I not move the window wherever by just grabbing in the center
 *     instead?") is the phone's: a finger drags the card itself, and so does a
 *     pointer. The added part became a part that copies one.
 */

/* ---------- the two knobs the module reads back out of CSS ---------- */
#stage[data-pip="on"] {
  /* The corner spring, as a transition.
   *
   * `ThumbnailFloatLayer.snap` is `Animation.spring(response: 0.32,
   * dampingFraction: 0.72)` — 320ms, and under-damped enough to overshoot
   * slightly and settle. The bezier is that curve's shape: fast out, a little
   * past the mark, back. A CSS transition cannot be a real spring, and the
   * alternative (a Web Animations spring polyfill) would be a physics engine
   * for one 320ms move. */
  --pip-fly: 320ms cubic-bezier(0.22, 1.2, 0.36, 1);
  /* How far the card keeps off the stage's edges and off an occupied corner's
   * controls — **the FALLBACK only**, for a page that has no free corner slot to
   * measure. `ThumbnailFloatInsets.field`'s 10pt horizontal inset, rounded up
   * for a pointer that can put a card within a pixel of an edge where a thumb
   * cannot.
   *
   * On `/demo` the real number is READ OFF `#corner-tl` (see `marginPx()` in
   * `pip.js`): `demo.css` stands every slot off by its own `--slot`, and its
   * comment says why the slots exist at all — *"a slot whose geometry were
   * computed twice is a slot the PiP lane would snap a player into by a pixel or
   * two."*
   *
   * **And it is read geometrically rather than as `var(--slot)`, which is what
   * this tried first and why the try is written down.** `--slot` is
   * `clamp(14px, 0.9vw, 22px)`, and an UNREGISTERED custom property has no type:
   * its computed value is the token stream with `var()` substituted, not a
   * length. So `getComputedStyle(stage).getPropertyValue("--pip-margin")` hands
   * back the literal string `"clamp(14px, 0.9vw, 22px)"`, `parseFloat` gives
   * `NaN`, and the module quietly falls back to 12 — a card 2px proud of its slot
   * at 1440 and 5px at 1920, with nothing anywhere saying so. A `@property`
   * registration would type it, but the element's own rectangle is already the
   * answer and cannot drift from the page. */
  --pip-margin: 12px;
}

/* ---------- the card ----------
 *
 * Two ids deep, so this beats whatever `demo.css` says about `#twitchframe`
 * without that file having to know this one exists. `pip.css` is linked after
 * `demo.css`, so equal-specificity rules would go this way anyway — the depth
 * is what makes it true regardless of order. */
#stage[data-pip="on"] > #twitchframe {
  position: absolute;
  left: var(--pip-x);
  top: var(--pip-y);
  width: var(--pip-w);
  height: var(--pip-h);
  /* The ladder is a WIDTH ladder and the height is 16:9 arithmetic done in the
     module, so nothing here may add its own idea of a height. */
  margin: 0;
  /* Over the field, under the shield (below) and under anything the page floats
     on its own account. `demo.css`'s own stacking story is unchanged: this is a
     positioned child of `#stage`, not a new stacking context on `.wrap`. */
  z-index: 5;
  border: 1px solid var(--line);
  border-radius: 10px;
  /* NOT `overflow: hidden` — finding 155. It was, to round the player's
     corners, and it clipped everything a child hangs outside the box: the
     resize grip and the sound control now live on a strip just outside the
     card (below), and under a clip they were neither drawn nor hit — the hit at
     their centre was the field's iframe. The corners are rounded on the player
     itself instead (its rule follows the handle's, below). */
  overflow: visible;
  /* THE HAIRLINE, DRAWN OVER THE PLAYER — finding 157. The player covers the
     card's border-box exactly now (its rule at the end of this file), so the
     `border` above is layout only — it keeps the content box 2px under the slot
     and every `calc(100% + 2px)` honest — and nobody sees it. What is seen is
     this outline, inset by its own width onto the pixels the border occupies.
     An outline is not paint the player's visibility test counts: the
     independent review of finding 155 read Chromium's occlusion hit-test
     (outlines are not hit-tested at all) and measured the card's focus ring as
     harmless, and `site-pip.spec.ts` §"nothing of ours occludes the player"
     reads the observer with this outline on. The focus ring below replaces it
     while the card is focused — the same property, the more specific rule. */
  outline: 1px solid var(--line);
  outline-offset: -1px;
  background: var(--ink);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.04),
    0 14px 44px rgba(8, 8, 11, 0.55);
  transition:
    left var(--pip-fly),
    top var(--pip-fly),
    width var(--pip-fly),
    height var(--pip-fly);
}

/* A live gesture is not a flight. While the pointer is down the card must be
   exactly where the pointer put it, this frame — a 320ms ease on `left` during
   a drag is a card that lags the hand. */
#stage[data-pip-live] > #twitchframe {
  transition: none;
}

/* `prefers-reduced-motion` → instant placement, which is the brief's own word
   and the same posture `demo.css` takes for everything else that moves. The
   card still goes to the corner; it simply arrives. */
@media (prefers-reduced-motion: reduce) {
  #stage[data-pip="on"] > #twitchframe {
    transition: none;
  }
}

/* ---------- the handle: the whole card ----------
 *
 * FINDING 145 (Kevin, walk 11, 2026-09-17): *"i see you have a top drag bar,
 * can I not move the window wherever by just grabbing in the center instead?"*
 *
 * What stood here was a 16px bar along the top edge with a little grey pill on
 * it, fading in on hover — the port's one ADDED part, on the argument that a
 * pointer needs somewhere to grab that is not the player's own controls. His
 * answer is that a window is grabbed by the window. So the bar is gone and this
 * is a transparent sheet over the whole card, present at ALL times rather than
 * during a gesture: it is what makes "grab it anywhere" reach this document at
 * all, because the player under it is a cross-origin iframe and a pointer over
 * one of those is that document's pointer.
 *
 * NOTHING IS DRAWN, AND `opacity: 0` IS WHAT MAKES THAT TRUE TO TWITCH —
 * finding 155. The player refuses to autoplay, and pauses a playing stream
 * within a frame, whenever its box is "not visible", and it decides that with
 * the browser's own IntersectionObserver v2 (`trackVisibility`): any element of
 * ours painted over ANY part of the player's rect — a transparent sheet with no
 * background included, a 2px dot at its centre included — reads as occlusion
 * and the picture stops. Measured on the live page 2026-09-17 21:5xZ: this
 * sheet as shipped by finding 145 gave `isVisible: false` and Twitch's console
 * said "Autoplay disabled … style visibility"; `pointer-events: none` changed
 * nothing; `display: none` and `opacity: 0` both gave `isVisible: true`, and at
 * opacity 0 the hit at the picture's centre is still this sheet. Chromium's
 * occlusion hit-test skips zero-opacity objects and its pointer hit-test does
 * not, which is the one seam that lets a grab-anywhere card and an autoplaying
 * player coexist. There is no fade, no pill and no tint — the card's own
 * hairline and radius are the whole affordance, and the cursor says the rest.
 * The phone is the same shape: `ThumbnailCardView` has no handle because the
 * card IS the handle.
 *
 * THE PLAYER'S OWN HOVER CONTROLS ARE UNREACHABLE while the card floats, and
 * that is accepted by the ruling rather than overlooked: this card is a
 * thumbnail, the demo's player is muted autoplay by Twitch's own rule, and
 * nothing on this page ever promised the player's controls. `docs/knobs.md`
 * carries it as a row. */
#stage[data-pip="on"] > #twitchframe > .pip-grip {
  position: absolute;
  /* THE PLAYER'S RECT, TO THE PIXEL — finding 157. `inset: 0` was the content
     box, and the player (its rule at the end of this file) is the card's
     border-box, 1px bigger on every side — so the player's outermost column and
     row on the right and bottom were the Twitch document's to hover. Same `-1px`
     as the player: the two rects are one rect, and `site-pip.spec.ts` hit-tests
     its edge pixels and its four corners. */
  inset: -1px;
  z-index: 2;
  opacity: 0;
  cursor: grab;
  /* A pointer/keyboard part, never a text selection. A drag that starts by
     selecting the page behind it is the one thing that makes a home-made PiP
     feel home-made. */
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}

/* The focus ring is on the CARD, because the card is the focus stop now
   (`tabindex` and `aria-label` are written onto `#twitchframe` by `pip.js`).
   `--line-hi`, inset, so it reads against the picture rather than against the
   field behind it. */
#stage[data-pip="on"] > #twitchframe:focus-visible {
  outline: 2px solid var(--line-hi);
  outline-offset: -2px;
}

#stage[data-pip-live="move"] > #twitchframe > .pip-grip {
  cursor: grabbing;
}

/* ---------- the resize grip ----------
 *
 * At the card's INNER corner — the one diagonally opposite its home — so it
 * always points into the stage and never off an edge. `pip.js` writes which
 * corner that is into `data-pip-grip`; the four rules below are that one fact
 * turned into a box.
 *
 * OUTSIDE THE PLAYER'S RECT since finding 155, hanging off the card's inner
 * edge by `--pip-hang` (set on `#twitchframe` in `demo.css`, where the sound
 * control that shares the strip is drawn): it was on the picture's corner, hidden at rest and
 * shown on hover, and the moment it showed it was paint over the player —
 * which is the occlusion that stops the stream (the note on `.pip-grip`
 * above). The player's iframe reaches 1px past the card's content box on its
 * right and bottom (`demo.css`, `.frame iframe`), so the stand-off is 4px and
 * not 1; measured: a box 1px off the card still occludes, 2px clears. It hangs
 * BELOW the card at the two top homes and ABOVE it at the two bottom homes,
 * always into the field, never off an edge — the same fact `data-pip-grip`
 * already states. The sound control (`.pip-sound`, `demo.css`) hangs on the
 * same strip, beside it, and both are laid out here so the two cannot be
 * placed by two files.
 *
 * This is the phone's pinch, re-expressed for a pointer: the phone's two
 * fingers are a continuous width and so is this, and both settle on the same
 * ladder with the same gate (`ThumbnailCardSize.settled`). */
#stage[data-pip="on"] > #twitchframe > .pip-size {
  position: absolute;
  width: 18px;
  height: 18px;
  /* ABOVE the full-card drag shield, and stated rather than left to tree order:
     since finding 145 the shield covers this corner too, and `demo.js`'s
     `replaceChildren` re-appends both parts in one call whose order this file
     must not depend on. */
  z-index: 3;
  opacity: 0;
  transition: opacity 140ms ease;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
#stage[data-pip="on"] > #twitchframe > .pip-size::after {
  content: "";
  position: absolute;
  inset: 4px;
  border-radius: 1px;
  border-bottom: 2px solid var(--fg-3);
  border-right: 2px solid var(--fg-3);
}
#stage[data-pip="on"] > #twitchframe:hover > .pip-size,
#stage[data-pip="on"] > #twitchframe > .pip-size:focus-visible,
#stage[data-pip-live] > #twitchframe > .pip-size {
  opacity: 1;
}

#twitchframe[data-pip-grip="br"] > .pip-size {
  right: 0;
  top: calc(100% + var(--pip-hang));
  cursor: nwse-resize;
}
#twitchframe[data-pip-grip="bl"] > .pip-size {
  left: 0;
  top: calc(100% + var(--pip-hang));
  cursor: nesw-resize;
  transform: scaleX(-1);
}
#twitchframe[data-pip-grip="tr"] > .pip-size {
  right: 0;
  bottom: calc(100% + var(--pip-hang));
  cursor: nesw-resize;
  transform: scaleY(-1);
}
#twitchframe[data-pip-grip="tl"] > .pip-size {
  left: 0;
  bottom: calc(100% + var(--pip-hang));
  cursor: nwse-resize;
  transform: scale(-1, -1);
}

/* ---------- the sound control shares the strip ----------
 *
 * `.pip-sound` (finding 151, `demo.css`) hangs off the card on the same strip
 * as the grip, for the grip's reason (finding 155: nothing of ours may paint on
 * the player). The strip is below the card at the top homes and above it at
 * the bottom homes, and the control keeps to the side the grip is not on: the
 * grip owns the card's inner corner, so where that corner is on the LEFT the
 * control steps past it by the grip's 18px and one half-unit of air, and where
 * it is on the right the control sits at the left edge. `data-pip-grip` is
 * already on the card (written by `pip.js`), so this is that one fact turned
 * into two offsets — no second opinion about which corner the grip is in. */
#stage[data-pip="on"] > #twitchframe[data-pip-grip^="b"] > .pip-sound {
  top: calc(100% + var(--pip-hang));
  bottom: auto;
}
#stage[data-pip="on"] > #twitchframe[data-pip-grip^="t"] > .pip-sound {
  top: auto;
  bottom: calc(100% + var(--pip-hang));
}
#stage[data-pip="on"] > #twitchframe[data-pip-grip$="l"] > .pip-sound {
  left: calc(18px + var(--ui) * 0.5);
}

/* ---------- the shield ----------
 *
 * The two frames on this stage are CROSS-ORIGIN. A pointer that crosses one
 * mid-drag stops being this document's pointer — the events go to the other
 * origin's document and the card freezes under the hand. `setPointerCapture`
 * handles the common case; a transparent sheet over the whole stage is what
 * handles it in every case, and it is what a native window manager does too.
 *
 * It exists only while a gesture is live: `pip.js` puts `data-pip-live` on
 * `#stage` at pointerdown and takes it off at pointerup.
 *
 * `opacity: 0` for `.pip-grip`'s reason (finding 155): while it is up it is over
 * the player too, and a sheet with no paint still reads as occlusion to the
 * player's visibility test unless its opacity is zero — a drag would have
 * paused the stream it was moving, and Twitch does not resume on its own. */
#stage[data-pip="on"] > .pip-shield {
  position: absolute;
  inset: 0;
  z-index: 4;
  opacity: 0;
  display: none;
}
#stage[data-pip-live] > .pip-shield {
  display: block;
}
#stage[data-pip-live="move"] > .pip-shield {
  cursor: grabbing;
}
#stage[data-pip-live="size"] > .pip-shield {
  cursor: nwse-resize;
}

/* The card's own player is INSIDE the card and therefore over the shield. Same
   problem, same answer, one layer down. */
#stage[data-pip-live] > #twitchframe iframe {
  pointer-events: none;
}

/* ---------- the player IS the size it looks (finding 155) ----------
 *
 * What stood here laid the player out at 544×306 forever and `transform:
 * scale()`d it into the card — the phone's `ThumbnailEmbed.swift` answer, ported
 * — on the argument that **Twitch refuses to autoplay an embed under 400×300 CSS
 * px and decides once per load** (contract, 2026-09-01), so a player stretched
 * to a 350px card would never start.
 *
 * MEASURED ON THE LIVE PAGE, 2026-09-17 22:1xZ, the transform was the wrong
 * half of that trade. Twitch also requires the player to be VISIBLE, checks it
 * with IntersectionObserver v2 continuously, and v2 counts a shrinking transform
 * on the target as "not visible": `scale(0.64)` on a PLAYING player gave
 * `isVisible: false`, Twitch's console said *"Autoplay disabled … style
 * visibility"* 850ms later and the stream paused — so the two smaller tap rungs
 * were a stopped picture. (Up-scaling, `scale(1.25)`, is allowed and read true;
 * a `border-radius` on the iframe read true.) The size rule, on the other hand,
 * is load-time only: the same playing player laid out at 348×196 — under the
 * floor by layout — kept playing, heartbeats and all.
 *
 * So the player is the card's own box and a rung change is a layout change.
 * AND THE 400×300 LOAD FLOOR ITSELF IS STALE: measured the same evening on the
 * live page, fresh players built at 348×196 — the smallest tap rung — autoplayed
 * on two channels (`play` 1.4 s after the load, no refusal in the console), and
 * the independent reviewer's own sample agreed. So a remembered small rung comes
 * back small, as it always did; nothing clamps a start. The slot stays the
 * resting size by the ruling, not by the floor.
 *
 * THE BOX IS THE CARD'S BORDER-BOX: 2px more than `demo.css`'s `.frame iframe`
 * 100% × 100%, because the card wears a 1px hairline and a player 2px narrower
 * than the slot is 2px under the number the slot exists for (`site-demo.spec.ts`
 * pins the resting player at 544 × 306 exactly).
 *
 * AND CENTRED ON IT — finding 157 (Kevin, walk 11: *"hovering the bottom or the
 * right edge of the frame for like that one pixel it shows the embed
 * controls"*). As finding 155 left it, the box was laid out from the CONTENT
 * box's origin, so it reached 1px past the card's border-box on the right and
 * bottom while the drag sheet (`inset: 0`, the content box) stopped 1px short
 * of it on those two sides: one column and one row of player nobody's sheet
 * covered, and a pointer resting there was the Twitch document's — its hover
 * bar came up. `-1px` on all four sides is the same 544×306 box moved up and
 * left by one pixel: it IS the border-box now, the sheet is the same rect (its
 * rule above), and the hairline is redrawn over it as the card's outline. The
 * strip its controls hang on is measured from the padding box, so `--pip-hang`
 * (`demo.css`) now clears the player by 3px rather than 2. `right`/`bottom`
 * are `auto` so the stated size wins over `.frame iframe`'s `inset: 0`. The
 * corners are rounded here rather than by clipping the card (its `overflow` is
 * `visible`, for the strip) — the card's own 10px, since the box is the card's. */
#stage[data-pip="on"] > #twitchframe > iframe {
  top: -1px;
  left: -1px;
  right: auto;
  bottom: auto;
  width: calc(100% + 2px);
  height: calc(100% + 2px);
  border-radius: 10px;
}
