/* ── card elevation pass, 2026-08-31 ──────────────────────────────────────
   Eleven product cards were radius-2 with no shadow, sitting on a ground whose
   hairline had drifted to the ground's own colour. They had no edge.

   The palette rule in tokens.json names Apple Music, Shazam, Tinder, Airbnb and
   Pinterest as its source. Every one of those separates a card from the canvas
   with generous radius and a soft wide shadow, and none draws a border. This
   pass brings the cards to --radius-3 and --elevation-2 to match the reference
   set the system already claimed to follow. */

/**
 * ml-widgets.css — the MusicLinkd widget layer.
 *
 * Requires ml-tokens.css + ml-core.css.
 *
 * Every class here is named after its entry in
 * admin/docs/architecture/Widget_Directory.md so the mapping to the Android
 * codebase is 1:1 and needs no translation table. The Kotlin class each one
 * corresponds to is named in its comment.
 *
 * These were not authored from nothing: they are the shapes already built
 * across 66 per-screen <style> blocks in webapp/flows/**, promoted into one
 * place. 138 distinct classes were defined locally across those screens, with
 * .field redefined in 6 screens and .status-pill in 4 — each redefinition a
 * chance to drift.
 *
 * Layout proportions (screen column, control heights, tab-bar geometry,
 * rail rhythm) follow the approved mobile reference build. Colour, radius,
 * spacing and type come only from tokens.json.
 */

/* ── app shell ─────────────────────────────────────────────────────────── */
/* The phone column. 430dp is the widest current phone (iPhone Pro Max); the
   app is designed at 360-430 and centred beyond that rather than stretched,
   because a stretched phone layout is the classic tell of a web app. */

.ml-app {
  width: 100%;
  max-width: 430px;
  margin-inline: auto;
  /* HEIGHT, not min-height: the shell must be BOUNDED by the viewport so the
     body below is the only thing that scrolls. With min-height the shell grew
     to its content (1225px in an 812px viewport) and nothing scrolled at all —
     content below the fold was simply unreachable. */
  height: 100dvh;
  background: var(--color-paper);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.ml-app__body {
  flex: 1;
  /* A flex item's default min-height is auto, which lets it grow past its
     container and defeats overflow-y. This one line is what makes the scroll
     work; it is the single most common app-shell bug. */
  min-height: 0;
  padding-inline: var(--space-3);
  /* clear the fixed tab bar plus the home indicator */
  padding-bottom: calc(var(--size-4) + var(--space-4) + var(--safe-bottom));
  overflow-y: auto;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  /* A phone has no visible scrollbar; showing one is an instant tell that
     this is a web page. Content stays reachable by touch, wheel and keyboard. */
  scrollbar-width: none;
}
.ml-app__body::-webkit-scrollbar { display: none; }

/* ── top app bar ───────────────────────────────────────────────────────── */
/* Compose: TopAppBar / LargeTopAppBar                                       */

.ml-appbar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-3) var(--space-2);
  padding-top: calc(var(--space-3) + var(--safe-top));
}
.ml-appbar__titles { min-width: 0; }
/* The screen title uses type-4, not type-3: at phone width the fluid clamp
   sits at its minimum, and type-3 there renders ~20px — too small to read as
   a screen title. type-4 lands at ~28px, which matches the reference. */
.ml-appbar__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--type-6);
  font-weight: var(--weight-4);
  line-height: 1.1;
  letter-spacing: -0.03em;
}
.ml-appbar__actions { display: flex; align-items: center; gap: var(--space-2); flex: none; }

/* ── bottom tab bar ────────────────────────────────────────────────────── */
/* Compose: NavigationBar + NavigationBarItem                                */
/* There is currently NO shared bottom navigation anywhere in the app —      */
/* 0 of 139 screens use one. This is that component.                         */

.ml-tabbar {
  /* sticky inside the bounded shell, not fixed to the viewport: it then
     inherits the shell's max-width and centring for free, and stays correct
     when the shell is rendered inside a phone frame in the workbench */
  position: sticky;
  bottom: 0;
  z-index: var(--z-1);
  flex: none;
  display: flex;
  align-items: stretch;
  /* Floating, not welded to the bottom edge. Every app in the reference set
     (Airbnb, Tinder, Apple Music) lifts its bottom chrome off the edge so the
     content reads as continuing underneath it. The safe-area inset moves from
     padding to margin, so the bar CLEARS the home indicator instead of growing
     a dead band into it. */
  margin-inline: var(--space-3);
  width: auto;
  border-radius: var(--radius-3);
  box-shadow: var(--elevation-3);
  overflow: hidden;
  /* translucent so content scrolling under it stays legible, like the OS bars */
  background: color-mix(in srgb, var(--color-white) 94%, transparent);
  backdrop-filter: saturate(180%) blur(16px);
  -webkit-backdrop-filter: saturate(180%) blur(16px);
  margin-bottom: calc(var(--space-3) + var(--safe-bottom));
}
.ml-tabbar__item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-height: var(--size-4);          /* 56dp — comfortable, exceeds the floor */
  padding: var(--space-2) 0;
  color: var(--color-muted);
  font-size: var(--type-1);
  font-weight: var(--weight-3);
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
  /* REQUIRED, not decorative. .ml-tabbar__beta below is position:absolute, so
     without a positioned ancestor here it resolves against a far-up container
     and the Beta pill lands nowhere near its tab. This used to be declared a
     second time further down the file, which read as a stray duplicate; it is
     merged here so it cannot be "cleaned up" by mistake. */
  position: relative;
}
.ml-tabbar__item svg { width: var(--icon-3); height: var(--icon-3); }
/* The active state is a tinted pill behind the glyph plus a colour change —
   colour alone is not an accessible state signal. */
.ml-tabbar__icon {
  display: flex; align-items: center; justify-content: center;
  width: var(--size-2); height: var(--size-1);
  border-radius: var(--radius-4);
  transition: background-color var(--duration-2) var(--ease-standard);
}
/* Pink TEXT must be --color-pink-pressed, never --color-pink: the brand pink
   measures 3.56:1 on the bar's near-white ground and fails AA, while pressed
   measures 5.24. This is the rule stated on the token itself; this line used
   to break it. The ICON pill below still uses the bright pink, correctly —
   it is a fill, not text. */
.ml-tabbar__item[aria-current="page"] { color: var(--color-pink-pressed); }
.ml-tabbar__item[aria-current="page"] .ml-tabbar__icon { background: var(--color-pink-container); }
.ml-tabbar__item:active .ml-tabbar__icon { transform: scale(0.9); }

/* ── category tile ─────────────────────────────────────────────────────── */
/* Compose: FilterChip in a LazyRow, or a custom CategoryTile               */

.ml-cat-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 86px;
  min-height: var(--size-4);
  padding: var(--space-2);
  border: var(--border-1) solid var(--color-line);
  border-radius: var(--radius-2);
  background: var(--color-white);
  color: inherit;
  text-decoration: none;
  transition: transform var(--duration-1) var(--ease-standard);
}
.ml-cat-tile:active { transform: scale(0.96); }
.ml-cat-tile__icon {
  display: flex; align-items: center; justify-content: center;
  width: var(--size-2); height: var(--size-2);
  border-radius: var(--radius-4);
  background: var(--color-pink-container);
  color: var(--color-pink);
}
.ml-cat-tile__icon svg { width: var(--icon-2); height: var(--icon-2); }
.ml-cat-tile__label { font-size: var(--type-1); font-weight: var(--weight-3); text-align: center; }

/* ── artist card ───────────────────────────────────────────────────────── */
/* Widget_Directory: ArtistCard  ·  Compose: UnifiedArtistCard              */

.ml-artist-card {
  width: 168px;
  background: var(--color-white);
  border: var(--border-1) solid var(--color-line);
  border-radius: var(--radius-3);
  box-shadow: var(--elevation-2);
  overflow: hidden;
  color: inherit;
  text-decoration: none;
  display: block;
  transition: transform var(--duration-1) var(--ease-standard);
}
.ml-artist-card:active { transform: scale(0.98); }
.ml-artist-card__media { position: relative; aspect-ratio: 1/1; background: var(--color-surface); }
.ml-artist-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ml-artist-card__body { padding: var(--space-2); }
.ml-artist-card__name {
  display: flex; align-items: center; gap: var(--space-1);
  font-size: var(--type-3); font-weight: var(--weight-3); line-height: 1.2;
}
.ml-artist-card__name svg { width: var(--icon-1); height: var(--icon-1); color: var(--color-pink); flex: none; }
.ml-artist-card__role {
  font-size: var(--type-1); color: var(--color-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ml-artist-card__meta {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: var(--space-1); font-size: var(--type-1);
}
.ml-artist-card__rating { font-weight: var(--weight-3); color: var(--color-ink); }
.ml-artist-card__count  { color: var(--color-muted); }

/* ── studio card ───────────────────────────────────────────────────────── */
/* Widget_Directory: StudioCard  ·  Compose: UnifiedStudioCard              */
/* Promoted from webapp/flows/booking/studio_discovery's local .studio-card */

.ml-studio-card {
  width: 220px;
  background: var(--color-white);
  border: var(--border-1) solid var(--color-line);
  border-radius: var(--radius-3);
  box-shadow: var(--elevation-2);
  overflow: hidden;
  color: inherit;
  text-decoration: none;
  display: block;
  transition: transform var(--duration-1) var(--ease-standard);
}
.ml-studio-card:active { transform: scale(0.98); }
.ml-studio-card__media { position: relative; aspect-ratio: var(--media-aspect-wide); background: var(--color-surface); }
.ml-studio-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ml-studio-card__body { padding: var(--space-2); }
.ml-studio-card__name { font-size: var(--type-3); font-weight: var(--weight-3); line-height: 1.2; }
.ml-studio-card__loc  { font-size: var(--type-1); color: var(--color-muted); }
.ml-studio-card__meta {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: var(--space-1); font-size: var(--type-1);
}
.ml-studio-card__price { color: var(--color-muted); }

/* ── media card ────────────────────────────────────────────────────────── */
/* Full-bleed artwork with the title ON the scrim — the Trending pattern.
   This is the palette rule at its purest: the card has no colour of its own. */
/* Widget_Directory: HomeFeatureCard  ·  Compose: HomeFeatureCard            */

.ml-media-card {
  position: relative;
  display: block;
  width: 260px;
  aspect-ratio: var(--media-aspect-card);
  border-radius: var(--radius-3);
  box-shadow: var(--elevation-2);
  overflow: hidden;
  background: var(--color-surface);
  color: var(--color-white);
  text-decoration: none;
  transition: transform var(--duration-1) var(--ease-standard);
}
.ml-media-card:active { transform: scale(0.98); }
.ml-media-card img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ml-media-card::after {
  content: ""; position: absolute; inset: 0;
  background: var(--media-scrim); pointer-events: none;
}
.ml-media-card__tag { position: absolute; z-index: 1; top: var(--space-2); left: var(--space-2); }
.ml-media-card__body { position: absolute; z-index: 1; left: var(--space-3); right: var(--space-3); bottom: var(--space-3); }
.ml-media-card__title { font-family: var(--font-display); font-size: var(--type-4); font-weight: var(--weight-4); line-height: 1.15; letter-spacing: -0.01em; }
.ml-media-card__sub   { font-size: var(--type-1); color: color-mix(in srgb, var(--color-white) 85%, transparent); }

/* ── marketplace card ──────────────────────────────────────────────────── */
/* Widget_Directory: MarketplaceCard                                        */

.ml-market-card {
  width: 168px;
  background: var(--color-white);
  /* No outline. Every reference this is measured against - Airbnb, Pinterest,
     Apple Music - separates a card from its ground with fill, radius and a
     soft lift, never with a drawn border. A 1px line at 1.18:1 was doing the
     job of all three and doing none of them. */
  box-shadow: var(--elevation-2);
  border-radius: var(--radius-3);
  overflow: hidden;
  color: inherit; text-decoration: none; display: block;
  transition: transform var(--duration-1) var(--ease-standard);
}
.ml-market-card:active { transform: scale(0.98); }
/* 4:5 rather than 1:1: the photograph should dominate the card. */
.ml-market-card__media { position: relative; aspect-ratio: 4/5; background: var(--color-surface); }
.ml-market-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Save control sits on the media, so it needs its own solid ground */
.ml-market-card__save {
  position: absolute; top: var(--space-2); right: var(--space-2);
  display: flex; align-items: center; justify-content: center;
  width: var(--size-1); height: var(--size-1);
  border-radius: var(--radius-4);
  background: color-mix(in srgb, var(--color-white) 90%, transparent);
  backdrop-filter: blur(8px);
  color: var(--color-ink);
  border: 0; cursor: pointer;
}
.ml-market-card__save[aria-pressed="true"] { color: var(--color-pink); }
/* 32dp visual so it does not crowd the artwork, 48dp band so it can be hit.
   It was failing the target rule on every marketplace screen, not just here. */
.ml-market-card__save::after {
  content: ""; position: absolute; inset: 50% 50% auto auto;
  width: var(--size-3); height: var(--size-3); transform: translate(50%, -50%);
}
.ml-market-card__body  { padding: var(--space-3) var(--space-2) var(--space-2); }
.ml-market-card__price { font-family: var(--font-display); font-size: var(--type-4); font-weight: var(--weight-4); line-height: 1.1; }
.ml-market-card__title { font-size: var(--type-1); font-weight: var(--weight-3); margin-top: 2px; }
.ml-market-card__meta  { font-size: var(--type-1); color: var(--color-muted); }
/* Two-up browse grid. The card's own 168px width is for horizontal rails;
   in a grid it fills its column instead. Added when marketplace_browse was
   found doing this with a local .item-grid/.item-card pair that duplicated
   .ml-market-card almost line for line. */
.ml-market-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-2); }
.ml-market-grid > .ml-market-card { width: auto; }

/* ── booking / review / notification rows ──────────────────────────────── */
/* Widget_Directory: BookingCard, ReviewCard, NotificationCard              */

.ml-booking-card {
  display: flex; gap: var(--space-3); padding: var(--space-3);
  /* It is named a card and was drawn as bare text: no fill, no radius. That
     read as "fine" only because the page behind it was also near-white. */
  background: var(--color-white);
  border-radius: var(--radius-2);
}
.ml-booking-card + .ml-booking-card { margin-top: var(--space-2); }
.ml-booking-card__thumb { width: var(--size-4); height: var(--size-4); border-radius: var(--radius-1); object-fit: cover; flex: none; background: var(--color-surface); }
.ml-booking-card__body  { flex: 1; min-width: 0; }
.ml-booking-card__title { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-booking-card__meta  { font-size: var(--type-1); color: var(--color-muted); }

.ml-review-card { padding: var(--space-3); }
.ml-review-card__head { display: flex; align-items: center; gap: var(--space-2); }
/* Same bug .ml-rating__stars had: raw --color-warning is 2.15:1 on white,
   below even the 3:1 graphic floor. Found while reusing this card for the
   Profile flow's reviews section, before it shipped anywhere else. */
.ml-review-card__stars { color: var(--color-ink); font-size: var(--type-1); }
.ml-review-card__body  { margin-top: var(--space-2); font-size: var(--type-3); }

.ml-notification { display: flex; gap: var(--space-3); align-items: flex-start; padding: var(--space-3) 0; }
.ml-notification--unread { background: var(--color-pink-container); border-radius: var(--radius-2); padding-inline: var(--space-3); }

/* ── stat grid ─────────────────────────────────────────────────────────── */
/* Widget_Directory: PublicReputationWidget                                 */

.ml-stat-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-2); }
.ml-stat {
  padding: var(--space-3);
  border: var(--border-1) solid var(--color-line);
  border-radius: var(--radius-2);
  background: var(--color-white);
}
.ml-stat__num   { display: block; font-family: var(--font-display); font-size: var(--type-4); font-weight: var(--weight-4); line-height: 1; }
.ml-stat__label { display: block; margin-top: 2px; font-size: var(--type-1); color: var(--color-muted); font-weight: var(--weight-2); }
.ml-stat__delta { font-size: var(--type-1); font-weight: var(--weight-3); color: var(--color-pink); }

