/* ============================================================================
   VIDEO DETAIL — `s/videos`, FE Client file `HrxSPGo2hxOHYdCkLPPnMu`

     Section          `Videos`                    46639:711029
     Screen           `s/videos` (detail)         22585:415856
     Body             `Body / Videos / Detail`    23304:478311
     Player overlay   `p/video player`            22585:433987
     Purchases modal  (frame `Closed Captions`)   22831:420444
     QR modal         `Modal / Default`           36032:120709
     Post actions     `Dropdown / Post Actions`   36573:1310492 / 36573:1310741
     Toast            `Toast`                     22772:403423

   ONE page, and `data-video-*` attributes select its states. Geometry is the
   same 1016 body the event detail uses — a 324-tall header card, then a
   576 + 16 + 424 split — so layout.css's `wide` body measure covers it and
   router.js routes it as `1-col` / `wide`.

   ── The frames are pre-tokenisation. Not one colour in `Body / Videos /
   Detail` is bound to a variable: `get_variable_defs` on 23304:478311 returns
   local styles only. Each was resolved through DESIGN.md instead, using the
   same table event.css established so the two detail pages agree:

   | Figma local style              | resolves to | Token used                    |
   |--------------------------------|-------------|-------------------------------|
   | General/White                  | #ffffff     | `surface/raised`              |
   | General/BG Light               | #f6f6f6     | `soft/neutral/default`        |
   | Text & Icons/Headlines         | #262626     | `content/headline`            |
   | Text & Icons/Primary           | #595959     | `content/primary`             |
   | Text & Icons/Marginal          | #8c8c8c     | `content/secondary`           |
   | Action/BigHeart/Primary        | #4211a0     | `content/brand` (never a hex) |
   | Elements/Borders & Dividers    | #e8e8e8     | `border/default`              |
   | Elements/Border Button Default | #d9d9d9     | `border/default`              |
   | Alert/Info/Primary             | #006aff     | `border/info/default`         |
   | Alert/Info/Secondary (10%)     | #e5f0ff     | `soft/info/default`           |
   | Tags/Purple/Secondary/Primary  | #f9f0ff/#120338 | see D1                    |
   | Elevation 1 / 2 (RAW shadows)  | —           | `Elevation/Overlay`           |

   `#4211a0` appears on the Watch button, `See Translation`, `See all` and the
   QR modal's Download button. Root CLAUDE.md non-negotiable 4 forbids a fifth
   literal copy of it; every one of those is `.btn--primary` or `.btn--link`
   and picks the brand up from the token, so the brand switch reaches all four.

   ── DELIBERATE DIVERGENCES from the frames. Each is a defect named in
   apps/fe-client/CLAUDE.md, COMPONENTS.md or DESIGN.md.

   D1  `Tag / Entity` — §12 F4: the entity tags use raw pastel/dark pairs, and
       the video pair is Tags/Purple (#f9f0ff on #120338). There is no purple
       ramp outside the BRAND ramp, and painting a category chip in the brand
       colour would say "BigHeart", not "video". `.tag--entity` (card.js's
       neutral treatment) is reused, so the chip matches the same object's
       chip on the listing page one click earlier. The glyph and the word
       already say "Video", twice.
   D2  The header's like count is `Elements/Other/Likes` (#f5222d) on the
       glyph with the number left at Primary. `content/danger` carries the
       glyph here and the `_fill` weight marks "liked", so the state has two
       channels — the same resolution comment.__likes already uses.
   D3  No focus and no pressed state exists on any component in these frames.
       Both are implemented, per DESIGN.md §7 and the CLAUDE.md known gaps.
   D4  `e/icon/custom/social/fb` and `…/linkedin` are third-party marks with
       their own hex fills. ASSETS.md §3.1 names them the one exception to
       "icons inherit colour", so they are <img>, not masks — the file is
       still referenced, never inlined (§6 rule 1).
   D5  The player's controls sit directly on the frame's video. There is no
       semantic token for ink on media, so they take the app-local
       `--color-content-on-artwork` stopgap tokens.semantic.css already
       declares for the greeting artwork — one place, not a second literal.
   D6  `Post / Body / Show More` fades the description into `General/White`.
       The token here is `surface/raised`, so the fade still works in Dark,
       where a white smear would sit on a #2f2f2f card.
   D7  The QR modal's Alert fills `soft/info` and then sets its body text to
       Text & Icons/Primary — `content/primary` on a filled surface, which is
       exactly what non-negotiable 6 forbids. `on-soft/info` is used instead.
   D8  Every share destination in the comp is a screenshot of a third-party
       composer (`modal/share/email`, `…/facebook`, `…/linkedin` are flattened
       PNGs). Those are not ours to build, so the three buttons open the real
       provider in a new tab and the page says so — see video.js.

   ── SHARED VOCABULARY. The card shell, `Row / Section Title`, the
   `Sections Items` group heading, `Row / Default`, `e/button v2 ✅ - Context`,
   `Community / All / Detail / Avatar` and `Post / Body / Show More` are the
   SAME DS components the event detail uses. They live once in detail.css,
   under `.detail-*`, and both pages consume them from there. What stays here
   is only what this page does differently, scoped under `.vdetail`.

   TODO(tokens): every bare px length in this file is a SIZE or a BREAKPOINT.
   DESIGN.md §12.4 records both as not built. No colour, radius, spacing,
   border-width or font-size literal appears anywhere in this file.
   ========================================================================== */