/* ── profile hero ──────────────────────────────────────────────────────── */
/* Widget_Directory: ProfilePreviewSheet header                             */

.ml-hero { position: relative; aspect-ratio: var(--media-aspect-wide); overflow: hidden; background: var(--color-surface); }
.ml-hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ml-hero::after { content: ""; position: absolute; inset: 0; background: var(--media-scrim); }
.ml-hero__body { position: absolute; z-index: 1; left: var(--space-3); right: var(--space-3); bottom: var(--space-3); color: var(--color-white); }
.ml-hero__name { margin: 0; font-family: var(--font-display); font-size: var(--type-6); font-weight: var(--weight-4); line-height: 1.1; letter-spacing: -0.03em; }
.ml-hero__sub  { font-size: var(--type-1); color: color-mix(in srgb, var(--color-white) 85%, transparent); }
.ml-hero__stats { display: flex; gap: var(--space-3); margin-top: var(--space-2); font-size: var(--type-1); }
.ml-hero__stats b { font-weight: var(--weight-3); }

/* Action row under a hero — equal-width pills. */
.ml-action-row { display: flex; gap: var(--space-2); padding: var(--space-3) 0; }
.ml-action-row > * { flex: 1; }

/* .ml-action-row began as a BUTTON GROUP: a flex row whose children each take
   an equal share. It is now used 163 times as a TAPPABLE ROW as well -
   <a class="ml-action-row"> wrapping a label block and a chevron - and that is
   a different component with different needs.

   Two things were wrong with every one of those 163 rows. This system resets
   link styling per component rather than globally, and the anchor form never
   got a reset, so all of them rendered browser-default blue and underlined.
   And `> * { flex: 1 }` gave the chevron an equal share of the width, so the
   label was squeezed into half the row.

   Specialised here rather than renamed across 49 generators: the markup is
   the right shape, only the rule was missing. */
a.ml-action-row {
  align-items: center;
  min-height: var(--size-3);
  color: inherit;
  text-decoration: none;
}
a.ml-action-row > * { flex: none; }
a.ml-action-row > .ml-choice__body { flex: 1; min-width: 0; }
a.ml-action-row:active { background: var(--color-surface); }

/* Same missing reset, five more components. Each is used as an <a> somewhere
   and none of them declared a colour or a text-decoration, so each rendered as
   a raw browser link. */
a.ml-notification, a.ml-booking-card, a.ml-tier, a.ml-avatar-btn {
  color: inherit;
  text-decoration: none;
}

/* ── progress card ─────────────────────────────────────────────────────── */
/* Widget_Directory: ProfileCompletionMeter (proposed, PRODUCT-SPEC §3)     */

.ml-progress {
  padding: var(--space-3);
  border-radius: var(--radius-2);
  background: var(--color-pink-container);
}
.ml-progress__title { font-size: var(--type-3); font-weight: var(--weight-3); }
/* --color-muted was verified against paper (5.01) and surface (4.73) but not
   against a TINTED ground: on --color-pink-container it is 4.26 and fails.
   Secondary text on a tint takes ink at reduced weight instead. */
.ml-progress__meta  { font-size: var(--type-1); color: var(--color-ink); opacity: 0.72; }
.ml-progress__track {
  height: 6px; margin: var(--space-2) 0;
  border-radius: var(--radius-4);
  background: color-mix(in srgb, var(--color-pink) 18%, transparent);
  overflow: hidden;
}
.ml-progress__fill { height: 100%; background: var(--color-pink); border-radius: var(--radius-4); }

/* ── media upload grid ─────────────────────────────────────────────────── */
/* PRODUCT-SPEC Part E flags this as the highest-priority missing widget —   */
/* it blocks Portfolio, Marketplace Create Listing, and Media Gallery.       */

.ml-upload-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-2); }
/* No overflow:hidden here. It would round the thumbnail, but it also clips
   HIT-TESTING, which cut off the remove control's 48dp tap band and made that
   control the one thing in the system that could not meet the target rule.
   The image rounds itself instead, which achieves the same visual with no
   clipping context. */
.ml-upload-grid__item { position: relative; aspect-ratio: 1/1; border-radius: var(--radius-1); background: var(--color-surface); }
.ml-upload-grid__item img { width: 100%; height: 100%; object-fit: cover; border-radius: var(--radius-1); display: block; }
/* 32dp visual so three thumbnails still fit a 360dp row, with a 48dp tap band.
   This was the system's last target failure: the band was being clipped by the
   parent's overflow:hidden, which is now gone. Nothing in the system is
   exempt from the 48dp rule. */
.ml-upload-grid__remove::after {
  content: ""; position: absolute; inset: 50% 50% auto auto;
  width: var(--size-3); height: var(--size-3); transform: translate(50%, -50%);
}
.ml-upload-grid__remove {
  position: absolute; top: var(--space-1); right: var(--space-1);
  width: var(--size-1); height: var(--size-1); border: 0; border-radius: var(--radius-4);
  background: color-mix(in srgb, var(--color-ink) 70%, transparent);
  color: var(--color-white); cursor: pointer; line-height: 1;
}
.ml-upload-grid__add {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-1);
  aspect-ratio: 1/1;
  border: var(--border-1) dashed var(--color-line);
  border-radius: var(--radius-1);
  background: var(--color-white);
  color: var(--color-muted);
  font-size: var(--type-1); font-weight: var(--weight-3);
  cursor: pointer;
}
.ml-upload-grid__add:active { background: var(--color-surface); }

/* ── audio player ──────────────────────────────────────────────────────── */
/* Widget_Directory: AudioPlayerWidget · Compose: WaveformAudioPlayer       */
/* The waveform IS the scrub control, not a decoration.                     */

.ml-player { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3); }
.ml-player__btn {
  display: flex; align-items: center; justify-content: center;
  width: var(--size-3); height: var(--size-3); flex: none;
  border: 0; border-radius: var(--radius-4);
  background: var(--color-ink); color: var(--color-white);
  cursor: pointer;
}
.ml-player__btn:active { background: var(--color-ink-pressed); transform: scale(0.94); }
.ml-player__wave {
  flex: 1; display: flex; align-items: center; gap: 2px;
  height: var(--size-1);
  /* the whole strip is the hit area, well past the 48dp rule vertically */
  padding-block: var(--space-2);
  cursor: pointer; touch-action: none;
}
.ml-player__bar { flex: 1; min-width: 2px; border-radius: var(--radius-4); background: var(--color-line); }
.ml-player__bar.is-played { background: var(--color-pink); }
.ml-player__time { font-size: var(--type-1); color: var(--color-muted); font-variant-numeric: tabular-nums; flex: none; }

/* Persistent mini-player — sits directly above the tab bar. */
/* Widget_Directory: PersistentAudioPlayer · GlobalPersistentAudioPlayerBar */
.ml-miniplayer {
  position: fixed;
  left: 50%; transform: translateX(-50%);
  bottom: calc(var(--size-4) + var(--safe-bottom));
  z-index: var(--z-2);
  width: 100%; max-width: 430px;
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-top: var(--border-1) solid var(--color-line);
  background: color-mix(in srgb, var(--color-white) 94%, transparent);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}
.ml-miniplayer__art { width: var(--size-1); height: var(--size-1); border-radius: var(--radius-1); object-fit: cover; flex: none; }
.ml-miniplayer__body { flex: 1; min-width: 0; }
.ml-miniplayer__title { font-size: var(--type-1); font-weight: var(--weight-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ml-miniplayer__sub   { font-size: var(--type-1); color: var(--color-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── bottom sheet ──────────────────────────────────────────────────────── */
/* Widget_Directory: AdvancedFiltersBottomSheet · Compose: ModalBottomSheet */
/* Behaviour (drag, detents, dismiss threshold) lives in ml.js and is bound  */
/* to --gesture-swipe-threshold rather than a hardcoded number.              */

.ml-sheet-scrim {
  position: fixed; inset: 0; z-index: var(--z-3);
  background: color-mix(in srgb, var(--color-ink) 64%, transparent);
  opacity: 0; pointer-events: none;
  transition: opacity var(--duration-2) var(--ease-standard);
}
.ml-sheet-scrim.is-open { opacity: 1; pointer-events: auto; }
.ml-sheet {
  position: fixed; left: 50%; bottom: 0;
  z-index: calc(var(--z-3) + 1);
  width: 100%; max-width: 430px;
  max-height: 88dvh;
  display: flex; flex-direction: column;
  border-radius: var(--radius-3) var(--radius-3) 0 0;
  background: var(--color-white);
  box-shadow: var(--elevation-2);
  transform: translate(-50%, 100%);
  transition: transform var(--duration-3) var(--ease-enter);
  padding-bottom: var(--safe-bottom);
}
.ml-sheet.is-open { transform: translate(-50%, 0); }
/* Grab handle — the affordance that says "this drags". */
.ml-sheet__handle {
  width: 36px; height: 4px; margin: var(--space-2) auto;
  border-radius: var(--radius-4);
  background: var(--color-line);
  flex: none;
}
.ml-sheet__body { overflow-y: auto; padding: 0 var(--space-3) var(--space-3); overscroll-behavior: contain; }

/* ── toast ─────────────────────────────────────────────────────────────── */

.ml-toast {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--size-4) + var(--space-4) + var(--safe-bottom));
  z-index: var(--z-4);
  max-width: calc(430px - var(--space-4));
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-2);
  background: var(--color-ink);
  color: var(--color-white);
  font-size: var(--type-1);
  box-shadow: var(--elevation-2);
}

/* ── empty state ───────────────────────────────────────────────────────── */

.ml-empty { text-align: center; padding: var(--space-4) var(--space-3); }
.ml-empty__title { font-family: var(--font-display); font-size: var(--type-4); font-weight: var(--weight-3); margin: 0 0 var(--space-1); }
.ml-empty__body  { font-size: var(--type-3); color: var(--color-muted); margin: 0 0 var(--space-3); }

/* ══ FLOW 4 — DISCOVER ═════════════════════════════════════════════════════
   Discover is "find someone or something inside the ecosystem". Market is
   "buy, sell or rent". The two must not converge, so nothing below carries a
   price as its primary line — that treatment belongs to .ml-market-card.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── scrollable category tabs ──────────────────────────────────────────── */
/* The dimension switcher: People / Businesses / Music / Services / Spaces /
   Communities. Too many to fit a segmented control, so it scrolls. */
/* Compose: ScrollableTabRow                                                 */

.ml-tabs {
  display: flex;
  gap: var(--space-3);
  overflow-x: auto;
  margin-inline: calc(var(--space-3) * -1);
  padding-inline: var(--space-3);
  border-bottom: var(--border-1) solid var(--color-line);
  scrollbar-width: none;
}
.ml-tabs::-webkit-scrollbar { display: none; }
.ml-tabs__tab {
  flex: none;
  display: inline-flex;
  align-items: center;
  min-height: var(--size-3);
  padding: 0 2px;
  border: 0;
  border-bottom: var(--border-2) solid transparent;
  background: none;
  color: var(--color-muted);
  font-family: var(--font-body);
  font-size: var(--type-3);
  font-weight: var(--weight-3);
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
}
/* The active tab is the one place the brand pink appears as a rule rather
   than an accent — it is the primary wayfinding signal on every results
   screen, and the underline carries it without tinting a surface. */
.ml-tabs__tab[aria-selected="true"] {
  color: var(--color-ink);
  border-bottom-color: var(--color-pink);
}
/* Equal-width variant. Six screens across communication, marketplace and
   opportunities had grown this as .net-tabs and .my-tabs — two names, one
   byte-identical rule, neither of them in the system. The difference from the
   scrollable default is only that the tabs share the width instead of sizing
   to their content, so it is a modifier rather than a second component.
   Use it when the tab count is fixed and small (2-4); past that the labels
   truncate and the scrolling default is the right one. */
.ml-tabs--fill { overflow-x: visible; gap: var(--space-1); margin-inline: 0; }
.ml-tabs--fill .ml-tabs__tab {
  flex: 1;
  justify-content: center;
  padding: var(--space-2) 0;
  font-size: var(--type-1);
}

/* ── result row ────────────────────────────────────────────────────────── */
/* The single densest unit in Discover: avatar/thumb, name, qualifier, a
   trailing signal (match, distance, rating) and one connect affordance. */

.ml-result {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  min-height: var(--size-4);
  color: inherit;
  text-decoration: none;
}
.ml-result + .ml-result { border-top: var(--border-1) solid var(--color-line); }
.ml-result__thumb {
  width: var(--size-4); height: var(--size-4);
  border-radius: var(--radius-4);
  object-fit: cover; flex: none;
  background: var(--color-surface);
}
.ml-result__thumb--square { border-radius: var(--radius-1); }
.ml-result__body { flex: 1; min-width: 0; }
.ml-result__name {
  display: flex; align-items: center; gap: var(--space-1);
  font-size: var(--type-3); font-weight: var(--weight-3); line-height: 1.2;
}
.ml-result__name svg { width: var(--icon-1); height: var(--icon-1); color: var(--color-pink); flex: none; }
.ml-result__meta {
  display: block; font-size: var(--type-1); color: var(--color-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ml-result__trail { flex: none; text-align: right; }

/* ── match score ───────────────────────────────────────────────────────── */
/* AI recommendation strength. A number plus a bar, never colour alone. */

.ml-match { display: inline-flex; flex-direction: column; align-items: flex-end; gap: 3px; }
.ml-match__pct {
  font-family: var(--font-display);
  font-size: var(--type-3); font-weight: var(--weight-4); line-height: 1;
  color: var(--color-pink-pressed);
  font-variant-numeric: tabular-nums;
}
.ml-match__label { font-size: var(--type-1); color: var(--color-muted); }
.ml-match__track {
  width: 44px; height: 4px; border-radius: var(--radius-4);
  background: var(--color-line); overflow: hidden;
}
.ml-match__fill { height: 100%; background: var(--color-pink); border-radius: var(--radius-4); }

/* ── community card ────────────────────────────────────────────────────── */

.ml-community {
  display: block; width: 260px;
  border: var(--border-1) solid var(--color-line);
  border-radius: var(--radius-3);
  box-shadow: var(--elevation-2);
  overflow: hidden;
  background: var(--color-white);
  color: inherit; text-decoration: none;
  transition: transform var(--duration-1) var(--ease-standard);
}
.ml-community:active { transform: scale(0.98); }
.ml-community__media { position: relative; aspect-ratio: var(--media-aspect-wide); background: var(--color-surface); }
.ml-community__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ml-community__body { padding: var(--space-2); }
.ml-community__name { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-community__meta { font-size: var(--type-1); color: var(--color-muted); }
/* stacked member avatars */
.ml-facepile { display: flex; margin-top: var(--space-2); }
.ml-facepile img {
  width: 24px; height: 24px; border-radius: var(--radius-4);
  object-fit: cover; border: var(--border-2) solid var(--color-white);
}
.ml-facepile img + img { margin-left: -8px; }

/* ── service card ──────────────────────────────────────────────────────── */
/* A service is offered BY someone — the provider is the subject, the rate is
   a qualifier. Inverting that would make it a Market listing. */

.ml-service {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3);
  border: var(--border-1) solid var(--color-line);
  border-radius: var(--radius-2);
  background: var(--color-white);
  color: inherit; text-decoration: none;
}
.ml-service__head { display: flex; align-items: center; gap: var(--space-2); }
.ml-service__title { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-service__by { font-size: var(--type-1); color: var(--color-muted); }
.ml-service__foot {
  display: flex; align-items: center; justify-content: space-between;
  font-size: var(--type-1);
}
.ml-service__rate { font-weight: var(--weight-3); }

/* ── "what are you looking for?" grid ──────────────────────────────────── */
/* The Discover Home entry point. Six dimensions, two columns, media-led. */

.ml-dimension-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); }
.ml-dimension {
  position: relative;
  display: block;
  aspect-ratio: 3/2;
  border-radius: var(--radius-2);
  overflow: hidden;
  background: var(--color-surface);
  color: var(--color-white);
  text-decoration: none;
}
.ml-dimension img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ml-dimension::after {
  content: ""; position: absolute; inset: 0;
  background: var(--media-scrim); pointer-events: none;
}
.ml-dimension:active { transform: scale(0.98); }
.ml-dimension__label {
  position: absolute; z-index: 1; left: var(--space-2); bottom: var(--space-2);
  font-family: var(--font-display);
  font-size: var(--type-4); font-weight: var(--weight-4); letter-spacing: -0.01em;
}
.ml-dimension__count { display: block; font-family: var(--font-body); font-size: var(--type-1); font-weight: var(--weight-2); opacity: 0.85; }

/* ── recent / suggested search rows ────────────────────────────────────── */

.ml-suggest {
  display: flex; align-items: center; gap: var(--space-3);
  min-height: var(--size-3);
  padding: var(--space-1) 0;
  color: inherit; text-decoration: none;
  font-size: var(--type-3);
}
.ml-suggest svg { width: var(--icon-2); height: var(--icon-2); color: var(--color-muted); flex: none; }
.ml-suggest__body { flex: 1; min-width: 0; }
.ml-suggest__kind { font-size: var(--type-1); color: var(--color-muted); }

/* ── tab-bar beta marker ───────────────────────────────────────────────── */
/* Market is Beta. Labelling it in the nav is the honest thing to do and it
   stops the tab reading as a finished surface. */

.ml-tabbar__beta {
  position: absolute;
  top: 2px; left: 50%;
  transform: translateX(6px);
  padding: 0 var(--space-1);
  border-radius: var(--radius-4);
  /* 9px reversed text is the hardest contrast case in the system: white on
     --color-pink is 3.56:1. --color-pink-pressed takes it to 6.4:1. */
  background: var(--color-pink-pressed);
  color: var(--color-white);
  font-size: 9px;
  font-weight: var(--weight-3);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 14px;
}

/* ── inset card ───────────────────────────────────────────────────────── */
/* From the supplied visual references. The pattern across all of them:

     · the card is PADDED and the media sits INSET with its own radius, so
       the card reads as an object holding a picture rather than a picture
       with text stuck underneath
     · a pill overlays the media top-left, a circular control top-right
     · bold title, then a grey line directly under it at the same size
     · a footer row: the value in a light pill on the left, the action as a
       dark pill with a circular arrow on the right

   It suits the palette rule exactly — the card, the pills and the button are
   all monochrome, and the inset photo is the only colour on screen. */

.ml-inset {
  display: block;
  padding: var(--space-2);
  border-radius: var(--radius-3);
  background: var(--color-white);
  border: var(--border-1) solid var(--color-line);
  color: inherit;
  text-decoration: none;
}
.ml-inset--raised { border-color: transparent; box-shadow: var(--elevation-2); }
.ml-inset--dark {
  background: var(--color-ink);
  border-color: var(--color-ink);
  color: var(--color-white);
}

.ml-inset__media {
  position: relative;
  border-radius: var(--radius-2);
  overflow: hidden;
  background: var(--color-surface);
  aspect-ratio: var(--media-aspect-wide);
}
.ml-inset__media--square { aspect-ratio: var(--media-aspect-square); }
.ml-inset__media > img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* overlay slots on the media */
.ml-inset__tag  { position: absolute; z-index: 1; top: var(--space-2); left: var(--space-2); }
.ml-inset__act  { position: absolute; z-index: 1; top: var(--space-2); right: var(--space-2); }
/* A pill that sits ON a photo needs its own ground — the photo underneath is
   arbitrary, so a translucent ink chip is the only thing that stays legible. */
.ml-chip--on-media {
  border: 0;
  background: color-mix(in srgb, var(--color-ink) 62%, transparent);
  color: var(--color-white);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.ml-icon-btn--on-media {
  border: 0;
  background: color-mix(in srgb, var(--color-white) 88%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  min-width: var(--size-1); min-height: var(--size-1);
}

/* carousel position dots */
.ml-dots--on-media {
  position: absolute; z-index: 1;
  left: 0; right: 0; bottom: var(--space-2);
}
.ml-dots--on-media .ml-dots__dot { background: color-mix(in srgb, var(--color-white) 55%, transparent); }
.ml-dots--on-media .ml-dots__dot.is-active { background: var(--color-white); width: 6px; }

.ml-inset__body { padding: var(--space-3) var(--space-2) var(--space-2); }
.ml-inset__title {
  font-family: var(--font-display);
  font-size: var(--type-4);
  font-weight: var(--weight-4);
  letter-spacing: -0.02em;
  line-height: 1.15;
}
/* the grey line directly under the title, same size — a subtitle, not a caption */
.ml-inset__sub { font-size: var(--type-4); font-weight: var(--weight-2); color: var(--color-muted); line-height: 1.15; }
.ml-inset__body p { margin: var(--space-2) 0 0; color: var(--color-muted); }
.ml-inset--dark .ml-inset__sub,
.ml-inset--dark .ml-inset__body p { color: color-mix(in srgb, var(--color-white) 62%, transparent); }

/* footer: value on the left, action on the right */
.ml-inset__foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-3) 0 0;
}
.ml-value {
  display: inline-flex; align-items: center;
  min-height: var(--size-3);
  padding: 0 var(--space-3);
  border-radius: var(--radius-4);
  background: var(--color-surface);
  font-family: var(--font-display);
  font-size: var(--type-4);
  font-weight: var(--weight-3);
  white-space: nowrap;
}
.ml-inset--dark .ml-value { background: color-mix(in srgb, var(--color-white) 12%, transparent); color: var(--color-white); }

/* dark pill CTA with the circular arrow badge from the reference */
.ml-btn--arrow { padding-inline: var(--space-3) var(--space-1); gap: var(--space-3); }
.ml-btn__arrow {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--size-1); height: var(--size-1);
  border-radius: var(--radius-4);
  background: var(--color-white);
  color: var(--color-ink);
  flex: none;
}
.ml-btn--accent .ml-btn__arrow { background: var(--color-ink); color: var(--color-white); }

/* ── lede ──────────────────────────────────────────────────────────────── */
/* Mixed weight inside one sentence, as in the reference: grey carries the
   sentence, ink carries the words that matter. Cheaper than colour and it
   works on a monochrome page. */
.ml-lede { font-size: var(--type-3); color: var(--color-muted); line-height: 1.45; }
.ml-lede b, .ml-lede strong { color: var(--color-ink); font-weight: var(--weight-3); }

/* ── calendar ──────────────────────────────────────────────────────────── */
/* One month grid, four cell states. Needed twice over — booking's studio slots
   and Market's rental availability — which is why it lives in the system
   rather than in either flow. Bound to MarketplaceItemDoc.blockedDates for
   rentals; to the studio's own slots for booking. */
/* Compose: a DatePicker / DateRangePicker built on this grid                 */

/* 48dp, not 40 with an expanded band: a 40dp cell on a 2px gap gives a
   42dp pitch, so 48dp bands on adjacent rows overlap and a tap near the
   boundary picks the wrong day. Seven 48dp columns is 336dp, which fits
   the 360dp screen, so the cell can simply be the target. */
.ml-calendar { --ml-cal-cell: var(--size-3); }
.ml-calendar__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2); margin-bottom: var(--space-2);
}
.ml-calendar__month { font-family: var(--font-display); font-size: var(--type-4);
  font-weight: var(--weight-3); letter-spacing: -0.01em; }