.vdetail {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

/* ============================================================================
   Content Object / Header — I23304:478311;22042:425821
   `p/card/content`: one raised card, the player at 576 of the 1016 measure and
   the summary taking the rest.
   ========================================================================== */

.vdetail-header {
  display: flex;
  align-items: stretch;
  overflow: clip;
  background: var(--color-surface-raised);
  border-radius: var(--shape-radius-card);
}

.vdetail-header__media {
  position: relative;
  flex: none;
  /* 576 of 1016 — kept as the ratio so the card reflows in the narrower bands
     instead of overflowing. Same figure event.css uses. */
  inline-size: 56.7%;
  /* TODO(tokens): size not built. 324 is the documented card height. */
  min-block-size: 324px;
  /* The frame's `Hero image` sits on black behind the video — the app-local
     `surface/media` stopgap, not a literal. See tokens.semantic.css. */
  background: var(--color-surface-media);
}

/* ============================================================================
   p/video player — the LAUNCHER

   The control bar, the clock, the seek slider and the full-screen overlay that
   used to be here are gone: playback is the floating player now (youtube.css),
   and controls wired to a video playing somewhere else are controls wired to
   nothing. See the ⚠️ above launcherMarkup() in video.js for the departure
   from `p/video player` (22075:411737) this represents.

   What is left is a <button> the size of the picture — the poster, how long it
   runs, and a disc that says it plays. Same three parts as the listing card's
   thumbnail, which is the point: the two behave alike because they do the
   same thing.
   ========================================================================== */

.vplayer {
  position: relative;
  display: block;
  inline-size: 100%;
  block-size: 100%;
  overflow: clip;
}

/* The whole poster is the target, so it is a real button — reset the UA's
   chrome rather than fight it. */
.vplayer--launch {
  padding: 0;
  border: none;
  background: var(--color-surface-media);
  cursor: pointer;
  /* `appearance: none` alone leaves Safari a bevel on <button>. */
  appearance: none;
  -webkit-appearance: none;
}

.vplayer__poster {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

/* The disc. `surface/scrim` at rest, deepening to solid `surface/media` on
   hover — the same treatment, and the same reasoning, as the listing card's
   `.ccard__play`: darken toward the picture, never go brand. */
.vplayer__disc {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  translate: -50% -50%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* TODO(tokens): size not built — DESIGN.md §12.4. 56 is the card's 40px disc
     one step up, for a frame four times the area. */
  inline-size: 56px;
  block-size: 56px;
  /* Non-negotiable 7 — the circle in this system is `shape-radius-avatar`. */
  border-radius: var(--shape-radius-avatar);
  background: var(--color-surface-scrim);
  color: var(--color-content-on-artwork);
  transition: background-color 100ms ease; /* TODO(tokens): motion not built */
}

.vplayer--launch:hover .vplayer__disc,
.vplayer--launch:active .vplayer__disc {
  background: var(--color-surface-media);
}

/* Focus is the known gap, implemented — on the button, inset so the ring is
   not clipped by the poster's own `overflow: clip`. */
.vplayer--launch:focus-visible {
  outline: var(--shape-focus-width) solid var(--color-border-focus);
  outline-offset: calc(var(--shape-focus-offset) * -2);
}

/* Runtime, bottom-end — where the listing card puts it, for the same reason:
   it is a property of the video, not of the player. */
.vplayer__runtime {
  position: absolute;
  inset-block-end: var(--space-sm);
  inset-inline-end: var(--space-sm);
  padding: var(--space-3xs) var(--space-xs);
  border-radius: var(--shape-radius-chip);
  background: var(--color-surface-scrim);
  color: var(--color-content-on-artwork);
  font-family: var(--font-family-text);
  font-size: var(--font-label-sm-size);
  line-height: var(--font-label-sm-line-height);
  font-weight: var(--font-weight-medium);
}

@media (prefers-reduced-motion: reduce) {
  .vplayer__disc {
    transition: none;
  }
}

/* ============================================================================
   Header body — Top row, Main Content, Price & Sponsors
   ========================================================================== */

.vdetail-header__body {
  display: flex;
  flex: 1 1 auto;
  min-inline-size: 0;
  flex-direction: column;
  gap: var(--space-md);
  padding: var(--space-md);
}

.vdetail-header__top {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: var(--space-xs);
}

.vdetail-header__buttons {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--space-xs);
}

/* D2 — `Content Object / Header / Likes`. The heart carries status ink and the
   count stays neutral; liked swaps the weight to `_fill`, so the state never
   rests on colour alone. */
/* An UNLIKED heart is not a red heart. Red is the liked state, and spending it
   on the default left the control looking already-pressed — the same "no
   selected state" confusion COMPONENTS.md §32 F3 logs against Comments Like,
   arrived at from the opposite direction. So: `content/primary` at rest,
   `content/danger` AND the `_fill` weight once liked. Two channels on the
   change, per ASSETS.md §3 — never colour alone. */
.vdetail-like .icon {
  color: var(--color-content-primary);
}
.vdetail-like[aria-pressed="true"] .icon {
  color: var(--color-content-danger);
  --icon-src: var(--icon-src-fill);
}

.vdetail-header__main {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-xs);
}