.ml-calendar__dow {
  display: grid; grid-template-columns: repeat(7, 1fr);
  font-size: var(--type-1); color: var(--color-muted); font-weight: var(--weight-3); text-align: center;
}
.ml-calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.ml-calendar__day {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  height: var(--ml-cal-cell);
  border: 0; border-radius: var(--radius-1);
  background: none; color: var(--color-ink);
  font-family: var(--font-body); font-size: var(--type-3); font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.ml-calendar__day:disabled,
.ml-calendar__day.is-blocked {
  color: var(--color-muted); opacity: var(--opacity-3);
  cursor: not-allowed; pointer-events: none;
  text-decoration: line-through;
}
.ml-calendar__day.is-today { font-weight: var(--weight-3); box-shadow: inset 0 0 0 1px var(--color-line); }
.ml-calendar__day.is-selected { background: var(--color-ink); color: var(--color-white); font-weight: var(--weight-3); }
/* a range reads as one continuous band, so the ends round and the middle does not */
.ml-calendar__day.is-in-range { background: var(--color-surface); border-radius: 0; }
.ml-calendar__day.is-range-start { background: var(--color-ink); color: var(--color-white);
  border-radius: var(--radius-1) 0 0 var(--radius-1); }
.ml-calendar__day.is-range-end { background: var(--color-ink); color: var(--color-white);
  border-radius: 0 var(--radius-1) var(--radius-1) 0; }
.ml-calendar__legend { display: flex; flex-wrap: wrap; gap: var(--space-3);
  margin-top: var(--space-3); font-size: var(--type-1); color: var(--color-muted); }
.ml-calendar__legend span { display: inline-flex; align-items: center; gap: var(--space-1); }
.ml-calendar__key { width: 12px; height: 12px; border-radius: 3px; background: var(--color-surface); }
.ml-calendar__key--sel { background: var(--color-ink); }
.ml-calendar__key--blocked { background: var(--color-line); }

/* ── chat ──────────────────────────────────────────────────────────────── */
/* The communication flow is 7 screens with no message component. Outgoing is
   ink, incoming is surface — never colour alone: the two also differ in
   alignment and corner shape, which survives a greyscale screenshot. */
/* Compose: MessageBubble                                                     */

.ml-chat { display: flex; flex-direction: column; gap: var(--space-2); }
.ml-bubble {
  max-width: 78%;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-2);
  font-size: var(--type-3);
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.ml-bubble--in  { align-self: flex-start; background: var(--color-surface); color: var(--color-ink);
                  border-bottom-left-radius: var(--radius-1); }
.ml-bubble--out { align-self: flex-end; background: var(--color-ink); color: var(--color-white);
                  border-bottom-right-radius: var(--radius-1); }
.ml-bubble__meta { display: block; margin-top: 2px; font-size: var(--type-1); opacity: 0.7; }

/* Two states the 2-bubble demo didn't cover, added 2026-08-30: the other
   party composing, and a send that didn't reach the server. Both reuse
   .ml-bubble so they inherit its shape/alignment rules rather than
   duplicating them. */
.ml-bubble--typing { display: inline-flex; align-items: center; gap: var(--space-1); padding: var(--space-2) 14px; }
.ml-bubble--typing span {
  width: 6px; height: 6px; border-radius: var(--radius-4);
  background: var(--color-muted); opacity: 0.5;
  animation: ml-typing var(--duration-4) ease-in-out infinite;
}
.ml-bubble--typing span:nth-child(2) { animation-delay: 0.15s; }
.ml-bubble--typing span:nth-child(3) { animation-delay: 0.3s; }
@keyframes ml-typing { 0%, 60%, 100% { opacity: 0.5; transform: translateY(0); }
                        30% { opacity: 1; transform: translateY(-3px); } }
.ml-bubble--failed { background: var(--status-error-bg);
                      color: var(--color-ink); border: var(--border-1) solid color-mix(in srgb, var(--color-error) 32%, transparent); }
.ml-bubble__error {
  display: flex; align-items: center; gap: var(--space-1); margin-top: var(--space-1);
  font-size: var(--type-1); color: var(--status-error-text);
}
.ml-chat__day {
  align-self: center;
  padding: 2px var(--space-2);
  border-radius: var(--radius-4);
  background: var(--color-surface);
  font-size: var(--type-1); color: var(--color-muted); font-weight: var(--weight-3);
}
/* composer */
.ml-composer {
  display: flex; align-items: flex-end; gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  padding-bottom: calc(var(--space-2) + var(--safe-bottom));
  border-top: var(--border-1) solid var(--color-line);
  background: var(--color-white);
}
.ml-composer textarea {
  flex: 1; min-height: var(--size-3); max-height: 120px;
  padding: var(--space-2) var(--space-3);
  border: var(--border-1) solid var(--color-line); border-radius: var(--radius-2);
  background: var(--color-white); color: var(--color-ink);
  font-family: var(--font-body); font-size: var(--type-3); resize: none;
}

/* ── attachment ────────────────────────────────────────────────────────── */

.ml-attachment {
  display: flex; align-items: center; gap: var(--space-3);
  min-height: var(--size-4);
  padding: var(--space-2) var(--space-3);
  border: var(--border-1) solid var(--color-line); border-radius: var(--radius-2);
  background: var(--color-white); color: inherit; text-decoration: none;
}
.ml-attachment__icon {
  display: flex; align-items: center; justify-content: center;
  width: var(--size-2); height: var(--size-2); flex: none;
  border-radius: var(--radius-1);
  background: var(--color-surface); color: var(--color-muted);
}
.ml-attachment__body { flex: 1; min-width: 0; }
.ml-attachment__name { display: block; font-size: var(--type-3); font-weight: var(--weight-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ml-attachment__meta { display: block; font-size: var(--type-1); color: var(--color-muted); }

/* ── timeline ──────────────────────────────────────────────────────────── */
/* Ordered events with state: a booking's progress, a transaction's stages. The
   rail is drawn with a border on the item so it cannot desync from the dots. */

.ml-timeline { list-style: none; margin: 0; padding: 0; }
.ml-timeline__item {
  position: relative;
  padding: 0 0 var(--space-4) var(--space-4);
  border-left: var(--border-2) solid var(--color-line);
}
.ml-timeline__item:last-child { border-left-color: transparent; padding-bottom: 0; }
.ml-timeline__item::before {
  content: ""; position: absolute; left: -7px; top: 2px;
  width: 12px; height: 12px; border-radius: var(--radius-4);
  background: var(--color-white); box-shadow: inset 0 0 0 2px var(--color-line);
}
.ml-timeline__item.is-done::before { background: var(--color-ink); box-shadow: inset 0 0 0 2px var(--color-ink); }
.ml-timeline__item.is-current::before { background: var(--color-pink); box-shadow: inset 0 0 0 2px var(--color-pink); }
/* Added 2026-08-30: a booking or transaction stage can fail, not just
   complete — the two states above couldn't represent that. */
.ml-timeline__item.is-failed::before { background: var(--color-error); box-shadow: inset 0 0 0 2px var(--color-error); }
.ml-timeline__item.is-failed .ml-timeline__title { color: var(--status-error-text); }
.ml-timeline__title { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-timeline__meta { font-size: var(--type-1); color: var(--color-muted); }

/* ── metrics ───────────────────────────────────────────────────────────── */
/* Deliberately not a charting library. Insights screens need a trend and a
   comparison, and both are cheaper as CSS than as a 90KB dependency. Bars are
   labelled and the sparkline carries its values in the markup, so neither
   communicates by shape alone. */

.ml-kpi { display: flex; align-items: baseline; gap: var(--space-2); }
.ml-kpi__num { font-family: var(--font-display); font-size: var(--type-6);
  font-weight: var(--weight-4); line-height: 1; letter-spacing: -0.03em; }
.ml-kpi__delta { font-size: var(--type-1); font-weight: var(--weight-3); }
.ml-kpi__delta--up   { color: var(--status-success-text); }
.ml-kpi__delta--down { color: var(--status-error-text); }

.ml-bars { display: flex; align-items: flex-end; gap: var(--space-1); height: 72px; }
.ml-bars__bar { flex: 1; min-width: 6px; border-radius: var(--radius-1) var(--radius-1) 0 0;
  background: var(--color-line); }
.ml-bars__bar.is-peak { background: var(--color-ink); }
.ml-bars__labels { display: flex; gap: var(--space-1); margin-top: var(--space-1);
  font-size: var(--type-1); color: var(--color-muted); }
.ml-bars__labels span { flex: 1; min-width: 6px; text-align: center; }

.ml-spark { display: flex; align-items: flex-end; gap: 2px; height: 32px; }
.ml-spark__bar { flex: 1; border-radius: 1px; background: var(--color-pink); opacity: 0.35; }
.ml-spark__bar.is-last { opacity: 1; }

/* ── equalizer ─────────────────────────────────────────────────────────── */
/* A playing indicator, not a control — it says "audio is running here" on a
   track row. Animated, and stopped entirely under reduced motion by the
   global guard in ml-core.css. */

.ml-eq { display: inline-flex; align-items: flex-end; gap: 2px; height: 14px; }
.ml-eq span {
  width: 3px; border-radius: 1px; background: var(--color-pink);
  animation: ml-eq-bounce 900ms var(--ease-standard) infinite alternate;
}
.ml-eq span:nth-child(1) { height: 40%; animation-delay: 0ms; }
.ml-eq span:nth-child(2) { height: 90%; animation-delay: 150ms; }
.ml-eq span:nth-child(3) { height: 60%; animation-delay: 300ms; }
.ml-eq span:nth-child(4) { height: 100%; animation-delay: 450ms; }
@keyframes ml-eq-bounce { from { transform: scaleY(0.35); } to { transform: scaleY(1); } }

/* ── feed post ─────────────────────────────────────────────────────────── */
/* The one component the audit found genuinely missing after the 16-component
   pass: communication's networking feed defines .post-card / .post-head /
   .post-body / .post-actions locally and nothing in the system expresses it.
   Everything else those screens define maps onto .ml-row, .ml-rail, .ml-card
   or .ml-status.
   Separated by a hairline rather than boxed — a feed reads as one column, and
   a card per post turns it into a grid of unrelated objects. */
/* Compose: FeedPostCard                                                     */

.ml-post { padding: var(--space-3) 0; border-bottom: var(--border-1) solid var(--color-line); }
.ml-post__head { display: flex; align-items: center; gap: var(--space-2); }
.ml-post__head img { width: var(--size-2); height: var(--size-2); border-radius: var(--radius-4); object-fit: cover; flex: none; }
.ml-post__who { flex: 1; min-width: 0; }
.ml-post__name { display: block; font-size: var(--type-3); font-weight: var(--weight-3); line-height: 1.2; }
.ml-post__meta { display: block; font-size: var(--type-1); color: var(--color-muted); }
.ml-post__body { margin: var(--space-2) 0 0; font-size: var(--type-3); line-height: 1.5; }
.ml-post__media { margin-top: var(--space-2); border-radius: var(--radius-2); overflow: hidden; }
.ml-post__media img { display: block; width: 100%; }
/* actions are text+icon, not filled buttons — a filled button per post would
   compete with the post itself for attention */
/* space-3, not space-4: each action carries a 48dp min-width below, so the
   larger gap is no longer needed to keep them apart and would only push the
   row wide. */
.ml-post__actions { display: flex; gap: var(--space-3); margin-top: var(--space-2); }
.ml-post__action {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1);
  /* both axes: a heart plus a count is only ~27dp wide, which fails the target
     rule horizontally even at 48dp tall */
  min-height: var(--size-3); min-width: var(--size-3);
  border: 0; background: none; padding: 0;
  color: var(--color-muted); font-family: var(--font-body);
  font-size: var(--type-1); font-weight: var(--weight-3); cursor: pointer;
}
.ml-post__action[aria-pressed="true"] { color: var(--color-pink-pressed); }

/* ══ ENTITY CARDS ══════════════════════════════════════════════════════════
   Each of these was expressible by composing .ml-card with rows and a status,
   which is why they were not built earlier. They get dedicated components
   because each appears across a whole flow with the same anatomy every time,
   and a composed version drifts screen by screen — which is exactly what the
   32 remaining local <style> blocks are.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── opportunity card ──────────────────────────────────────────────────── */
/* Widget_Directory: OpportunityCard · opportunities flow + Discover          */

.ml-opportunity-card {
  display: block;
  padding: var(--space-3);
  border: var(--border-1) solid var(--color-line);
  border-radius: var(--radius-3);
  box-shadow: var(--elevation-2);
  background: var(--color-white);
  color: inherit; text-decoration: none;
}
.ml-opportunity-card__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-2); }
.ml-opportunity-card__title { font-size: var(--type-3); font-weight: var(--weight-3); line-height: 1.25; }
.ml-opportunity-card__by { display: block; margin-top: 2px; font-size: var(--type-1); color: var(--color-muted); }
.ml-opportunity-card__body { margin: var(--space-2) 0 0; font-size: var(--type-3); }
.ml-opportunity-card__foot {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: var(--space-3); font-size: var(--type-1);
}
.ml-opportunity-card__budget { font-weight: var(--weight-3); font-size: var(--type-3); }
/* a deadline is the one thing that changes urgency, so it gets a colour rule */
.ml-opportunity-card__closes { color: var(--color-muted); }
.ml-opportunity-card__closes.is-soon { color: var(--status-warning-text); font-weight: var(--weight-3); }

/* ── application card ──────────────────────────────────────────────────── */
/* The applicant's side of an opportunity: what you sent and where it stands. */

.ml-application-card {
  display: flex; gap: var(--space-3);
  padding: var(--space-3);
  border: var(--border-1) solid var(--color-line); border-radius: var(--radius-3);
  box-shadow: var(--elevation-2);
  background: var(--color-white); color: inherit; text-decoration: none;
}
.ml-application-card__body { flex: 1; min-width: 0; }
.ml-application-card__title { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-application-card__meta { display: block; font-size: var(--type-1); color: var(--color-muted); }
.ml-application-card__foot { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); }

/* ── band card ─────────────────────────────────────────────────────────── */
/* A band is a group, so the facepile is the identity — not one avatar. */

.ml-band-card {
  display: block; width: 260px;
  border: var(--border-1) solid var(--color-line); border-radius: var(--radius-3);
  box-shadow: var(--elevation-2);
  overflow: hidden; background: var(--color-white);
  color: inherit; text-decoration: none;
}
.ml-band-card__media { position: relative; aspect-ratio: var(--media-aspect-wide); background: var(--color-surface); }
.ml-band-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ml-band-card__body { padding: var(--space-2); }
.ml-band-card__name { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-band-card__meta { font-size: var(--type-1); color: var(--color-muted); }

/* ── rating ────────────────────────────────────────────────────────────── */
/* Used on nearly every card. A number first, stars second: the number is what
   people compare, and it survives greyscale and a screen reader. */

.ml-rating { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--type-1); }
.ml-rating__score { font-weight: var(--weight-3); color: var(--color-ink); font-variant-numeric: tabular-nums; }
/* --color-warning is a FILL colour (#F59E0B, 2.15:1 on paper) and was being
   used here as text, below even the 3:1 non-text floor. The score beside it
   already carries the meaning, so the stars can take the ink colour and stay
   decorative — which is also why they are aria-hidden in the markup. */
.ml-rating__stars { color: var(--color-ink); letter-spacing: -1px; }
.ml-rating__count { color: var(--color-muted); }

/* ── credit row ────────────────────────────────────────────────────────── */
/* A verified credit: who did what on a release, and whether it is confirmed.
   The confirmation state is the whole point of the Credits Trust Engine. */

.ml-credit { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) 0; }
.ml-credit + .ml-credit { border-top: var(--border-1) solid var(--color-line); }
.ml-credit__body { flex: 1; min-width: 0; }
.ml-credit__who { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-credit__role { display: block; font-size: var(--type-1); color: var(--color-muted); }

/* ── order summary ─────────────────────────────────────────────────────── */
/* Line items with a total. Promised in the Flow 7 plan and never built; the
   booking flow needs it too. The total is separated by a rule and set in the
   display face, because it is the number people actually read. */

.ml-summary { border-radius: var(--radius-2); padding: var(--space-3); background: var(--color-white); box-shadow: var(--elevation-1); }
.ml-summary__row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); padding: var(--space-1) 0; font-size: var(--type-3); }
.ml-summary__row span:first-child { color: var(--color-muted); }
.ml-summary__row--total {
  margin-top: var(--space-2); padding-top: var(--space-3);
  border-top: var(--border-1) solid var(--color-line);
  font-family: var(--font-display); font-size: var(--type-4); font-weight: var(--weight-4); letter-spacing: -0.01em;
}
.ml-summary__row--total span:first-child { color: var(--color-ink); }
.ml-summary__note { margin-top: var(--space-2); font-size: var(--type-1); color: var(--color-muted); }

/* ── swipe deck ────────────────────────────────────────────────────────── */
/* Tinder-style discovery. Stacked cards with the next two peeking, so the
   deck reads as a deck rather than a single card. Drag distance is bound to
   --gesture-swipe-threshold rather than a literal, which is what the old
   SwipeDeckBlock hardcoded in App.tsx. */
/* Compose: a Box with draggable + animateTo                                  */

.ml-deck { position: relative; height: 460px; max-width: 340px; }
.ml-deck__card {
  position: absolute; inset: 0;
  border-radius: var(--radius-3);
  overflow: hidden;
  background: var(--color-surface);
  box-shadow: var(--elevation-2);
  transition: transform var(--duration-3) var(--ease-enter);
}
.ml-deck__card img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ml-deck__card::after { content: ""; position: absolute; inset: 0; background: var(--media-scrim); }
/* the two behind are scaled and pushed down, never rotated — rotation reads as
   playful, and this deck is used to hire people */
.ml-deck__card:nth-child(2) { transform: scale(0.96) translateY(12px); }
.ml-deck__card:nth-child(3) { transform: scale(0.92) translateY(24px); }
.ml-deck__body { position: absolute; z-index: 1; left: var(--space-3); right: var(--space-3); bottom: var(--space-3); color: var(--color-white); }
.ml-deck__name { font-family: var(--font-display); font-size: var(--type-6); font-weight: var(--weight-4); line-height: 1.05; letter-spacing: -0.03em; }
.ml-deck__meta { font-size: var(--type-3); color: color-mix(in srgb, var(--color-white) 85%, transparent); }
.ml-deck__actions { display: flex; justify-content: center; gap: var(--space-3); margin-top: var(--space-3); }
.ml-deck__btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--size-4); height: var(--size-4);
  border: var(--border-1) solid var(--color-line); border-radius: var(--radius-4);
  background: var(--color-white); cursor: pointer;
}
.ml-deck__btn--yes { background: var(--color-ink); color: var(--color-white); border-color: var(--color-ink); }
.ml-deck__btn:active { transform: scale(0.92); }

/* ── time picker ───────────────────────────────────────────────────────── */
/* Booking picks a slot, not an arbitrary clock time — so this is a list of
   real availability, not a dial. A dial would imply any time is bookable. */

.ml-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: var(--space-2); }
.ml-slot {
  display: flex; align-items: center; justify-content: center;
  min-height: var(--size-3);
  border: var(--border-1) solid var(--color-line); border-radius: var(--radius-4);
  background: var(--color-white); color: var(--color-ink);
  font-family: var(--font-body); font-size: var(--type-3); font-weight: var(--weight-3);
  font-variant-numeric: tabular-nums; cursor: pointer;
}
.ml-slot[aria-pressed="true"] { background: var(--color-ink); color: var(--color-white); border-color: var(--color-ink); }
.ml-slot:disabled { color: var(--color-muted); opacity: var(--opacity-3); text-decoration: line-through; pointer-events: none; }

/* ── drawer ────────────────────────────────────────────────────────────── */
/* Side panel. Distinct from .ml-sheet: a sheet is a decision about the current
   screen, a drawer is navigation away from it. */

.ml-drawer {
  position: fixed; inset: 0 auto 0 0; z-index: calc(var(--z-3) + 1);
  width: min(300px, 84vw);
  display: flex; flex-direction: column;
  background: var(--color-white);
  box-shadow: var(--elevation-2);
  transform: translateX(-100%);
  transition: transform var(--duration-3) var(--ease-enter);
  padding-top: var(--safe-top); padding-bottom: var(--safe-bottom);
}
.ml-drawer.is-open { transform: none; }
.ml-drawer__body { flex: 1; overflow-y: auto; padding: var(--space-3); }

/* ── video ─────────────────────────────────────────────────────────────── */

.ml-video { position: relative; aspect-ratio: var(--media-aspect-wide); border-radius: var(--radius-2); overflow: hidden; background: var(--color-ink); }
.ml-video video, .ml-video img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ml-video__play {
  position: absolute; inset: 50% auto auto 50%; transform: translate(-50%, -50%);
  display: flex; align-items: center; justify-content: center;
  width: var(--size-4); height: var(--size-4);
  border: 0; border-radius: var(--radius-4);
  background: color-mix(in srgb, var(--color-white) 92%, transparent);
  color: var(--color-ink); cursor: pointer;
}
.ml-video__time { position: absolute; right: var(--space-2); bottom: var(--space-2);
  padding: 2px var(--space-1); border-radius: var(--radius-1);
  background: color-mix(in srgb, var(--color-ink) 70%, transparent);
  color: var(--color-white); font-size: var(--type-1); font-variant-numeric: tabular-nums; }

/* ── stem mixer ────────────────────────────────────────────────────────── */
/* Per-track level and mute for a multi-stem preview. Genuinely music-specific
   and the reason a generic component library cannot cover this product. */

.ml-mixer { display: flex; flex-direction: column; gap: var(--space-2); }
.ml-mixer__track { display: flex; align-items: center; gap: var(--space-3); }
.ml-mixer__name { width: 80px; flex: none; font-size: var(--type-1); font-weight: var(--weight-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ml-mixer__level { flex: 1; }
/* 32dp visual so a four-track mixer fits a phone, 48dp band so it is
   reachable. Unlike the upload grid's remove control, nothing clips this one,
   so the band works. */
.ml-mixer__mute::after {
  content: ""; position: absolute; inset: 50% 50% auto auto;
  width: var(--size-3); height: var(--size-3); transform: translate(50%, -50%);
}
.ml-mixer__mute {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--size-1); height: var(--size-1); flex: none;
  border: var(--border-1) solid var(--color-line); border-radius: var(--radius-4);
  background: var(--color-white); color: var(--color-muted); cursor: pointer;
}
.ml-mixer__mute[aria-pressed="true"] { background: var(--color-ink); color: var(--color-white); border-color: var(--color-ink); }

/* ── editorial layouts ─────────────────────────────────────────────────── */
/* Masonry, bento and split. Named because the styleguide had all three and
   they are genuinely different shapes, not one grid with options. */

/* These were three one-line rules and the source they came from is much more
   specific. Rebuilt against styleguide.html's own values, mapped to tokens:
   .pin-masonry column-gap 32px -> --space-4, .pin-masonry-item radius 16px ->
   --radius-2; .sc-bento-card radius 32px/padding 32px -> --radius-3/--space-4,
   .sc-bento-img height 180px object-fit cover; .sc-split-section gap 32px with
   a hairline rule beneath. */

/* ── masonry ───────────────────────────────────────────────────────────── */
/* Compose: LazyVerticalStaggeredGrid                                        */
/* A real mosaic: unequal tile heights in balanced columns. Uses CSS columns,
   not grid, because the tiles are different heights and their order does not
   matter — which is exactly the case grid handles badly. 2 up on a phone,
   3 at 600dp, 4 at 840dp, following the Material window size classes. */

.ml-masonry { columns: 2; column-gap: var(--space-2); }
.ml-masonry > * { break-inside: avoid; margin-bottom: var(--space-2); display: block; }
@media (min-width: 600px) { .ml-masonry { columns: 3; column-gap: var(--space-3); } }
@media (min-width: 840px) { .ml-masonry { columns: 4; column-gap: var(--space-4); } }
.ml-masonry--roomy { column-gap: var(--space-4); }
.ml-masonry--roomy > * { margin-bottom: var(--space-4); }

.ml-mosaic-tile { position: relative; border-radius: var(--radius-2); overflow: hidden; }
.ml-mosaic-tile img { width: 100%; height: auto; display: block; }
/* The caption sits ON the image, so it needs its own ground: a scrim, not a
   colour. Same rule as .ml-media--scrim — the media carries the colour. */
.ml-mosaic-tile__cap {
  position: absolute; inset: auto 0 0 0;
  padding: var(--space-4) var(--space-3) var(--space-3);
  color: var(--color-white);
  background: linear-gradient(to top, rgba(13,13,13,0.78), transparent);
  font-size: var(--type-1); font-weight: var(--weight-3);
}
.ml-mosaic-tile__pin {
  position: absolute; top: var(--space-2); right: var(--space-2);
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--size-1); height: var(--size-1);
  border-radius: var(--radius-4);
  background: color-mix(in srgb, var(--color-white) 88%, transparent);
  color: var(--color-ink); border: 0; cursor: pointer;
}
/* 32px visual, 48px effective — same expand-without-resizing technique as
   .ml-chip and .ml-crumbs__item. */