.vdetail-header__title {
  margin: 0;
}

/* `Content Object / Date And Time` and `… / Users` — one metadata line each. */
.vdetail-header__meta {
  display: flex;
  margin: 0;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
  color: var(--color-content-primary);
}

.vdetail-header__meta .icon {
  /* TODO(tokens): size not built. 14px is the DS's metadata icon slot. */
  inline-size: 14px;
  block-size: 14px;
  color: var(--color-content-secondary);
}

/* `Avatar v2 Group` — the overlapped stack in front of the count. The frame
   pulls each avatar 4px over the one before it. */
.vdetail-avatars {
  display: flex;
  flex: none;
  align-items: center;
}

.vdetail-avatars .avatar {
  /* The ring is what separates two overlapping faces, and it has to be the
     colour of the surface behind them — §16.1. */
  box-shadow: 0 0 0 var(--border-width-thin) var(--color-surface-raised);
}

.vdetail-avatars .avatar + .avatar {
  margin-inline-start: calc(var(--space-2xs) * -1);
}

/* `Avatar v2` at the 16px step — below avatar.css's declared ladder, which
   starts at 24. Declared here rather than added to the ladder because the
   metadata line is the only place the DS uses it.
   TODO(tokens): size not built. */
.vdetail-avatars .avatar--16 {
  inline-size: 16px;
  block-size: 16px;
}

/* `Price & Sponsors` — one row. The sponsors half of it is removed (see
   REMOVED FROM THE PAGE in video.js), so the price is all that is left in it;
   the row keeps its `space-between` so a second item can return to it. */
.vdetail-header__price-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
}

/* `Content Object / Header / Price` — Heading/MD Medium now, Body/MD struck
   through was. The listing card already renders this pair; the sizes differ,
   so it is not `.ccard__price`. */
.vdetail-price {
  display: flex;
  margin: 0;
  align-items: baseline;
  gap: var(--space-2xs);
}

.vdetail-price__now {
  color: var(--color-content-headline);
  font-family: var(--font-family-display);
  font-size: var(--font-heading-md-size);
  line-height: var(--font-heading-md-line-height);
  font-weight: var(--font-weight-medium);
  letter-spacing: var(--font-letter-spacing-default);
}

.vdetail-price__was {
  color: var(--color-content-secondary);
  font-size: var(--font-body-md-size);
  line-height: var(--font-body-md-line-height);
  text-decoration: line-through;
}

/* A logo inside an avatar is inset, not cropped to the circle — ASSETS.md §5
   rule 1: choose the variant, never restyle the mark. */
.vdetail-logo {
  /* BigHeart's logomark fills its 32x32 box; BWHI's is a wide 38x20 mask
     inside the same box, so at a 60% inset it read as a hairline. 80% with
     `contain` gives the wide mark room and still keeps the square one off the
     circle's edge — an inset, not a crop. */
  inline-size: 80%;
  block-size: 80%;
  object-fit: contain;
}

/* `Avatar v2 / Content — Text` had a rule here, sizing the initials fallback
   down for the 24px and 32px steps. The sponsors were the only avatars in this
   page that ever fell back to initials, so it went with them. */

/* ============================================================================
   Two-column content — `Content` frame, 576 + 16 + 424 inside the 1016 body
   ========================================================================== */

.vdetail__content {
  display: flex;
  align-items: start;
  gap: var(--layout-gap);
}

.vdetail__body {
  display: flex;
  flex: 1 1 auto;
  min-inline-size: 0;
  flex-direction: column;
  gap: var(--space-md);
}

.vdetail__side {
  display: flex;
  /* TODO(tokens): size not built. The Sidebar frame is a fixed 424. */
  inline-size: 424px;
  flex: none;
  flex-direction: column;
  gap: var(--space-md);
}

/* Below the width where 576 + 424 stops fitting the middle column, the two
   stack and the header card unstacks its player from its summary. Same
   threshold layout.css uses for the collapsed band.
   TODO(tokens): breakpoints not built — DESIGN.md §12.4. */
@media (max-width: 1199px) {
  .vdetail__content {
    flex-direction: column;
    align-items: stretch;
  }
  .vdetail__side {
    inline-size: auto;
  }
  .vdetail-header {
    flex-direction: column;
  }
  .vdetail-header__media {
    inline-size: 100%;
    /* 16:9 once it spans the card, which is what the player is. */
    min-block-size: 0;
    aspect-ratio: 16 / 9;
  }
}

.vdetail-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.vdetail-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

/* ============================================================================
   Body / Video Detail / Actions — I…;22039:392406
   One card holding the full-width `Watch` primary.
   ========================================================================== */

.vdetail-actions .btn {
  inline-size: 100%;
}

/* ============================================================================
   Community / All / Detail / Description — I…;24942:552013, the page-local half

   The card shell, the collapse and the `Post / Body / Show More` fade
   (D6 — the fade runs to `surface/raised`, not the frame's General/White) are
   shared; see detail.css. What is specific to this page is the prose, a single
   preformatted paragraph rather than the event detail's rich text, and the
   height it collapses to.
   ========================================================================== */

.vdetail .detail-desc {
  /* TODO(tokens): size not built. The frame's `b/co/all/detail/desc/videos` is
     198 tall and shows its five lines IN FULL — `Post / Body / Show More` is
     present in the component but has nothing to hide at this length. 220 is
     that height, so the comp's copy is never clipped (video.js then drops the
     control, rather than offering to expand what is already expanded) and
     longer copy still collapses. */
  --detail-desc-collapsed-size: 220px;
}

.vdetail .detail-desc__prose {
  margin: 0;
  color: var(--color-content-primary);
  /* The frame's copy contains a double space and a hard wrap; the text is
     data, so it is rendered as written rather than re-typeset. */
  white-space: pre-wrap;
}

/* `See Translation` — Button / Default, type Link Primary, left-aligned under
   the copy. Hidden once the translation is showing. */
.vdetail-desc__translate {
  align-self: start;
}

/* ============================================================================
   Community / All / Detail / Share — I…;22038:403121
   Five `Button / Default` tiles that wrap: icon in a soft disc, label under.
   ========================================================================== */

.vdetail-share {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
}

.vdetail-share__btn {
  display: flex;
  /* The frame's tile is `flex: 1 0 0` between a 120 floor and a 140 ceiling,
     which is what makes three fit a 424 sidebar and the last two wrap.
     TODO(tokens): size not built. */
  flex: 1 0 0;
  min-inline-size: 120px;
  max-inline-size: 140px;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  padding: var(--space-md);
  border: var(--border-width-thin) solid transparent;
  border-radius: var(--shape-radius-button);
  background: none;
  color: var(--color-content-primary);
  font-family: var(--font-family-text);
  font-size: var(--font-body-md-size);
  line-height: var(--font-body-md-line-height);
  text-align: center;
  transition: background-color 100ms ease; /* TODO(tokens): motion not built */
}
.vdetail-share__btn:hover {
  background: var(--color-soft-neutral-default);
}
/* D3 — pressed. */
.vdetail-share__btn:active {
  background: var(--color-soft-neutral-pressed);
}