.ml-mosaic-tile__pin::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: var(--size-3); height: var(--size-3);
}

/* ── bento ─────────────────────────────────────────────────────────────── */
/* Compose: custom Grid with spans                                           */
/* Source .sc-bento-grid is 3 columns at 32px with 32px-radius cards. The
   asymmetry is the point: tile 1 spans two columns and tile 4 spans two rows,
   so the eye is given a route through the grid instead of a checkerboard. */

.ml-bento { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-2); }
@media (min-width: 600px) {
  .ml-bento { grid-template-columns: repeat(3, 1fr); gap: var(--space-3); }
  .ml-bento > :first-child { grid-column: span 2; }
  .ml-bento > :nth-child(4) { grid-row: span 2; }
}
.ml-bento > :first-child { grid-column: 1 / -1; }
.ml-bento__card {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4);
  border: var(--border-1) solid var(--color-line);
  border-radius: var(--radius-3);
  background: var(--color-white);
  transition: transform var(--duration-3) var(--ease-standard);
}
.ml-bento__card:hover { transform: translateY(-2px); }
.ml-bento__img {
  width: 100%; height: 180px; object-fit: cover;
  border-radius: var(--radius-2); background: var(--color-paper);
}
.ml-bento__title { font-family: var(--font-display); font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-bento__body { font-size: var(--type-1); color: var(--color-muted); }
/* Dark bento: source uses rgba white 5% on 10% — translucent, so it works over
   whatever the section ground is instead of assuming black. */
.ml-bento--dark { color: var(--color-white); }
.ml-bento--dark .ml-bento__card {
  background: color-mix(in srgb, var(--color-white) 5%, transparent);
  border-color: color-mix(in srgb, var(--color-white) 12%, transparent);
}
.ml-bento--dark .ml-bento__body { color: color-mix(in srgb, var(--color-white) 72%, transparent); }

/* ── split editorial ───────────────────────────────────────────────────── */
/* Compose: Row with weight(1f) each                                         */
/* Source .sc-split-section: two equal columns, centred, with a hairline rule
   under each band so a run of them reads as an article, not a slideshow. */

.ml-split {
  display: grid; gap: var(--space-3);
  padding-bottom: var(--space-4);
  border-bottom: var(--border-1) solid var(--color-line);
}
.ml-split:last-child { border-bottom: 0; }
@media (min-width: 600px) {
  .ml-split { grid-template-columns: 1fr 1fr; gap: var(--space-4); align-items: center; }
  .ml-split--flip > :first-child { order: 2; }
}
.ml-split__media { border-radius: var(--radius-3); overflow: hidden; }
.ml-split__media img, .ml-split__media video { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The editorial pull-quote. Source sets Inter Tight 800 at clamp(32,4vw,48),
   which is --type-6 to within a pixel. */
.ml-quote {
  font-family: var(--font-display);
  font-size: var(--type-6); font-weight: var(--weight-4);
  line-height: 1.1; letter-spacing: -0.02em;
  margin: 0;
}
.ml-quote__cite { display: block; margin-top: var(--space-3); font-size: var(--type-1); font-weight: var(--weight-3); color: var(--color-muted); }

/* ── article grid ──────────────────────────────────────────────────────── */
/* Compose: LazyVerticalGrid(Adaptive(300.dp))                               */
/* Source .adv-article-grid: auto-fill minmax(300px, 1fr) at 32px. Kept as
   auto-fill so it reflows by available width rather than by breakpoint. */

.ml-articles { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--space-4); }
.ml-article { display: flex; flex-direction: column; gap: var(--space-3); text-decoration: none; color: inherit; }
.ml-article__media { border-radius: var(--radius-2); overflow: hidden; aspect-ratio: 3 / 2; }
.ml-article__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ml-article__tag { font-size: var(--type-1); font-weight: var(--weight-3); text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-pink-pressed); }
.ml-article__title { font-family: var(--font-display); font-size: var(--type-3); font-weight: var(--weight-3); line-height: 1.25; }
.ml-article__meta { font-size: var(--type-1); color: var(--color-muted); }

/* ── surface effects ───────────────────────────────────────────────────── */
/* Restrained on purpose. The palette rule says the chrome is monochrome and
   the media carries the colour, so these adjust SURFACE, never hue: a frosted
   panel over media, a soft lift, and a paper grain. No coloured glows. */

.ml-glass {
  background: color-mix(in srgb, var(--color-white) 72%, transparent);
  backdrop-filter: saturate(180%) blur(16px);
  -webkit-backdrop-filter: saturate(180%) blur(16px);
  border: var(--border-1) solid color-mix(in srgb, var(--color-white) 40%, transparent);
}
.ml-glass--dark {
  background: color-mix(in srgb, var(--color-ink) 62%, transparent);
  color: var(--color-white);
  border-color: color-mix(in srgb, var(--color-white) 16%, transparent);
}
.ml-lift { box-shadow: var(--elevation-2); }
/* a very low-contrast grain, for a large flat surface that would otherwise
   band. Uses currentColor so it inherits rather than introducing a value. */
.ml-grain { position: relative; }
.ml-grain::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  opacity: 0.035;
  background-image: radial-gradient(currentColor 0.5px, transparent 0.5px);
  background-size: 3px 3px;
}

/* ══ MIGRATION BACKFILL ════════════════════════════════════════════════════
   Everything below was present in styleguide.html or the Gestalt catalogue
   and had no counterpart here. Found on 2026-08-30 by matching SELECTORS
   rather than heading text — the earlier heading-text check passed because
   the words appeared in prose, which proved nothing.

   Gestalt imports with no counterpart: ComboBox, Popover, PopoverMessage,
   PopoverEducational, Tooltip, HelpButton, Collage, ButtonSocial.
   styleguide classes with no counterpart: tooltip-*, dropdown-*, g-combobox-*,
   g-popover-*, breadcrumb*, p-tier/p-price/p-feat, trends-table/trend-*.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── tooltip ───────────────────────────────────────────────────────────── */
/* Compose: PlainTooltip (Material3)                                        */
/* A tooltip is a DESKTOP affordance: it needs hover, and a touch screen has
   none. It is here because the catalogue and the admin console are viewed on
   desktop, and because Gestalt shipped one. On a phone the same text belongs
   in .ml-field__help or a .ml-popover opened by an explicit tap — which is
   what .ml-help below is for. Driven by [data-tip] so there is no second
   element to keep in sync, and hidden from AT because the accessible name
   must live on the control itself, not in a decorative bubble. */

.ml-tip { position: relative; display: inline-flex; }
.ml-tip::after {
  content: attr(data-tip);
  position: absolute; bottom: calc(100% + var(--space-2)); left: 50%;
  transform: translateX(-50%) translateY(2px);
  z-index: var(--z-3);
  max-width: 220px; width: max-content;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-1);
  background: var(--color-ink); color: var(--color-white);
  font-size: var(--type-1); line-height: 1.35; text-align: center;
  white-space: normal;
  opacity: 0; pointer-events: none;
  transition: opacity var(--duration-1) var(--ease-standard),
              transform var(--duration-1) var(--ease-standard);
}
.ml-tip::before {
  content: ""; position: absolute; bottom: calc(100% + 2px); left: 50%;
  transform: translateX(-50%);
  border: 4px solid transparent; border-top-color: var(--color-ink);
  opacity: 0; pointer-events: none; z-index: var(--z-3);
  transition: opacity var(--duration-1) var(--ease-standard);
}
/* --gesture-tooltip-delay is why a tooltip does not fire the instant a pointer
   crosses the control: without it, moving the mouse across a toolbar flashes
   every tip in the row. The token existed and nothing referenced it, so the
   tooltip had no delay at all — it appeared immediately on hover, which is the
   behaviour the token was written to prevent. Focus is exempt: a keyboard user
   has already committed to the control, so the tip shows at once. */
.ml-tip:hover::after, .ml-tip:hover::before {
  transition-delay: var(--gesture-tooltip-delay);
}
.ml-tip:hover::after, .ml-tip:focus-within::after {
  opacity: 1; transform: translateX(-50%) translateY(0);
}
.ml-tip:hover::before, .ml-tip:focus-within::before { opacity: 1; }
.ml-tip:focus-within::after, .ml-tip:focus-within::before { transition-delay: 0s; }
.ml-tip--below::after { bottom: auto; top: calc(100% + var(--space-2)); }
.ml-tip--below::before {
  bottom: auto; top: calc(100% + 2px);
  border-top-color: transparent; border-bottom-color: var(--color-ink);
}
@media (prefers-reduced-motion: reduce) {
  .ml-tip::after, .ml-tip::before { transition: none; }
}

/* radius: source said 12px, which the 8/16/32 scale does not carry.
   --radius-2 chosen over --radius-1 so overlays match .ml-card beside them. */
/* ── popover ───────────────────────────────────────────────────────────── */
/* Compose: DropdownMenu / ExposedDropdownMenu                              */
/* Covers four Gestalt components — Popover, PopoverMessage, PopoverEducational
   and Dropdown's menu — because they differ only in what goes inside. Anchored
   to a positioned parent and toggled by [hidden], so the same ?state= plumbing
   that drives every other overlay drives this one too.

   On a phone this should usually be a .ml-sheet instead: a bottom sheet is
   reachable by thumb and a popover pinned to a top-right control is not. */

.ml-pop-anchor { position: relative; display: inline-flex; }
.ml-pop {
  position: absolute; top: calc(100% + var(--space-1)); right: 0;
  z-index: var(--z-3);
  min-width: 200px; max-width: 280px;
  padding: var(--space-1);
  border-radius: var(--radius-2);
  background: var(--color-white);
  border: var(--border-1) solid var(--color-line);
  box-shadow: var(--elevation-2);
}
.ml-pop--left { right: auto; left: 0; }
.ml-pop__item {
  display: flex; align-items: center; gap: var(--space-2);
  width: 100%;
  padding: var(--space-2);
  border: 0; background: none;
  border-radius: var(--radius-1);
  font: inherit; font-size: var(--type-3); color: var(--color-ink);
  text-align: left; cursor: pointer;
  min-height: var(--size-3);
}
.ml-pop__item:hover { background: var(--color-surface); }
.ml-pop__item[aria-checked="true"] { font-weight: var(--weight-3); }
.ml-pop__item--danger { color: var(--color-error); }
.ml-pop__sep { height: 1px; margin: var(--space-1) 0; background: var(--color-line); }
/* PopoverMessage / PopoverEducational: prose, not a menu. */
.ml-pop__msg { padding: var(--space-3); font-size: var(--type-3); line-height: 1.5; }
.ml-pop__msg-title { font-weight: var(--weight-3); margin-bottom: var(--space-1); }

/* ── help button ───────────────────────────────────────────────────────── */
/* Compose: IconButton + DropdownMenu                                       */
/* Gestalt's HelpButton. The touch-safe answer to the tooltip problem above:
   an explicit control that opens a .ml-pop, so the explanation is reachable
   without hover and is announced as a real button. */

.ml-help {
  position: relative;                 /* anchors ::after's 48dp expansion below */
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--icon-2); height: var(--icon-2); padding: 0;
  border: 0; border-radius: var(--radius-4);
  background: var(--color-surface); color: var(--color-muted);
  font-size: var(--type-1); font-weight: var(--weight-3); cursor: pointer;
}
.ml-help::after {                      /* expand to 48dp without moving layout */
  content: ""; position: absolute;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: var(--size-3); height: var(--size-3);
}
.ml-help:hover { background: var(--color-line); color: var(--color-ink); }

/* ── combobox ──────────────────────────────────────────────────────────── */
/* Compose: ExposedDropdownMenuBox                                          */
/* Gestalt's ComboBox, and the only one of these that is genuinely mobile:
   type-to-filter over a long list — instruments, genres, cities — where a
   native <select> would be a 400-row scroll. Built on .ml-field so the input
   inherits every focus and error state already defined there. */

.ml-combo { position: relative; }
.ml-combo__list {
  position: absolute; top: calc(100% + var(--space-1)); left: 0; right: 0;
  z-index: var(--z-3);
  max-height: 240px; overflow-y: auto;
  padding: var(--space-1);
  border-radius: var(--radius-2);
  background: var(--color-white);
  border: var(--border-1) solid var(--color-line);
  box-shadow: var(--elevation-2);
}
/* Markup contract: an icon may be the first child, but the label must be a
   SINGLE element. Loose text beside an inline <mark> becomes a second flex
   item and the gap opens a hole inside the word. */
.ml-combo__opt {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-1);
  font-size: var(--type-3); cursor: pointer;
  min-height: var(--size-3);
}
.ml-combo__opt[aria-selected="true"] { background: var(--color-pink-container); font-weight: var(--weight-3); }
.ml-combo__opt:hover { background: var(--color-surface); }
.ml-combo__opt mark { background: none; color: var(--color-pink-pressed); font-weight: var(--weight-3); }
.ml-combo__empty { padding: var(--space-3); font-size: var(--type-1); color: var(--color-muted); text-align: center; }
.ml-combo__clear {
  position: absolute; top: 50%; right: var(--space-2); transform: translateY(-50%);
  display: flex; border: 0; background: none; color: var(--color-muted); cursor: pointer;
}

/* ── breadcrumb ────────────────────────────────────────────────────────── */
/* Compose: BreadcrumbRow (custom)                                          */
/* Admin and marketplace category depth only. A phone screen uses the app bar
   back affordance instead — two ways back on one screen is one too many, so
   this is scoped to wide viewports on purpose. Separators are generated, not
   typed, so they never end up in the accessible name. */

.ml-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); font-size: var(--type-1); }
.ml-crumbs__item {
  position: relative; color: var(--color-muted); text-decoration: none;
  display: inline-flex; align-items: center; min-height: var(--size-1);
}
/* Text-sized links are 19px tall. The band restores a 44px target without
   changing the line box, the same technique .ml-chip uses. */
.ml-crumbs__item::after {
  content: ""; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%); height: var(--size-3);
}
.ml-crumbs__item:hover { color: var(--color-ink); text-decoration: underline; }
.ml-crumbs__item[aria-current="page"] { color: var(--color-ink); font-weight: var(--weight-3); }
.ml-crumbs__item + .ml-crumbs__item::before {
  content: "/"; margin-right: var(--space-1); color: var(--color-muted);
}

/* ── pricing tiers ─────────────────────────────────────────────────────── */
/* Compose: PlanCard (custom)                                               */
/* Subscription plans. One tier is marked recommended and it is marked with a
   BORDER and a label, never with colour alone — the palette has one accent and
   spending it here would make the page shout. Scrolls horizontally on a phone
   rather than stacking, so the tiers stay comparable side by side. */

.ml-tiers { display: grid; gap: var(--space-3); grid-template-columns: 1fr; }
@media (min-width: 600px) { .ml-tiers { grid-template-columns: repeat(3, 1fr); } }
.ml-tier {
  position: relative;
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-4);
  border-radius: var(--radius-2);
  background: var(--color-white);
  border: var(--border-1) solid var(--color-line);
}
.ml-tier--featured { border: var(--border-2) solid var(--color-ink); }
.ml-tier__flag {
  position: absolute; top: calc(var(--space-2) * -1); left: var(--space-4);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-4);
  background: var(--color-ink); color: var(--color-white);
  font-size: var(--type-1); font-weight: var(--weight-3); text-transform: uppercase; letter-spacing: 0.04em;
}
.ml-tier__name { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-tier__price { font-size: var(--type-6); font-weight: var(--weight-4); line-height: 1; font-variant-numeric: tabular-nums; }
.ml-tier__unit { font-size: var(--type-1); color: var(--color-muted); }
.ml-tier__feats { display: flex; flex-direction: column; gap: var(--space-1); margin: var(--space-2) 0; padding: 0; list-style: none; }
.ml-tier__feat { display: flex; align-items: flex-start; gap: var(--space-2); font-size: var(--type-1); }
/* 1.4.11: an icon needs 3:1 and raw --color-success is 2.28 on white. */
.ml-tier__feat svg { flex: none; margin-top: 2px; color: var(--status-success-text); }
.ml-tier__feat--off { color: var(--color-muted); }
.ml-tier__feat--off svg { color: var(--color-line); }

/* ── trends table ──────────────────────────────────────────────────────── */
/* Compose: TrendRow in a LazyColumn                                        */
/* MusicLinkd Trends, Gear & Studio trends, Top searches this week — one
   component, three data sets. Rank is rendered as text rather than an ordered
   list marker so it can be tabular-aligned. Direction is carried by an arrow
   AND a sign, never by red/green alone. */

.ml-trends {
  display: flex; flex-direction: column;
  padding: 0 var(--space-3);
  background: var(--color-white);
  border-radius: var(--radius-2);
  box-shadow: var(--elevation-1);
}
.ml-trends__head {
  display: grid; grid-template-columns: 3ch 1fr auto;
  gap: var(--space-3); padding: var(--space-2) 0;
  border-bottom: var(--border-1) solid var(--color-line);
  font-size: var(--type-1); font-weight: var(--weight-3);
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--color-muted);
}
.ml-trends__row {
  display: grid; grid-template-columns: 3ch 1fr auto;
  gap: var(--space-3); align-items: center;
  padding: var(--space-3) 0;
  border-bottom: var(--border-1) solid var(--color-line);
  text-decoration: none; color: inherit;
  min-height: var(--size-3);
}
.ml-trends__row:hover { background: var(--color-surface); }
.ml-trends__rank { font-size: var(--type-3); font-weight: var(--weight-3); color: var(--color-muted); font-variant-numeric: tabular-nums; }
.ml-trends__term { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.ml-trends__thumb { width: var(--size-1); height: var(--size-1); flex: none; border-radius: var(--radius-1); object-fit: cover; background: var(--color-surface); }
.ml-trends__label { font-size: var(--type-3); font-weight: var(--weight-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ml-trends__sub { font-size: var(--type-1); color: var(--color-muted); }
.ml-trends__delta { display: flex; align-items: center; gap: 2px; font-size: var(--type-1); font-weight: var(--weight-3); font-variant-numeric: tabular-nums; }
.ml-trends__delta--up { color: var(--status-success-text); }
.ml-trends__delta--down { color: var(--status-error-text); }
.ml-trends__delta--flat { color: var(--color-muted); }

/* ── collage ───────────────────────────────────────────────────────────── */
/* Compose: CollageGrid (custom)                                            */
/* Gestalt's Collage. One hero image with satellites — the shape behind band
   photo sets and studio galleries, where a uniform grid would flatten the one
   image that matters. Fixed slots, not a masonry flow: the count is known. */

.ml-collage {
  display: grid; gap: var(--space-1);
  grid-template-columns: 2fr 1fr 1fr;
  grid-template-rows: repeat(2, 1fr);
  border-radius: var(--radius-2); overflow: hidden;
  aspect-ratio: 3 / 2;
}
.ml-collage > * { min-width: 0; min-height: 0; }
.ml-collage img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ml-collage__hero { grid-row: span 2; }
.ml-collage__more {
  position: relative; display: flex; align-items: center; justify-content: center;
  background: var(--color-ink); color: var(--color-white);
  font-size: var(--type-3); font-weight: var(--weight-3);
}

/* ── social sign-in button ─────────────────────────────────────────────── */
/* Compose: SocialSignInButton (custom)                                     */
/* Gestalt's ButtonSocial. The provider marks are the ONE documented exception
   to the palette rule: Google's four brand colours and Apple's black are
   fixed by each provider's brand terms and cannot be re-tinted. The button
   itself stays on our palette; only the glyph is foreign. */

.ml-btn--social {
  justify-content: center; gap: var(--space-2);
  background: var(--color-white); color: var(--color-ink);
  border: var(--border-1) solid var(--color-line);
}
.ml-btn--social:hover { background: var(--color-surface); }
.ml-btn--social svg { flex: none; width: var(--icon-2); height: var(--icon-2); }

/* ── native keyboard & IME ─────────────────────────────────────────────── */
/* Compose: WindowInsets.ime + KeyboardOptions                              */
/* Not a widget — a set of rules for the one piece of UI we do not draw. The
   keyboard covers up to 45% of a phone screen, so anything the user is typing
   into must stay visible above it.

   .ml-ime-pad is the web stand-in for WindowInsets.ime: on Android the value
   comes from the framework, here from the visual viewport. .ml-ime-bar is the
   accessory strip that must ride ON TOP of the keyboard — a Done control, or
   the send button in the composer.

   The inputmode/enterkeyhint attributes are the other half and belong in the
   markup: inputmode="numeric" for OTP and price, "email" for sign-in, "tel"
   for phone; enterkeyhint="search" | "send" | "next" | "done" so the return
   key says what it will actually do. */

.ml-ime-pad { padding-bottom: env(keyboard-inset-height, 0px); transition: padding-bottom var(--duration-2) var(--ease-standard); }
.ml-ime-bar {
  position: sticky; bottom: 0; z-index: var(--z-2);
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--color-white);
  border-top: var(--border-1) solid var(--color-line);
  padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
}
.ml-ime-bar__done { margin-left: auto; }
@media (prefers-reduced-motion: reduce) { .ml-ime-pad { transition: none; } }

/* ══ BLOCKS BACKFILL ═══════════════════════════════════════════════════════
   Blocks present in styleguide.html or the Gestalt catalogue that the system
   had no counterpart for. Source values mapped to tokens, not re-invented:
   .adv-feature-grid auto-fit minmax(280px) gap 40px, .t-card, .pain-card,
   .aac-img-wrapper radius 24px aspect 16/10 with a centred overlay control,
   .collage-sticker rotated pink pill, .adv-masonry-wrapper 80vh black panel
   with a rotated 30%-opacity mosaic behind a centred card.
   ═════════════════════════════════════════════════════════════════════════ */

/* ── feature grid ──────────────────────────────────────────────────────── */
/* Compose: LazyVerticalGrid(Adaptive(280.dp))                               */
/* Three-up marketing grid. auto-fit, so it collapses to one column on a
   phone without a breakpoint. The icon sits in a tinted disc — the one place
   --color-pink-container is allowed to carry a shape rather than a state. */

.ml-features { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-4); }
.ml-feature { display: flex; flex-direction: column; gap: var(--space-2); }
.ml-feature__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--size-3); height: var(--size-3);
  border-radius: var(--radius-2);
  background: var(--color-pink-container);
  color: var(--color-pink-pressed);
}
.ml-feature__title { font-family: var(--font-display); font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-feature__body { font-size: var(--type-1); color: var(--color-muted); line-height: 1.55; }
/* The problem-statement variant: same shape, neutral disc, because a pain
   point is not an accent. */
.ml-feature--problem .ml-feature__icon { background: var(--color-surface); color: var(--color-ink); }

/* ── testimonial ───────────────────────────────────────────────────────── */
/* Compose: TestimonialCard (custom)                                         */
/* The quote leads and the attribution follows, never the reverse — a card
   that opens with a face and a name reads as a profile, not a recommendation.
   Stats are optional and render as numbers, not badges. */

.ml-testimonial {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4);
  border: var(--border-1) solid var(--color-line);
  border-radius: var(--radius-3);
  background: var(--color-white);
}
.ml-testimonial__quote { font-size: var(--type-3); line-height: 1.5; margin: 0; }
.ml-testimonial__user { display: flex; align-items: center; gap: var(--space-2); }
.ml-testimonial__name { font-size: var(--type-1); font-weight: var(--weight-3); }
.ml-testimonial__role { font-size: var(--type-1); color: var(--color-muted); }
.ml-testimonial__stats { display: flex; gap: var(--space-4); padding-top: var(--space-3); border-top: var(--border-1) solid var(--color-line); }
.ml-testimonial__stat b { display: block; font-family: var(--font-display); font-size: var(--type-4); font-weight: var(--weight-4); font-variant-numeric: tabular-nums; }
.ml-testimonial__stat span { font-size: var(--type-1); color: var(--color-muted); }

/* ── ambient album card ────────────────────────────────────────────────── */
/* Compose: AlbumCard (custom)                                               */
/* Cover art with a play control floated over its centre. The art is the whole
   card — no title bar over the image, because album art is designed to be
   read whole. Metadata sits BELOW the frame, which is what keeps it ambient. */

.ml-album { display: flex; flex-direction: column; gap: var(--space-2); text-decoration: none; color: inherit; }
.ml-album__frame { position: relative; border-radius: var(--radius-2); overflow: hidden; aspect-ratio: 16 / 10; background: var(--color-surface); }
.ml-album__frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ml-album__play {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--size-3); height: var(--size-3); padding: 0;
  border: 0; border-radius: var(--radius-2);
  background: var(--color-white); color: var(--color-ink);
  box-shadow: var(--elevation-2); cursor: pointer;
  transition: transform var(--duration-2) var(--ease-spring);
}
.ml-album:hover .ml-album__play { transform: translate(-50%, -50%) scale(1.06); }
.ml-album__play:active { transform: translate(-50%, -50%) scale(0.94); }
.ml-album__title { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-album__meta { font-size: var(--type-1); color: var(--color-muted); }

/* ── trust & verification ──────────────────────────────────────────────── */
/* Compose: TrustRow (custom)                                                */
/* The Credits Trust Engine surfaced as a block. Every claim carries WHO
   verified it — a tick with no source is decoration, and this system already
   refuses those elsewhere. */

.ml-trust { display: grid; gap: var(--space-3); padding: var(--space-4); border: var(--border-1) solid var(--color-line); border-radius: var(--radius-3); background: var(--color-white); }
.ml-trust__row { display: flex; align-items: flex-start; gap: var(--space-3); }
.ml-trust__mark {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none; width: var(--size-1); height: var(--size-1);
  border-radius: var(--radius-4);
  background: var(--status-success-bg);
  color: var(--status-success-text);
}
.ml-trust__what { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-trust__who  { font-size: var(--type-1); color: var(--color-muted); }
.ml-trust__stats { display: flex; gap: var(--space-4); padding-top: var(--space-3); border-top: var(--border-1) solid var(--color-line); }

/* ── sticker ───────────────────────────────────────────────────────────── */
/* Compose: overlay Text with rotation                                       */
/* Source .collage-sticker: a rotated pink pill dropped on top of artwork.
   The one place brand pink appears as a FILL over media — it is a stamp, and
   a stamp that matched the photography would not read as one. */

.ml-sticker {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%) rotate(-4deg);
  z-index: 2;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-4);
  /* White TEXT on --color-pink is 3.56:1 and fails. My own line, and the same
     mistake the tab bar had. Pressed pink keeps the stamp loud and passes. */
  background: var(--color-pink-pressed);
  color: var(--color-white);
  font-family: var(--font-display); font-weight: var(--weight-4); font-size: var(--type-1);
  box-shadow: var(--elevation-2);
  white-space: nowrap;
}

/* ── ambient mosaic panel ──────────────────────────────────────────────── */
/* Compose: Box with a blurred image wall behind                             */
/* Source .adv-masonry-wrapper / .adv-masonry-bg: a dark panel with a rotated
   wall of covers at 30% behind a centred card. The wall is decorative and is
   marked aria-hidden; the card carries all the meaning. Motion is opt-in and
   respects prefers-reduced-motion. */

.ml-wall { position: relative; overflow: hidden; border-radius: var(--radius-3); background: var(--color-ink); display: grid; place-items: center; padding: var(--space-4); min-height: 320px; }
.ml-wall__bg { position: absolute; inset: -20%; display: flex; gap: var(--space-3); opacity: 0.3; transform: rotate(5deg) scale(1.2); pointer-events: none; }
.ml-wall__col { display: flex; flex-direction: column; gap: var(--space-3); width: 160px; flex: none; }
.ml-wall__col img { width: 100%; border-radius: var(--radius-2); display: block; }
.ml-wall__col--drift { animation: ml-wall-drift 40s linear infinite; }
.ml-wall__card { position: relative; z-index: 1; max-width: 380px; padding: var(--space-4); border-radius: var(--radius-3); background: var(--color-white); box-shadow: var(--elevation-2); text-align: center; }
@keyframes ml-wall-drift { from { transform: translateY(0); } to { transform: translateY(-50%); } }
@media (prefers-reduced-motion: reduce) { .ml-wall__col--drift { animation: none; } }

/* ══ STYLEGUIDE PARITY ═════════════════════════════════════════════════════
   The last 13 components from styleguide.html with no counterpart here,
   found 2026-08-30 by comparing all 262 of its h2 headings rather than its
   section names. Everything else in that file resolves: 81 match by name,
   116 under a different name, 42 are prose, 4 are scaffolds it marks
   "no files yet", 5 are marketing copy.
   ═════════════════════════════════════════════════════════════════════════ */

/* ── permission dialog ─────────────────────────────────────────────────── */
/* Compose: rememberPermissionState + AlertDialog                            */
/* Android asks once and remembers "don't ask again", so the pre-prompt has to
   earn the system prompt BEFORE it fires. States why, then hands over. The
   dismissive action is never styled as the quiet one — refusing must be as
   easy as accepting or the prompt is coercive. */

.ml-permission { display: grid; gap: var(--space-3); padding: var(--space-4); text-align: center; }
.ml-permission__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--size-4); height: var(--size-4); margin-inline: auto;
  border-radius: var(--radius-4);
  background: var(--color-pink-container); color: var(--color-pink-pressed);
}
.ml-permission__title { font-family: var(--font-display); font-size: var(--type-4); font-weight: var(--weight-3); margin: 0; }
.ml-permission__why { font-size: var(--type-3); color: var(--color-muted); margin: 0; }
.ml-permission__acts { display: grid; gap: var(--space-2); }

/* ── camera picker ─────────────────────────────────────────────────────── */
/* Compose: ActivityResultContracts.TakePicture / PickVisualMedia            */
/* The choice sheet, not the camera. Android has two separate contracts and
   the user picks between them here, so the sheet is the component and the
   capture surface belongs to the OS. */

.ml-capture { display: grid; gap: var(--space-1); }
.ml-capture__opt {
  display: flex; align-items: center; gap: var(--space-3);
  width: 100%; min-height: var(--size-4);
  padding: var(--space-3);
  border: 0; border-radius: var(--radius-2);
  background: none; color: inherit; text-align: left;
  font: inherit; font-size: var(--type-3); cursor: pointer;
}
.ml-capture__opt:hover { background: var(--color-surface); }
.ml-capture__opt svg { flex: none; color: var(--color-muted); }

/* ── audio recorder ────────────────────────────────────────────────────── */
/* Compose: MediaRecorder + AudioAmplitude                                   */
/* Recording needs three things visible at once: that it IS recording, for how
   long, and that sound is arriving. The dot pulses, the timer counts, the
   meter moves — drop any one and the user cannot tell a dead mic from silence. */

.ml-rec { display: grid; gap: var(--space-3); padding: var(--space-4); border: var(--border-1) solid var(--color-line); border-radius: var(--radius-3); background: var(--color-white); }
.ml-rec__head { display: flex; align-items: center; gap: var(--space-2); }
.ml-rec__dot { width: 10px; height: 10px; border-radius: var(--radius-4); background: var(--color-error); flex: none; }
.ml-rec.is-live .ml-rec__dot { animation: ml-rec-pulse 1.4s ease-in-out infinite; }
.ml-rec__time { font-family: var(--font-display); font-size: var(--type-4); font-weight: var(--weight-4); font-variant-numeric: tabular-nums; margin-left: auto; }
.ml-rec__meter { display: flex; align-items: flex-end; gap: 2px; height: var(--size-2); }
.ml-rec__bar { flex: 1; background: var(--color-ink); border-radius: 1px; min-height: 3px; }
.ml-rec__acts { display: flex; gap: var(--space-2); }
@keyframes ml-rec-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.25; } }
@media (prefers-reduced-motion: reduce) { .ml-rec.is-live .ml-rec__dot { animation: none; } }