/* `Icon Wrapper` — General/BG Light at a 16px radius, i.e. a disc. */
.vdetail-share__disc {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-xs);
  border-radius: var(--radius-full);
  background: var(--color-soft-neutral-default);
  color: var(--color-on-soft-neutral);
}

.vdetail-share__btn:hover .vdetail-share__disc {
  background: var(--color-soft-neutral-hover);
}

/* D4 — the social marks keep their own colours, so no mask and no disc tint
   behind them competing with the brand blue. */
.vdetail-share__mark {
  inline-size: 16px; /* TODO(tokens): size not built — the DS's 16px glyph slot */
  block-size: 16px;
}

/* ============================================================================
   Community / All / Detail / Members — the `Latest purchases` card
   I…;23794:458079. The master is the event's Going list, which prints an
   amount and a message action under each face; the video instance prints
   neither, so neither is rendered.
   ========================================================================== */

.vdetail-people {
  display: flex;
  align-items: start;
  gap: var(--space-xs);
}

/* The tile itself is `Community / All / Detail / Avatar` — `.detail-person` in
   detail.css. Four fit the 424 sidebar; the frame clips the fourth name
   mid-word, which is not copied — they wrap. */

/* ============================================================================
   Purchases modal — frame 22831:420444
   A plain `Row / Default` list, one face and one name per row, divided.
   ========================================================================== */

.vdetail-purchases {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* NO DIVIDERS. Each row carries a 32px avatar and two lines of text, which
   already groups it — a rule between every pair on top of that draws fifteen
   lines nobody needed, and the modal reads as a table instead of a list. The
   avatar column is the alignment the eye follows. */
.vdetail-purchases .detail-row {
  gap: var(--space-sm);
}

/* Room for the trailing menu, and the anchor it positions against. */
.detail-row__menu {
  position: relative;
  flex: none;
}

.detail-row__menu .dropdown {
  /* TODO(tokens): size not built. One short item, so it does not need the
     header menu's 176 — but "Message the user" plus its glyph does. */
  min-inline-size: 200px;
}

/* ============================================================================
   QR modal — 36032:120709
   ========================================================================== */

.vdetail-qr {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-lg);
}

/* The frame ships a flattened 240px QR artwork, which encodes the comp's own
   URL — so it is NOT reused: a barcode is a rendering of live data, and
   shipping that asset would hand every viewer a link to the mock.

   ⚠️ NOT BUILT. The app carries no QR encoder, and hand-rolling Reed-Solomon
   and mask selection here would ship a barcode nobody can verify without a
   camera. The square below is a labelled stand-in that prints the URL it
   stands for, so the flow, the Alert and the Download action are all real and
   only the glyph is pending.
   TODO(api): the endpoint returns the QR as an image, which is also what the
   frame assumes; drop it into an <img> here and Download saves that file. */
.vdetail-qr__code {
  /* TODO(tokens): size not built — the frame's 240px square. */
  inline-size: 240px;
  block-size: 240px;
  padding: var(--space-xs);
  border-radius: var(--shape-radius-card);
  /* A QR code must stay dark-on-light in both themes or it stops scanning, so
     this one surface deliberately does not follow the mode. The pair is the
     app-local `surface/scannable` stopgap in tokens.semantic.css — a token
     name, not a literal, and not a forked Light/Dark rule either. */
  background: var(--color-surface-scannable);
  color: var(--color-content-scannable);
}

.vdetail-qr__code {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  border: var(--border-width-thin) dashed var(--color-border-strong);
  text-align: center;
}

.vdetail-qr__pending {
  margin: 0;
  color: var(--color-content-scannable);
  font-size: var(--font-body-sm-size);
  line-height: var(--font-body-sm-line-height);
  font-weight: var(--font-weight-medium);
}

.vdetail-qr__url {
  margin: 0;
  max-inline-size: 100%;
  color: var(--color-content-scannable);
  font-size: var(--font-body-xs-size);
  line-height: var(--font-body-xs-line-height);
  overflow-wrap: anywhere;
}

/* `Alert` — §5.1. D7: `on-soft/info` on the `soft/info` fill, not the frame's
   `content/primary`. */
.vdetail-alert {
  display: flex;
  align-items: start;
  gap: var(--space-xs);
  inline-size: 100%;
  padding: var(--space-md);
  border: var(--border-width-thin) solid var(--color-border-info-default);
  border-radius: var(--shape-radius-card);
  background: var(--color-soft-info-default);
  color: var(--color-on-soft-info);
}

.vdetail-alert .icon {
  flex: none;
  --icon-src: var(--icon-src-fill);
}