/* ── stem dropzone ─────────────────────────────────────────────────────── */
/* Compose: dragAndDropTarget                                                */
/* Distinct from .ml-upload-grid, which shows what is ALREADY uploaded. This
   is the target: idle, hovered, and busy, because a dropzone with no hover
   state gives no signal that the drop will land. */

.ml-dropzone {
  display: grid; place-items: center; gap: var(--space-2);
  padding: var(--space-4);
  border: var(--border-2) dashed var(--color-line);
  border-radius: var(--radius-3);
  background: var(--color-white);
  text-align: center;
  transition: border-color var(--duration-2) var(--ease-standard),
              background-color var(--duration-2) var(--ease-standard);
}
.ml-dropzone.is-over { border-color: var(--color-pink); background: var(--color-pink-container); }
.ml-dropzone.is-busy { border-style: solid; }
/* --color-muted is 5.01 on paper but 4.26 on --color-pink-container (is-over
   state) — the same "muted on a tint" failure .ml-progress__meta had. Ink at
   reduced opacity replaces it there, same fix here. */
.ml-dropzone__hint { font-size: var(--type-1); color: var(--color-muted); }
.ml-dropzone.is-over .ml-dropzone__hint { color: var(--color-ink); opacity: 0.72; }
.ml-dropzone__formats { font-size: var(--type-1); color: var(--color-muted); }

/* ── inline editor ─────────────────────────────────────────────────────── */
/* Compose: editable Text with a focus affordance                            */
/* Edit in place rather than pushing a form screen. The pencil is a hint, not
   the control — the whole row is the button, because a 16px pencil is not a
   target. Saving is explicit: silent autosave on a profile is how people
   publish half a sentence. */

.ml-inline { display: flex; align-items: center; gap: var(--space-2); width: 100%; min-height: var(--size-3); padding: var(--space-2); border: var(--border-1) solid transparent; border-radius: var(--radius-1); background: none; font: inherit; text-align: left; cursor: text; }
.ml-inline:hover { border-color: var(--color-line); background: var(--color-white); }
.ml-inline__pencil { margin-left: auto; color: var(--color-muted); flex: none; opacity: 0; }
.ml-inline:hover .ml-inline__pencil, .ml-inline:focus-within .ml-inline__pencil { opacity: 1; }
.ml-inline.is-editing { border-color: var(--color-ink); background: var(--color-white); cursor: text; }
.ml-inline__acts { display: flex; gap: var(--space-2); margin-top: var(--space-2); }

/* ── error boundary ────────────────────────────────────────────────────── */
/* Compose: ErrorScreen                                                      */
/* What renders when a surface fails. It says what broke, offers the one
   action that helps, and carries a reference — an error with no reference is
   unreportable, which is how bugs stay unfixed. */

.ml-error { display: grid; gap: var(--space-3); place-items: center; padding: var(--space-4); text-align: center; }
.ml-error__mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--size-4); height: var(--size-4);
  border-radius: var(--radius-4);
  background: var(--status-error-bg);
  color: var(--status-error-text);
}
.ml-error__title { font-family: var(--font-display); font-size: var(--type-4); font-weight: var(--weight-3); margin: 0; }
.ml-error__body { font-size: var(--type-3); color: var(--color-muted); margin: 0; max-width: 40ch; }
.ml-error__ref { font-size: var(--type-1); color: var(--color-muted); font-variant-numeric: tabular-nums; }

/* ── offline & adaptive banners ────────────────────────────────────────── */
/* Compose: ConnectivityBanner / InstallPromptBanner                          */
/* Two more .ml-banner variants. Offline is NEUTRAL, not an error: losing
   signal in a lift is not a fault, and painting it red trains people to
   ignore red. The adaptive banner is the install/upgrade prompt and is the
   only banner that may be dismissed. */

.ml-banner--offline { background: var(--color-ink); color: var(--color-white); }
.ml-banner--offline .ml-banner__body { color: color-mix(in srgb, var(--color-white) 78%, transparent); }
.ml-banner--promo { background: var(--color-surface); border: var(--border-1) solid var(--color-line); }
.ml-banner__dismiss { position: relative; margin-left: auto; border: 0; background: none; color: inherit; padding: var(--space-1); cursor: pointer; display: inline-flex; }
.ml-banner__dismiss::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: var(--size-3); height: var(--size-3);
}

/* ── QR share ──────────────────────────────────────────────────────────── */
/* Compose: QrBitmap + ShareSheet                                            */
/* A profile handed over in person. The code needs a quiet zone and a light
   ground to scan — it is one of the few places the system fixes white
   regardless of surface, because scanners require the contrast. */

.ml-qr { display: grid; gap: var(--space-3); justify-items: center; padding: var(--space-4); border: var(--border-1) solid var(--color-line); border-radius: var(--radius-3); background: var(--color-white); text-align: center; }
.ml-qr__code { padding: var(--space-3); background: var(--color-white); border-radius: var(--radius-2); line-height: 0; }
.ml-qr__handle { font-family: var(--font-display); font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-qr__hint { font-size: var(--type-1); color: var(--color-muted); }

/* ── animated success ──────────────────────────────────────────────────── */
/* Compose: AnimatedVisibility + scaleIn                                     */
/* .ml-check is the static mark; this is the arrival. The ring draws, then the
   tick. Both are decorative — the heading carries the message — so the whole
   thing is aria-hidden and disappears under reduced motion rather than
   snapping. */

.ml-success { display: grid; gap: var(--space-3); place-items: center; text-align: center; padding: var(--space-4); }
.ml-success__mark { position: relative; width: var(--size-4); height: var(--size-4); }
.ml-success__ring { position: absolute; inset: 0; border-radius: var(--radius-4); background: var(--status-success-bg); animation: ml-pop var(--duration-3) var(--ease-spring) both; }
.ml-success__tick { position: absolute; inset: 0; display: grid; place-items: center; color: var(--status-success-text); animation: ml-pop var(--duration-3) var(--ease-spring) var(--duration-1) both; }
@keyframes ml-pop { from { transform: scale(0.4); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .ml-success__ring, .ml-success__tick { animation: none; }
}

/* ── footer CTA ────────────────────────────────────────────────────────── */
/* Compose: FooterCta (custom)                                               */
/* The end-of-page ask. Ink ground so it closes the page, and the ONE place a
   full-bleed dark block is allowed outside media. Links below are secondary
   and set at 78% white so they do not compete with the action. */

.ml-footer-cta { display: grid; gap: var(--space-3); padding: var(--space-4); border-radius: var(--radius-3); background: var(--color-ink); color: var(--color-white); text-align: center; }
.ml-footer-cta__title { font-family: var(--font-display); font-size: var(--type-4); font-weight: var(--weight-4); line-height: 1.15; margin: 0; }
.ml-footer-cta__sub { font-size: var(--type-3); color: color-mix(in srgb, var(--color-white) 78%, transparent); margin: 0; }
.ml-footer-cta__links { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-3); padding-top: var(--space-3); border-top: var(--border-1) solid color-mix(in srgb, var(--color-white) 16%, transparent); }
.ml-footer-cta__links a { color: color-mix(in srgb, var(--color-white) 78%, transparent); text-decoration: none; font-size: var(--type-1); position: relative; }
/* These sit in a flex row, not in a sentence, so WCAG 2.2's inline-link
   exemption does not cover them — they measured 34x18 and were the last real
   touch-target gap on the page. Expanded rather than padded so the row's
   --space-3 gap and centring are untouched. */
.ml-footer-cta__links a::before {
  content: ""; position: absolute; top: 50%; left: 50%;
  translate: -50% -50%;
  min-width: var(--size-3); min-height: var(--size-3);
  width: 100%; height: 100%;
}
.ml-footer-cta__links a:hover { color: var(--color-white); text-decoration: underline; }

/* ── what's new ────────────────────────────────────────────────────────── */
/* Compose: ReleaseNotesSheet                                                */
/* Release notes, shown once per version. Each entry is tagged by KIND rather
   than coloured, so the list survives greyscale and a screen reader. */

.ml-whatsnew { display: grid; gap: var(--space-3); }
.ml-whatsnew__ver { font-size: var(--type-1); color: var(--color-muted); font-variant-numeric: tabular-nums; }
.ml-whatsnew__item { display: grid; grid-template-columns: auto 1fr; gap: var(--space-3); align-items: start; }
.ml-whatsnew__kind { font-size: var(--type-1); font-weight: var(--weight-3); text-transform: uppercase; letter-spacing: 0.04em; padding: 2px var(--space-2); border-radius: var(--radius-4); background: var(--color-surface); color: var(--color-ink); white-space: nowrap; }
.ml-whatsnew__what { font-size: var(--type-3); }
.ml-whatsnew__why { font-size: var(--type-1); color: var(--color-muted); }

/* ── android app grid ──────────────────────────────────────────────────── */
/* Compose: adaptive-icon layers                                             */
/* Android's adaptive icon: a 108dp canvas of which only the centre 72dp is
   guaranteed visible, because OEM masks vary. The overlay shows the safe zone
   against the mask so a mark is never approved on the full square. */

.ml-appgrid { display: flex; flex-wrap: wrap; gap: var(--space-4); }
.ml-appgrid__cell { display: grid; gap: var(--space-2); justify-items: center; }
/* 96px mirrors Android's real 108dp adaptive-icon canvas at typical mdpi
   preview scale — a platform constant, not a design-scale value. */
.ml-appgrid__canvas { position: relative; width: 96px; height: 96px; display: grid; place-items: center; background: var(--color-surface); border: var(--border-1) solid var(--color-line); }
.ml-appgrid__canvas--circle { border-radius: var(--radius-4); }
.ml-appgrid__canvas--squircle { border-radius: 28px; } /* approximates the OEM squircle mask at this canvas size */
.ml-appgrid__canvas--rounded { border-radius: var(--radius-2); }
.ml-appgrid__safe { position: absolute; width: 66.6%; height: 66.6%; border: var(--border-1) dashed var(--color-pink); border-radius: var(--radius-4); pointer-events: none; }
.ml-appgrid__label { font-size: var(--type-1); color: var(--color-muted); }

/* ── person row ────────────────────────────────────────────────────────── */
/* Compose: ListItem with leadingContent = Avatar                            */
/* Avatar, name, one line of context, and an optional trailing control. This
   existed FOUR times under four names — .thread-row, .collab-row, .req-row
   and .applicant-row across communication and opportunities — with
   byte-identical inner rules and one accidental difference: the applicant
   variant sized its avatar 44px where the other three used 48. That was drift,
   not a decision, so all four are 48 (--avatar-3) here.

   The inner selectors are scoped element/class matches rather than BEM
   because the markup already writes .meta / b / span / p and rewriting five
   screens' inner structure to rename them would be churn with no benefit.
   Scoping them under .ml-person-row keeps a name as generic as .meta safe. */

.ml-person-row {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: var(--border-1) solid var(--color-line);
  text-decoration: none; color: inherit;
  min-height: var(--size-3);
}
.ml-person-row:last-child { border-bottom: 0; }
.ml-person-row > img {
  width: var(--avatar-3); height: var(--avatar-3);
  border-radius: var(--radius-4); object-fit: cover; flex: none;
}
/* flex:1 with min-width:0 is what lets the long subtitle ellipsis instead of
   pushing the trailing control off the row — without min-width a flex item
   refuses to shrink below its content. */
.ml-person-row .meta { flex: 1; min-width: 0; }
.ml-person-row .meta b { font-size: var(--type-3); }
/* DIRECT children only. The unbounded descendant version caught the unread
   dot nested inside <b> and gave it display:block, which pushed it onto its
   own line under the name — visible immediately on chat_thread_list. */
.ml-person-row .meta > span,
.ml-person-row .meta > p {
  display: block; margin: 2px 0 0;
  font-size: var(--type-1); color: var(--color-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ml-person-row .time { font-size: var(--type-1); color: var(--color-muted); flex: none; }

/* ══════════════════════════════════════════════════════════════════════════
   WIDGET DIRECTORY BACKFILL — 2026-08-31

   The fourteen widgets Widget_Directory.md named and the system had never
   built. Until now the guide reported them honestly in its Gaps ledger and
   stopped there, which meant every screen needing one invented its own
   markup — the duplication the system exists to prevent.

   Each section carries its Widget_Directory name in a Compose note, which is
   also how readGaps() in build-guide.mjs decides a widget is built: the
   ledger below is driven by these comments, so a widget cannot be quietly
   marked done without the CSS actually existing.

   Where a widget is a VARIANT of something already here it is built as a
   modifier rather than a new block — StudioVenueListItem is a compact
   StudioCard, RecentProjectCard a compact ProjectCard. Inventing parallel
   blocks for those is how .thread-row / .collab-row / .req-row / .applicant-row
   happened, and .ml-person-row already had to absorb all four.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── recommended collaborator card ─────────────────────────────────────── */
/* Widget_Directory: RecommendedCollaboratorCard · AI collaborator matching   */
/* Compose: RecommendedCollaboratorCard                                       */

.ml-collab-card {
  display: grid; gap: var(--space-2);
  padding: var(--space-3);
  border: var(--border-1) solid var(--color-line);
  border-radius: var(--radius-3);
  box-shadow: var(--elevation-2);
  background: var(--color-white);
  color: inherit; text-decoration: none;
}
.ml-collab-card__head { display: flex; align-items: center; gap: var(--space-2); }
.ml-collab-card__head img {
  width: var(--avatar-3); height: var(--avatar-3);
  border-radius: var(--radius-4); object-fit: cover; flex: none;
}
.ml-collab-card__id { flex: 1; min-width: 0; }
.ml-collab-card__name { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-collab-card__role {
  display: block; font-size: var(--type-1); color: var(--color-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The match figure reuses .ml-match, not a second percentage treatment. */
.ml-collab-card__why { margin: 0; font-size: var(--type-1); color: var(--color-muted); }
.ml-collab-card__why li { margin-top: 2px; }
.ml-collab-card__actions { display: flex; gap: var(--space-2); }
.ml-collab-card:hover { border-color: var(--color-ink); }
.ml-collab-card:active { transform: scale(0.99); }

/* ── collaborator match rail ───────────────────────────────────────────── */
/* Widget_Directory: CollaboratorMatchWidget · a rail OF the card above       */
/* Compose: RecommendedCollaboratorsSection                                   */

/* Deliberately only a layout: the card is .ml-collab-card. A "widget that is
   a collection of another widget" must not restyle its children, or the two
   drift apart the moment one is edited. */
.ml-collab-rail { display: flex; gap: var(--space-3); overflow-x: auto; scroll-snap-type: x mandatory;
                  padding-bottom: var(--space-2); -webkit-overflow-scrolling: touch; }
.ml-collab-rail > .ml-collab-card { flex: 0 0 82%; max-width: 320px; scroll-snap-align: start; }

/* ── discovery search result card ──────────────────────────────────────── */
/* Widget_Directory: DiscoverySearchResultCard · global search result item    */
/* Compose: DiscoverySearchResultCard                                         */

/* .ml-result is the compact ROW used inside a results list. This is the
   richer card the global-search screen shows: it adds genres and match data,
   which a row has no room for. */
.ml-search-result {
  display: flex; gap: var(--space-3); align-items: flex-start;
  padding: var(--space-3);
  border-bottom: var(--border-1) solid var(--color-line);
  color: inherit; text-decoration: none;
}
.ml-search-result:last-child { border-bottom: 0; }
.ml-search-result > img {
  width: var(--avatar-4); height: var(--avatar-4);
  border-radius: var(--radius-2); object-fit: cover; flex: none;
}
.ml-search-result__body { flex: 1; min-width: 0; }
.ml-search-result__name { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-search-result__meta { display: block; margin-top: 2px; font-size: var(--type-1); color: var(--color-muted); }
.ml-search-result__tags { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-2); }
.ml-search-result:hover { background: var(--color-surface); }
.ml-search-result:active { background: var(--color-surface); transform: scale(0.995); }

/* ── networking feed ───────────────────────────────────────────────────── */
/* Widget_Directory: NetworkingFeed · social activity feed                    */
/* Compose: NetworkingFeed                                                    */

/* The POST is .ml-post, which already exists. This is the feed that holds
   them, plus the two states a feed has and a post does not: loading more,
   and nothing to show. */
.ml-feed { display: grid; }
.ml-feed__end { padding: var(--space-4) var(--space-3); text-align: center;
                font-size: var(--type-1); color: var(--color-muted); }
.ml-feed.is-loading .ml-feed__end::after { content: "Loading…"; }
.ml-feed.is-empty > .ml-post { display: none; }

/* ── band members ──────────────────────────────────────────────────────── */
/* Widget_Directory: BandMembersWidget · roster with instrument roles         */
/* Compose: BandMembersWidget                                                 */

.ml-roster { display: grid; gap: var(--space-2); }
.ml-roster__member { display: flex; align-items: center; gap: var(--space-2); }
.ml-roster__member img {
  width: var(--avatar-3); height: var(--avatar-3);
  border-radius: var(--radius-4); object-fit: cover; flex: none;
}
.ml-roster__id { flex: 1; min-width: 0; }
.ml-roster__name { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-roster__instrument { display: block; font-size: var(--type-1); color: var(--color-muted); }
/* Lead is a role, not a decoration — it reuses the verified mark's language
   rather than inventing a second "this one is special" treatment. */
.ml-roster__member[data-lead="true"] .ml-roster__name::after {
  content: "Lead"; margin-left: var(--space-2);
  padding: 0 var(--space-1);
  border-radius: var(--radius-1);
  background: var(--color-ink); color: var(--color-white);
  font-size: var(--type-1); font-weight: var(--weight-2); vertical-align: middle;
}

/* ── studio venue list item ────────────────────────────────────────────── */
/* Widget_Directory: StudioVenueListItem · compact StudioCard                 */
/* Compose: StudioVenueListItem                                               */

/* A MODIFIER on .ml-studio-card, not a new block. Same data, one-line layout. */
.ml-studio-card--compact { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2); }
.ml-studio-card--compact .ml-studio-card__media {
  width: var(--avatar-4); height: var(--avatar-4);
  border-radius: var(--radius-2); flex: none; aspect-ratio: auto;
}
.ml-studio-card--compact .ml-studio-card__body { flex: 1; min-width: 0; padding: 0; }

/* ── featured studio card ──────────────────────────────────────────────── */
/* Widget_Directory: FeaturedStudioCard · hero studio promotion               */
/* Compose: FeaturedStudioCard                                                */

.ml-featured-studio { position: relative; display: block; overflow: hidden;
                      border-radius: var(--radius-3); color: var(--color-white); text-decoration: none; }
.ml-featured-studio .ml-media { aspect-ratio: var(--media-aspect-card); }
.ml-featured-studio__body { position: absolute; z-index: 1; inset: auto var(--space-3) var(--space-3); }
.ml-featured-studio__name { font-family: var(--font-display); font-size: var(--type-4);
                            font-weight: var(--weight-3); line-height: 1.15; }
.ml-featured-studio__meta { display: block; margin-top: 2px; font-size: var(--type-1); opacity: 0.86; }
.ml-featured-studio__rate { margin-top: var(--space-2); font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-featured-studio:active { transform: scale(0.99); }

/* ── studio spec ───────────────────────────────────────────────────────── */
/* Widget_Directory: StudioSpecWidget · console, monitors, mic inventory      */
/* Compose: StudioSpecWidget                                                  */

/* Tabular by nature — a spec sheet is read by scanning one column, so this is
   a definition list, not a card grid. */
.ml-spec { display: grid; gap: var(--space-2); margin: 0; }
.ml-spec__row { display: grid; grid-template-columns: 40% 1fr; gap: var(--space-3);
                padding-bottom: var(--space-2); border-bottom: var(--border-1) solid var(--color-line); }
.ml-spec__row:last-child { border-bottom: 0; padding-bottom: 0; }
.ml-spec__key { font-size: var(--type-1); color: var(--color-muted); }
.ml-spec__val { font-size: var(--type-3); }
.ml-spec__val .ml-tabular { font-variant-numeric: tabular-nums; }

/* ── project card ──────────────────────────────────────────────────────── */
/* Widget_Directory: ProjectCard · portfolio release presentation             */
/* Compose: UnifiedProjectCard                                                */

.ml-project-card {
  display: grid; gap: var(--space-2);
  border: var(--border-1) solid var(--color-line);
  border-radius: var(--radius-3);
  box-shadow: var(--elevation-2);
  background: var(--color-white);
  overflow: hidden; color: inherit; text-decoration: none;
}
.ml-project-card .ml-media { aspect-ratio: var(--media-aspect-square); }
.ml-project-card__body { padding: 0 var(--space-3) var(--space-3); display: grid; gap: var(--space-1); }
.ml-project-card__title { font-size: var(--type-3); font-weight: var(--weight-3); }
.ml-project-card__meta { font-size: var(--type-1); color: var(--color-muted); }
.ml-project-card__credits { display: flex; align-items: center; gap: var(--space-1); margin-top: var(--space-1); }
.ml-project-card:hover { border-color: var(--color-ink); }
.ml-project-card:active { transform: scale(0.99); }

/* ── recent project card ───────────────────────────────────────────────── */
/* Widget_Directory: RecentProjectCard · compact dashboard ProjectCard        */
/* Compose: RecentProjectCard                                                 */

.ml-project-card--compact { grid-template-columns: auto 1fr; align-items: center;
                            gap: var(--space-3); padding: var(--space-2); }
.ml-project-card--compact .ml-media {
  width: var(--avatar-4); height: var(--avatar-4);
  border-radius: var(--radius-2); aspect-ratio: auto;
}
.ml-project-card--compact .ml-project-card__body { padding: 0; }

/* ── track card ────────────────────────────────────────────────────────── */
/* Widget_Directory: TrackCard · track release discovery item                 */
/* Compose: UnifiedTrackCard                                                  */

.ml-track { display: flex; align-items: center; gap: var(--space-3);
            padding: var(--space-2) 0; color: inherit; text-decoration: none; }
.ml-track > .ml-media {
  width: var(--avatar-4); height: var(--avatar-4);
  border-radius: var(--radius-1); flex: none;
}
.ml-track__body { flex: 1; min-width: 0; }
.ml-track__title { font-size: var(--type-3); font-weight: var(--weight-3);
                   white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ml-track__meta { display: flex; align-items: center; gap: var(--space-2);
                  margin-top: 2px; font-size: var(--type-1); color: var(--color-muted); }
/* Platform marks are BADGES, not brand logos — the system ships no third-party
   logo artwork, so the platform is named in text inside the existing badge. */
.ml-track__platforms { display: flex; gap: var(--space-1); }
.ml-track__plays { font-variant-numeric: tabular-nums; flex: none; font-size: var(--type-1);
                   color: var(--color-muted); }
.ml-track:hover .ml-track__title { text-decoration: underline; }
.ml-track:active { transform: scale(0.995); }

/* ── audio podcast demo ────────────────────────────────────────────────── */
/* Widget_Directory: AudioPodcastDemoWidget · inline demo player              */
/* Compose: AudioPodcastDemoWidget                                            */

/* A MODIFIER on .ml-player — the audio component's real class name. Written
   against a guessed `.ml-audio` first, which does not exist: the demo rendered
   unstyled and the guide's own class check caught it. Same transport and
   waveform, boxed for use inside a dashboard card rather than sitting on the
   page ground, and it adds the one thing the bare player has no slot for — a
   title, since a demo reel needs saying what it is. */
.ml-player--inline {
  display: grid; gap: var(--space-2);
  padding: var(--space-3);
  border: var(--border-1) solid var(--color-line);
  border-radius: var(--radius-2);
  background: var(--color-surface);
}
.ml-player__title { font-size: var(--type-1); color: var(--color-muted); }
.ml-player--inline .ml-player__transport { display: flex; align-items: center; gap: var(--space-2); }

/* ── hire contact modal ────────────────────────────────────────────────── */
/* Widget_Directory: HireContactModal · hiring inquiry                        */
/* Compose: HireContactModal                                                  */

/* The shell is .ml-sheet (mobile) or .ml-dialog (expanded) — this is only the
   form body, so the widget cannot fork the sheet's open/close behaviour. */
.ml-hire { display: grid; gap: var(--space-3); }
.ml-hire__scope { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.ml-hire__row { display: grid; gap: var(--space-1); }
.ml-hire__label { font-size: var(--type-1); color: var(--color-muted); }
.ml-hire__budget { display: flex; align-items: center; gap: var(--space-2); }
.ml-hire__budget .ml-field { flex: 1; }
.ml-hire[aria-busy="true"] .ml-btn { pointer-events: none; opacity: var(--opacity-3); }

/* ── share sheet ───────────────────────────────────────────────────────── */
/* Widget_Directory: WhatsAppShareSheet · external share with preview         */
/* Compose: WhatsAppShareSheet                                                */

/* Named for the JOB, not the destination. WhatsApp is one channel; building
   .ml-whatsapp-sheet would guarantee a near-identical .ml-telegram-sheet the
   first time a second channel is added. */
.ml-share { display: grid; gap: var(--space-3); }
.ml-share__preview { display: flex; gap: var(--space-3); align-items: center;
                     padding: var(--space-3); border: var(--border-1) solid var(--color-line);
                     border-radius: var(--radius-2); background: var(--color-surface); }
.ml-share__preview img {
  width: var(--avatar-4); height: var(--avatar-4);
  border-radius: var(--radius-1); object-fit: cover; flex: none;
}
.ml-share__msg { font-size: var(--type-3); }
.ml-share__channels { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* ── touch targets ─────────────────────────────────────────────────────────
   Measured on the rendered catalogue 2026-08-31: 49 controls sat under the
   48dp floor this system's own header claims for "every interactive element",
   eleven of them under even WCAG 2.2's looser 24x24. The contradiction was
   real — meta.tokenMap deliberately sets chip height to --size-1 (32), so the
   claim and the tokens disagreed.

   Resolved the way iOS and Material both resolve it: the control keeps its
   VISUAL size and gains an invisible 48dp TARGET. Growing the boxes instead
   would break the 32dp chip on purpose-built dense surfaces.

   ::before, not ::after — every one of these controls already uses ::after for
   its ripple, tick or dismiss glyph, and a blanket ::after rule would have
   silently replaced them. .ml-check-ctl is the exception: its ::before is the
   checkmark, so it takes ::after.

   The expander is absolutely positioned and centred, so it overhangs without
   affecting layout, and sits at z-index 0 so it never covers the control's own
   content. */
/* .ml-market-card__save is deliberately NOT in this list. It sets
   position:absolute to sit on the artwork, and this rule was overriding that
   back to relative, which dropped the save control out of the image and into
   the card body below it. An absolutely-positioned element is already a
   containing block for its own ::before, so it never needed this. */
.ml-mosaic-tile__pin,
.ml-mixer__mute,
.ml-banner__dismiss,
.ml-upload-grid__remove,
.ml-upload-grid__add { position: relative; }
.ml-mosaic-tile__pin::before,
.ml-mixer__mute::before,
.ml-banner__dismiss::before,
.ml-market-card__save::before,
.ml-upload-grid__remove::before,
.ml-upload-grid__add::before {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  translate: -50% -50%;
  min-width: var(--size-3);
  min-height: var(--size-3);
  width: 100%;
  height: 100%;
  z-index: 0;
  background: none;
  pointer-events: auto;
}

/* ── entry choreography ───────────────────────────────────────────────── */
/* Motion plan Phase 2 (docs/09-Roadmap/motion-plan.md). Precondition: Phase 1
   in ml-core.css ([data-state] transition + @starting-style) must be landed,
   or these fire on every render instead of once per state entry — they do
   land inside the same allow-discrete/@starting-style mechanism, so a card
   only animates in when its containing [data-state] block goes from
   display:none to visible, not on every paint.

   Excessive Motion rule caps this at the primary collection per screen — five
   selectors only, the recycled collection components, not every card-shaped
   thing in the system. transform/opacity only, per the frame-budget rule.
   Stagger 40ms/item, capped at 8 — beyond that the plan calls it "slow, not
   choreographed", so item 9+ reuses item 8's delay rather than continuing to
   climb. */
.ml-artist-card,
.ml-studio-card,
.ml-market-card,
.ml-track,
.ml-collab-rail {
  transition: opacity var(--duration-2) var(--ease-enter),
              transform var(--duration-2) var(--ease-enter);
}
@starting-style {
  .ml-artist-card,
  .ml-studio-card,
  .ml-market-card,
  .ml-track,
  .ml-collab-rail {
    opacity: 0;
    transform: translateY(var(--space-2));
  }
}
.ml-artist-card:nth-child(1), .ml-studio-card:nth-child(1), .ml-market-card:nth-child(1), .ml-track:nth-child(1), .ml-collab-rail:nth-child(1) { transition-delay: 0ms; }
.ml-artist-card:nth-child(2), .ml-studio-card:nth-child(2), .ml-market-card:nth-child(2), .ml-track:nth-child(2), .ml-collab-rail:nth-child(2) { transition-delay: 40ms; }
.ml-artist-card:nth-child(3), .ml-studio-card:nth-child(3), .ml-market-card:nth-child(3), .ml-track:nth-child(3), .ml-collab-rail:nth-child(3) { transition-delay: 80ms; }
.ml-artist-card:nth-child(4), .ml-studio-card:nth-child(4), .ml-market-card:nth-child(4), .ml-track:nth-child(4), .ml-collab-rail:nth-child(4) { transition-delay: 120ms; }
.ml-artist-card:nth-child(5), .ml-studio-card:nth-child(5), .ml-market-card:nth-child(5), .ml-track:nth-child(5), .ml-collab-rail:nth-child(5) { transition-delay: 160ms; }
.ml-artist-card:nth-child(6), .ml-studio-card:nth-child(6), .ml-market-card:nth-child(6), .ml-track:nth-child(6), .ml-collab-rail:nth-child(6) { transition-delay: 200ms; }
.ml-artist-card:nth-child(7), .ml-studio-card:nth-child(7), .ml-market-card:nth-child(7), .ml-track:nth-child(7), .ml-collab-rail:nth-child(7) { transition-delay: 240ms; }
.ml-artist-card:nth-child(n+8), .ml-studio-card:nth-child(n+8), .ml-market-card:nth-child(n+8), .ml-track:nth-child(n+8), .ml-collab-rail:nth-child(n+8) { transition-delay: 280ms; }

/* ── scroll-driven reveal ─────────────────────────────────────────────── */
/* Motion plan Phase 3. .ml-section-block wraps an .ml-section heading plus
   everything that belongs to it, so one reveal covers the whole section
   (heading + content) rather than the heading alone — see the wrapper
   rollout note below. animation-timeline: view() is CSS-only: no JS, no
   IntersectionObserver, no scroll listener, so it cannot jank the main
   thread. Guarded by @supports so unsupported browsers render normally —
   content is readable first, animated second, per the Motion Sensitivity
   rule; there is no unguarded opacity:0 baseline anywhere outside this
   block, so nothing is ever invisible-by-default if the feature is missing.

   Not applied to .ml-section alone: single-section screens whose one
   .ml-section lives inside a [data-state] wizard step (verification_screen
   and 20 others) already animate on state entry via the Phase 1 rule in
   ml-core.css — adding a second, scroll-triggered animation on the same
   content would stack two motions on one element, which the Excessive
   Motion rule (animate 1-2 key elements per view) rules out. Only screens
   where .ml-section-block sits in a long, independently-scrollable page
   (80 files, wrapped 2026-09-01) get this. */
@supports (animation-timeline: view()) {
  @keyframes ml-reveal {
    from { opacity: 0; transform: translateY(var(--space-3)); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .ml-section-block {
    animation: ml-reveal linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 40%;
  }
}