.vdetail-alert p {
  margin: 0;
}

/* ============================================================================
   Dropdown / Post Actions — 36573:1310492 (org admin) / 36573:1310741 (user)

   The panel, the items and their states are dropdown.css's. Only the anchor is
   new: the frame hangs it off the kebab in the header card's top-right, so it
   opens under the trigger and aligns to its trailing edge — which is
   `.dropdown`'s default. The DS's own panel floor is 320, too wide for a menu
   of three short labels; the frame draws 144.
   ========================================================================== */

.vdetail-menu {
  position: relative;
  flex: none;
}

.vdetail-menu .dropdown {
  min-inline-size: 176px; /* TODO(tokens): size not built — the frame's 144 + room for a longer label */
}

/* ============================================================================
   MONETIZATION — the board's 🔒 / ✅ pair on the detail page

   Board: `Functionality Ideation | BigHeart`, `High-level screen flow`
   (zMvmFpUrJiMG1ONIAmNY3q, 6476:56307). The board is boxes and arrows, not
   comps, so nothing below is transcribed — it is this page's own components
   put into the two states the board names. Behaviour is video.js; the dialog
   the Buy control opens is checkout.css.
   ========================================================================== */

/* --- The trailer badge --------------------------------------------------
   The header's player is never gated: every video carries a trailer and this
   is what plays it. What it needs is a NAME, so nobody watches 1:24 of a
   52:16 sermon and concludes the player is broken.

   There was a whole locked-player treatment here — dimmed poster, a lock disc,
   "Buy this video to watch it in full", no controls. It went for two reasons.
   It was factually wrong once trailers are in the picture: the thumbnail does
   play. And "locked" is internal vocabulary for "under a paywall", not
   language the product should speak. The paywall is now said once, in the only
   place it is actionable — the price, and the Buy control beneath it.

   `Badge / Default` (§14.1) is the DS's set for this and a CLAUDE.md
   reference implementation for `on-*` pairing, so the treatment is its
   contract: a solid fill with its own ink token, never `content/primary` on
   a filled surface (DESIGN.md §4.4). Neutral, not brand — it labels what you
   are watching, it does not advertise. */
.vplayer__badge {
  position: absolute;
  /* Top-start, clear of the controls bar along the bottom edge. */
  inset-block-start: var(--space-sm);
  inset-inline-start: var(--space-sm);
  z-index: 1;
  padding: var(--space-3xs) var(--space-xs);
  border-radius: var(--shape-radius-chip);
  background: var(--color-solid-neutral-default);
  color: var(--color-on-solid-neutral);
  font-family: var(--font-family-text);
  font-size: var(--font-label-sm-size);
  line-height: var(--font-label-sm-line-height);
  font-weight: var(--font-weight-medium);
  letter-spacing: var(--font-letter-spacing-wide);
  text-transform: uppercase;
}

/* --- The header price slot, bought and free ------------------------------
   Both sit where `.vdetail-price` sits, at the same optical weight, so the
   header does not reflow between the three states. Each pairs a hue with a
   glyph AND a word — the root CLAUDE.md gap on status carried by wording
   alone, answered in all three directions at once. */
.vdetail-owned,
.vdetail-free {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  margin: 0;
  font-family: var(--font-family-text);
  font-size: var(--font-body-lg-size);
  line-height: var(--font-body-lg-line-height);
  font-weight: var(--font-weight-medium);
}

.vdetail-owned {
  color: var(--color-content-success);
}

/* Free — a chip, matching the shelf card's FREE, so the two surfaces say it
   the same way. `soft/success` with its own `on-*` partner. */
.vdetail-free > span {
  padding: var(--space-3xs) var(--space-xs);
  border-radius: var(--shape-radius-chip);
  background: var(--color-soft-success-default);
  color: var(--color-on-soft-success);
  font-size: var(--font-label-sm-size);
  line-height: var(--font-label-sm-line-height);
  letter-spacing: var(--font-letter-spacing-wide);
  text-transform: uppercase;
}

/* The Buy control's reassurance line — "One-time purchase · yours to watch any
   time. Paid securely with Stripe." — used to sit under the primary. Removed;
   see the note in actionsCard() in video.js. The card is back to one
   full-width button, which is what the `Actions` frame draws. */

/* `display: flex` on the class outranks the UA's `[hidden]` rule — the trap
   button.css documents for `.btn[hidden]`. The header's price states are
   toggled by the step machine. */
.vdetail-owned[hidden],
.vdetail-free[hidden] {
  display: none;
}
