/*
 * /custom-apps' hero window — the app's custom-app build flow, drawn whole and
 * run as a 28-second loop.
 *
 * NOT generated by `pnpm run port`. Hand-maintained, imported by the
 * custom-apps page alone (through `astroPage`'s `extraCss` slot in
 * tools/04-generate.mjs — the page borrows the `product` key for its chrome)
 * and paired with tools/fragments/custom-apps-hero.mjs, whose header carries
 * the storyboard, the file behind every state and the loop's score. This file
 * carries the measurements and the keyframes.
 *
 * SCALE: ONE TRANSFORM, EVERY LENGTH THE APP'S OWN
 * ------------------------------------------------
 * `.ca-screen` is 1440x790 app px and takes `transform: scale(0.75)` from its
 * top-left corner, which lands it on the captured pane's 1080x592 exactly.
 * That is `nav-panel.css`'s and `bid-surfaces.css`'s model rather than
 * `hero-mockup.css`'s: nothing below is multiplied — `size-8` is 32, `h-12` is
 * 48, `rounded-[18px]` is 18, `text-sm` is 14 — and the app's number is the
 * number in the rule. Type therefore lands at 0.75 of the app's (14 renders as
 * 10.5) where the baked siblings pick a tier off the capture's product ladder;
 * see the fragment's SCALE note for why that trade is taken here. Hairlines
 * stay 1 app px, which is 0.75 css px: the browser paints them, and a scaled
 * hairline is what every other rule in the screen sits against.
 *
 * THE RESTING STILL IS VIEW MODE
 * ------------------------------
 * Every declaration outside a `@keyframes` block describes ONE frame: the
 * finished app open with "Edit app" in the header, the page row named, no chat
 * bar, no toast, no popover, no pointer. The score at the foot only ever moves
 * things away from that and back, so `prefers-reduced-motion: reduce` is one
 * `animation: none` block and a browser that cannot run a property degrades to
 * that same still. Read the layout rules as the destination, and the keyframes
 * as the trip.
 *
 * COLOUR
 * ------
 * The mapping the sibling windows use, onto this site's own tokens:
 *
 *      app                     this file          note
 *   --background            --ca-surface       main region, composer field, app
 *   --sidebar / --muted     --ca-chrome        sidebar, the docked chat bar
 *   --popover               --ca-pop           background in light, a step up in dark
 *   --border                --ca-line
 *   --foreground            --ca-text
 *   --muted-foreground      --ca-text-sec
 *   --secondary-opaque      --ca-text-dim      placeholders, ghost glyphs
 *   --hover                 --ca-hover         a 6% wash of the foreground
 *   --hover-opaque          --ca-hover-opaque  card-03, which inverts on its own
 *   --primary               --ca-accent        #2483e2 in both themes
 *   --green-foreground      --ca-ok            the diff card's "+N"
 *   --destructive           --ca-bad           its "-0"
 *
 * The dark block restates three values: the popover's paper and the two diff
 * tones. Everything else is a token and inverts by itself.
 *
 * THE TRAP
 * --------
 * `src/scripts/empty-mockups.js` finds a window by a `<div>` with exactly three
 * small round children and hides its siblings; the captured title bar above
 * this pane IS that div, which is why the pane carries `data-mockup-filled`.
 * Every box inside the pane is a `<span>` — the tabs pill holds three round
 * children and the empty state's orbit four — so `:where(span) { display:
 * block }` at the top undoes the inline default once. It is `:where()` for the
 * specificity reason `bid-hero-chat.css` records, and it sits ABOVE every
 * layout rule so that source order, not weight, decides — the second half of
 * the same trap, which a sixth fork of that chat paid a build for before it
 * was deleted with /automate's second hero window on 2026-09-08.
 */

.custom-apps-hero {
  --ca-surface: var(--color-theme-bg);
  --ca-chrome: var(--color-theme-card-hex);
  --ca-pop: var(--color-theme-bg);
  --ca-line: var(--color-theme-border-02);
  --ca-text: var(--color-theme-product-text);
  --ca-text-sec: var(--color-theme-product-text-sec);
  --ca-text-dim: var(--color-theme-product-text-tertiary);
  --ca-accent: #2483e2; /* --primary, blue in both themes */
  --ca-hover: color-mix(in srgb, var(--color-theme-fg) 6%, transparent);
  --ca-hover-half: color-mix(in srgb, var(--color-theme-fg) 3%, transparent);
  --ca-hover-opaque: var(--color-theme-card-03-hex);
  --ca-active: var(--color-theme-card-03-hex);
  --ca-ok: #036e4c; /* --green-foreground */
  --ca-bad: #d94a35; /* --destructive */

  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--ca-surface);
  color: var(--ca-text);
  font-family: var(--font-system);
  text-align: left;
  cursor: default;
}

:root[data-theme='dark'] .custom-apps-hero {
  --ca-pop: var(--color-theme-card-02-hex);
  --ca-ok: #2c8966;
  --ca-bad: #e0604b;
}

/* THE THREE GENERIC RULES SIT HERE, ABOVE EVERY LAYOUT RULE — see THE TRAP. */

.custom-apps-hero *,
.custom-apps-hero *::before,
.custom-apps-hero *::after {
  box-sizing: border-box;
}

.custom-apps-hero :where(span) {
  display: block;
}

/* Lucide at the app's own 24 viewBox. `non-scaling-stroke` makes the width
   mean device pixels; it does not inherit, so it goes on the shapes. The
   toast's check and the pointer are filled art and restate their own paint. */
.custom-apps-hero svg:where(:not(.ca-check)) {
  display: block;
  flex: none;
  width: 16px; /* size-4 */
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1; /* lucide's 2 user units on a 24 box at 16px is 1.33 css px; at the screen's 0.75 that is 1 device px, which is what every sibling sets */
  stroke-linecap: round;
  stroke-linejoin: round;
}

.custom-apps-hero svg:where(:not(.ca-check)) * {
  vector-effect: non-scaling-stroke;
}

.ca-ellipsis {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Two states stacked in one grid cell; the score picks which one shows. */
.ca-swap {
  display: grid;
  min-width: 0;
}

.ca-swap > span {
  grid-area: 1 / 1;
  display: flex;
  align-items: center;
  min-width: 0;
}

/* At rest the page is the app, so the "Untitled" half is hidden and the named
   half shows — stated here, not only in the keyframes, or the two halves
   paint on top of each other the moment the score is switched off. */
.ca-t-new {
  opacity: 0;
  visibility: hidden;
}

/* =========================================================================
   The screen: 1440x790 app px, scaled once. `data-complexity="advanced"` is
   what the markup is; nothing reads it.
   ========================================================================= */

.ca-screen {
  position: absolute;
  left: 0;
  top: 0;
  display: flex;
  transform: scale(0.75);
  transform-origin: 0 0;
  overflow: hidden;
  background: var(--ca-surface);
  font-size: 14px; /* text-sm */
  line-height: 20px;
}

/* =========================================================================
   The sidebar — the siblings' shell at the app's own values: a 52px head, the
   advanced nav strip, the pages list, and the footer row that holds the "New"
   pill beside two `size-10` outline buttons (sidebar/index.tsx:30-36).
   ========================================================================= */

.ca-side {
  position: relative;
  width: 221px;
  flex: none;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--ca-chrome);
  border-right: 1px solid var(--ca-line);
}

.ca-side-head {
  height: 52px; /* h-13 */
  flex: none;
  display: flex;
  align-items: center;
  padding: 0 8px;
}

.ca-team {
  flex: 1;
  min-width: 0;
  height: 36px; /* h-9 */
  display: flex;
  align-items: center;
  padding-right: 8px;
  border-radius: 999px;
  font-weight: 500;
}

.ca-team-well {
  width: 40px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
}

.ca-avatar {
  width: 20px; /* size-5 */
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: var(--ca-accent);
  color: #fff;
  font-size: 10px;
  line-height: 1;
  font-weight: 500;
}

/* `p-2 pt-0 flex flex-row items-center gap-1`; Search is `ml-auto`. */
.ca-nav {
  flex: none;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 0 8px 8px;
  font-size: 15px; /* text-[15px] */
}

.ca-nav-item {
  height: 36px; /* h-sidebar-nav-item */
  display: flex;
  align-items: center;
  border-radius: 999px;
  color: var(--ca-text-dim);
}

.ca-nav-well {
  width: 36px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ca-accent);
}

.ca-nav-well svg {
  width: 18px; /* size-4.5 */
  height: 18px;
}

/* Home is lit: the route is a page in the pages list. */
.ca-nav-item[data-active] {
  padding-right: 12px;
  background: var(--ca-active);
  color: var(--ca-text);
}

.ca-nav-item:not([data-active]) .ca-nav-label {
  display: none;
}

.ca-nav-item--search {
  margin-left: auto;
}

.ca-nav-item--search .ca-nav-well {
  color: var(--ca-text-dim);
}

.ca-pages {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 2px; /* space-y-0.5 */
  padding: 8px; /* p-2 */
}

.ca-section {
  height: 32px; /* h-sidebar-item under advanced */
  flex: none;
  display: flex;
  align-items: center;
  padding: 0 4px 0 8px; /* pl-2 pr-1 */
  border-radius: 6px;
  color: var(--ca-text-dim);
  font-weight: 500;
}

/* `SIDEBAR_ROW_CLASS`: `h-sidebar-item rounded-md`, muted at rest. */
.ca-page {
  height: 32px;
  flex: none;
  display: flex;
  align-items: center;
  padding-right: 8px;
  border-radius: 6px;
  color: var(--ca-text-sec);
}

.ca-page-well {
  width: 32px; /* size-sidebar-item */
  height: 32px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ca-text-dim);
}

.ca-page svg {
  width: 18px; /* SIDEBAR_ROW_GLYPH_CLASS size-4.5 */
  height: 18px;
}

/* `rowClassName(true)`: `bg-hover text-foreground`, `font-medium` under
   advanced. This is the open page, so it is lit throughout. */
.ca-page--lit {
  background: var(--ca-hover);
  color: var(--ca-text);
  font-weight: 500;
}

.ca-swap--row {
  flex: 1;
}

/* `AppWindow` in `text-primary` once the page is an app. */
.ca-page-well--app {
  color: var(--ca-accent);
}

.ca-side-foot {
  flex: none;
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: 8px; /* gap-2 */
  padding: 12px; /* p-3 */
}

/* `SidebarNewButton`: `h-10 flex-1 rounded-full border px-3 text-sm shadow-2xs`,
   `text-muted-foreground`. */
.ca-new {
  flex: 1;
  min-width: 0;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 0 12px;
  border: 1px solid var(--ca-line);
  border-radius: 999px;
  color: var(--ca-text-sec);
  box-shadow: 0 1px 1px rgb(0 0 0 / 0.03);
}

/* `Button variant="outline" size="icon"` at `size-10 rounded-full`. */
.ca-round-btn {
  width: 40px;
  height: 40px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--ca-line);
  border-radius: 999px;
  color: var(--ca-text-dim);
  box-shadow: 0 1px 1px rgb(0 0 0 / 0.03);
}

/* -------------------------------------------------------------------------
   The New popover. `side="top" align="center"`, `w-(--trigger) min-w-52 p-1`
   on `rounded-xl bg-popover shadow-popover`; 208 outgrows the 101px trigger
   and the collision padding walks it in from the sidebar's edge. `bottom` is
   the footer's 12 + the pill's 40 + the 4px side offset.
   ------------------------------------------------------------------------- */

.ca-pop {
  position: absolute;
  left: 8px;
  bottom: 56px;
  z-index: 5;
  width: 208px; /* min-w-52 */
  padding: 4px; /* p-1 */
  border-radius: 14px; /* rounded-xl */
  background: var(--ca-pop);
  box-shadow:
    0 0 0 1px var(--ca-line),
    0 10px 24px -6px rgb(0 0 0 / 0.18),
    0 4px 8px -4px rgb(0 0 0 / 0.1);
  transform-origin: 50% 100%;
  opacity: 0;
  visibility: hidden;
  scale: 0.95;
  translate: 0 8px;
}

.ca-pop-label {
  padding: 6px 8px; /* px-2 py-1.5 */
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  color: var(--ca-text-dim); /* text-secondary */
}

/* `MenuItem`: `h-8 rounded-sm px-2 gap-2 text-sm`, glyph in
   `text-muted-foreground-opaque`. */
.ca-menu-row {
  height: 32px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 8px;
  border-radius: 6px;
}

.ca-menu-row svg {
  color: var(--ca-text-sec);
}

.ca-pop-rule {
  height: 1px;
  margin: 4px -4px; /* -mx-1 my-1 */
  background: var(--ca-line);
}

/* =========================================================================
   The main region and the docked chat bar.

   `.ca-main` is absolute inside `.ca-mainwrap` so that its `right` inset can
   be animated: the docked slot is a flex sibling in the app and the main panel
   gives up 320px when it opens (`CHAT_BAR_DEFAULT_WIDTH`), which is what the
   `right` keyframes below do.
   ========================================================================= */

.ca-mainwrap {
  position: relative;
  flex: 1;
  min-width: 0;
  min-height: 0;
}

.ca-main {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--ca-surface);
}

/* `.group/content-header`: 52px, `padding: 0 .5rem` under advanced. */
.ca-head {
  height: 52px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 0 8px;
}

.ca-crumb {
  display: flex;
  flex: 1;
  align-items: center;
  min-width: 0;
}

.ca-swap--crumb {
  flex: 1;
}

/* DocumentCrumb's `size-9` icon well holding the document's glyph at `size-5`,
   `text-muted-foreground-opaque`. */
.ca-crumb-well {
  width: 36px;
  height: 36px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ca-text-sec);
}

.ca-crumb-well svg {
  width: 20px;
  height: 20px;
}

/* The name is an inline field, not a heading: `h-9 -ml-1 px-2 rounded-md
   border-transparent text-sm font-medium`. */
.ca-crumb-title {
  height: 36px;
  margin-left: -4px;
  padding: 0 8px;
  line-height: 36px;
  font-weight: 500;
}

/* Three action clusters in one grid cell, right-aligned; the score shows
   one at a time. */
.ca-actions {
  position: relative;
  flex: none;
  display: grid;
  justify-items: end;
}

.ca-hd {
  grid-area: 1 / 1;
  display: flex;
  align-items: center;
  gap: 8px; /* gap-2 */
  opacity: 0;
  visibility: hidden;
}

/* View mode is the resting still. */
.ca-hd--view {
  opacity: 1;
  visibility: visible;
}

/* `Button variant="ghost"` at `rounded-full size-9`, `text-muted-foreground-
   opaque`; the ones with a word are `h-9 px-3` with `gap-1.5`. */
.ca-ghost-btn {
  width: 36px;
  height: 36px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: var(--ca-text-dim);
}

.ca-ghost-btn--text {
  width: auto;
  gap: 6px;
  padding: 0 12px;
  font-weight: 500;
  white-space: nowrap;
}

/* `DisabledSharePlaceholder` and `FavoriteButton disabled` before the app
   exists — `disabled:opacity-50`. */
.ca-ghost-btn.is-disabled {
  opacity: 0.5;
}

/* `border-r h-5 mx-1` — the rule between the app's actions and the
   document's. */
.ca-head-rule {
  width: 1px;
  height: 20px;
  flex: none;
  margin: 0 4px;
  background: var(--ca-line);
}

/* `text-sm font-medium text-primary`, build-surface.tsx:459. */
.ca-edit-lbl {
  font-weight: 500;
  color: var(--ca-accent);
  white-space: nowrap;
}

/* `HeaderNavTabs`: `flex items-center gap-1 rounded-full bg-hover/50`, each
   tab an `h-8` pill — only the active one carries its word (`pl-0 pr-3`),
   the others are their glyph's `size-8` circle (`pl-0 pr-0`). */
.ca-tabs {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border-radius: 999px;
  background: var(--ca-hover-half);
}

.ca-tab {
  display: inline-flex;
  flex: none;
  align-items: center;
  height: 32px;
  border-radius: 999px;
  color: var(--ca-text-dim);
  white-space: nowrap;
}

.ca-tab > svg {
  margin: 0 8px; /* the size-8 icon well */
}

.ca-tab.is-active {
  padding-right: 12px;
  background: var(--ca-hover);
  color: var(--ca-text);
}

.ca-tab.is-active svg {
  color: var(--ca-accent);
}

/* -------------------------------------------------------------------------
   `AppSaveButton`: a `size="sm"` primary pill whose four labels stay mounted
   and only the active one has width (`grid-cols-[1fr]` / `[0fr]`), beside a
   `w-7` chevron half that is itself width-animated and shares the fill.
   Resting state is the finished one: "Done", no chevron, fully rounded.
   ------------------------------------------------------------------------- */

.ca-save {
  display: inline-flex;
  flex: none;
  align-items: stretch;
  height: 32px;
}

.ca-save-main {
  display: flex;
  align-items: center;
  height: 32px;
  padding: 0 12px; /* px-3; `pr-2` while the chevron is open */
  border-radius: 999px;
  background: var(--ca-accent);
  color: #fff;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
}

.ca-sl {
  display: grid;
  grid-template-columns: 0fr;
  overflow: hidden;
  opacity: 0;
}

.ca-sl > span {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
}

.ca-sl--done {
  grid-template-columns: 1fr;
  opacity: 1;
}

/* `w-7 rounded-l-none border-l border-primary-foreground/20`. Width is the
   whole of its open/closed state — no padding, so `width: 0` really is 0. */
.ca-save-chev {
  width: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-left: 1px solid rgb(255 255 255 / 0.2);
  border-radius: 0 999px 999px 0;
  background: var(--ca-accent);
  color: #fff;
  opacity: 0;
}

/* `Edit app`: the ghost pencil button, `h-9 gap-1.5 rounded-full px-3`. */
.ca-edit-app svg {
  color: var(--ca-text-dim);
}

/* =========================================================================
   The stage under the header — three surfaces stacked, one visible at a
   time: the builder prompt, the building empty state, the app.
   ========================================================================= */

.ca-stage {
  position: relative;
  flex: 1;
  min-height: 0;
}

/* ---- The builder ------------------------------------------------------- */

.ca-builder {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
}

/* `Button size="sm" variant="ghost"` at `absolute left-2 top-2 h-7`. */
.ca-back {
  position: absolute;
  left: 8px;
  top: 8px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  border-radius: 999px;
  color: var(--ca-text-sec);
  font-weight: 500;
}

/* `max-w-2xl px-2` centred in the surface: a 656px column. */
.ca-builder-col {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 656px;
  translate: -50% -50%;
  display: flex;
  flex-direction: column;
}

/* `AgentHeader`: `flex-col items-center pb-4 mb-4`, avatar `size-12 mb-3`,
   name `text-xl font-medium mb-1`. */
.ca-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-bottom: 16px;
  margin-bottom: 16px;
}

/* `AgentAvatar size="lg"` on the `default` colour: a `size-12` round plate
   holding the app-window fallback at `size-6`. */
.ca-agent-av {
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 12px;
  border-radius: 999px;
  background: var(--ca-hover-opaque);
  color: var(--ca-text-sec);
}

.ca-agent-av svg {
  width: 24px;
  height: 24px;
}

.ca-agent-name {
  margin-bottom: 4px;
  font-size: 20px;
  line-height: 28px;
  font-weight: 500;
}

/* ---- The composer (`InputGroup` at `rounded-[18px]`, both windows) ------- */

.ca-input {
  position: relative;
  border: 1px solid var(--ca-line);
  border-radius: 18px;
  background: var(--ca-surface);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.05); /* shadow-xs */
}

/* The textarea: `min-h-14 pt-4 px-4 pb-0`, `font-message` 16/24. */
.ca-input-text {
  position: relative;
  display: flex;
  align-items: flex-start;
  min-height: 56px;
  padding: 16px 16px 0;
  font-size: 16px;
  line-height: 24px;
}

.ca-ph {
  position: absolute;
  left: 16px;
  top: 16px;
  color: var(--ca-text-dim); /* placeholder:text-secondary */
  white-space: nowrap;
}

/* The typed text: a max-content wrapper so `width: 100%` on the clip is
   exactly the sentence, and the typing is a width on `steps()`. */
.ca-typed-wrap {
  display: inline-block;
  width: max-content;
  max-width: 100%;
}

/* The 2px of padding is the caret's room: at `width: 0` the box is still 2px
   wide (border-box), so the caret shows in an empty field, and at 100% the
   wrapper's max-content includes it, so nothing is cut. */
.ca-typed {
  position: relative;
  display: block;
  width: 0;
  padding-right: 2px;
  overflow: hidden;
  white-space: nowrap;
}

/* The caret rides the clip's right edge, which is the typed edge. */
.ca-typed::after {
  content: '';
  position: absolute;
  right: 0;
  top: 2px;
  width: 1.5px;
  height: 20px;
  background: var(--ca-text); /* caret-foreground */
  animation: ca-blink 1s steps(2, jump-none) infinite;
}

/* The footer: `h-12 px-2 pb-2` around 32px controls. */
.ca-input-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 48px;
  padding: 0 8px 8px;
}

.ca-foot-left,
.ca-foot-right {
  display: flex;
  align-items: center;
}

/* `PromptInputButton` ghost at `icon-sm` (32). */
.ca-round {
  width: 32px;
  height: 32px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: var(--ca-text-sec);
}

/* `ProviderModelPicker`'s trigger: `h-8 rounded-full px-2.5 text-sm gap-1.5`,
   the name and level a word apart, the chevron `size-3 text-secondary-opaque`.
   No provider glyph — see the fragment. */
.ca-model {
  height: 32px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  border-radius: 999px;
  white-space: nowrap;
}

.ca-model-level {
  margin-left: -2px;
  color: var(--ca-text-dim);
}

.ca-model svg {
  width: 12px;
  height: 12px;
  color: var(--ca-text-dim);
}

/* The context-usage ring, `-mr-1` against the dictation button. */
.ca-ring {
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-right: -4px;
  color: var(--ca-text-sec);
}

.ca-ring svg {
  width: 20px;
  height: 20px;
  stroke-width: 0;
}

/* Dictation is `variant="default"` — a filled primary circle — while the field
   is empty and `ghost` once there is text; the score swaps it at typeStart. */
.ca-mic,
.ca-round--primary {
  margin-left: 6px; /* ml-1.5 */
  background: var(--ca-accent);
  color: #fff;
}

.ca-mic svg,
.ca-round--primary svg {
  width: 20px; /* [&_svg]:size-5 */
  height: 20px;
}

/* `PromptInputSubmit`: `size="icon-sm"` primary with `ArrowUp size-4`,
   rendered only once the text is non-empty. */
.ca-submit {
  margin-left: 6px;
  background: var(--ca-accent);
  color: #fff;
  opacity: 0;
}

/* ---- The building empty state (`AppBuildingEmptyState`) ------------------
   Ported from app-building-empty-state.module.css value for value, rem
   resolved at 16 since the screen is in app px: `.mark` 8.5rem is 136,
   `.orbit` 6.75rem 108, and so on. The abspos children centre through the
   grid's `place-items`, exactly as the app relies on.
   ------------------------------------------------------------------------- */

.ca-empty {
  position: absolute;
  inset: 0;
  isolation: isolate;
  display: grid;
  place-items: center;
  overflow: hidden;
  padding: 48px 24px;
  background: var(--ca-surface);
  text-align: center;
  opacity: 0;
  visibility: hidden;
}

.ca-glow {
  position: absolute;
  z-index: -2;
  width: min(720px, 115%);
  aspect-ratio: 1.45;
  border-radius: 50%;
  background:
    radial-gradient(circle at 36% 46%, color-mix(in srgb, var(--ca-accent) 30%, transparent), transparent 36%),
    radial-gradient(circle at 66% 44%, color-mix(in srgb, var(--ca-accent) 20%, transparent), transparent 38%),
    radial-gradient(circle at 50% 62%, color-mix(in srgb, var(--ca-accent) 13%, transparent), transparent 44%);
  filter: blur(34px);
  opacity: 0.72;
  animation: ca-glow-drift 8s ease-in-out infinite alternate;
}

.ca-dots {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: radial-gradient(
    circle,
    color-mix(in srgb, var(--color-theme-fg) 17%, transparent) 1px,
    transparent 1.25px
  );
  background-position: center;
  background-size: 22px 22px;
  opacity: 0.72;
  -webkit-mask-image: radial-gradient(ellipse 52% 66% at center, #000 18%, transparent 76%);
  mask-image: radial-gradient(ellipse 52% 66% at center, #000 18%, transparent 76%);
  animation: ca-grid-breathe 4.8s ease-in-out infinite;
}

.ca-empty-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 28px; /* 1.75rem */
}

.ca-mark {
  position: relative;
  display: grid;
  width: 136px;
  height: 136px;
  place-items: center;
}

.ca-orbit {
  position: relative;
  width: 108px;
  height: 108px;
  animation: ca-turn 9s linear infinite;
}

.ca-shape {
  position: absolute;
  transform-origin: center;
  will-change: transform, border-radius;
}

.ca-shape-1 {
  top: 12px;
  left: 10px;
  width: 36px;
  height: 36px;
  border-radius: 12px;
  background: var(--ca-text);
  opacity: 0.94;
  animation: ca-morph-1 4.8s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

.ca-shape-2 {
  top: 14px;
  right: 7px;
  width: 42px;
  height: 23px;
  border-radius: 999px;
  background: var(--ca-text-sec);
  opacity: 0.78;
  animation: ca-morph-2 4.8s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

.ca-shape-3 {
  right: 14px;
  bottom: 9px;
  width: 33px;
  height: 33px;
  border: 7px solid var(--ca-text);
  border-radius: 50%;
  background: transparent;
  opacity: 0.9;
  animation: ca-morph-3 4.8s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

.ca-shape-4 {
  bottom: 5px;
  left: 14px;
  width: 24px;
  height: 45px;
  border-radius: 999px;
  background: var(--ca-text-sec);
  opacity: 0.64;
  animation: ca-morph-4 4.8s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

.ca-empty-text {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  max-width: 384px; /* max-w-sm */
}

.ca-empty-h {
  font-size: 16px; /* text-base font-medium */
  line-height: 24px;
  font-weight: 500;
}

.ca-empty-p {
  color: var(--ca-text-sec);
}

/* `Badge` default: `rounded-full border px-2 py-0.5 text-xs font-medium
   bg-primary text-primary-foreground gap-1.5`, `mt-2.5`. */
.ca-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  padding: 2px 8px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--ca-accent);
  color: #fff;
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  white-space: nowrap;
}

.ca-status-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  animation: ca-status-pulse 1.5s ease-in-out infinite;
}

/* ---- The built app --------------------------------------------------------
   `mt-2 w-full flex-1` is the live preview frame; what is inside it is a
   small app drawn in markup, since the real one is an iframe. Its own
   surface, its own header, one table.
   ------------------------------------------------------------------------- */

.ca-app {
  position: absolute;
  left: 0;
  right: 0;
  top: 8px; /* mt-2 */
  bottom: 0;
  overflow: hidden;
  background: var(--ca-surface);
}

.ca-appv {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 24px;
}

.ca-appv-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.ca-appv-title {
  font-size: 18px;
  line-height: 28px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.ca-appv-sub {
  color: var(--ca-text-sec);
}

.ca-appv-right {
  display: flex;
  flex: none;
  align-items: center;
  gap: 12px;
}

.ca-appv-date {
  color: var(--ca-text-sec);
  white-space: nowrap;
}

.ca-btn {
  display: inline-flex;
  align-items: center;
  height: 32px;
  padding: 0 12px;
  border-radius: 999px;
  font-weight: 500;
  white-space: nowrap;
}

.ca-btn--primary {
  background: var(--ca-accent);
  color: #fff;
}

.ca-table {
  overflow: hidden;
  border: 1px solid var(--ca-line);
  border-radius: 10px;
  background: var(--ca-surface);
}

.ca-tr {
  display: flex;
  align-items: center;
  height: 44px;
  padding: 0 12px;
  border-top: 1px solid var(--ca-line);
}

.ca-tr:first-child {
  border-top: 0;
}

.ca-tr--head {
  height: 40px;
  background: var(--ca-hover-half);
  font-size: 12px;
  font-weight: 500;
  color: var(--ca-text-sec);
}

.ca-td {
  flex: none;
}

.ca-td--name {
  flex: 1;
  min-width: 0;
  font-weight: 500;
}

.ca-tr--head .ca-td--name {
  font-weight: 500;
}

.ca-td--unit {
  width: 64px;
  color: var(--ca-text-sec);
}

.ca-td--num {
  width: 104px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.ca-td--done {
  width: 168px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  padding-left: 16px;
}

/* The one editable cell: today's placed quantity. */
.ca-td-input {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  height: 28px;
  min-width: 72px;
  padding: 0 8px;
  border: 1px solid var(--ca-line);
  border-radius: 6px;
  background: var(--ca-surface);
  font-variant-numeric: tabular-nums;
}

.ca-td-input.is-empty {
  color: var(--ca-text-dim);
}

.ca-bar-track {
  flex: 1;
  height: 6px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-theme-fg) 10%, transparent);
  overflow: hidden;
}

.ca-bar-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--ca-accent);
}

.ca-pct {
  width: 40px;
  flex: none;
  text-align: right;
  color: var(--ca-text-sec);
  font-variant-numeric: tabular-nums;
}

/* =========================================================================
   The docked chat bar — `CHAT_BAR_DEFAULT_WIDTH` 320 on the main region's
   right, `ml-0` under advanced, sliding with `translate-x`. Off screen at
   rest.
   ========================================================================= */

.ca-bar {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  width: 320px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--ca-chrome);
  border-left: 1px solid var(--ca-line);
  translate: 100% 0;
}

/* `p-2 flex items-center justify-between`, borderless under advanced. */
.ca-bar-head {
  height: 52px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 8px;
}

/* New app chat + Open chat history, both `size-9 rounded-full` ghosts;
   they render once `builder.app_id` is bound, i.e. after create_app. */
.ca-bar-acts {
  display: flex;
  flex: none;
  align-items: center;
  gap: 2px;
}

.ca-icon-btn {
  width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: var(--ca-text-dim);
}

/* `ConversationContent`: `gap-2 px-3 pb-8`, below its own `@lg`. */
.ca-log {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px 12px 32px;
  overflow: hidden;
  font-size: 16px; /* .font-message */
  line-height: 24px;
}

.ca-msg--user {
  display: flex;
  justify-content: flex-end;
}

/* `ml-auto max-w-[min(484px,85%)] bg-primary/20 rounded-2xl rounded-tr-xs
   py-2.5 px-4`. */
.ca-bubble {
  max-width: 85%;
  padding: 10px 16px;
  border-radius: 16px 2px 16px 16px;
  background: rgb(36 131 226 / 0.2);
}

.ca-msg--bot {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* `ChainOfThought`: `space-y-4 pl-1`; the header a `-ml-3 -mt-1.5 h-8 px-3`
   pill in `text-sm text-muted-foreground` with its chevron turned open. */
.ca-cot {
  padding-left: 4px;
}

.ca-cot-hd {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  margin: -6px 0 0 -12px;
  padding: 0 12px;
  border-radius: 999px;
  color: var(--ca-text-sec);
  font-size: 14px;
  line-height: 20px;
}

.ca-cot-labels {
  display: grid;
}

.ca-ch {
  grid-area: 1 / 1;
  white-space: nowrap;
  opacity: 0;
}

.ca-ch--hide {
  opacity: 1;
}

.ca-cot-hd > svg {
  color: var(--ca-text-dim);
  transform: rotate(180deg);
}

/* `ChainOfThoughtContent`: `mt-2` inside the root's `space-y-4`, and `pb-4`
   because there is text below the chain. */
.ca-cot-body {
  margin-top: 24px;
  padding-bottom: 16px;
}

/* A step: `pl-6 pb-3 text-sm`, its dot and rail painted by pseudo-elements;
   `complete` steps are `text-muted-foreground`, the active one
   `text-foreground`. */
.ca-step {
  position: relative;
  padding-left: 24px;
  padding-bottom: 12px;
  font-size: 14px;
  line-height: 20px;
  color: var(--ca-text-sec);
}

.ca-step:last-child {
  padding-bottom: 0;
}

.ca-step::before {
  content: '';
  position: absolute;
  top: 7px;
  left: 5px;
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--ca-text-dim);
  opacity: 0.6;
}

.ca-step::after {
  content: '';
  position: absolute;
  top: 18px;
  bottom: -2px;
  left: 7px;
  width: 1px;
  background: var(--ca-text-dim);
  opacity: 0.2;
}

.ca-step:last-child::after {
  display: none;
}

.ca-step-lab {
  display: grid;
}

.ca-la,
.ca-ld {
  grid-area: 1 / 1;
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
}

.ca-la {
  color: var(--ca-text);
  opacity: 0;
}

.ca-ld > span:first-child {
  flex: none;
}

/* `app-edit-card.tsx`: the path in `font-mono`, the counts `font-mono text-xs`
   in `text-green-foreground` and `text-destructive`. */
.ca-mono {
  font-family: var(--font-mono);
}

.ca-diff {
  flex: none;
  font-family: var(--font-mono);
  font-size: 12px;
}

.ca-add {
  display: inline;
  color: var(--ca-ok);
}

.ca-del {
  display: inline;
  color: var(--ca-bad);
}

/* `Shimmer`: a band of light swept across muted text, 2s linear, on a 250%
   background. The app's band is the raw background colour, i.e. a 48px hole
   the text vanishes into for a frame — which at this scale, frozen in a
   screenshot, read as missing letters. The band here is a mix that stays
   legible: the same sweep, a shade lighter rather than gone. */
.ca-shim {
  display: inline-block;
  background: linear-gradient(
    90deg,
    var(--ca-text-sec) 0 calc(50% - 28px),
    color-mix(in srgb, var(--ca-text-sec) 30%, var(--ca-surface)) 50%,
    var(--ca-text-sec) calc(50% + 28px) 100%
  );
  background-size: 250% 100%;
  background-position: 100% 0;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: ca-shimmer 2s linear infinite;
}

.ca-reply {
  padding: 0 2px;
}

/* The bar's composer: `px-3 pb-3` with the app's scroll fade above it. */
.ca-bar-composer {
  position: relative;
  flex: none;
  padding: 0 12px 12px;
}

.ca-bar-composer::before {
  content: '';
  position: absolute;
  top: -24px;
  left: 0;
  right: 0;
  height: 24px;
  background: linear-gradient(to bottom, transparent, var(--ca-chrome));
}

/* =========================================================================
   The toast and the pointer, both children of the screen.
   ========================================================================= */

/* `SuccessToast`: `max-w-[364px] rounded-lg shadow-lg bg-primary p-4`, the
   app's own filled Check at `size-5 mr-3`, `text-sm font-medium`.

   THE APP MOUNTS SONNER AT THE VIEWPORT'S BOTTOM-RIGHT, 22px IN — app/layout.tsx
   `<Toaster position="bottom-right" offset={{ bottom: 22, right: 22 }}>` — and
   in this state that corner is the docked chat bar's composer, so the real
   toast rises over the field's mic and "+" and, while it is still translucent
   on the way in, the buttons show through its last word. That is the one
   placement this window departs from: `right` is the app's 22, `bottom` is
   the composer's 12 of pad + its 106px field + 12 of air, so the toast
   surfaces on the empty log above the field and covers nothing. It is also
   the top-most layer in the pane — above the bar (2), the popover (5) and the
   pointer (9). */
.ca-toast {
  position: absolute;
  right: 22px;
  bottom: 130px; /* the app's 22, lifted clear of the composer — see above */
  z-index: 20;
  display: flex;
  align-items: center;
  max-width: 364px;
  padding: 16px;
  border-radius: 10px;
  background: var(--ca-accent);
  color: #fff;
  font-weight: 500;
  white-space: nowrap;
  box-shadow:
    0 10px 15px -3px rgb(0 0 0 / 0.1),
    0 4px 6px -4px rgb(0 0 0 / 0.1);
  opacity: 0;
  translate: 0 24px;
}

.ca-check {
  display: block;
  flex: none;
  width: 20px;
  height: 20px;
  margin-right: 12px;
  fill: currentColor;
  stroke: none;
}

/* The product's own pointer, 28 app px. The paint is restated here because
   the shared `svg` rule above would otherwise blank it: a `#C9CDD3` 38 ring
   under a `#FAFAFA` 30 one over black, in user units of its own 512 box, so
   `non-scaling-stroke` comes back off. The drop shadow is the asset's own
   `feDropShadow` restated at this size. */
.ca-pointer {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 9;
  width: 28px;
  height: 28px;
  opacity: 0;
  translate: 1175.84px 635.84px;
  filter: drop-shadow(0 1px 1.5px rgb(75 85 99 / 0.22));
  will-change: translate, opacity;
}

.ca-pointer svg {
  width: 100%;
  height: 100%;
}

.ca-pointer path {
  fill: #000;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: none;
}

.ca-pointer path:first-child {
  stroke: #c9cdd3;
  stroke-width: 38;
}

.ca-pointer path:last-child {
  stroke: #fafafa;
  stroke-width: 30;
}

/* =========================================================================
   THE SCORE. One 28s `linear` timeline, every animated element on it, no
   delays. The percentages are the fragment's `SCORE` table restated as
   keyframe stops — read them down, not across, and retime all of them or
   none. A `steps(1, end)` on a stop means "hold this value, then jump at the
   next stop"; a bare stop means "interpolate to the next one".

     0-2      builder at rest           47.5-53   toast; "Done"; chevron closes
     2-10     typing                    48-55     pointer follows to Done, click
     9-12.5   pointer to submit, click  55-57     bar out, main widens, Edit app
     12.5-14.5 handoff: bar in, main    57-70     VIEW MODE HOLD (the still)
              narrows, empty state in   70-75     pointer to New, click, popover
     14-30    create_app, three writes  77-82.5   pointer to App, click, close
     30-32    app in, edit chrome       83-85     reset to the builder
     31       reply                     85-100    builder at rest = 0
     40-44    pointer to Save, click
     44-47.5  "Saving…"
   ========================================================================= */

/* ---- The builder surface ------------------------------------------------ */

.ca-builder {
  animation: ca-builder 28s linear infinite;
}

@keyframes ca-builder {
  0%,
  12.5% {
    opacity: 1;
    visibility: visible;
  }

  14%,
  83% {
    opacity: 0;
    visibility: hidden;
  }

  85%,
  100% {
    opacity: 1;
    visibility: visible;
  }
}

/* The placeholder is the only thing in the field until typing starts. It is
   scoped to the builder: the bar's composer keeps its own placeholder. */
.ca-builder .ca-ph {
  animation: ca-ph 28s linear infinite;
}

@keyframes ca-ph {
  0%,
  1.9% {
    opacity: 1;
  }

  2%,
  82.9% {
    opacity: 0;
  }

  83%,
  100% {
    opacity: 1;
  }
}

/* 70 characters on 70 steps over 2.24s. */
.ca-typed {
  animation: ca-typed 28s linear infinite;
}

@keyframes ca-typed {
  0%,
  2% {
    width: 0;
    animation-timing-function: steps(70, end);
  }

  10%,
  82.9% {
    width: 100%;
  }

  83%,
  100% {
    width: 0;
  }
}

.ca-typed::after {
  animation:
    ca-blink 1s steps(2, jump-none) infinite,
    ca-caret 28s linear infinite;
}

@keyframes ca-caret {
  0%,
  12.5% {
    visibility: visible;
  }

  12.6%,
  82.9% {
    visibility: hidden;
  }

  83%,
  100% {
    visibility: visible;
  }
}

@keyframes ca-blink {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

/* The submit arrow exists only once there is text; dictation drops from the
   filled default to a ghost at the same moment. */
.ca-submit {
  animation: ca-submit 28s linear infinite;
}

@keyframes ca-submit {
  0%,
  2.2% {
    opacity: 0;
  }

  2.6%,
  82.9% {
    opacity: 1;
  }

  83%,
  100% {
    opacity: 0;
  }
}

.ca-mic {
  animation: ca-mic 28s linear infinite;
}

@keyframes ca-mic {
  0%,
  2.2% {
    background-color: var(--ca-accent);
    color: #fff;
  }

  2.6%,
  82.9% {
    background-color: transparent;
    color: var(--ca-text-sec);
  }

  83%,
  100% {
    background-color: var(--ca-accent);
    color: #fff;
  }
}

/* ---- The docked bar and the main region's width ------------------------- */

/* `right` is a layout property where the bar's `translate` is not, and it is
   animated on purpose: the app's main panel really does reflow when the bar
   docks — the table's Item column grows and the header's actions move — and
   a translate would slide the app out of its own frame instead. It runs for
   two seconds twice a loop on a 900px region, which is cheap enough. */
.ca-main {
  animation: ca-main 28s linear infinite;
}

@keyframes ca-main {
  0%,
  12.5% {
    right: 0;
    animation-timing-function: ease-in-out;
  }

  14.5%,
  55% {
    right: 320px;
    animation-timing-function: ease-in-out;
  }

  57%,
  100% {
    right: 0;
  }
}

.ca-bar {
  animation: ca-bar 28s linear infinite;
}

@keyframes ca-bar {
  0%,
  12.5% {
    translate: 100% 0;
    animation-timing-function: ease-in-out;
  }

  14.5%,
  55% {
    translate: 0 0;
    animation-timing-function: ease-in-out;
  }

  57%,
  100% {
    translate: 100% 0;
  }
}

/* ---- The empty state and the app ---------------------------------------- */

.ca-empty {
  animation: ca-empty 28s linear infinite;
}

@keyframes ca-empty {
  0%,
  13% {
    opacity: 0;
    visibility: hidden;
  }

  14.5%,
  30% {
    opacity: 1;
    visibility: visible;
  }

  31.5%,
  100% {
    opacity: 0;
    visibility: hidden;
  }
}

.ca-app {
  animation: ca-app 28s linear infinite;
}

@keyframes ca-app {
  0%,
  30% {
    opacity: 0;
    visibility: hidden;
  }

  32%,
  83% {
    opacity: 1;
    visibility: visible;
  }

  84.5%,
  100% {
    opacity: 0;
    visibility: hidden;
  }
}

/* ---- The header's three states ------------------------------------------ */

.ca-hd--blank {
  animation: ca-hd-blank 28s linear infinite;
}

@keyframes ca-hd-blank {
  0%,
  30% {
    opacity: 1;
    visibility: visible;
  }

  30.5%,
  83% {
    opacity: 0;
    visibility: hidden;
  }

  83.5%,
  100% {
    opacity: 1;
    visibility: visible;
  }
}

.ca-hd--edit {
  animation: ca-hd-edit 28s linear infinite;
}

@keyframes ca-hd-edit {
  0%,
  30% {
    opacity: 0;
    visibility: hidden;
  }

  30.5%,
  55% {
    opacity: 1;
    visibility: visible;
  }

  55.5%,
  100% {
    opacity: 0;
    visibility: hidden;
  }
}

.ca-hd--view {
  animation: ca-hd-view 28s linear infinite;
}

@keyframes ca-hd-view {
  0%,
  55% {
    opacity: 0;
    visibility: hidden;
  }

  55.5%,
  83% {
    opacity: 1;
    visibility: visible;
  }

  83.5%,
  100% {
    opacity: 0;
    visibility: hidden;
  }
}

/* The page's name — in the crumb and in the sidebar row — goes "Untitled" ->
   the app's name when the app exists, and back at the reset. */
.ca-t-new {
  animation: ca-t-new 28s linear infinite;
}

.ca-t-app {
  animation: ca-t-app 28s linear infinite;
}

@keyframes ca-t-new {
  0%,
  30% {
    opacity: 1;
    visibility: visible;
  }

  30.3%,
  83% {
    opacity: 0;
    visibility: hidden;
  }

  83.3%,
  100% {
    opacity: 1;
    visibility: visible;
  }
}

@keyframes ca-t-app {
  0%,
  30% {
    opacity: 0;
    visibility: hidden;
  }

  30.3%,
  83% {
    opacity: 1;
    visibility: visible;
  }

  83.3%,
  100% {
    opacity: 0;
    visibility: hidden;
  }
}

/* ---- The save button's four states -------------------------------------- */

.ca-sl--sp {
  animation: ca-sl-sp 28s linear infinite;
}

.ca-sl--saving {
  animation: ca-sl-saving 28s linear infinite;
}

.ca-sl--done {
  animation: ca-sl-done 28s linear infinite;
}

.ca-save-chev {
  animation: ca-save-chev 28s linear infinite;
}

.ca-save-main {
  animation: ca-save-main 28s linear infinite;
}

@keyframes ca-sl-sp {
  0%,
  30% {
    grid-template-columns: 0fr;
    opacity: 0;
  }

  30.2%,
  44% {
    grid-template-columns: 1fr;
    opacity: 1;
  }

  44.2%,
  100% {
    grid-template-columns: 0fr;
    opacity: 0;
  }
}

@keyframes ca-sl-saving {
  0%,
  44% {
    grid-template-columns: 0fr;
    opacity: 0;
  }

  44.2%,
  47.5% {
    grid-template-columns: 1fr;
    opacity: 1;
  }

  47.7%,
  100% {
    grid-template-columns: 0fr;
    opacity: 0;
  }
}

@keyframes ca-sl-done {
  0%,
  47.5% {
    grid-template-columns: 0fr;
    opacity: 0;
  }

  47.7%,
  100% {
    grid-template-columns: 1fr;
    opacity: 1;
  }
}

/* Open while there are working changes, disabled while saving, closed after. */
@keyframes ca-save-chev {
  0%,
  30% {
    width: 0;
    opacity: 0;
  }

  30.2%,
  44% {
    width: 28px;
    opacity: 1;
  }

  44.2%,
  47.5% {
    width: 28px;
    opacity: 0.5;
  }

  48.5%,
  100% {
    width: 0;
    opacity: 0;
  }
}

/* `rounded-r-none pr-2` while the chevron half is open. */
@keyframes ca-save-main {
  0%,
  30% {
    border-radius: 999px;
    padding-right: 12px;
  }

  30.2%,
  47.5% {
    border-radius: 999px 0 0 999px;
    padding-right: 8px;
  }

  48.5%,
  100% {
    border-radius: 999px;
    padding-right: 12px;
  }
}

/* ---- The toast ------------------------------------------------------------ */

.ca-toast {
  animation: ca-toast 28s linear infinite;
}

@keyframes ca-toast {
  0%,
  47.5% {
    opacity: 0;
    translate: 0 24px;
  }

  48.5%,
  53% {
    opacity: 1;
    translate: 0 0;
  }

  54%,
  100% {
    opacity: 0;
    translate: 0 24px;
  }
}

/* ---- The New pill and its popover --------------------------------------- */

.ca-new {
  animation: ca-new 28s linear infinite;
}

/* Lit on arrival, at the same beat the pointer reaches it. */
@keyframes ca-new {
  0%,
  73.9% {
    background-color: transparent;
  }

  74%,
  77% {
    background-color: var(--ca-hover);
    color: var(--ca-text);
  }

  78%,
  100% {
    background-color: transparent;
  }
}

.ca-pop {
  animation: ca-pop 28s linear infinite;
}

@keyframes ca-pop {
  0%,
  75% {
    opacity: 0;
    visibility: hidden;
    scale: 0.95;
    translate: 0 8px;
  }

  76%,
  82.5% {
    opacity: 1;
    visibility: visible;
    scale: 1;
    translate: 0 0;
  }

  83%,
  100% {
    opacity: 0;
    visibility: hidden;
    scale: 0.95;
    translate: 0 8px;
  }
}

.ca-pop-app {
  animation: ca-pop-app 28s linear infinite;
}

@keyframes ca-pop-app {
  0%,
  81% {
    background-color: transparent;
  }

  81.2%,
  82.5% {
    background-color: var(--ca-hover);
  }

  82.7%,
  100% {
    background-color: transparent;
  }
}

/* ---- The chain of thought ------------------------------------------------- */

/* Two header strings only. `PendingThinkingStatus` draws "Determining next
   steps..." before the first part arrives; from then on the chain is open
   (`setIsOpen(isGenerationStreaming)`) and an open chain's header is
   "Hide thinking" — `chat-conversation.tsx:862-866` — whatever step is
   running. The per-step label ("Requesting create app...") is the step's own,
   never the header's. */
.ca-ch--det {
  animation: ca-ch-det 28s linear infinite;
}

.ca-ch--hide {
  animation: ca-ch-hide 28s linear infinite;
}

@keyframes ca-ch-det {
  0%,
  12.5% {
    opacity: 0;
  }

  12.7%,
  14% {
    opacity: 1;
  }

  14.2%,
  100% {
    opacity: 0;
  }
}

@keyframes ca-ch-hide {
  0%,
  14% {
    opacity: 0;
  }

  14.2%,
  100% {
    opacity: 1;
  }
}

/* Each step arrives on `fade-in-0 slide-in-from-top-2` at its own beat. */
.ca-step-1 {
  animation: ca-step-1 28s linear infinite;
}

.ca-step-2 {
  animation: ca-step-2 28s linear infinite;
}

.ca-step-3 {
  animation: ca-step-3 28s linear infinite;
}

.ca-step-4 {
  animation: ca-step-4 28s linear infinite;
}

@keyframes ca-step-1 {
  0%,
  14% {
    opacity: 0;
    translate: 0 -8px;
  }

  14.5%,
  82.9% {
    opacity: 1;
    translate: 0 0;
  }

  83%,
  100% {
    opacity: 0;
    translate: 0 -8px;
  }
}

@keyframes ca-step-2 {
  0%,
  18% {
    opacity: 0;
    translate: 0 -8px;
  }

  18.5%,
  82.9% {
    opacity: 1;
    translate: 0 0;
  }

  83%,
  100% {
    opacity: 0;
    translate: 0 -8px;
  }
}

@keyframes ca-step-3 {
  0%,
  22% {
    opacity: 0;
    translate: 0 -8px;
  }

  22.5%,
  82.9% {
    opacity: 1;
    translate: 0 0;
  }

  83%,
  100% {
    opacity: 0;
    translate: 0 -8px;
  }
}

@keyframes ca-step-4 {
  0%,
  26% {
    opacity: 0;
    translate: 0 -8px;
  }

  26.5%,
  82.9% {
    opacity: 1;
    translate: 0 0;
  }

  83%,
  100% {
    opacity: 0;
    translate: 0 -8px;
  }
}

/* The active label shows from the step's start to its completion; the done
   label from completion on. */
.ca-step-1 .ca-la {
  animation: ca-la-1 28s linear infinite;
}

.ca-step-2 .ca-la {
  animation: ca-la-2 28s linear infinite;
}

.ca-step-3 .ca-la {
  animation: ca-la-3 28s linear infinite;
}

.ca-step-4 .ca-la {
  animation: ca-la-4 28s linear infinite;
}

.ca-step-1 .ca-ld {
  animation: ca-ld-1 28s linear infinite;
}

.ca-step-2 .ca-ld {
  animation: ca-ld-2 28s linear infinite;
}

.ca-step-3 .ca-ld {
  animation: ca-ld-3 28s linear infinite;
}

.ca-step-4 .ca-ld {
  animation: ca-ld-4 28s linear infinite;
}

@keyframes ca-la-1 {
  0%,
  14% {
    opacity: 0;
  }

  14.2%,
  18% {
    opacity: 1;
  }

  18.2%,
  100% {
    opacity: 0;
  }
}

@keyframes ca-la-2 {
  0%,
  18% {
    opacity: 0;
  }

  18.2%,
  22% {
    opacity: 1;
  }

  22.2%,
  100% {
    opacity: 0;
  }
}

@keyframes ca-la-3 {
  0%,
  22% {
    opacity: 0;
  }

  22.2%,
  26% {
    opacity: 1;
  }

  26.2%,
  100% {
    opacity: 0;
  }
}

@keyframes ca-la-4 {
  0%,
  26% {
    opacity: 0;
  }

  26.2%,
  30% {
    opacity: 1;
  }

  30.2%,
  100% {
    opacity: 0;
  }
}

@keyframes ca-ld-1 {
  0%,
  18% {
    opacity: 0;
  }

  18.2%,
  82.9% {
    opacity: 1;
  }

  83%,
  100% {
    opacity: 0;
  }
}

@keyframes ca-ld-2 {
  0%,
  22% {
    opacity: 0;
  }

  22.2%,
  82.9% {
    opacity: 1;
  }

  83%,
  100% {
    opacity: 0;
  }
}

@keyframes ca-ld-3 {
  0%,
  26% {
    opacity: 0;
  }

  26.2%,
  82.9% {
    opacity: 1;
  }

  83%,
  100% {
    opacity: 0;
  }
}

@keyframes ca-ld-4 {
  0%,
  30% {
    opacity: 0;
  }

  30.2%,
  82.9% {
    opacity: 1;
  }

  83%,
  100% {
    opacity: 0;
  }
}

.ca-reply {
  animation: ca-reply 28s linear infinite;
}

@keyframes ca-reply {
  0%,
  31% {
    opacity: 0;
  }

  31.5%,
  82.9% {
    opacity: 1;
  }

  83%,
  100% {
    opacity: 0;
  }
}

.ca-bar-acts {
  animation: ca-bar-acts 28s linear infinite;
}

@keyframes ca-bar-acts {
  0%,
  18% {
    opacity: 0;
  }

  18.5%,
  82.9% {
    opacity: 1;
  }

  83%,
  100% {
    opacity: 0;
  }
}

/* ---- The pointer ---------------------------------------------------------- */

/* Each stop is the fragment's `POINTER_TARGETS` less the glyph's own 4.16px
   tip offset; the test pins the two files equal. A press is 0.88 of scale,
   which is the only thing a pointer can do to say "mouse down". The two
   invisible legs (83-85) are the teleport back to the start. */
.ca-pointer {
  animation: ca-pointer 28s linear infinite;
}

@keyframes ca-pointer {
  0%,
  9% {
    translate: 1175.84px 635.84px;
    scale: 1;
    opacity: 0;
    animation-timing-function: ease-in-out;
  }

  9.5% {
    opacity: 1;
  }

  11.5% {
    translate: 1129.34px 502.34px;
    scale: 1;
  }

  12% {
    scale: 0.88;
  }

  12.5%,
  40% {
    translate: 1129.34px 502.34px;
    scale: 1;
    animation-timing-function: ease-in-out;
  }

  43% {
    translate: 845.84px 21.84px;
    scale: 1;
  }

  43.5% {
    scale: 0.88;
  }

  /* The split collapses to "Done" at 47.5 and the button's centre moves 57px
     right with it, so the pointer follows straight away rather than sitting
     on the Settings tab until the click. */
  44%,
  48% {
    translate: 845.84px 21.84px;
    scale: 1;
    animation-timing-function: ease-in-out;
  }

  50%,
  54% {
    translate: 902.34px 21.84px;
    scale: 1;
  }

  54.5% {
    scale: 0.88;
  }

  55%,
  57% {
    translate: 902.34px 21.84px;
    scale: 1;
    animation-timing-function: ease-in-out;
  }

  59%,
  70% {
    translate: 755.84px 375.84px;
    scale: 1;
    animation-timing-function: ease-in-out;
  }

  74% {
    translate: 57.84px 752.84px;
    scale: 1;
  }

  74.5% {
    scale: 0.88;
  }

  75%,
  77% {
    translate: 57.84px 752.84px;
    scale: 1;
    animation-timing-function: ease-in-out;
  }

  81% {
    translate: 55.84px 635.84px;
    scale: 1;
  }

  81.5% {
    scale: 0.88;
  }

  82%,
  83% {
    translate: 55.84px 635.84px;
    scale: 1;
    opacity: 1;
  }

  84% {
    opacity: 0;
  }

  84.5% {
    translate: 55.84px 635.84px;
    animation-timing-function: steps(1, end);
  }

  85%,
  100% {
    translate: 1175.84px 635.84px;
    opacity: 0;
  }
}

/* ---- The decorations that run on their own clocks ------------------------ */

@keyframes ca-shimmer {
  from {
    background-position: 100% 0;
  }

  to {
    background-position: 0% 0;
  }
}

@keyframes ca-glow-drift {
  from {
    transform: translate3d(-3%, 1%, 0) scale(0.94) rotate(-2deg);
    opacity: 0.56;
  }

  to {
    transform: translate3d(3%, -2%, 0) scale(1.06) rotate(2deg);
    opacity: 0.82;
  }
}

@keyframes ca-grid-breathe {
  0%,
  100% {
    transform: scale(0.985);
    opacity: 0.5;
  }

  50% {
    transform: scale(1.015);
    opacity: 0.78;
  }
}

@keyframes ca-turn {
  to {
    transform: rotate(360deg);
  }
}

@keyframes ca-status-pulse {
  0%,
  100% {
    transform: scale(0.72);
    opacity: 0.4;
  }

  50% {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes ca-morph-1 {
  0%,
  100% {
    transform: translate(0, 0) rotate(0deg);
    border-radius: 12px;
  }

  25% {
    transform: translate(7px, -5px) rotate(44deg);
    border-radius: 50%;
  }

  50% {
    transform: translate(6px, 7px) rotate(92deg) scale(0.78, 1.22);
    border-radius: 999px;
  }

  75% {
    transform: translate(-4px, 5px) rotate(138deg);
    border-radius: 10px 18px 10px 18px;
  }
}

@keyframes ca-morph-2 {
  0%,
  100% {
    transform: translate(0, 0) rotate(0deg);
  }

  25% {
    transform: translate(5px, 8px) rotate(50deg) scale(0.8, 1.12);
  }

  50% {
    transform: translate(-7px, 8px) rotate(96deg) scale(1.08, 0.86);
  }

  75% {
    transform: translate(-6px, -5px) rotate(142deg) scale(0.84, 1.08);
  }
}

@keyframes ca-morph-3 {
  0%,
  100% {
    transform: translate(0, 0) rotate(0deg);
    border-radius: 50%;
  }

  25% {
    transform: translate(-7px, 5px) rotate(48deg) scale(0.84);
    border-radius: 10px;
  }

  50% {
    transform: translate(-7px, -7px) rotate(94deg) scale(0.9, 1.14);
    border-radius: 50%;
  }

  75% {
    transform: translate(5px, -5px) rotate(140deg);
    border-radius: 9px 16px 9px 16px;
  }
}

@keyframes ca-morph-4 {
  0%,
  100% {
    transform: translate(0, 0) rotate(0deg);
  }

  25% {
    transform: translate(-5px, -7px) rotate(46deg) scale(1.08, 0.84);
  }

  50% {
    transform: translate(8px, -8px) rotate(92deg) scale(0.92, 1.06);
  }

  75% {
    transform: translate(7px, 5px) rotate(138deg) scale(1.1, 0.82);
  }
}

/* THE ONE MEDIA QUERY IN THIS FILE, AND IT IS NOT A BREAKPOINT. Everything
   above the score is one still — the finished app in view mode — and a reader
   who has asked their system for less motion gets exactly that. */
@media (prefers-reduced-motion: reduce) {
  .ca-builder,
  .ca-builder .ca-ph,
  .ca-typed,
  .ca-typed::after,
  .ca-submit,
  .ca-mic,
  .ca-main,
  .ca-bar,
  .ca-empty,
  .ca-app,
  .ca-hd--blank,
  .ca-hd--edit,
  .ca-hd--view,
  .ca-t-new,
  .ca-t-app,
  .ca-sl--sp,
  .ca-sl--saving,
  .ca-sl--done,
  .ca-save-chev,
  .ca-save-main,
  .ca-toast,
  .ca-new,
  .ca-pop,
  .ca-pop-app,
  .ca-ch--det,
  .ca-ch--hide,
  .ca-step-1,
  .ca-step-2,
  .ca-step-3,
  .ca-step-4,
  .ca-step .ca-la,
  .ca-step .ca-ld,
  .ca-reply,
  .ca-bar-acts,
  .ca-pointer,
  .ca-shim,
  .ca-glow,
  .ca-dots,
  .ca-orbit,
  .ca-shape,
  .ca-status-dot {
    animation: none;
  }
}
/*
 * /build — the hand-maintained half of tools/fragments/build-page.mjs.
 *
 * NOT generated by `pnpm run port`. `04-generate.mjs` imports it into the cloud
 * page alone and never rewrites it, the way it treats every other `*-extras`
 * sheet. It loads *after* `src/styles/integrations.css`, which matters once
 * below.
 *
 * It does two jobs.
 *
 * 1. RESTATES THE UTILITIES `cloud.css` NEVER EMITTED
 *    `src/styles/<page>.css` is frozen Tailwind output for that capture, so a
 *    class the captured page never used has no rule behind it. The three cards
 *    relocated here from the home page (`export-formats`, `api`, `mobile`) were
 *    written against `index.css` and use eighteen utilities this capture does
 *    not have — `pb-g1.75` on every figure, and seventeen more inside the phone
 *    mockup, which would otherwise render at the wrong type size with no
 *    padding. Every declaration below is `index.css`'s own, copied verbatim, and
 *    all of them are scoped to `.bu-cards` so nothing leaks into markup that did
 *    not ask for it. `home-feature-quote.css` does the same thing for the same
 *    reason.
 *
 * 2. DRAWS THE TWO GRAPHICS
 *    Both sit in the square `.integration-mockup-box` panel and `.ig-scaler`
 *    stage `integrations.css` defines, at that file's own 340px drawn width — so
 *    from 1280 up they render 1:1 and every length here is the app's own value
 *    in the app's own pixels. This is the opposite of `hero-mockup.css` and
 *    `hero-chat.css`, which scale their contents and therefore pick type off the
 *    capture's `--text-product-*` ladder. Do not import that ladder here.
 *
 * COLOUR
 * ------
 * The map `integrations.css` documents, since `theme-dark.css` already
 * re-anchored the capture's dark palette on the app's neutrals:
 *
 *     app token            this sheet
 *     --background         --color-theme-bg
 *     --card               --color-theme-card-02-hex   (via .ig-surface)
 *     --border             --color-theme-border-02
 *     --foreground         --color-theme-product-text
 *     --muted-foreground   --color-theme-product-text-sec
 *     --hover              --color-theme-card-hover-hex
 *     --primary            #2483e2  (blue, NOT the Bidlo green of the logo)
 *
 * `#2483e2` is the only fixed hex in the file and is fixed in both themes,
 * because the app fixes it. Everything else is a theme token or an opacity on
 * `currentColor`, so nothing in here needs a dark variant.
 */

/* ==================================================================== *
 * 1 — Utilities index.css emitted and cloud.css did not.
 *
 * Scoped to `.bu-cards`, which build-page.mjs puts on every card grid.
 * ==================================================================== */

/* The figure's bottom gap, on every card with a graphic in it — theirs and
   ours. `--spacing-g1.75` is a token cloud.css does carry; only the utility
   that reads it is missing. */
.bu-cards .pb-g1\.75 {
  padding-bottom: var(--spacing-g1\.75);
}

/* A ceiling on the media panel, which this page needs and the home page does
   not.
 *
 * `.integration-mockup-box` is square — its `::after` spacer is `padding-top:
 * 100%` — so the panel is as tall as the card is wide. On the home page that is
 * fine: the grid is `md:grid-cols-2 lg:grid-cols-3`, so a single column only
 * happens below 768px and the widest panel measured is 570. `cloud.css` emits
 * no `md:` or `lg:` column count at all, so this page's grid is
 * `grid-cols-1 xl:grid-cols-3` and runs one column all the way to 1139px —
 * where an uncapped panel would be a ~1090px square holding a 340px graphic.
 *
 * 420 is the ceiling because it is just above the widest panel the three-column
 * range produces (~382 at 1440), so it never binds where the grid is doing the
 * work, and it keeps every panel inside the 340px graphic's 1:1 range. No
 * `margin-inline: auto`: the card's copy below is left-aligned and so is this.
 *
 * `.integration-mockup` is the class every one of these figures carries and the
 * only one with no rule of its own anywhere in the site — it exists to be
 * addressed, and this is the first thing to address it. */
.bu-cards .integration-mockup {
  max-width: 420px;
}

/* The rest belong to the phone mockup inside `RELOCATED_CARDS['mobile']`
   (tools/fragments/integrations-phone.html), which is captured markup carried
   over verbatim from the home page. Ordered as Tailwind emits them: layout,
   spacing, then type. */
.bu-cards .block {
  display: block;
}

.bu-cards .align-middle {
  vertical-align: middle;
}

.bu-cards .h-\[15px\] {
  height: 15px;
}

.bu-cards .w-\[15px\] {
  width: 15px;
}

.bu-cards .top-6 {
  top: calc(var(--spacing) * 6);
}

.bu-cards .mt-\[3px\] {
  margin-top: 3px;
}

.bu-cards .mb-4 {
  margin-bottom: calc(var(--spacing) * 4);
}

.bu-cards .-ml-\[4\.5px\] {
  margin-left: -4.5px;
}

.bu-cards .-mr-\[4\.5px\] {
  margin-right: -4.5px;
}

.bu-cards .pt-6 {
  padding-top: calc(var(--spacing) * 6);
}

.bu-cards .pl-1\.5 {
  padding-left: calc(var(--spacing) * 1.5);
}

.bu-cards .pb-px {
  padding-bottom: 1px;
}

.bu-cards .font-\[system-ui\] {
  font-family: system-ui;
}

.bu-cards .font-\[510\] {
  --tw-font-weight: 510;
  font-weight: 510;
}

.bu-cards .text-\[19px\] {
  font-size: 19px;
}

.bu-cards .tracking-\[-0\.26px\] {
  --tw-tracking: -0.26px;
  letter-spacing: -0.26px;
}

.bu-cards .leading-\[1\.25\] {
  --tw-leading: 1.25;
  line-height: 1.25;
}

.bu-cards .leading-\[1\.35\] {
  --tw-leading: 1.35;
  line-height: 1.35;
}

/* ==================================================================== *
 * 1b — The phone mockup's own rules, for the same reason.
 *
 * `RELOCATED_CARDS['mobile']` carries `tools/fragments/integrations-phone.html`
 * — captured markup, and the one graphic in the section that was never
 * redrawn. Its rules are not in `integrations.css` with the rest of the card
 * styling; they are in `src/styles/index.css`, because that is where the
 * capture put them and the card had never left the home page before. Without
 * them the phone renders as unstyled black-on-white text: no palette, no row
 * hairlines, no working indicator.
 *
 * `cloud.css` does already carry `.shimmer`, `.mobile-interaction-pulse` and
 * their keyframes, so only what is missing is restated here — copied verbatim
 * from `index.css`, prefixed with `.bu-cards` so it cannot reach anything else
 * on the page. The `@keyframes` cannot be scoped and are not: their names are
 * unique and this page defines them nowhere else.
 *
 * NOT copied: `data-[active]:text-[color:var(--mobile-phone-blue)]`, which the
 * markup carries and `index.css` never emitted a rule for — it is inert on the
 * home page too, and restating it here would be inventing one.
 *
 * IF THIS CARD EVER LANDS ON A THIRD PAGE, hoist this block into
 * `integrations.css`, which every page already imports. It is duplicated here
 * rather than moved because that sheet belongs to the home-page cull running in
 * parallel with this rebuild.
 * ==================================================================== */

.bu-cards .mobile-phone-screen {
  --mobile-phone-bg: #f8f8f6;
  --mobile-phone-text: #1f211d;
  --mobile-phone-text-primary: #000000d1;
  --mobile-phone-text-soft: #000000b3;
  --mobile-phone-muted: #00000061;
  --mobile-phone-section-label: #0000007a;
  --mobile-phone-row-border: #00000009;
  --mobile-phone-dot: #0003;
  --mobile-phone-dot-active: #278dcc;
  --mobile-phone-working-outer: #65afe0;
  --mobile-phone-control-bg: #0000000a;
  --mobile-phone-control-text: #00000094;
  --mobile-phone-placeholder: #0000004d;
  --mobile-phone-primary-bg: #ffffffeb;
  --mobile-phone-bubble-bg: #f2f2f0e6;
  --mobile-phone-bubble-border: #00000009;
  --mobile-phone-link: #2b7fa3;
  --mobile-phone-green: #16896a;
  --mobile-phone-red: #c73d59;
  --mobile-phone-blue: #5c9fc5;
  color: var(--mobile-phone-text);
}

[data-theme='dark'] .bu-cards .mobile-phone-screen {
  --mobile-phone-bg: #1f1e1b;
  --mobile-phone-text: #f4f0e8;
  --mobile-phone-text-primary: #ffffffdb;
  --mobile-phone-text-soft: #ffffffb3;
  --mobile-phone-muted: #ffffff6b;
  --mobile-phone-section-label: #ffffff7a;
  --mobile-phone-row-border: #ffffff0e;
  --mobile-phone-dot: #fff3;
  --mobile-phone-dot-active: #5eb8ed;
  --mobile-phone-working-outer: #83c8f0;
  --mobile-phone-control-bg: #ffffff14;
  --mobile-phone-control-text: #ffffffad;
  --mobile-phone-placeholder: #ffffff57;
  --mobile-phone-primary-bg: #ffffff0f;
  --mobile-phone-bubble-bg: #ffffff13;
  --mobile-phone-bubble-border: #ffffff0f;
  --mobile-phone-link: #7fbede;
  --mobile-phone-green: #5cc29a;
  --mobile-phone-red: #e16c85;
  --mobile-phone-blue: #79b7d8;
}

.bu-cards .text-\[color\:var\(--mobile-phone-text-primary\)\] {
  color: var(--mobile-phone-text-primary);
}

.bu-cards .text-\[color\:var\(--mobile-phone-section-label\)\] {
  color: var(--mobile-phone-section-label);
}

.bu-cards .text-\[color\:var\(--mobile-phone-dot\)\] {
  color: var(--mobile-phone-dot);
}

.bu-cards .text-\[color\:var\(--mobile-phone-muted\)\] {
  color: var(--mobile-phone-muted);
}

.bu-cards .border-\[color\:var\(--mobile-phone-row-border\)\] {
  border-color: var(--mobile-phone-row-border);
}

/* The fade over the foot of the phone's list. */
.bu-cards
  .\[mask-image\:linear-gradient\(to_bottom\,black\,black_56\%\,rgba\(0\,0\,0\,0\.9\)_65\%\,rgba\(0\,0\,0\,0\.6\)_76\%\,rgba\(0\,0\,0\,0\.25\)\)\] {
  -webkit-mask-image: linear-gradient(#000, #000 56%, #000000e6 65%, #0009 76%, #00000040);
  mask-image: linear-gradient(#000, #000 56%, #000000e6 65%, #0009 76%, #00000040);
}

/* The working indicator: five dots at 20% of a 15px box, so 3px each — under
   `empty-mockups.js`'s 4px floor, which is why six of these on one card are
   safe even though every dot is fully round. */
.bu-cards .mobile-working-icon {
  display: block;
  position: relative;
}

.bu-cards .mobile-working-icon__dot {
  background: var(--mobile-phone-working-outer);
  border-radius: 50%;
  width: 20%;
  height: 20%;
  animation-duration: 3.1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  position: absolute;
}

.bu-cards .mobile-working-icon__dot--top {
  top: 12%;
  left: 50%;
  transform: translate(-50%);
}

.bu-cards .mobile-working-icon__dot--right {
  top: 50%;
  right: 12%;
  transform: translateY(-50%);
}

.bu-cards .mobile-working-icon__dot--bottom {
  bottom: 12%;
  left: 50%;
  transform: translate(-50%);
}

.bu-cards .mobile-working-icon__dot--left {
  top: 50%;
  left: 12%;
  transform: translateY(-50%);
}

.bu-cards .mobile-working-icon__dot--vertical {
  animation-name: mobileWorkingVerticalOpacity;
}

.bu-cards .mobile-working-icon__dot--horizontal {
  animation-name: mobileWorkingHorizontalOpacity;
}

.bu-cards .mobile-working-icon__dot--center {
  background: var(--mobile-phone-dot-active);
  animation-name: mobileWorkingCenterOpacity;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

@keyframes mobileWorkingVerticalOpacity {
  0%,
  to {
    opacity: 0.72;
  }
  20% {
    opacity: 0.28;
  }
  40% {
    opacity: 0.72;
  }
  60%,
  80% {
    opacity: 0.28;
  }
}

@keyframes mobileWorkingHorizontalOpacity {
  0%,
  to {
    opacity: 0.72;
  }
  20%,
  40% {
    opacity: 0.28;
  }
  60% {
    opacity: 0.72;
  }
  80% {
    opacity: 0.28;
  }
}

@keyframes mobileWorkingCenterOpacity {
  0%,
  40%,
  60%,
  to {
    opacity: 0.18;
  }
  20%,
  80% {
    opacity: 0.64;
  }
}

/* `.shimmer` itself is in cloud.css; only its synchroniser is not. */
.bu-cards .shimmer-synced {
  animation-delay: 0s !important;
}

/* ==================================================================== *
 * 2 — Shared
 * ==================================================================== */

/* lucide's default box. Stroke width is read in *device* pixels by
   `integrations.css`'s `.ig-scaler svg *` rule, which is what stops a 1.5-unit
   stroke on a 24 viewBox drifting between 1.0 and 1.5px as the stage scales. */
.bu-glyph {
  width: 16px;
  height: 16px;
  flex: none;
}

/* ==================================================================== *
 * 3 — The layout selector
 *
 * `features/views/components/header/components/settings/pages/layout/
 * components/layout-selector.tsx`, drawn at its own sizes: the grid is
 * `grid-cols-3 gap-2 px-3.5 pb-1`, each option a `flex flex-col items-center
 * justify-center border rounded-sm p-1.5 text-xs` button holding a `size-4
 * my-1` glyph over a `capitalize` label.
 *
 * One of those classes needs its anchored value rather than Tailwind's
 * default: the app sets `--radius: 0.625rem`, so `rounded-sm` is
 * `calc(0.625rem - 4px)` = **6px**.
 *
 * 2026-09-08 — TWO DEPARTURES, both at the owner's direction, and the
 * fragment's own header carries the argument.
 *
 * THE POPOVER CARD IS GONE. The graphic drew no `.ig-surface` any more, so
 * every length that existed to sit inside one goes with it: the block's own
 * `14px 0 8px`, the head's `0 14px 8px` and the grid's `0 14px 4px` are all
 * zero, and the label and the tiles run the full 340px stage. The head keeps
 * a bottom margin, which is the one gap the card was not paying for.
 *
 * TYPE IS `bid-preview.css`'s LADDER AND NOT THE APP'S `text-xs`. The app
 * sets a section label and a tile label at the same 12/16, which inside a
 * popover is fine — the card's own edge separates the two. On a bare plate
 * nothing does, so this takes the pair the /bid dataset stack one row over
 * already uses: **13px** (`type-product-lg`) on **1.375** for a tile label
 * and **11px** (`type-product-sm`) on the same leading for the head. Move
 * them together, and move them with that file.
 * ==================================================================== */

.bu-lay {
  padding: 0;
}

/* The settings popover labels its sections in the muted colour. It is a bare
   label on the plate now, so the only thing left of the card's own geometry is
   the gap under it. */
.bu-lay__head {
  margin-bottom: 8px;
  font-size: 0.6875rem; /* type-product-sm */
  line-height: 1.375; /* leading-snug */
  color: var(--color-theme-product-text-sec);
}

.bu-lay__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px; /* gap-2 */
}

/* `text-muted-foreground-opaque` on every tile but the current one. The
   "opaque" spelling is an alpha-free value rather than a lighter step, which is
   why this is the secondary colour and not the tertiary. */
.bu-lay__tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px; /* the glyph's my-1, collapsed into one gap */
  padding: 6px; /* p-1.5 */
  border: 1px solid var(--color-theme-border-02);
  border-radius: 6px; /* rounded-sm, anchored on --radius: 0.625rem */
  font-size: 0.8125rem; /* type-product-lg */
  line-height: 1.375; /* leading-snug */
  color: var(--color-theme-product-text-sec);
}

/* `text-primary border-primary` plus `outline-1 outline-primary`. The outline
   is drawn as a second inset ring so it lands outside the border the way an
   outline does, without taking part in the grid's sizing. */
.bu-lay__tile.is-current {
  color: #2483e2; /* --primary */
  border-color: #2483e2;
  box-shadow: 0 0 0 1px #2483e2;
}

/* 2026-09-08, at the owner's direction: the tile answers the pointer, which is
   the app's own behaviour and simply had not been ported.
   `VIEW_LAYOUT_OPTION_TILE_CLASS` is `... cursor-pointer ... transition-none
   hover:bg-hover ...`, and the selected tile adds `cursor-default
   hover:bg-transparent` — so in the app every tile but the current one lights
   under the pointer, and the current one does not, being the thing you already
   have.

   Three things are load-bearing.

   `bg-hover` is `--hover`, which is a 6% WASH of the foreground and not the
   opaque card step — `bg-hover-opaque` is the other value and is a fixed light
   gray. Being translucent it needs no dark restatement, the foreground token
   inverting on its own; same call `bid-context.css` makes for its own
   `--bx-hover`.

   THERE IS NO TRANSITION, and that is the app rather than an omission: the
   class says `transition-none` outright. A fade here would be this file
   inventing a behaviour the product does not have.

   And it is GUARDED ON `hover: hover`, which is this site's own rule for a
   hover state inside a mockup (`integrations.css`'s model rows,
   `bid-hero-mockup.css`'s overlay controls). A touch device fires hover on tap
   and leaves it stuck, so a phone would sit with one tile lit and no pointer
   on it — a state the app never has. Touch readers get the still.

   `focus-visible:ring-2` is deliberately not ported: these tiles are `<span>`s
   in a drawing, so there is nothing to focus. Port it with the day this
   graphic becomes a real control. */
@media (hover: hover) {
  .bu-lay__tile {
    cursor: pointer;
  }

  .bu-lay__tile:hover {
    background: color-mix(in srgb, var(--color-theme-fg) 6%, transparent);
  }

  /* `cursor-default hover:bg-transparent` — the current layout is not a thing
     to pick, so it neither lights nor offers a pointer. (0,3,0) against the
     rule above, and below it in source. */
  .bu-lay__tile.is-current {
    cursor: default;
  }

  .bu-lay__tile.is-current:hover {
    background: transparent;
  }
}

/* ==================================================================== *
 * 4 — The dashboard
 *
 * A widget grid drawn straight on the plate. Widths are whatever the 340px
 * stage leaves; heights are the content's.
 *
 * 2026-09-10: THE OUTER CARD IS GONE, at the owner's direction, and the three
 * widgets took the returned-document card's fill — see `dashboardArt`'s own
 * header in `tools/fragments/build-page.mjs` for the full reasoning. Two
 * lengths moved with it and both existed to inset the contents from an edge
 * that is no longer drawn: this rule's own 12px of padding, and the head's 2px
 * of it. Both go to 0, which runs the head and the widgets the full 340 of the
 * stage — the same move `.bu-lay` made two days earlier one block up. The 8px
 * gaps between the widgets are untouched: those are the air BETWEEN cards, not
 * the air inside one.
 * ==================================================================== */

/* THE CHART IS ONE HUE STEPPED DOWN, and both halves of that are declared
   here, once, so the arc and the swatch that labels it read the same two
   values — see the donut's own block below for the whole of the reasoning.

   `--bu-c` is `--primary`, `oklch(0.606 0.1672 252.84)`, which the app
   declares IDENTICALLY in its light and dark blocks, so this needs no dark
   restatement — the same fixed hue `custom-apps-phone.css` carries as
   `--cx-tp-accent` and the layout picker's own current tile paints.

   `--bu-c-bg` is what a faded slice fades TOWARD, and it is the one departure:
   `mixWithBackground` mixes against `--background` because in the app the
   chart sits on the page, where here it sits inside a widget painted
   `--color-theme-card-02-hex`. Mixing against the page would make the faint
   slices LIGHTER than the widget in light mode, i.e. glowing rather than
   fading, so the arc fades toward the surface actually behind it. It follows
   the theme on its own, which is the other reason there is no dark block. */
.bu-dash {
  padding: 0;
  --bu-c: #2483e2;
  --bu-c-bg: var(--color-theme-card-02-hex);
}

/* One child since 2026-09-10, the `⬇ PDF` pill having come off beside it —
   see `build-page.mjs`' glyph-table header. Kept as a flex row so the title's
   own `flex: 1` ellipsis still has a line to run out of. */
.bu-dash__head {
  display: flex;
  align-items: center;
  padding: 0 0 10px;
}

.bu-dash__title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  font-weight: 500;
  color: var(--color-theme-product-text);
}

.bu-dash__body {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.bu-dash__stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

/* One widget, and AS OF 2026-09-10 IT IS CARD 1'S RETURNED-DOCUMENT CARD:
   `--color-theme-card-02-hex` under a 1px `--color-theme-border-02` hairline
   on an 8px corner, which is `.cx-doc` in `src/styles/custom-apps-cards.css`
   value for value. The hairline and the corner were already those; the FILL is
   the change, from `--color-theme-bg`.

   IT IS THE OTHER HALF OF THE OUTER CARD GOING and must not be read alone. It
   painted `bg-background` because it sat inside a `bg-card` dashboard, which
   is the app's own nesting — the widget is the page surface and the dashboard
   around it is the card. With no dashboard card there is no page surface to
   be: `--color-theme-bg` is two steps off either plate and reads as a hole
   punched in it, where card-02 is the one step a card lying on a plate takes.

   A PASS ON 2026-09-09 MADE THIS EDIT ALONE AND WAS REVERTED — it repainted
   this widget `--card` and zeroed `.bu-dash`'s padding while `dashboardArt`
   still emitted `.ig-surface`, so three widget-shaped cards sat flush to the
   edges of the card they were still inside. Both halves have to move together:
   do not put `--color-theme-bg` back without putting the outer card back with
   it, and do not put the outer card back without repainting this. */
.bu-w {
  padding: 10px;
  background: var(--color-theme-card-02-hex);
  border: 1px solid var(--color-theme-border-02);
  border-radius: 8px;
}

.bu-w__title {
  font-size: 11px;
  color: var(--color-theme-product-text-sec);
}

.bu-w__stat {
  margin-top: 6px;
  font-size: 20px;
  font-variant-numeric: tabular-nums;
  color: var(--color-theme-product-text);
}

.bu-w__chart {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 8px;
}

/* ------------------------------------------------------------------ *
 * The donut
 *
 * `.ig-scaler svg *` sets `vector-effect: non-scaling-stroke`, which is right
 * for a 1.5-unit hairline on a 24 viewBox and wrong for a 16-unit ring on a 120
 * one: read in device pixels the ring would stay 16px wide while its radius
 * shrank with the stage. This sheet loads after that one, so an equal-weight
 * selector would already win, but the override is written at higher specificity
 * so the two files' import order is not load-bearing.
 *
 * 2026-09-10: ONE HUE STEPPED DOWN, WHERE IT DREW FIVE HAND-PICKED OPACITIES.
 * At the owner's direction, over two rounds — see `dashboardArt`'s own header
 * for the full reasoning and for the five-hue version that shipped for an hour
 * in between. The short version: the platform's own selected-colour palettes
 * fade ONE base across the slice count, and that is what this card should draw.
 *
 * THE RAMP IS `getPaletteSliceFill`'s ARITHMETIC AND IS NOT IN THIS FILE.
 * `floor(100 - index * 100 / sliceCount)` is computed per slice in
 * `build-page.mjs` and emitted as `--bu-mix` on the arc AND on the swatch that
 * labels it, so this sheet mixes rather than steps and a sixth scope re-solves
 * the ramp with no rule to edit. At five slices it is 100 / 80 / 60 / 40 / 20.
 *
 * IT IS A MIX AND NOT AN `opacity`, which is the app's own choice and matters
 * here: a faded slice stays opaque, so the ring never shows the plate through
 * itself and the two overlapping ends of a dash cannot double up. Putting an
 * opacity ladder back is a deliberate change and a test fails on one.
 *
 * VALIDATED RATHER THAN EYEBALLED, and the scope is the right one: this is a
 * SEQUENTIAL ramp used for identity, so what it owes is lightness monotonicity
 * rather than the categorical checks — every step is strictly lighter than the
 * one before it against the light fill and strictly darker against the dark
 * one. Identity is carried by the legend, which names AND figures every slice,
 * so nothing on this card is identified by colour alone; and the slices are in
 * value order, largest first, so the ramp encodes the magnitude as well.
 * ------------------------------------------------------------------ */

.ig-scaler .bu-donut circle {
  vector-effect: none;
}

.bu-donut {
  width: 110px; /* `DRAWN_PX` in build-page.mjs — the gap is solved against it */
  height: 110px;
  flex: none;
}

.bu-donut__arc {
  stroke: color-mix(in srgb, var(--bu-c) var(--bu-mix), var(--bu-c-bg));
}

.bu-legend {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.bu-legend__row {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  color: var(--color-theme-product-text-sec);
}

/* 8px at a 2px radius, which is deliberately not round: `empty-mockups.js`
   treats any div holding exactly three children between 4 and 16px wide,
   near-square and rounded to at least half their width as a window's traffic
   lights. These are squares, there are five of them, and each sits in its own
   row — the trap is missed three ways.

   The fill is the slice's own step and never the row's ink: the row around it
   is set in the muted colour, and a swatch tinted by that would not match the
   arc it labels. It is the SAME declaration the arc carries, read against the
   same `--bu-mix` the fragment emitted on both, so the two cannot drift. */
.bu-legend__dot {
  width: 8px;
  height: 8px;
  flex: none;
  border-radius: 2px;
  background: color-mix(in srgb, var(--bu-c) var(--bu-mix), var(--bu-c-bg));
}

.bu-legend__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Right-aligned and tabular so the magnitudes stack, the way
   `integrations.css` sets every figure it prints beside a label. */
.bu-legend__figure {
  flex: none;
  font-variant-numeric: tabular-nums;
  color: var(--color-theme-product-text);
}
/*
 * /custom-apps — the three graphic cards in #build.
 *
 * NOT generated by `pnpm run port`. `04-generate.mjs` imports it into this
 * page alone, through the same `extraCss` slot `custom-apps-hero.css` arrives
 * on, and never rewrites it.
 *
 * Two of the three graphics are the dormant /build rebuild's own —
 * `BUILD_GRAPHICS.dashboard` and `.layout`, imported by
 * tools/fragments/custom-apps-page.mjs rather than copied — so their rules
 * arrive with `src/styles/build-extras.css`, which this page now imports for
 * that reason and no other. The third is card 1's build thread, redrawn from
 * the capture and the one graphic on this page whose every length lives here;
 * it replaced `integrations.css`'s `.ig-cmp` composer on 2026-09-07. What is
 * left besides it is the plate, the panel's per-card ratio, and the one thing
 * a borrowed graphic needs that its own page does not.
 *
 * PREFIX is `cx-`, not `ca-`: src/styles/custom-apps-hero.css already owns
 * 211 unscoped `ca-` rules on this same page.
 */

/* ------------------------------------------------------------------ *
 * The plate and the panel
 * ------------------------------------------------------------------ */

/* THE CAP IS ON THE PANEL AND NOT ON THE PLATE, AS OF 2026-09-04, and the
 * difference is the whole of what this page looked wrong for on a phone.
 * This grid is `grid-cols-1 xl:grid-cols-3`, so it runs ONE column all the way
 * to 1139px and the plate there is ~949px wide — and a 340px graphic needs a
 * cap somewhere or the square panel below becomes a 949px-tall box. It used to
 * be capped on the plate itself, left-aligned, on the reading that the copy
 * above it is left-aligned too; what that actually drew below xl was a 420px
 * grey box in a 949px card with 529px of empty card beside it, which reads as
 * a graphic that failed to load rather than as a composition.
 *
 * So the plate fills the card, as every other captured plate on this site
 * does, and the 420 caps the PANEL inside it, centred. That is the answer
 * `bid-capabilities.css` already gives with `.bk-fit` and for the same reason:
 * auto margins absorb positive free space only, so this centres inside the
 * wide single-column plate and simply sits at the left edge once the
 * three-column grid takes the panel back under 420. Nothing moves at xl and
 * up — measured, 0 differing pixels and 0 px of page height at 1440/1280. */
/* `integration-mockup-box` is a fixed 4:5 and this is SQUARE, which is the one
 * number in this file worth knowing. `.ig-scaler` divides the panel by a
 * single `--ig-fit` (372) on both axes and takes the smaller, so a panel
 * shorter than 372 shrinks the graphic even where the width has room to spare
 * — a 46%-tall panel drew the composer at 0.48. Square is the shortest box
 * that still renders all three 1:1 from 1024 up, where the grid's one column
 * caps the panel at 420. The rest is `integration-mockup-box`'s own recipe,
 * restated because it is the half that does not change. */
.cx-art__box {
  display: grid;
  grid-template-rows: repeat(1, minmax(0, 1fr));
  grid-template-columns: repeat(1, minmax(0, 1fr));
  position: relative;
  width: 100%;
  /* The cap, and the auto margins that centre it — see the note above. */
  max-width: 420px;
  margin-inline: auto;
  background-color: transparent;
}

.cx-art__box::after {
  content: "";
  pointer-events: none;
  grid-area: 1 / 1 / -1 / -1;
  padding-top: 100%;
}

/* ------------------------------------------------------------------ *
 * Card 1 — the build thread
 *
 * 2026-09-07, at the owner's direction: the composer that used to stand here
 * came out for the capture's own agent turn — the "Self-testing" card on what
 * ships as /manage. `tools/fragments/custom-apps-page.mjs` carries the full
 * argument; the part that matters here is that this page renders under the
 * **product** key, `product.css` is frozen Tailwind output for /bid's capture,
 * and 29 of that block's 72 classes have no rule in it because they are
 * cloud.css's. So the block is redrawn VALUE FOR VALUE rather than lifted
 * class for class, and the class each number came from is in the comment
 * beside it. Re-measure against `cursor-clone/cloud.html`, never by eye.
 *
 * Everything is drawn 1:1 in card pixels on the capture's own
 * `--text-product-*` ladder — `.ig-scaler` fits the 340px stage to the panel
 * with one transform, so nothing in here is scaled by hand.
 *
 * The document card half is a deliberate SECOND copy of `hero-chat.css`'s
 * `.hc-doc*`, which ports the same card at the same type ladder. Its rules
 * hang off tokens declared on `.hero-chat`, so borrowing them would mean
 * importing a whole chat window's stylesheet for a 77px card. Same fork rule
 * the six window mockups follow: a change that belongs to both is made twice.
 * ------------------------------------------------------------------ */

.cx-turn {
  display: flex;
  flex-direction: column;
  gap: 8px; /* space-y-2 */
  min-height: 260px; /* min-h-[260px] */
  /* The sheet the thumbnail draws. The capture says `bg-theme-bg`, and in
     light that is right — #f7f7f4 over the #ebeae5 card reads as paper lifted
     off it. Dark inverts the relationship (bg #191919 against a #242424 card
     is a hole rather than a page), so dark continues the theme's own ladder by
     the same step the light sheet sits above its card. Same value and the same
     reasoning as `--hc-doc-paper`, whose card is this card. */
  --cx-doc-paper: var(--color-theme-bg);
  --cx-cursor-edge: #fff;
  --cx-cursor-body: #1a1a1a;
  /* The form's two bands — the app's own `--orange-primary` and
     `--blue-primary`, converted from oklch and copied per the fork rule. See
     the page block further down for why they are restated in dark. */
  --cx-doc-accent: #e48900; /* --orange-primary oklch(0.7059 0.1958 75.66) */
  --cx-doc-head: #0067a5; /* --blue-primary   oklch(0.4962 0.1286 244.16) */
  --cx-doc-on-accent: #fff; /* knocked out of a filled band in both themes */
  --cx-doc-wash: color-mix(in srgb, var(--cx-doc-head) 8%, var(--cx-doc-paper));
}

:root[data-theme='dark'] .cx-turn {
  --cx-doc-paper: #303030;
  --cx-cursor-edge: #1a1a1a;
  --cx-cursor-body: #fff;
  --cx-doc-accent: #e28600; /* --orange-primary oklch(0.7005 0.1835 72.15) */
  --cx-doc-head: #2575c7; /* --blue-primary   oklch(0.5586 0.148 252.7) */
}

/* The reader's own words. `bg-theme-card-04-hex border-theme-border-02
   text-theme-text type-product-lg rounded-md border px-3 py-2`, all of it. */
.cx-turn__ask {
  padding: 8px 12px; /* py-2 px-3 */
  border: 1px solid var(--color-theme-border-02);
  border-radius: var(--radius-md); /* rounded-md */
  background: var(--color-theme-card-04-hex);
  color: var(--color-theme-text);
  font-size: var(--text-product-lg); /* type-product-lg */
  line-height: var(--leading-product-base);
}

/* `space-y-2 pl-2` — the steps are indented under the ask, which is what says
   they belong to it without drawing a rail. */
.cx-turn__steps {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-left: 8px;
}

/* One step. The capture draws its verb in full secondary ink and its object at
   60% on the same line, which is what makes a column of them read as a list of
   actions rather than as a paragraph. `type-product-lg text-theme-text-sec
   leading-snug`, the two spans `gap-1`. */
.cx-step {
  display: flex;
  align-items: baseline;
  gap: 4px; /* gap-1 */
  min-width: 0;
  color: var(--color-theme-text-sec);
  font-size: var(--text-product-lg);
  line-height: var(--leading-snug); /* leading-snug */
}

.cx-step__verb {
  flex: none; /* flex-shrink-0 */
}

/* `min-w-0 truncate opacity-60` — a path longer than the stage loses its tail
   rather than wrapping, which is what keeps the four steps on four lines. */
.cx-step__obj {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0.6;
}

/* ---- the app it hands back ---------------------------------------- *
 * `rounded-lg border border-theme-border-02 bg-theme-card-02-hex
 * overflow-hidden` over a `flex items-stretch` pair.
 *
 * THE HEIGHT IS STATED AND IT IS SOLVED. The thumbnail is `items-stretch`, so
 * the card's height IS the graphic's height — and in the capture that height
 * comes from a title ("Clipboard exfiltration vulnerability demo") that wraps
 * to two lines: 12 + 12 of padding, 2 x 17.9 of title, 4, and 15.1 of sub, i.e.
 * ~79, so the thumbnail is 88 x 79 and reads at about 1.11. "Daily quantities"
 * is one line and would leave the card 61 tall, i.e. an 88 x 61 letterbox at
 * 1.44 — a different object. 77 puts it back at 1.14. Re-solve it against the
 * drawn name rather than nudging it.
 *
 * It runs the FULL width of the block, as the capture's own block-level card
 * does — `hero-chat.css`'s copy caps itself at 340 to make its long title wrap,
 * and this name is two words. Do not add `align-self: flex-start` back: a card
 * shrunk to a two-word name reads as a chip rather than as the thing the thread
 * handed over.
 * ------------------------------------------------------------------- */
.cx-doc {
  display: flex;
  align-items: stretch;
  min-height: 77px;
  overflow: hidden;
  border: 1px solid var(--color-theme-border-02);
  border-radius: var(--radius-lg); /* rounded-lg */
  background: var(--color-theme-card-02-hex);
}

.cx-doc__thumb {
  position: relative;
  flex: none;
  width: 88px; /* w-[88px] */
  overflow: hidden;
}

/* The page: inset on three sides and running off the bottom, which is what
   makes it read as a sheet the frame is cropping rather than as an icon.
   `absolute inset-x-2 top-2.5 bottom-2 rounded-[3px]` and the capture's own
   two-layer shadow. The capture's `<img src=data:,>` and the `bg-black/[0.06]`
   wash over it are dropped: SingleFile stripped whatever image sat behind the
   page and the wash existed to darken it, so both are scenery for something
   that is not there. */
.cx-doc__page {
  position: absolute;
  top: 10px; /* top-2.5 */
  right: 8px; /* inset-x-2 */
  bottom: 8px;
  left: 8px;
  overflow: hidden;
  border-radius: 3px;
  background: var(--cx-doc-paper);
  box-shadow:
    rgb(0 0 0 / 0.25) 0 2px 8px,
    rgb(0 0 0 / 0.12) 0 0 0 0.5px;
}

/* THE TRAP, AND IT IS A REAL ONE. This is a container with exactly three round
   children, which is the signature `src/scripts/empty-mockups.js` uses to find
   a window mockup — it would take this thumbnail for a window and
   `display: none` its siblings. It is safe TWICE over: that script queries
   `div` and this bar is a `<span>`, and the dots are 3px where it requires
   `width > 4`. Do not make it a div and do not grow them past 4px. */
.cx-doc__bar {
  display: flex;
  align-items: center;
  gap: 3px; /* gap-[3px] */
  padding: 4px 6px; /* py-1 px-1.5 */
  border-bottom: 0.5px solid
    color-mix(in srgb, var(--color-theme-fg) 8%, transparent);
}

.cx-doc__bar i {
  width: 3px; /* h-[3px] w-[3px] — see THE TRAP above */
  height: 3px;
  border-radius: 999px;
  background: var(--color-theme-fg);
  opacity: 0.15;
}

/* ---- the page inside the thumbnail -------------------------------- *
 * A FORM AS OF 2026-09-10, WHERE IT WAS SIX GREY RULES. At the owner's
 * direction, against a job-site daily log they supplied: a masthead band, a
 * field strip, a table under a filled header row, then the next section's band
 * running off the crop. `buildTurnArt`'s own header carries the reasoning; what
 * matters here is that the page is **72 x 57 css px**, so every run of text is
 * a bar and nothing on the sheet is legible — which is also what keeps an
 * invented form from making a claim.
 *
 * THE TWO HUES ARE THE APP'S OWN `--orange-primary` AND `--blue-primary`,
 * converted from oklch by arithmetic and copied per the fork rule (the same
 * pair `custom-apps-phone.css` carries as `--cx-tp-orange` / `--cx-tp-blue`).
 * They are restated in dark because the app restates them: a masthead is
 * printed ink, but this thumbnail's own paper already inverts, so a band that
 * did not would be the light hue on a dark sheet.
 *
 * The wash under alternate rows is a `color-mix` against that same blue and
 * the sheet's own paper, so it follows both and needs no dark restatement.
 * ------------------------------------------------------------------- */

/* No padding of its own: the masthead runs edge to edge as a masthead does,
   and every block below it states the inset it wants. */
.cx-doc__lines {
  display: flex;
  flex-direction: column;
}

/* The masthead, and its one white bar is the title. */
.cx-doc__head {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 8px;
  flex: none;
  background: var(--cx-doc-accent);
}

.cx-doc__head i {
  display: block;
  width: 40%;
  height: 3px;
  border-radius: 999px;
  background: var(--cx-doc-on-accent);
  opacity: 0.85;
}

/* Two label lines under it — the Project / Date / Log # block — on the same
   wash the table's alternate rows take, which is what makes it read as a
   fielded band rather than as two stray rules under a masthead. */
.cx-doc__fields {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: none;
  padding: 2px 6px 1px;
  background: var(--cx-doc-wash);
}

.cx-doc__fields i {
  display: block;
  height: 2px;
  border-radius: 999px;
  background: var(--color-theme-fg);
  opacity: 0.12;
}

.cx-doc__fields i:first-child { width: 62%; }
.cx-doc__fields i:nth-child(2) { width: 44%; }

/* The table's own header row: filled, with three column labels knocked out of
   it. They are wide bars rather than dots — see THE TRAP above, which they
   miss on every one of its three tests. */
.cx-doc__th {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: none;
  height: 5px;
  margin: 1px 6px 0;
  padding: 0 3px;
  background: var(--cx-doc-head);
}

.cx-doc__th i {
  display: block;
  flex: 1;
  height: 2px;
  border-radius: 999px;
  background: var(--cx-doc-on-accent);
  opacity: 0.8;
}

/* The rows under it, on the table's own hairline, every other one washed. */
.cx-doc__rows {
  display: flex;
  flex-direction: column;
  flex: none;
  margin: 0 6px;
}

.cx-doc__rows i {
  display: block;
  height: 5px;
  border-bottom: 0.5px solid
    color-mix(in srgb, var(--color-theme-fg) 10%, transparent);
}

.cx-doc__rows i:nth-child(even) {
  background: var(--cx-doc-wash);
}

/* The next section's own band. IT IS THE LAST THING THE CROP HOLDS AND THAT
   IS SOLVED: the page is 57 tall, the chrome bar takes 12, and the blocks
   above come to 8 + 9 + 1 + 5 + 15 = 38, so the band sits 51..56 with the rows
   under it all but one pixel off the cut — which is what says the form carries on. Re-solve
   the stack against `.cx-doc`'s own 77 rather than nudging one height. */
.cx-doc__band {
  flex: none;
  height: 5px;
  margin: 1px 6px 0;
  background: var(--cx-doc-accent);
}

/* Filled, two-tone and mirrored (`-scale-x-100`), so it opts out of
   `.ig-scaler svg`'s lucide handling rather than reading it. Both fills are
   variables — see `--cx-cursor-*` at the top of this block. */
.cx-doc__cursor {
  position: absolute;
  right: 6px; /* right-1.5 */
  bottom: 6px; /* bottom-1.5 */
  width: 12px;
  height: 12px;
  stroke: none;
  transform: scaleX(-1);
  filter: drop-shadow(0 0.5px 1px rgb(0 0 0 / 0.3)); /* drop-shadow-[0_0.5px_1px_…] */
}

.cx-doc__meta {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
  padding: 12px; /* px-3 py-3 */
}

.cx-doc__name {
  color: var(--color-theme-text);
  font-size: var(--text-product-lg); /* type-product-lg */
  line-height: var(--leading-snug); /* leading-snug */
}

.cx-doc__sub {
  margin-top: 4px; /* mt-1 */
  color: var(--color-theme-text-tertiary); /* text-theme-text-tertiary */
  font-size: var(--text-product-sm); /* type-product-sm */
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-product-sm);
}

/* ------------------------------------------------------------------ *
 * Card 2 — the dashboard
 *
 * THIS FILE DRAWS NOTHING FOR IT AS OF 2026-09-10, and the rule that is gone
 * is worth naming. `.cx-art .ig-surface` gave the dashboard's outer card a
 * two-layer shadow, because that card is `--color-theme-card-02-hex` and this
 * page's plates are one step from it in both themes, so it read as a rectangle
 * drawn on the plate rather than as a card lying on it. That outer card came
 * off at the owner's direction the same day — the three widgets inside it
 * carry the edge now, on card 1's own returned-document fill — so the rule had
 * nothing left to select and is deleted rather than left dormant. `.bu-dash`
 * and `.bu-w` are `src/styles/build-extras.css`'s, which this page imports for
 * the two graphics it borrows; both halves of the change live in that file's
 * own note beside them.
 *
 * NOTHING ON THIS PAGE EMITS `.ig-surface` NOW. If a graphic here ever grows
 * one, it needs that shadow back or it will read flat on the plate again.
 * ------------------------------------------------------------------ */

/* ------------------------------------------------------------------ *
 * Card 3 — the layout grid, measured against /bid's #context row
 *
 * 2026-09-08, at the owner's direction, and the SECOND attempt — the first is
 * the lesson. This graphic is the only one of the three that stands on the
 * plate with no card edge of its own, so the plate's own air is the whole of
 * what frames it, and it ran the full 340 of the stage against a 391.66 plate:
 * **25.83px a side**, which is a bare grid pinned to two edges of the box it
 * sits in. It shipped for one build with the ROWS grown to 73.22 so the block
 * came to 340 x 340 and the air was 25.83 on all four sides, and that was
 * rejected — equal air is not the ask, and 26px is not "spacing" at this size.
 *
 * THE MEASURE IS /bid's #context ROW, which the owner named. Measured on the
 * built page at 1440, in the same 391.7 plate: the price card is **286 x 302.7
 * on 52.8 a side**, the planholder card **287 x 228.3 on 52.3**, and the market
 * card **287 x 237 on 52.3 a side and 70.5 above and below**. So the band is
 * ~287 of ink on ~52 of side air, and the vertical is whatever the content
 * comes to.
 *
 * So the fix is a WIDTH and nothing else. `--ig-w` is 340 for all three cards
 * on this row and is not this file's to move — it is the stage every borrowed
 * graphic is drawn in — so the cap goes on this block alone, with the auto
 * margins that centre it. That is the answer `bid-capabilities.css` already
 * gives with `.bk-fit`: auto margins absorb positive free space only, so it
 * centres in the plate and simply sits flush once the plate is narrower.
 * Measured after: **287 x 254.6 on 52.3 a side and 68.5 above and below**,
 * which is the market card's own 52.3 / 70.5 to within two pixels.
 *
 * NOTHING INSIDE THE GRAPHIC MOVED. The tiles keep the app's own `p-1.5` and
 * its own row height; the 3-column track simply divides a narrower box, so a
 * tile is 90.3 wide where it was 108. **Re-measure against #context rather
 * than nudging this** — 287 is read off that row, not picked here.
 *
 * Scoped to this page's plate, so the dormant /build card's own copy of this
 * graphic — eleven tiles on the same four rows — is untouched.
 * ------------------------------------------------------------------ */

.cx-art .bu-lay {
  max-width: 287px;
  margin-inline: auto;
}

/* ------------------------------------------------------------------ *
 * #integrations — the app's connection preview, three times
 *
 * 2026-09-08, at the owner's direction: the three copy cards under "Plugged
 * into everything" became three example apps, each ending on the band the
 * app draws on every connection's install dialog —
 * `ConnectionPreviewGraphic` in
 * bidlo-light-1.0/features/settings/components/connection-dialog-chrome.tsx.
 * The fragment's own header carries the argument; what is here is the band
 * VALUE FOR VALUE, the Tailwind class each number came from beside it, for
 * the reason card 1's build thread is drawn the same way: product.css has no
 * rule for any of it. Drawn 1:1 on the 340px stage `.ig-scaler` fits to the
 * panel, so nothing in here is scaled by hand.
 *
 * `--cx-primary` is the app's `--primary` (#2483e2, blue — NOT the Bidlo
 * green of the logo), fixed in both themes as it is in the app; every alpha
 * in the component is a `color-mix` off it, so the band reads the same tint
 * over the light plate and the dark one.
 *
 * THREE DEPARTURES from the component, all in the fragment header.
 *
 * (1) THE BAND HAS NO GROUND — no `bg-primary/10`, no `border-primary/20`,
 * no radius. The figure is #build's grey plate and its square
 * `.cx-art__box`, and `.cx-conn` fills that box edge to edge; the app's
 * tinted box drawn inside the grey one shipped for a build and was sent back
 * as a container inside a container. The panel is a percentage of the box,
 * so nothing is scaled, and its list runs off the bottom of it as the
 * component's does.
 *
 * (2) THE LEFT COLUMN HOLDS TWO LOGOS AND AN AMPERSAND, top-aligned on a
 * stated gutter rather than one logo centred in 42%. An install dialog is
 * about ONE connector; each of these apps reads two, and the `&` is what
 * says both rather than either. See `.cx-conn__src`, which carries why the
 * app's own percentages collide on a square box and how to centre it again.
 *
 * (3) EVERY MARK IS DRAWN IN A FRAME OF ONE SIZE, holding a contained box
 * the fragment states per source, rather than at the app's own per-asset
 * rendition — that balances a single logo against nothing, and six have to
 * balance against each other. Four are BARE, as the app draws them; the two
 * agency PNGs are square grounds and are drawn as tiles of one size on one
 * small corner, and only City of Austin's ground is supplied here. Both are
 * legibility rather than style. See `.cx-conn__logo`,
 * `.cx-conn__logo--tile` and `.cx-conn__logo--plate`.
 *
 * THE TRAP, TWICE OVER. `.cx-conn__dots` holds exactly three 6px round
 * children, which is the shape `empty-mockups.js` hunts for; it is a
 * `<span>` — that detector queries `div` — and must stay one. (The list's
 * ellipsis was a third for one build, as three stacked `<i>` boxes; it is a
 * GLYPH now and has no children at all, which is one more reason to leave
 * it one.) And `.cx-conn__src` IS a `div`; on a PAIR card it has exactly
 * three children (tile, ampersand, tile) and is safe only because that
 * detector also requires every child to be under 16px wide and near-square,
 * which a 64px frame and a 64x26 ampersand row both fail. Do not shrink the
 * frame past 16px — the tiles inside it are a paint box, not the child that
 * detector measures. On a LIST card it has six children and does not match
 * at all; a list of exactly two marks plus the ellipsis would put it back to
 * three, so the count rule stays.
 * ------------------------------------------------------------------ */

.cx-conn {
  --cx-primary: #2483e2; /* --primary */
  /* THE LEFT COLUMN'S FOUR NUMBERS. Everything about the logo stack and the
     dots is solved off these, so moving one moves the rest rather than
     leaving two things that used to agree.
       --cx-tile   THE FRAME'S WIDTH, and only its width as of 2026-09-10:
                   the app's own `size-16` preview box, kept at 64 because
                   the dots' `left` is solved off it and all three cards'
                   dots share one x. The HEIGHT is `--cx-row` on every card
       --cx-amp    the row a list card's closing ellipsis takes. It was the
                   ampersand's row between a pair's two tiles as well, until
                   2026-09-10; see `.cx-conn__more`. NOTHING now sits between
                   a pair's two tiles — they butt, as a list card's rows do,
                   which is what makes the three columns read at one rhythm
       --cx-gutter the stack's inset from the plate's left edge
       --cx-top    the panel's own `top-9`, which the stack top-aligns to
       --cx-tile-r the corner the two agency marks' own grounds take, which
                   is `rounded-sm` on the app's 0.625rem ladder
       --cx-row    THE ROW EVERY CARD STACKS ON — pair and list alike as of
                   2026-09-10, where a pair used to stack on `--cx-tile`'s
                   own 64; see `.cx-conn__logo` */
  --cx-tile: 64px;
  --cx-row: 48px;
  --cx-tile-r: 6px;
  --cx-amp: 26px;
  --cx-gutter: 16px;
  --cx-top: 36px;
  /* Fills `.cx-art__box` — the square #build's cards share — edge to edge.
     The box's own `::after` is what gives it its height, so this cannot be
     in flow. NO ground: the app's border, radius and tint are the departure
     the note above names. */
  position: absolute;
  inset: 0;
  /* NOT `overflow: hidden`: the plate (`media-border-container
     overflow-hidden`) is the clip. Below xl the box is 420 in a ~949 plate,
     and clipping at the box would end the panel on an invisible edge
     mid-plate; clipping at the plate lets it run off the right edge there
     exactly as it does at 1440, where box and plate are one. */
  font-family: var(--font-system);
  font-size: 14px; /* text-sm */
  line-height: 1.35;
  color: var(--color-theme-product-text);
  text-align: left;
  -webkit-font-smoothing: antialiased;
}

/* The box this section's figure uses is #build's `.cx-art__box` with its
   auto margins off: below xl the graphic then starts at the plate's left
   edge and its panel runs to the plate's right, rather than sitting centred
   in ~530px of bare grey. */
.cx-art__box--conn {
  margin-inline: 0;
}

/* The app's `absolute inset-y-0 left-0 flex w-[42%] items-center
   justify-center`, REPLACED — and this is the third departure the header
   names. That rule centres ONE logo in 42% of a 176px-tall dialog strip;
   this box is a 392px SQUARE holding TWO logos, so 42% of it is 164px of
   column whose centre is at 21% (82px) and whose contents therefore run
   straight over the dots at 29% (114px). That collision is what a wide
   wordmark like HCSS made visible.
   So the stack is left-anchored on a stated gutter instead of centred on a
   percentage, which makes its right edge a KNOWN number at every width
   (`--cx-gutter + --cx-tile`, i.e. 80) rather than one that grows with the
   plate — and the dots below are then centred in the gap between that edge
   and the panel's own 38%, so they can never touch either side.
   TOP-ALIGNED, at the owner's direction: the first tile's top edge is the
   panel's own `top-9`, so the mark and the "Bidlo" header start on one
   line and all three cards' stacks start on the same line as each other.
   Centre it again by putting `bottom: 0` and `justify-content: center`
   back and dropping the `top` — one line, and the dots' `top` re-solves
   itself. */
.cx-conn__src {
  position: absolute;
  top: var(--cx-top);
  left: var(--cx-gutter);
  display: flex;
  flex-direction: column;
  align-items: center;
}


/* `ConnectionLogo size="preview"`: a frame holding its mark at
   `object-contain`, on NO GROUND OF ITS OWN — which is the app, and which is
   what most of the nine marks here keep. The frame is stated rather than
   left to the mark because nine marks at nine aspects have to line up down
   and across the three cards; what it holds is the contained box the
   fragment states per source.
   ONE ROW HEIGHT ON ALL THREE CARDS AS OF 2026-09-10, at the owner's
   direction. A pair stacked on `--cx-tile` (64) and a list on `--cx-row`
   (48), so the two marks on cards 1 and 3 sat 64 apart while card 2's five
   sat 48 apart — three columns at two rhythms, which is what was reported.
   THE ONE HEIGHT HAS TO BE 48 AND THE LIST IS WHY: its stack must END
   inside the narrowest box this graphic is drawn in — 319.69 at a 390px
   viewport, measured, against 391.67 at 1440 — because the ellipsis is the
   whole point of a list and a cropped ellipsis says nothing. That budget is
   `--cx-top` (36) + five rows + `--cx-amp` (26) plus a margin, so
   `5R <= 248` and R tops out at 49.5; 48 is the step under it AND the app's
   own `size-12`, which is what `connection-logos.ts` gives a preview by
   default. So the list cannot come up to 64 and the pairs come down to 48.
   The stack then stands 302 on a list card and 96 on a pair, both
   top-aligned at `--cx-top`, and the dots land on one y across all three —
   see `.cx-conn__dots`.
   WHAT IT COSTS is the pair frames' air: a mark may now be 40 tall rather
   than 46, which every mark in `CONN_APPS` already is. WIDTH IS UNTOUCHED
   at `--cx-tile`, because the dots' `left` is solved off it.
   Re-solve against the measured box, never by eye: one more mark on the
   list is 48px this column does not have. */
.cx-conn__logo {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--cx-tile);
  height: var(--cx-row);
}

.cx-conn__logo img {
  display: block;
  object-fit: contain;
}

/* A MARK THAT CARRIES A GROUND IS A TILE; THE BARE ONES ARE NOT, as of
   2026-09-09 at the owner's direction. Two do: TxDOT's white is baked into
   its own file and City of Austin's comes from the plate below — so both
   want the ground to be one box and one corner. (E-Verify's white is baked
   in too, but over its own ink and not as a card: it is square-cornered and
   flush, so it takes neither this rule nor the plate. See the fragment.)
   THE BOX IS THE MARK'S OWN `w`/`h`, WHICH IS WHY THIS RULE IS ON THE IMG
   AND NOT ON THE SPAN. The span is the 64px frame, and while the plate sat
   on it a 64px white card stood beside TxDOT's 40px baked ground and read
   as a card beside a logo — the report this answers. Rounding the img
   instead gives both marks the same 40px square, because the img box is
   what each ground actually fills; a `border-radius` clips an `<img>`'s
   replaced content as well as its background, so TxDOT needs nothing else.
   The corner is a step DOWN from the 14 a 64px tile wore: 6 is the app's
   own `rounded-sm`, and 6 of 40 against 14 of 64 is the "a little" of the
   ask. The frame itself stays bare — see `.cx-conn__logo`. */
.cx-conn__logo--tile img {
  border-radius: var(--cx-tile-r);
}

/* THE ONE PLATE, AND IT IS LEGIBILITY RATHER THAN STYLE. Every mark wore
   one for a build and it came out at the owner's direction — a white ground
   under all six is a row of stickers, not a row of logos. City of Austin is
   the exception the fragment names: a navy wordmark on transparent, i.e.
   navy on near-black over the dark card.
   **PREFER BAKING THE WHITE INTO THE FILE OVER REACHING FOR THIS.** E-Verify
   has the same problem — its white is a HOLE rather than paint, so the dark
   card shows through its own `E` — and it wore this rule for part of
   2026-09-09 and was told to take it off: a plate is a rounded card with 3px
   of padding and a drop UNDER a mark, which reads as a card beside a logo.
   Its white is now inside its own PNG, over its own ink and not one pixel
   further. This rule survives for Austin because a city seal is not ours to
   repaint; it is the last resort, not the first answer.
   WHITE IS A LITERAL AND NOT A TOKEN, in both themes and deliberately: the
   mark is brand colour and a ground that inverted with the theme would put
   it back on black, which is the whole thing this rule is for. The ring and
   the drop are black at low alpha for the same reason a shadow is.
   WHITE IS A LITERAL AND NOT A TOKEN, in both themes and deliberately: the
   mark is brand colour and a ground that inverted with the theme would put
   it back on black, which is the whole thing this rule is for. The ring and
   the drop are black at low alpha for the same reason a shadow is.
   THE PADDING IS SOLVED OFF TxDOT'S OWN FILE, not picked: its ink leaves 4
   of 64 at the left and 3 at the right, i.e. ~2.5px once that file is drawn
   at 40, so 3px here puts Austin's wordmark at 34 against TxDOT's 35.6 and
   the two grounds carry the same margin. It works because `object-fit`
   fits the content into the CONTENT box while `background` paints the
   padding box, so one number shrinks the mark and leaves the tile 40px. */
.cx-conn__logo--plate img {
  box-sizing: border-box;
  padding: 3px;
  background: #fff;
  box-shadow:
    inset 0 0 0 1px rgb(0 0 0 / 0.07),
    0 1px 2px 0 rgb(0 0 0 / 0.06);
}


/* THE LIST'S CLOSING ELLIPSIS, and it is the last of this rule's two marks.
   It was written as one selector with `.cx-conn__amp` — the ampersand
   between a pair's two tiles — precisely so the two could not drift apart,
   and the AMPERSAND CAME OUT on 2026-09-10 at the owner's direction, along
   with the row it stood in: a pair's two tiles now BUTT, exactly as a list
   card's rows do, so all three columns read at one rhythm. Do not put a
   `gap` back on a pair to "make room" — the room was the ampersand. What is
   left of the shared rule is the ellipsis alone. Its WIDTH is the tile's, so
   the glyph centres under the mark above it, and its HEIGHT is `--cx-amp`.
   It shipped for one build as three 6px boxes stacked on
   a 4px gap and was reported as too big: 26px of drawn ink beside 15px of
   type. The fragment picks between drawing it and not off the source
   count. */
.cx-conn__more {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--cx-tile);
  height: var(--cx-amp);
  font-size: 15px;
  line-height: 1;
  font-weight: 500;
  color: var(--color-theme-text-sec);
}

/* `absolute left-[29%] top-1/2 z-10 flex -translate-y-1/2 items-center gap-2`
   over three `size-1.5 rounded-full bg-primary/70` — the dots themselves are
   the app's, and only where they hang is solved here.
   ACROSS: the midpoint of the gap between the stack's right edge
   (`--cx-gutter + --cx-tile`) and the panel's own `left: 38%`, so the block
   is symmetric in that gap at every width by construction and touches
   neither side. The app can hardcode 29% because its band is a wide dialog
   strip; on this square 9% of the box is 35px at 1440 and 29 at 390, which
   is narrower than the 34px the dots occupy.
   DOWN: the stack's own middle — the bottom of the first tile plus half the
   ampersand row — so a stack that is top-aligned rather than centred does
   not leave the dots stranded 80px below it. */
/* THE DOTS ARE ON THE FIRST MARK'S OWN LINE AS OF 2026-09-09, at the
   owner's direction — not on the stack's middle, where they sat between
   the two marks of a pair and under the second mark of a list. The `top`
   is the centre of the FIRST row, and as of 2026-09-10 that is ONE number
   rather than two: every card stacks on `--cx-row`, so the three cards'
   dots share one y by construction. `--cx-first` and the `:has()` rule
   that switched it per shape are DELETED with the two row heights they
   reconciled — putting either back means putting a second row height back
   first. Across, the solve still reads `--cx-tile`, which is the frame's
   width on every card, so all three cards' dots share one x too. */
.cx-conn__dots {
  position: absolute;
  left: calc((var(--cx-gutter) + var(--cx-tile) + 38%) / 2);
  top: calc(var(--cx-top) + var(--cx-row) / 2);
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 8px; /* gap-2 */
  transform: translate(-50%, -50%);
}

.cx-conn__dots i {
  display: block;
  width: 6px; /* size-1.5 — see THE TRAP above */
  height: 6px;
  border-radius: 999px;
  /* THE APP'S OWN `bg-primary/70` IS DELIBERATELY NOT DRAWN, at the owner's
     direction (2026-09-10). Three saturated blue dots between a company's
     mark and a window of the product were the loudest thing on a card whose
     subject is either end of them, and the only blue in the row that belonged
     to nothing else. They are grey ink instead, so the whole left column of
     the card reads as one connector. Put `--cx-primary` back here to follow
     the app again.

     AND IT IS ONE STEP UNDER `.cx-conn__more`'s, at the owner's direction the
     same day: `--color-theme-text-mid` is the foreground at 50% against that
     rule's `-sec` 60%. A dot is a solid disc where the ellipsis is a thin
     glyph, so at equal alpha the three read heavier than the marks they join
     — and what the dots are is the join, not the subject. `-tertiary` (40%)
     is the next step down and reads as a rule that has faded out. Alpha is
     safe here where it is not on a lucide glyph: nothing overlaps. */
  background: var(--color-theme-text-mid);
}

/* ==================================================================
 * THE WINDOW (2026-09-09, at the owner's direction).
 *
 * What stood here until that day was `ConnectionPreviewGraphic`'s own panel —
 * a `bg-card` plate of `bg-primary/20` skeleton bars, either eight ticked rows
 * or five chat exchanges — plus the two `::after` hacks that carried its
 * ground and its row rules out to the plate below xl. All of it is gone, with
 * `connRow`, `connChat`, the Bidlo chip in its header and the two lucide
 * glyphs beside its bars. The owner asked for the fidelity of cursor.com's own
 * product cards, which put a browser window with real content in it where this
 * had abstraction, and named the container to reuse.
 *
 * SO THIS IS THE CAPTURE'S OWN `demo-window-slack-cloud`, VALUE FOR VALUE, and
 * the class each number came from is beside it. It is restated as `cx-` rules
 * rather than emitted as captured classes for the reason the rest of this
 * graphic already is: product.css is frozen Tailwind output for /bid and emits
 * no rule for `h-2.5` or `rounded-[10px]`.
 *
 * TWO NUMBERS ARE THIS FILE'S OWN AND BOTH ARE SOLVED.
 *
 * `left: 38%` is the panel's, kept because the dots' own `left` is solved off
 * it (see `.cx-conn__dots`) and the two cannot move apart. `width: 200%` puts
 * the window's right edge off the plate at every width the figure is drawn
 * at — 391.66 at 1440 down to 319.69 at 390, and 420 in a ~949 plate below xl
 * — and the `media-border-container` is the clip, which is what deletes both
 * extension hacks: the ground and every row rule now simply carry on.
 *
 * `--cx-win-w` IS THE CROP. At 1440 the box is 391.66 and the window starts at
 * 38% of it, so 242.83 of window is on the plate; less the body's own 12px
 * left pad that is 230, and 230 is what a NAME is capped at and ellipsised in.
 * NOTHING ELSE IS CAPPED — a secondary line runs its natural width and is cut
 * by the plate, which is what a window carrying on past a card looks like and
 * is also what keeps a row filling the frame below xl, where the plate is 949
 * wide and the window is most of it. Re-measure against the box rather than
 * nudging it.
 *
 * Top and bottom are inset by `--cx-top`, the same 36 the logo stack
 * top-aligns to, so the window's bottom edge and both of its left corners are
 * on the plate: it reads as an object lying on the card rather than as a panel
 * welded to its edge.
 *
 * THAT INSET WAS DROPPED FOR ONE BUILD ON 2026-09-10 AND CAME STRAIGHT BACK.
 * "Get rid of the border/padding surrounding the app so that it goes to the
 * edge of the mockup" was read as this inset and is not: the owner liked how
 * the mockups are sized on the page, and what they meant is the file viewer's
 * own 6px margin round the report page INSIDE the window — see
 * `--cx-rp-margin`, which is where the fix landed. The radius and the ring
 * came back with it.
 *
 * EVERY LENGTH IS DRAWN 1:1 IN CARD PIXELS on the capture's own
 * `--text-product-*` ladder — 13 for a name, 12 for the title bar, 11 for a
 * secondary line — which is `bid-preview.css`'s model and not the app's. This
 * is a picture ON a card, not a scaled copy of an app screen, so there is no
 * `transform: scale()` in here and there should not be one.
 *
 * THE TRAP, IN TWO PLACES NOW. `empty-mockups.js` finds a window by a `<div>`
 * holding exactly three small round children, which is both `.cx-conn__dots`
 * and this bar's three lights. Both are `<span>`s (that detector queries
 * `div`) and so is every box inside the window, which is what the blockifier
 * below is for. Do not make any of them a div, and keep the blockifier in
 * `:where()`: a bare `.cx-win span` scores (0,1,1) and would outrank every
 * single-class rule under it, which is the trap `automate-hero-mockup.css`
 * paid a build for.
 * ================================================================== */

.cx-win {
  /* The name's cap, solved off the crop — see the note above. */
  --cx-win-w: 230px;
  --cx-win-pad: 12px; /* px-3, the captured window's own body gutter */
  position: absolute;
  top: var(--cx-top);
  bottom: var(--cx-top);
  left: 38%;
  width: 200%;
  display: flex;
  flex-direction: column;
  border-radius: 10px; /* rounded-[10px] */
  overflow: hidden;
  background: var(--color-theme-product-chrome);
  /* THE RING IS THE CAPTURE'S AND THE TWO SOFT LAYERS ARE NOT (2026-09-10, at
     the owner's direction — the three windows "seem flat"). The captured
     `demo-window-slack-cloud` carries the 1px ring alone, because on its own
     page it stands on a wallpaper photograph that separates it for free; here
     it lies on a flat card plate one step from its own chrome, and a ring
     alone reads as a rectangle drawn ON the plate rather than a window lying
     on it. The lift is `.bx-popover`'s / `.bx-heat`'s own two-layer shadow,
     verbatim from bid-context.css — the elevation this site gives a surface
     resting on a plate, and the same fix that sheet records for its map — so
     the three cannot drift; the dark half is restated at that sheet's 0.3
     below, a shadow having to stay black in both themes. IT WAS MERGED AWAY
     ONCE the same day (three parallel redraws branched before it landed) and
     the owner caught it; the test pins it. The ring left with the inset for
     one build later the same day and came back with it. */
  box-shadow:
    0 0 0 1px var(--color-theme-border-02),
    0 10px 15px -3px rgb(0 0 0 / 0.1),
    0 4px 6px -4px rgb(0 0 0 / 0.1);
}

:root[data-theme='dark'] .cx-win {
  box-shadow:
    0 0 0 1px var(--color-theme-border-02),
    0 10px 15px -3px rgb(0 0 0 / 0.3),
    0 4px 6px -4px rgb(0 0 0 / 0.3);
}

/* Every box in the window is a span — see THE TRAP. `:where()` contributes
   nothing, so each rule below wins on its own class and on source order. */
.cx-win :where(span) {
  display: block;
}

/* `border-theme-border-02 relative flex h-7 items-center justify-between
   border-b px-2` */
.cx-win__bar {
  position: relative;
  display: flex;
  align-items: center;
  flex: none;
  height: 28px; /* h-7 */
  padding: 0 8px; /* px-2 */
  border-bottom: 1px solid var(--color-theme-border-02);
}

/* `flex items-center gap-1.5` over three `inline-block h-2.5 w-2.5
   rounded-full` lights on `var(--color-theme-fg-20)` — the capture's own. */
.cx-win__lights {
  display: flex;
  align-items: center;
  gap: 6px; /* gap-1.5 */
}

.cx-win__lights i {
  display: block;
  width: 10px; /* h-2.5 w-2.5 */
  height: 10px;
  border-radius: 999px;
  background: var(--color-theme-fg-20);
}

/* `type-product-base pointer-events-none absolute top-1/2 left-1/2
   -translate-x-1/2 -translate-y-1/2 truncate text-center opacity-70`.
   THE ONE DEPARTURE: the capture centres on the WINDOW, and this window is
   200% of the box with its right half off the plate — so a centred title
   would be off the plate with it. It is centred on the window's own CONTENT
   instead (`--cx-win-w` plus its two pads), which at 1440 lands within 6px of
   the middle of what a reader can actually see. */
.cx-win__title {
  position: absolute;
  top: 50%;
  left: calc((var(--cx-win-w) + 2 * var(--cx-win-pad)) / 2);
  transform: translate(-50%, -50%);
  font-size: var(--text-product-base); /* type-product-base */
  line-height: var(--leading-product-base);
  white-space: nowrap;
  opacity: 0.7; /* opacity-70 */
  color: var(--color-theme-product-text);
}

/* `min-h-0 w-full flex-1 overflow-hidden` — the capture's own content pane. */
.cx-win__body {
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

.cx-win__head {
  padding: 12px var(--cx-win-pad) 10px;
}

.cx-win__h {
  font-size: var(--text-product-lg); /* type-product-lg — 13 */
  line-height: var(--leading-product-base);
  font-weight: var(--font-weight-medium);
  color: var(--color-theme-product-text);
}

.cx-win__sub {
  margin-top: 2px;
  font-size: var(--text-product-sm); /* type-product-sm — 11 */
  line-height: var(--leading-product-sm);
  color: var(--color-theme-product-text-sec);
  white-space: nowrap;
}

/* A row is a name over a secondary line, separated by the window's own
   hairline. It spans the whole window rather than the content column, so the
   rule runs to the plate's edge — which is what the deleted `::after` hack
   was doing by hand. */
.cx-win__row {
  padding: 8px var(--cx-win-pad);
  border-top: 1px solid var(--color-theme-border-02);
}

.cx-win__line {
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: var(--cx-win-w);
}

.cx-win__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-product-lg);
  line-height: var(--leading-product-base);
  color: var(--color-theme-product-text);
}

/* The unread marker, on the inbox card alone. */
.cx-win__unread {
  display: block;
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--cx-primary);
}

/* NOT capped — see `--cx-win-w` in the note above. `max-content` so the row's
   ink is its own width rather than the window's 200%: what a reader sees end
   is the plate's edge, and a stretched flex box would hide that from anything
   measuring it. */
.cx-win__meta {
  display: flex;
  align-items: center;
  width: max-content;
  gap: 6px;
  margin-top: 2px;
  font-size: var(--text-product-sm);
  line-height: var(--leading-product-sm);
  color: var(--color-theme-product-text-sec);
  white-space: nowrap;
}

/* The app's `Color` badge — `DocumentPkAvatar`'s last branch, `rounded px-1.5
   leading-5 text-sm` on a `bg-<c>-badge` fill. Drawn on THIS graphic's 11px
   ladder rather than the app's 14, so its box follows: `leading-5` is 20 at
   14, i.e. 16 here. Every tone is `bid-hero-mockup.css`'s, which converted it
   from the app's own oklch — copied from that sheet, never guessed, and a test
   pins each pair against it. */
.cx-win__tag {
  display: inline-flex;
  align-items: center;
  flex: none;
  height: 16px;
  padding: 0 5px; /* px-1.5 */
  border-radius: 4px; /* rounded */
  background: var(--cx-tag-bg);
  color: var(--cx-tag-fg);
}

.cx-win__tag[data-tone='blue'] {
  --cx-tag-bg: rgb(65 139 198 / 0.27);
  --cx-tag-fg: #001f43;
}

.cx-win__tag[data-tone='green'] {
  --cx-tag-bg: rgb(80 158 114 / 0.27);
  --cx-tag-fg: #002f23;
}

.cx-win__tag[data-tone='purple'] {
  --cx-tag-bg: rgb(155 120 201 / 0.27);
  --cx-tag-fg: #362348;
}

.cx-win__tag[data-tone='red'] {
  --cx-tag-bg: rgb(255 171 158 / 0.4);
  --cx-tag-fg: #71000b;
}

:root[data-theme='dark'] .cx-win__tag {
  --cx-tag-fg: rgb(255 255 255 / 0.87);
}

:root[data-theme='dark'] .cx-win__tag[data-tone='blue'] {
  --cx-tag-bg: rgb(0 103 165 / 0.5);
}

:root[data-theme='dark'] .cx-win__tag[data-tone='green'] {
  --cx-tag-bg: rgb(0 128 85 / 0.5);
}

:root[data-theme='dark'] .cx-win__tag[data-tone='purple'] {
  --cx-tag-bg: rgb(138 103 229 / 0.34);
}

:root[data-theme='dark'] .cx-win__tag[data-tone='red'] {
  --cx-tag-bg: rgb(238 107 93 / 0.45);
}

/* ------------------------------------------------------------------ *
 * #integrations card 1 — the daily progress report, drawn as the DOCUMENT
 * at the reference page's own proportions (2026-09-10, at the owner's
 * direction, twice: it was five list rows, then for one build a 222px page
 * with every band boxed at 10px and no photos — reported as "looks nothing
 * like what I gave you", which it did not: a US-Letter page squeezed into
 * the crop reads as a mobile form).
 *
 * THE PAGE IS 480 WIDE AND THE CROP SHOWS ITS LEFT HALF. The window is 200%
 * of the box and starts at 38% of it, so at 1440 the plate shows 242.83px of
 * window: less the viewer's 6px margin that is ~237 of page — the masthead's
 * title and the start of the date band, two of the four photo slots, the
 * 6:00 AM column and most of the 12:00 PM column, Crew and the head of the
 * Description column. That is card 2's own crop (its Owner and Bid Date run
 * off the plate too) and it reads as a document carrying on past the card;
 * below 1140 the grid goes one column, the box jumps to ~949 and the whole
 * page sits at the left with the viewer's ground carrying on. 480 is a
 * letter page's proportion at this type size: the reference's 1100px page
 * sets its body at ~22px, i.e. 50 body-heights across, and 480 at 12 is 40.
 *
 * THE SHEET IS WHITE IN BOTH THEMES and every colour ON it is a literal: a
 * document is the colour the document is, which is the call `manage-files.
 * css`' `.mf-pg` and `bid-extend.css`' plan sheet already make. TEN hexes, a
 * ladder rather than a swatch box: paper, the masthead's slate (the
 * reference's own #2f4a5b), the meta band's near-black, the zebra wash, three
 * steps of ink from the title down to a rule, the sun's amber, and the two
 * slates of the photo wash (a soft sky-to-ground gradient — a photo SLOT a
 * real picture drops into, not a grey box). The viewer's ground is a token
 * (`--color-theme-product-editor`, the pane the app's file viewer draws a
 * page on) and inverts with the theme; the page does not.
 *
 * TYPE IS THE ROW'S 13/12/11 LADDER PLUS ONE DISPLAY STEP, `--cx-rp-display`
 * (20px), read by the masthead title and the temperatures alone — the two
 * things the reference sets huge. Body copy (descriptions, the meta values,
 * the bullet lines) is 12 and captions 11; nothing is under 11, and the
 * display step is 20 rather than the 24 a 480px page could carry because the
 * vertical budget is the window's 278px body: masthead, meta band, photo
 * strip and Weather have to fit above the fold with the Work Logs band and
 * its column header on the plate at 1440, and every one of those bands is
 * solved to that sum (see `.cx-rp-photo`'s height). No `transform: scale()`.
 *
 * Every box is a `<span>` (THE TRAP, see `.cx-win`), so each rule below
 * states its own `display` over the `:where(span)` blockifier. No id, no
 * `<defs>`, no image.
 * ------------------------------------------------------------------ */

.cx-rp-view {
  --cx-rp-paper: #fff;
  --cx-rp-mast: #2f4a5b; /* the reference masthead's own slate */
  --cx-rp-band: #1a1a1a;
  --cx-rp-wash: #f1f1f1;
  --cx-rp-ink: #1f1f1f;
  --cx-rp-muted: #5c5c5c;
  --cx-rp-rule: #d9d9d9;
  --cx-rp-sun: #f0b429; /* the reference's own yellow sun */
  /* The meta band's two inks, 2026-09-10, at the owner's direction ("a little
     smaller and less prominent, with a little more transparency"): the band
     ran 12px semibold white beside 12px white, so a strip of chrome carrying
     a date and a job number was the second-loudest thing on the page after
     the masthead title. The LABEL is the chrome and takes the deeper fade;
     the VALUE is the content and stays legible. Alpha on the ink and never
     on `.cx-rp-meta`, which is the element carrying the band's own fill. */
  --cx-rp-meta-k: rgb(255 255 255 / 0.6);
  --cx-rp-meta-v: rgb(255 255 255 / 0.85);
  /* The photo frame's ground, which shows only until its picture paints —
     `--cx-rp-ground`, the wash's lower half, went with the wash on
     2026-09-10 and is DELETED rather than left dormant. */
  --cx-rp-sky: #c3ccd3;
  /* THE DISPLAY STEP IS 17 AS OF 2026-09-10, DOWN FROM 20, and the section
     gap and every band's padding grew with it in one pass. The card was
     reported as "blasting text" beside card 3, which is an app SCREEN and has
     air where this is a PAGE and had none: a 20px masthead title over five
     bands stacked on 4px with 2-4px of padding inside each, filling the
     window's 278px body to the pixel. Nothing is cut and nothing else is
     smaller — the air is bought by the three lines the title and the two
     temperatures give back. See `.cx-rp-sec` for the fold budget. */
  --cx-rp-display: 17px;
  --cx-rp-gutter: 14px; /* the page's own margin, both sides */
  display: block;
  height: 100%;
  background: var(--color-theme-product-editor);
  font-family: var(--font-system);
}

/* US Letter, portrait: 480 wide is 621 tall, and the window's body (278)
   cuts it at the fold, which is what a page in a viewer looks like.

   THE PAGE SITS FLUSH IN THE VIEWER AS OF 2026-09-10, at the owner's
   direction — the viewer's own `--cx-rp-margin` (6px, on `.cx-rp-view`'s
   padding) is DELETED rather than set to zero. On a full window that margin
   is the gutter a document sits in; on a card cropped to ~242px of window it
   is a dark band down the top and left of the page, and in dark mode
   `--color-theme-product-editor` is #202020, so it read as a black border
   round the report ("this little black border around it"). The ground itself
   STAYS and is still visible where it belongs: to the RIGHT of the 480px
   page, which is on the plate below xl.

   THE PAGE'S OWN DROP SHADOW GOES WITH THE MARGIN. It cast onto the gutter,
   and with the page flush its top and left halves are clipped by the
   window's `overflow: hidden` while what is left is a 1px dark line ON the
   crop's own left edge — the same band the margin was, one pixel wide. */
.cx-rp-page {
  display: block;
  width: 480px;
  min-height: 621px;
  background: var(--cx-rp-paper);
  color: var(--cx-rp-ink);
}

/* ---- masthead: the job set large over the deck, the wordmark right ---- */

.cx-rp-mast {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 16px;
  padding: 10px var(--cx-rp-gutter) 9px;
  background: var(--cx-rp-mast);
  color: var(--cx-rp-paper);
}

.cx-rp-mast__l {
  display: contents;
}

.cx-rp-title {
  display: block;
  grid-column: 1;
  grid-row: 1;
  font-size: var(--cx-rp-display); /* 17 — the one display step */
  line-height: 21px;
  font-weight: var(--font-weight-bold);
  letter-spacing: -0.01em;
  white-space: nowrap;
}

/* 12 rather than 11 as of 2026-09-10, at the owner's direction ("a tiny bit
   of text size variation"). The deck is the masthead's CONTENT and the band
   under it is chrome, and at 11 the two were one size — so the page's whole
   top read as one display title over an undifferentiated 11px field. The
   masthead now carries its own two steps (the display step over 12) and the
   band alone holds 11.
   THE LINE BOX IS PINNED AT 14 — `--leading-product-sm` (1.27273) on 11 is
   exactly 14, so stating it holds the masthead's height at the number the
   fold budget solved. 0 px. */
.cx-rp-deck {
  display: block;
  grid-column: 1;
  grid-row: 2;
  font-size: var(--text-product-base); /* 12 */
  line-height: 14px;
  opacity: 0.85;
  white-space: nowrap;
}

/* The contractor's wordmark, two lines set right — a string, not a logo. */
.cx-rp-lockup {
  display: block;
  grid-column: 2;
  grid-row: 1 / 3;
  text-align: right;
}

.cx-rp-lockup__l {
  display: block;
  font-size: var(--text-product-lg); /* 13 */
  line-height: 15px;
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.08em;
}

/* ---- meta band: three cells spread across the width, label beside value ---- */

.cx-rp-meta {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 6px var(--cx-rp-gutter);
  background: var(--cx-rp-band);
  color: var(--cx-rp-paper);
}

.cx-rp-meta__c {
  display: flex;
  gap: 5px;
  flex: none;
  white-space: nowrap;
}

/* 11 rather than 12, and the LINE-HEIGHT IS HELD AT 16 on purpose: the band's
   height is a term in the fold budget above (masthead + band + strip + Weather
   has to clear the window's 278px body), so the type steps down and the box
   does not move. 0 px. */
.cx-rp-meta__k,
.cx-rp-meta__v {
  display: block;
  font-size: var(--text-product-sm); /* 11 */
  line-height: 16px;
}

.cx-rp-meta__k {
  font-weight: var(--font-weight-medium);
  color: var(--cx-rp-meta-k);
}

.cx-rp-meta__v {
  color: var(--cx-rp-meta-v);
}

/* ---- photo strip: four captioned PHOTOGRAPHS, one per crew ----
   FOUR REAL PICTURES AS OF 2026-09-10, at the owner's direction; they were
   two-tone slate washes until then and four grey boxes in a row read as a
   document that failed to load. Each is a committed webp under
   `public/brand/report/` at 330 x 156, i.e. 3x the slot, generated with the
   Codex CLI from one brief so the four are one day on one job — the fragment
   carries the whole argument and names them invented.

   52 tall is what the fold budget leaves the strip (see the note above); at
   4 across on a 4px gap each slot is 110 wide, landscape at 2.1, which is the
   ratio the four files are cut to — so `object-fit: cover` has nothing to
   crop and the caption sits where the reference's captions sit.

   THE CAPTION NEEDS A SCRIM AND NOT A SHADOW. Over a wash a text-shadow was
   enough; over a photograph a white 11px line lands on whatever the picture
   put there (this strip's paver is a yellow machine under a pale sky), so the
   foot of every frame carries the reference's own darkening gradient and the
   shadow stays under it. It is a `::after` on the frame rather than a fill on
   the caption, so a short caption does not draw a black tab. */

.cx-rp-photos {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 5px;
  margin: 6px var(--cx-rp-gutter) 0;
}

.cx-rp-photo {
  position: relative;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  height: 52px;
  overflow: hidden;
  border-radius: 4px; /* 2026-09-10: was 2, which at 110 x 52 read as a square */
  background: var(--cx-rp-sky);
}

/* The picture fills the frame. It is the frame's own background layer rather
   than a flex item, so the caption stays the only thing the flex box lays
   out and the frame's height is still the 52 the fold budget solved. */
.cx-rp-photo__i {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The scrim — see the note above. */
.cx-rp-photo::after {
  content: '';
  position: absolute;
  inset: auto 0 0;
  height: 24px;
  background: linear-gradient(180deg, rgb(0 0 0 / 0) 0%, rgb(0 0 0 / 0.5) 100%);
}

.cx-rp-photo__cap {
  position: relative;
  z-index: 1;
  display: block;
  padding: 0 4px 4px;
  font-size: var(--text-product-sm); /* 11 */
  line-height: var(--leading-product-sm);
  font-weight: var(--font-weight-medium);
  color: var(--cx-rp-paper);
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.35);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- section: the reference's slate header bar over a white body, one
   hairline round the pair and the corners rounded ---- */

.cx-rp-sec {
  display: block;
  margin: 6px var(--cx-rp-gutter) 0;
  border: 1px solid var(--cx-rp-rule);
  /* 2026-09-10: was 3. A step above the photo's 4, the section being the
     larger box; both are `overflow: hidden`, so each header bar and each
     picture is clipped to its own corner. The PAGE stays square — a sheet of
     paper has corners, and it sits flush in the viewer. */
  border-radius: 5px;
  overflow: hidden;
}

.cx-rp-sec__h {
  display: block;
  padding: 4px 10px 5px;
  background: var(--cx-rp-mast);
  color: var(--cx-rp-paper);
  font-size: var(--text-product-lg); /* 13 */
  line-height: 15px;
  font-weight: var(--font-weight-medium);
  /* THE ONE DEPARTURE FROM THE REFERENCE'S BANDS: its labels are centred on
     an 8.5" page; centred on a 452px band the 1440 crop cuts "Weather" to
     "Wea" and "Work Logs" to "Work", so the label sits on the band's left
     edge, where the crop always holds it whole. */
  text-align: left;
}

.cx-rp-sec__b {
  display: block;
}

/* ---- weather: three wide columns, the temperature large beside its glyph ---- */

.cx-rp-wx {
  display: flex;
}

.cx-rp-wx__c {
  display: block;
  flex: 1 1 0;
  min-width: 0;
  /* 2px of side pad: the wind | rain line is 139.3 at 11px in a 150px column,
     one line as the reference sets it, and 4px a side left it 1.7 spare. */
  /* THE WEATHER COLUMN CARRIES THE LAST OF THE AIR AND IS WHERE THE FOLD IS
     SOLVED. Measured on the built page, the bands run masthead 0-54, meta
     54-82, strip 88-140, Weather 146-258 and the Work Logs bar 264-288
     against a 291.7px window body — so the crop closes on that bar with ~4px
     of its white body under it. It went 7/8 for one build and the fold then
     landed 9px INTO the table's column header, cutting a row of 11px labels
     through the middle, which reads as broken rather than as a page carrying
     on. Re-measure rather than nudging.

     8/9 AS OF THE CONDITION LINE COMING OUT. Dropping "Clear" took 15px off
     the band, which put the fold back inside that header row — so the target
     moved from "close on the Work Logs bar" to "show the whole column header
     and nothing under it": the bar now ends at 270 and the 22px header row
     runs 270-292 against the 291.7px body. Both are clean cuts; this one
     spends the 15px on the table rather than on air. */
  padding: 8px 2px 9px;
  text-align: center;
  border-left: 1px solid var(--cx-rp-rule);
}

.cx-rp-wx__c:first-child {
  border-left: 0;
}

.cx-rp-wx__t,
.cx-rp-wx__line {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cx-rp-wx__t,
.cx-rp-wx__line {
  font-size: var(--text-product-sm); /* 11 */
  line-height: var(--leading-product-sm);
  color: var(--cx-rp-muted);
}

/* The temperature and its glyph on one line, centred together. */
.cx-rp-wx__big {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

.cx-rp-wx__deg {
  display: block;
  font-size: var(--cx-rp-display); /* 17 — the one display step */
  line-height: 22px;
  font-weight: var(--font-weight-bold);
  letter-spacing: -0.01em;
}

.cx-rp-wx__g {
  display: block;
  flex: none;
  width: 18px;
  height: 18px;
  color: var(--cx-rp-sun);
}

/* ---- work logs: zebra rows, a bold total ---- */

.cx-rp-tbl {
  display: block;
}

.cx-rp-tr {
  display: grid;
  /* Crew holds "Driveway" (51.4 at 12px), Workers its own bold head (48.8),
     Labor Hrs its (57.9); Description takes the rest — 254 of the 452 — and
     every description is measured against it (see `REPORT_CREWS`). */
  grid-template-columns: 56px minmax(0, 1fr) 50px 58px;
  /* THE ROW'S INLINE PAD STAYS 8 WHERE THE SECTION HEADER ABOVE IT TOOK 10.
     Every description is measured against the Description column (see
     `REPORT_CREWS`) and the longest runs 251 — at 10px a side the column is
     250 and three of the four ellipsise. Do not "align" it with the bar. */
  column-gap: 6px;
  padding: 0 8px;
}

.cx-rp-tr:nth-child(odd) {
  background: var(--cx-rp-wash);
}

/* THE COLUMN HEADER IS CHROME, NOT A ROW, as of 2026-09-10 — it ran at the
   cells' own 12px semibold, so the head and the first zebra row under it read
   as two rows of the same table with one of them bolder. 11 with a little
   tracking and the muted ink is the treatment a document gives a column
   label. The line box stays `.cx-rp-td`'s 16, so the row's height does not
   move and the fold budget is unchanged. */
.cx-rp-tr--h {
  background: var(--cx-rp-paper);
  border-bottom: 1px solid var(--cx-rp-rule);
  font-weight: var(--font-weight-semibold);
  color: var(--cx-rp-muted);
}

.cx-rp-tr--h .cx-rp-td {
  font-size: var(--text-product-sm); /* 11 */
  letter-spacing: 0.02em;
}

.cx-rp-tr--t {
  background: var(--cx-rp-wash);
  border-top: 1px solid var(--cx-rp-rule);
  font-weight: var(--font-weight-semibold);
}

.cx-rp-td {
  display: block;
  min-width: 0;
  padding: 3px 0;
  font-size: var(--text-product-base); /* 12 */
  line-height: 16px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cx-rp-td--n {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* ---- equipment & production: two columns of bullet lines ---- */

.cx-rp-two {
  display: grid;
  grid-template-columns: 1fr 1fr;
}

.cx-rp-two__c {
  display: block;
  padding: 6px 10px 7px;
  border-left: 1px solid var(--cx-rp-rule);
}

.cx-rp-two__c:first-child {
  border-left: 0;
}

.cx-rp-two__h {
  display: block;
  margin-bottom: 1px;
  font-size: var(--text-product-base); /* 12 */
  line-height: 16px;
  font-weight: var(--font-weight-semibold);
}

.cx-rp-two__l {
  display: block;
  font-size: var(--text-product-sm); /* 11 */
  line-height: 15px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- site notes: two labelled lines ---- */

.cx-rp-notes {
  display: block;
  padding: 6px 10px 7px;
}

.cx-rp-note {
  display: block;
  font-size: var(--text-product-sm); /* 11 */
  line-height: 15px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cx-rp-note__k {
  display: inline;
  font-weight: var(--font-weight-semibold);
}

/* ---- footer line ---- */

.cx-rp-foot {
  display: block;
  margin: 6px var(--cx-rp-gutter) 0;
  padding-top: 4px;
  border-top: 1px solid var(--cx-rp-rule);
  font-size: var(--text-product-sm); /* 11 */
  line-height: var(--leading-product-sm);
  color: var(--cx-rp-muted);
  text-align: center;
  white-space: nowrap;
}

/* ------------------------------------------------------------------ *
 * #integrations card 2 — the app's TABLE VIEW over the seven lettings
 * (2026-09-10, at the owner's direction). See `inboxBody` in the fragment.
 *
 * Drawn 1:1 IN CARD PIXELS on the row's own 13 / 12 / 11 ladder, like the
 * window shell around it — no `transform: scale()` — with every length the
 * app's own number: `VIEW_TABLE_ROW_HEIGHT` 36, `TABLE_HEADER_HEIGHT` 36,
 * the region's `px-2`, `p-2 gap-2` cells, `PillWrapper`'s `h-8 rounded-full
 * px-2` pill, `buttonVariants`' `rounded-full` New. The pill and the header
 * cell are FORKED from `bid-hero-mockup.css` / `bid-surfaces.css` rather than
 * imported, per the fork rule, and where those sheets draw the app's px under
 * a scale this draws them straight.
 *
 * THE TABLE IS WIDER THAN THE CROP ON PURPOSE. The window is 200% of the box
 * and the plate is its clip; the four columns come to 466 + the region's pad,
 * so at 1440 (~243px of window on the plate) a reader sees Status and the
 * whole Name cell and the rest carries on off the card, and below xl — where
 * the plate is ~949 — all four columns and the add-column filler are on it.
 * A row is the window's full width, as the app's is, and the add-column
 * filler at its end is what carries every rule out to the window's right
 * edge — which is off the plate at every width, so the rules simply carry on.
 *
 * Every box in here is a `<span>` (the empty-mockups trap, see the window's
 * own note) and every rule is single-class, so it wins over the `:where()`
 * blockifier on its own. Colour is tokens only; the one hue is the app's
 * `--primary`, which `.cx-conn` already declares as `--cx-primary`.
 * ------------------------------------------------------------------ */

/* THE CONTENT PANE IS `bg-muted`, NOT THE WINDOW'S OWN CHROME (2026-09-10,
   at the owner's direction — "it's just too dark as is"). The app draws a
   view region on `--background`, which in dark is the darkest token there
   is (#191919) and is what the window ground already paints, so the table
   sat a step BELOW the plate it lies on and read as a hole in the card
   rather than as a window on it. `--color-theme-card-hex` is this site's map
   of the app's `--muted` (#202020 dark, #f2f1ed light) — one step up in
   dark, and in light identical to the chrome, which is why nothing moves
   there. A DELIBERATE DEPARTURE from the app's own `--background`; the title
   bar keeps the chrome and its hairline still separates the two. */
.cx-tb-view {
  display: flex;
  flex-direction: column;
  min-height: 100%;
  background: var(--color-theme-card-hex);
}

/* `DocumentHeaderActions` under advanced complexity: the layout's glyph and
   the view's name, `font-medium` at the row's 13. 34 rather than the app's 45
   for the reason bid-hero's is 39: a still spends its height on the table. */
.cx-tb-head {
  display: flex;
  align-items: center;
  flex: none;
  gap: 6px; /* gap-1.5 */
  height: 34px;
  padding: 0 var(--cx-win-pad);
  color: var(--color-theme-product-text);
}

.cx-tb-head svg {
  width: 14px;
  height: 14px;
  flex: none;
  color: var(--color-theme-product-text-sec);
}

.cx-tb-head__t {
  font-size: var(--text-product-lg); /* 13 */
  line-height: var(--leading-product-base);
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
}

/* `ActiveSortsAndFilters` — `flex items-center gap-1`, `px-2`, the pills at
   the app's `h-8`. The row is 36 like the rows under it. */
.cx-tb-bar {
  display: flex;
  align-items: center;
  flex: none;
  gap: 4px; /* gap-1 */
  height: 36px;
  padding: 0 8px; /* px-2 */
  width: max-content;
}

/* `PillWrapper`: `flex min-w-0 rounded-full px-2 h-7 items-center font-medium
   border`, `h-8` from `HorizontalFilterItem`, and an active AND filter is
   `border-transparent text-primary bg-primary/20` over an opaque `bg-muted`
   plate — drawn as the 20% mix layered on the window's own ground, as
   `bid-hero-mockup.css`'s `.bh-pill` does. Type is the row's 11 where the app's
   is `text-sm`: this pill sits in a window whose rows are 13, not in the app. */
.cx-tb-pill {
  display: flex;
  align-items: center;
  flex: none;
  height: 28px; /* h-8 = 32 at the app's 14; 28 on this ladder, bh-pill's own */
  padding: 0 8px; /* px-2 */
  border: 1px solid transparent;
  border-radius: 999px;
  background:
    linear-gradient(
      color-mix(in oklab, var(--cx-primary) 20%, transparent),
      color-mix(in oklab, var(--cx-primary) 20%, transparent)
    ),
    var(--color-theme-product-chrome);
  color: var(--cx-primary);
  font-size: var(--text-product-sm); /* 11 */
  line-height: var(--leading-product-sm);
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
}

/* `FieldAvatar` at `size-3.5 mr-[5px]`, then the name and PillValue, then
   `ChevronDown` at `size-3.5 ml-0.5`. */
.cx-tb-pill svg {
  width: 12px; /* size-3.5 = 14 */
  height: 12px;
  flex: none;
}

.cx-tb-pill__t {
  margin: 0 2px 0 5px; /* mr-[5px] on the glyph, ml-0.5 before the chevron */
}

/* `<Button size="sm">New</Button>` — `rounded-full` from `buttonVariants`'
   root, the one saturated fill in the window. */
.cx-tb-new {
  display: flex;
  align-items: center;
  flex: none;
  height: 28px;
  padding: 0 10px; /* px-3 */
  border-radius: 999px;
  background: var(--cx-primary);
  color: #fff;
  font-size: var(--text-product-sm);
  line-height: var(--leading-product-sm);
  font-weight: var(--font-weight-medium);
}

/* A row: `VIEW_TABLE_ROW_HEIGHT` 36 on the region's `px-2`, the window's
   full width. */
.cx-tb-tr {
  display: flex;
  align-items: stretch;
  flex: none;
  height: 36px;
  padding: 0 8px; /* px-2 */
  width: 100%;
}

/* A cell: `p-2 gap-2`, one line, clipped. */
.cx-tb-td {
  display: flex;
  align-items: center;
  gap: 8px; /* gap-2 */
  flex: none;
  min-width: 0;
  padding: 0 8px; /* p-2 */
  overflow: hidden;
  white-space: nowrap;
  border-bottom: 1px solid var(--color-theme-border-02);
  font-size: var(--text-product-lg); /* 13 */
  line-height: var(--leading-product-base);
  color: var(--color-theme-product-text);
}

/* The add-column column: `flexGrow: 1`, `ADD_FIELD_MIN_WIDTH` 48, empty. It
   is what carries the rule to the window's edge below xl. */
.cx-tb-td--add {
  flex: 1;
  min-width: 48px;
  padding: 0 4px;
}

.cx-tb-td--date {
  font-variant-numeric: tabular-nums;
}

/* STATUS IS DRAWN AS A PINNED COLUMN (2026-09-10, at the owner's direction).
   `sortable-header.tsx` gives a head a right edge only where a column is
   pinned, and a pinned column carries `border-r` down its whole side — so
   one rule on the FIRST cell of every row, the header's included, is the
   whole of it. The row is `align-items: stretch`, so the rule spans the full
   36 and the line runs unbroken from the header to the last row, in the row
   rules' own `--color-theme-border-02`.

   IT IS AN INSET SHADOW AND NOT A `border-right`, which is the header row's
   own arrangement one rule down and cost a build here: `box-sizing` is
   `border-box`, so a real border takes a pixel out of Status's SOLVED 88 and
   `Interested` — ~62 of ink in a `p-2` cell, i.e. 78 of the 88 — ellipsised
   to `Interest…`. A shadow paints over the cell instead, so every width in
   `TB_COLS` stands. This is the ONE vertical rule in the table:
   `div-table.tsx`'s cell draws `border-b` and nothing else, so a second one
   would be an invention — see `automate-runs.css`. */
.cx-tb-tr > .cx-tb-td:first-child {
  box-shadow: inset -1px 0 0 0 var(--color-theme-border-02);
}

/* `SortableHeader`: the field-type glyph at `size-4.5` then the label, in
   `font-medium text-muted-foreground` at the body's own size — the header is
   not smaller and not uppercase. Its rule is an inset shadow rather than a
   border so the row keeps its exact height, the app's own arrangement. */
.cx-tb-thead .cx-tb-td {
  border-bottom: 0;
  box-shadow: inset 0 -1px 0 0 var(--color-theme-border-02);
  color: var(--color-theme-product-text-sec);
  font-weight: var(--font-weight-medium);
}

.cx-tb-thead .cx-tb-td svg {
  width: 14px; /* size-4.5 = 18 at the app's 14 */
  height: 14px;
  flex: none;
  color: var(--color-theme-product-text-sec);
}

/* THE TITLE FIELD'S "Aa" IS DRAWN RATHER THAN STROKED, and an SVG `<text>`
   with no `fill` defaults to BLACK — so the Name column's glyph rendered
   black beside four muted ones until 2026-09-10. `bid-surfaces.css`'s own
   `svg[data-filled]` rule, restated here for the window's own scope; the
   fragment already marks that one glyph. */
.cx-tb-view svg[data-filled] {
  fill: currentColor;
  stroke: none;
}

.cx-tb-thead .cx-tb-td--add {
  box-shadow: none;
}

/* The header's first cell carries BOTH: its own bottom rule and the pinned
   edge, one `box-shadow` overriding the other otherwise. */
.cx-tb-thead .cx-tb-td:first-child {
  box-shadow:
    inset -1px 0 0 0 var(--color-theme-border-02),
    inset 0 -1px 0 0 var(--color-theme-border-02);
}

/* The primary cell's leading mark — `DocumentPkAvatar` at `size-4`: the
   document's own stored image in `DocumentIconGlyph`'s `rounded-[4px]
   overflow-hidden` box under `object-cover`, or the collection's glyph in
   muted ink for a document that stores none. */
.cx-tb-ico {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 16px; /* size-4 */
  height: 16px;
  color: var(--color-theme-product-text-sec);
}

.cx-tb-ico svg {
  width: 14px;
  height: 14px;
}

.cx-tb-ico--img {
  border-radius: 4px; /* rounded-[4px] */
  overflow: hidden;
}

.cx-tb-ico--img img {
  display: block;
  width: 16px;
  height: 16px;
  object-fit: cover;
}

/* A badge in a cell truncates like a name does — `Color` is drawn inside the
   cell's own `truncate`, so `City of Liberty Hill` ellipsises inside Owner's
   128 rather than running under Bid Date, which is what /bid's own table does
   with the same chip in the same column (`.bs-chip.bs-ellipsis`). The shared
   badge is `inline-flex` and `flex: none`; a block with an ellipsis is what a
   cell needs, and the 16px line box keeps the badge its own height. */
.cx-tb-td .cx-win__tag {
  display: block;
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 16px;
}

/* `truncate` on a name and on a header label. */
.cx-tb-el {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* THE FIFTH TONE, for the stored `Interested` option — `bid-hero-mockup.css`'s
   own yellow, converted there from the app's oklch and COPIED, not guessed;
   the badge test pins the pair against that sheet. It lives beside the table
   it was added for rather than in the shared badge block above. */
.cx-win__tag[data-tone='yellow'] {
  --cx-tag-bg: rgb(243 171 28 / 0.39);
  --cx-tag-fg: #4b3314;
}

:root[data-theme='dark'] .cx-win__tag[data-tone='yellow'] {
  --cx-tag-bg: rgb(246 168 0 / 0.5);
}

/* ------------------------------------------------------------------ *
 * #integrations card 3 — the subcontractor directory, A CUSTOM APP OF ITS
 * OWN (2026-09-10, at the owner's direction)
 *
 * The other two windows on this row draw Bidlo's own surfaces on Bidlo's own
 * tokens. This one draws an app a team BUILT, and a custom app's CSS is the
 * app author's, not the host's — so this block is the one place in the sheet
 * that wears no theme token at all. It is a plain light web-app table, and it
 * stays light in dark for the reason `bid-extend.css`'s plan sheet and
 * `manage-files.css`'s pages stay white there: a document is the colour the
 * document is, and an app is the colour its author painted it. The window's
 * title bar above it is still Bidlo's chrome and still follows the theme,
 * which is what says "a custom app open inside Bidlo".
 *
 * IT FOLLOWS THE SITE'S THEME AS OF 2026-09-10 (later the same day), AT THE
 * OWNER'S DIRECTION — "the sub-verifier needs to work in dark mode". The
 * white-in-both-themes call shipped for one render and is reversed: the app
 * is still the app author's own palette, but the author drew a dark ladder
 * too, and both are LITERALS declared on `.cx-sv-app` — nine in light, the
 * same nine restated under `:root[data-theme='dark']` — with no host token
 * inside the block, so the rule that a custom app's CSS is its author's and
 * not the host's still holds; what changed is that the author has two sheets.
 *
 * THE PALETTE IS NINE LITERALS AND IT IS A LADDER, not a swatch box:
 *                   light     dark      dark contrast on the dark paper
 *   --cx-sv-paper   #ffffff   #242424   the app's ground — in dark it is
 *                                       `--color-theme-card-02-hex`'s value,
 *                                       two steps up the theme's own ladder
 *                                       from the window chrome, which is
 *                                       `--color-theme-product-chrome` =
 *                                       `--color-theme-bg` = #191919 there
 *                                       (measured on the built page; #202020
 *                                       is the step between), and the value
 *                                       `bid-context.css` gives a popover in
 *                                       dark — so the table reads as a pane
 *                                       lifted inside the window rather than
 *                                       as a hole in it
 *   --cx-sv-rule    #dfe3e8   #363636   hairlines, and the hollow star
 *   --cx-sv-tint    #f3f5f7   #2b2b2b   the column-header row
 *   --cx-sv-banner  #c2410c   #c2410c   the brand band across the top — ONE
 *                                       orange in both themes, a brand colour
 *                                       being a brand colour; it is restated
 *                                       in the dark ladder so the block still
 *                                       declares every name twice. The brick
 *                                       #a8380a shipped for one render and was
 *                                       reported as a poor orange; this is the
 *                                       darkest orange that still reads as one
 *                                       and holds white at 5.18:1.
 *   --cx-sv-banner-ink  #ffffff  #ffffff   5.18:1  the app's name
 *   --cx-sv-banner-sub  #ffedde  #ffedde   4.54:1  the line under it
 *   --cx-sv-ink     #1f2933   #ececec   13.14:1  a name, a trade, the title
 *   --cx-sv-mute    #6b7280   #9a9a9a    5.52:1  column labels, second line
 *   --cx-sv-link    #1a5fb4   #6fb0ff    6.90:1  a company name, as a link
 *   --cx-sv-star    #f59e0b   #fbbf24    9.30:1  a filled star
 *   --cx-sv-ok      #16a34a   #4ade80    8.91:1  the verified check
 *   --cx-sv-alert   #dc2626   #f87171    5.61:1  the two alert triangles
 * The dark accents keep the light ones' hue and are lifted one step each
 * (Tailwind's 400 against the light's 500/600), which is what a saturated
 * 600 needs on a near-black ground — `#dc2626` on #242424 is 3.21:1 and the
 * light link's `#1a5fb4` is 2.3:1 there. In light the link is 6.29:1 on
 * white, the check 3.30:1 and the alert 4.83:1; the filled star is 2.15:1
 * in light, which is the reference's own orange-on-white and is a filled
 * 12px shape rather than a stroke or a glyph. The values are recorded in
 * the card's own test, which re-derives every ratio from the two ladders.
 *
 * THE COLUMNS ARE SOLVED AGAINST THE CROP. At 1440 the plate shows ~243px of
 * the window (see `--cx-win-w`), so the verification column, the company and
 * the five stars have to land inside it: 10 + 22 + 6 + 132 + 6 + 64 = 240,
 * i.e. the stars' right edge 4.8px inside the plate, measured on the built
 * page. 132 IS THE WIDEST NAME ON THE BENCH plus two pixels of slack —
 * `Kettleburn Grading Co.` measures 129.64 at 12px — so no name on the card
 * ellipsises; a longer bench name has to be re-measured here before it
 * ships, because the company cell ellipsises rather than wrapping the row.
 * 64 is the five 12px stars on their 1px gaps. The trade runs off the plate
 * at 1440 and comes into view below xl, where the plate is ~949 — which is
 * what the 200% window is for.
 *
 * TYPE IS THE ROW'S OWN LADDER, 1:1 in card pixels, no `transform: scale()`:
 * 13 for the app's title, 12 for a cell, 11 for a column label and for both of
 * the app's own second lines — the banner's and the company cell's.
 *
 * THE BANNER, THE TWO-LINE ROW AND THE ROW COUNT ARE ONE SOLVE (2026-09-10, at
 * the owner's direction — the card had to read as purely generated rather than
 * as one more host list, and then the rows came back as condensed). The window
 * body is 291.66 at 1440: the 391.66 plate less `--cx-top` twice, less the 28px
 * title bar. The banner takes 9 + 17.33 + 2 + 14 + 8 = 50.33, so 241.33 is left
 * for the table — the column header plus the rows.
 * A ROW IS 42: 16 of name, a 2px gap, 14 of job count, on 5px of air a side.
 * AND THAT IS WHY THE DIRECTORY IS FIVE SUBS AND NOT SIX. The two are one
 * number: 26 + 5 x 42 = 236 against 241.33, i.e. 5.33px of slack, where six
 * would want 278 and there is no banner short enough to buy it (the banner's
 * own floor is a two-line block, its second line being the tie to card 1's
 * job). Six 36px rows fitted and were reported as condensed; the air is worth
 * more than the sixth name, the list being a crop either way — it prints no
 * count, and both alerts are inside the five. RE-SOLVE THE THREE TOGETHER
 * rather than nudging one: a sixth row, a third line or a taller band cuts a
 * row's hairline, which reads as a rendering fault rather than as a list
 * carrying on.
 *
 * A HALF STAR IS TWO STARS. The hollow star is stroked in the rule's grey;
 * the filled one sits over it in a clip span whose width is the fill — 100%
 * or 50% — and there is no `clipPath` because this file mints no `id`: the
 * card gallery's harvest rewrites every `id=` without rewriting what points
 * at it. Every box in here is a `<span>`, so the window's own `:where(span)`
 * blockifier reaches all of them and each single-class rule below wins over
 * it on its own.
 * ================================================================== */

.cx-sv-app {
  --cx-sv-paper: #ffffff;
  --cx-sv-rule: #dfe3e8;
  --cx-sv-tint: #f3f5f7;
  --cx-sv-banner: #c2410c;
  --cx-sv-banner-ink: #ffffff;
  --cx-sv-banner-sub: #ffedde;
  --cx-sv-ink: #1f2933;
  --cx-sv-mute: #6b7280;
  --cx-sv-link: #1a5fb4;
  --cx-sv-star: #f59e0b;
  --cx-sv-ok: #16a34a;
  --cx-sv-alert: #dc2626;
  /* The four columns, solved against the crop — see the note above. */
  --cx-sv-c0: 22px;
  --cx-sv-c1: 132px;
  --cx-sv-c2: 64px;
  --cx-sv-c3: 128px;
  --cx-sv-gap: 6px;
  --cx-sv-pad: 10px;
  /* Two lines of company — 12/16 over a 2px gap over 11/14 — on 5px of air
     a side. See the solve in the note above. */
  --cx-sv-row: 42px;
  --cx-sv-row-head: 26px;
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--cx-sv-paper);
  color: var(--cx-sv-ink);
}

/* The author's dark ladder — the same nine, restated. See the note above. */
:root[data-theme='dark'] .cx-sv-app {
  --cx-sv-paper: #242424;
  --cx-sv-rule: #363636;
  --cx-sv-tint: #2b2b2b;
  --cx-sv-banner: #c2410c;
  --cx-sv-banner-ink: #ffffff;
  --cx-sv-banner-sub: #ffedde;
  --cx-sv-ink: #ececec;
  --cx-sv-mute: #9a9a9a;
  --cx-sv-link: #6fb0ff;
  --cx-sv-star: #fbbf24;
  --cx-sv-ok: #4ade80;
  --cx-sv-alert: #f87171;
}

/* THE BANNER. A generated app paints its own brand band across the top and
   sets its name on it; the host's own surfaces never do, which is what makes
   this window read as somebody's app rather than as another Bidlo list. It
   runs the whole 200% window, so it leaves the plate on the right the way
   every row's hairline does. */
.cx-sv-head {
  padding: 9px var(--cx-sv-pad) 8px;
  background: var(--cx-sv-banner);
}

/* The table fills what the header leaves; the pane's own `overflow: hidden`
   is the cut, and six rows end 18px above it at 1440. */
.cx-sv-table {
  display: block;
  flex: 1;
  min-height: 0;
}

.cx-sv-title {
  font-size: var(--text-product-lg); /* 13 */
  line-height: var(--leading-product-base);
  font-weight: var(--font-weight-medium);
  color: var(--cx-sv-banner-ink);
}

.cx-sv-sub {
  margin-top: 2px;
  font-size: var(--text-product-sm); /* 11 */
  line-height: var(--leading-product-sm);
  color: var(--cx-sv-banner-sub);
  white-space: nowrap;
}

/* A row spans the whole 200% window so its hairline runs to the plate's edge;
   the cells inside it are fixed and run their solved widths. */
.cx-sv-tr {
  display: flex;
  align-items: center;
  gap: var(--cx-sv-gap);
  height: var(--cx-sv-row);
  padding: 0 var(--cx-sv-pad);
  border-top: 1px solid var(--cx-sv-rule);
}

.cx-sv-tr--head {
  height: var(--cx-sv-row-head);
  background: var(--cx-sv-tint);
}

.cx-sv-th,
.cx-sv-td {
  flex: none;
  display: flex;
  align-items: center;
  min-width: 0;
  white-space: nowrap;
  font-size: var(--text-product-base); /* 12 */
  line-height: var(--leading-product-base);
  color: var(--cx-sv-ink);
}

.cx-sv-th {
  font-size: var(--text-product-sm); /* 11 */
  line-height: var(--leading-product-sm);
  font-weight: var(--font-weight-medium);
  color: var(--cx-sv-mute);
}

.cx-sv-c0 {
  width: var(--cx-sv-c0);
  justify-content: center;
}

.cx-sv-c1 {
  width: var(--cx-sv-c1);
}

.cx-sv-c2 {
  width: var(--cx-sv-c2);
}

.cx-sv-c3 {
  width: var(--cx-sv-c3);
}

/* THE COMPANY CELL IS TWO LINES — the name, and under it how many of this
   contractor's jobs are live. A single-line row of name / rating / trade is
   the shape every list on the host draws; two lines is not, which is the
   whole point of it. The second line is the app author's own muted step. */
.cx-sv-stack {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
  width: 100%;
}

.cx-sv-jobs {
  display: block;
  margin-top: 2px;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: var(--text-product-sm); /* 11 */
  line-height: var(--leading-product-sm);
  color: var(--cx-sv-mute);
}

/* The company is a link, as the reference draws it — and it ellipsises
   rather than wrapping, a wrapped row breaking the pitch the table reads by. */
.cx-sv-link {
  display: block;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--cx-sv-link);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}

/* THE VERIFICATION MARK STANDS ON A PLATE (2026-09-10, at the owner's
   direction). A bare 14px glyph beside a two-line cell is a mark floating in
   a column of air; a bordered square is a badge, and it gives the taller row
   something with an edge to sit against. It is the app author's own tint and
   rule rather than a fourth pair of literals per status — the glyph inside
   carries the colour, which is where the status belongs. */
.cx-sv-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: 1px solid var(--cx-sv-rule);
  border-radius: 6px;
  background: var(--cx-sv-tint);
}

.cx-sv-ok,
.cx-sv-alert {
  display: block;
  width: 13px;
  height: 13px;
}

.cx-sv-ok {
  color: var(--cx-sv-ok);
}

.cx-sv-alert {
  color: var(--cx-sv-alert);
}

.cx-sv-stars {
  display: flex;
  align-items: center;
  gap: 1px;
}

/* One star cell: the hollow star, and over it the fill clipped to its width. */
.cx-sv-star {
  position: relative;
  display: block;
  width: 12px;
  height: 12px;
}

.cx-sv-star__o,
.cx-sv-star__i {
  display: block;
  width: 12px;
  height: 12px;
}

.cx-sv-star__o {
  color: var(--cx-sv-rule);
}

.cx-sv-star__f {
  position: absolute;
  inset: 0 auto 0 0;
  display: block;
  overflow: hidden;
  color: var(--cx-sv-star);
}

.cx-sv-star__f[data-fill='full'] {
  width: 100%;
}

.cx-sv-star__f[data-fill='half'] {
  width: 50%;
}

.cx-sv-star__i {
  fill: currentColor;
  stroke: currentColor;
}

/* ------------------------------------------------------------------ *
 * #share card 1 — the app's own share popover, on its Share tab
 * (2026-09-09, at the owner's direction)
 *
 * It drew all THREE of this section's cards for part of that day — the same
 * panel with its audience menu open, and again on its Publish tab — and the
 * owner replaced the second and third: one popover drawn three times read as
 * one picture before a reader got to a sentence. What went with them is in
 * the fragment's own header; the rules for the menu, the link row, the
 * sign-in switch and the `LiveDot` were deleted rather than left dormant.
 *
 * `tools/fragments/custom-apps-page.mjs`'s own header carries the argument;
 * what follows is the drawing. Sources are named there file by file.
 *
 * EVERY LENGTH IS THE APP'S OWN NUMBER. The panel is `SHARE_PANEL_WIDTH`
 * (380) and `.ig-scaler` fits it to the square plate with one transform, so
 * nothing in here is scaled by hand and nothing reads the capture's
 * `--text-product-*` ladder — this is `integrations.css`'s model, not
 * `hero-chat.css`'s.
 *
 * TYPE IS THE APP'S LEADING, NOT THE SCALER'S. `.ig-scaler` sets a flat
 * `line-height: 1.35`, and every stacked row in this panel is a `text-sm`
 * line over a `text-xs` one whose heights the app takes from Tailwind's own
 * defaults — 20 and 16. Left at 1.35 a person row measured 47.1 where the
 * app's is 48, and the three states' heights are solved off that 48. So the
 * two sizes state their leading, and moving one re-solves the fit.
 *
 * THE RADIUS LADDER IS THE APP'S AND NOT TAILWIND'S: it anchors on
 * `--radius: 0.625rem`, so `rounded-sm` is 6, `rounded-md` 8 and
 * `rounded-xl` **14**.
 *
 * THE TRAP. `empty-mockups.js` finds a window by a `div` holding exactly
 * three children that are each 4-16px wide, near-square and fully round.
 * Nothing here matches: `.cx-shr__person` and `.cx-shr__row` both hold three
 * children and both lead with a 32px circle. The row that WAS close — the
 * Publish tab's link row, whose first child is an 8px `LiveDot` saved only by
 * the 300px URL beside it — left with that tab on 2026-09-09. Do not put a
 * three-child container in here whose every child is small and round.
 * ------------------------------------------------------------------ */

.cx-shr-wrap {
  /* `--popover` is `var(--background)` in the app, and its dark value is
     oklch(0.26 0 0) = #242424, which is this site's `--color-theme-card-02-hex`
     — the mapping `bid-context.css` and `manage-surfaces.css` both make. The
     avatar and dial circles are `bg-accent`, one step off the panel, so they
     take the step ABOVE the popover in whichever theme is running. */
  --cx-shr-pop: var(--color-theme-bg);
  --cx-shr-fill: var(--color-theme-card-02-hex);
  --cx-shr-line: var(--color-theme-border-02);
  --cx-shr-text: var(--color-theme-product-text);
  /* `--muted-foreground` / `--secondary` are the foreground at 60 % ALPHA, and
     a lucide line glyph is several paths sharing one stroke — so at an alpha
     every crossing composites a step darker than the lines running into it.
     Flattened against the panel's own ground, which is what the app carries
     `text-secondary-opaque` for. */
  --cx-shr-sec: color-mix(in srgb, var(--color-theme-fg) 60%, var(--cx-shr-pop));
  --cx-shr-accent: #2483e2; /* --primary */

  color: var(--cx-shr-text);
}

:root[data-theme='dark'] .cx-shr-wrap {
  --cx-shr-pop: var(--color-theme-card-02-hex);
  --cx-shr-fill: var(--color-theme-card-03-hex);
}

/* `PopoverContent`: `rounded-xl bg-popover shadow-popover overflow-hidden`
   with NO border — the edge is the third layer of `--popover-shadow`, which is
   the one shadow here that cannot come off a theme token because it has to
   stay black in both. The menu below carries the same surface, so the two
   share this rule and cannot drift. */
.cx-shr{
  background: var(--cx-shr-pop);
  border-radius: 14px; /* rounded-xl = --radius + 4 */
  overflow: hidden;
  box-shadow:
    rgba(0, 0, 0, 0.1) 0 14px 28px -6px,
    rgba(0, 0, 0, 0.06) 0 2px 4px -1px,
    rgba(84, 72, 49, 0.08) 0 0 0 1px;
}

/* `--popover-shadow`'s dark half, verbatim: the ambient pools deepen and the
   1px ring flips to white at 9.4 %, a black ring reading as nothing on a dark
   surface. */
:root[data-theme='dark'] .cx-shr{
  box-shadow:
    rgba(0, 0, 0, 0.2) 0 14px 28px -6px,
    rgba(0, 0, 0, 0.12) 0 2px 4px -1px,
    rgba(255, 255, 255, 0.094) 0 0 0 1px;
}

.cx-shr {
  width: 100%;
}

/* `<div className="flex h-10 shrink-0 items-center border-b border-border
   px-2">` around a `TabsList variant="line" gap-0.5`. Forty INCLUDING the
   rule; Tailwind's preflight is what puts `box-sizing: border-box` under it. */
.cx-shr__tabs {
  display: flex;
  align-items: center;
  gap: 2px; /* gap-0.5 */
  height: 40px; /* h-10 */
  padding: 0 8px; /* px-2 */
  border-bottom: 1px solid var(--cx-shr-line);
}

/* SHARE_TAB_TRIGGER_CLASS: `rounded-sm px-2 py-1 text-sm font-medium
   text-secondary`, active taking `text-foreground` on a transparent fill. */
.cx-shr__tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px; /* gap-1.5 */
  padding: 4px 8px; /* py-1 px-2 */
  border-radius: 6px; /* rounded-sm */
  font-size: 14px;
  line-height: 20px;
  font-weight: var(--font-weight-medium);
  color: var(--cx-shr-sec);
}

.cx-shr__tab--on {
  color: var(--cx-shr-text);
}

/* `after:bottom-[-7px] after:left-2 after:right-2 after:h-0.5
   after:bg-foreground`. The trigger is 28 tall centred in the row's 39px
   content box, so its own bottom is at 33.5 and the underline lands at 40.5 —
   the row's bottom rule, with the underline drawn over it. */
.cx-shr__tab--on::after {
  content: '';
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: -7px;
  height: 2px;
  background: var(--cx-shr-text);
}

/* The invite field. `<div className="shrink-0 px-3 pt-2">` around an `Input`
   (`h-9 rounded-md border shadow-xs`) whose own `inputClassName` narrows it to
   `h-9 px-2 text-sm`, with `placeholder:text-secondary`. */
.cx-shr__invite {
  display: flex;
  align-items: center;
  margin: 8px 12px 0; /* pt-2, px-3 */
  height: 36px; /* h-9 */
  padding: 0 8px; /* px-2 */
  border: 1px solid var(--cx-shr-line);
  border-radius: 8px; /* rounded-md */
  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); /* shadow-xs */
}

.cx-shr__ph {
  color: var(--cx-shr-sec);
}

/* `PopoverGroup` is `p-2` and every group in this panel adds `px-3`, so all of
   them are 8/12. */
.cx-shr__group {
  padding: 8px 12px;
}

/* `MenuLabel`: `py-1.5 px-2` around a `text-sm font-medium text-secondary`.
   `--gap` is the second one's own `className="pt-3"`, which is what separates
   the access list from the dial under it. */
.cx-shr__label {
  padding: 6px 8px;
  font-size: 14px;
  line-height: 20px;
  font-weight: var(--font-weight-medium);
  color: var(--cx-shr-sec);
}

.cx-shr__label--gap {
  padding-top: 18px; /* py-1.5 + pt-3 */
}

/* `PersonRow`: `flex items-center gap-2 rounded-sm px-2 py-1.5` around a
   `size-8` avatar, the name, and the email at `text-xs text-muted-foreground`.
   Forty-eight tall — the two lines' own 20 + 16 clearing the avatar's 32. */
.cx-shr__person {
  display: flex;
  align-items: center;
  gap: 8px; /* gap-2 */
  padding: 6px 8px; /* py-1.5 px-2 */
  border-radius: 6px; /* rounded-sm */
}

/* `AvatarFallback`: `bg-accent text-[11px] font-medium uppercase
   text-secondary` in a `size-8` circle — "filled circle either way, so a row
   without a photo doesn't read as a smaller avatar", which is that
   component's own comment and why a row with no photograph draws an initial
   rather than nothing. As of 2026-09-10 all three rows carry one — see
   `SHR_PHOTOS` in the fragment — so the fill and the letter are the fallback
   a fourth person without a file would take; `overflow: hidden` is `Avatar`'s
   own and is what clips the photograph to the circle. */
.cx-shr__av {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 32px; /* size-8 */
  height: 32px;
  border-radius: 9999px;
  overflow: hidden;
  background: var(--cx-shr-fill);
  font-size: 11px;
  line-height: 1;
  font-weight: var(--font-weight-medium);
  text-transform: uppercase;
  color: var(--cx-shr-sec);
}

/* `AvatarImage`: `aspect-square size-full` and `person-row.tsx`'s own
   `object-cover`. `display: block` because the sheet's generic img rules do
   not reach this span, and an inline replaced box would sit on a baseline. */
.cx-shr__av-img {
  display: block;
  width: 100%;
  height: 100%;
  aspect-ratio: 1;
  object-fit: cover;
}

.cx-shr__who {
  display: flex;
  flex: 1;
  min-width: 0;
  flex-direction: column;
}

.cx-shr__name,
.cx-shr__rowtitle,
.cx-shr__rowsub,
.cx-shr__mail{
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cx-shr__name {
  line-height: 20px; /* text-sm */
}

.cx-shr__mail {
  font-size: 12px; /* text-xs */
  line-height: 16px;
  color: var(--cx-shr-sec);
}

/* `LevelMenu`'s trigger — `flex shrink-0 items-center gap-1 rounded-sm px-1.5
   text-xs text-secondary` closing on a `size-3` chevron. `StaticLevel` is the
   same box with an empty `size-3` span where the chevron goes, "so every row's
   level sits on one line and one right edge": that is why the owner's row
   draws a spacer rather than nothing, and why one class serves both. */
.cx-shr__lvl {
  display: flex;
  flex: none;
  align-items: center;
  gap: 4px; /* gap-1 */
  padding: 0 6px; /* px-1.5 */
  border-radius: 6px;
  font-size: 12px;
  line-height: 16px;
  color: var(--cx-shr-sec);
}

.cx-shr__lvl-i {
  display: block;
  flex: none;
  width: 12px; /* size-3 */
  height: 12px;
}

/* The audience dial: a `MenuItem className="h-auto py-1.5"` whose leading mark
   takes "the same footprint as the avatars above it" — the app's own comment
   on that `size-8 rounded-full bg-accent` span. */
.cx-shr__row {
  display: flex;
  align-items: center;
  gap: 8px; /* gap-2 */
  padding: 6px 8px; /* py-1.5 px-2 */
  border-radius: 6px;
}

.cx-shr__ico {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 32px; /* size-8 */
  height: 32px;
  border-radius: 9999px;
  background: var(--cx-shr-fill);
  color: var(--cx-shr-sec);
}

.cx-shr__rowtext {
  display: flex;
  flex: 1;
  min-width: 0;
  flex-direction: column;
}

.cx-shr__rowtitle {
  line-height: 20px;
}

.cx-shr__rowsub {
  font-size: 12px; /* text-xs text-muted-foreground */
  line-height: 16px;
  color: var(--cx-shr-sec);
}

/* Every glyph is a lucide icon on a 24 viewBox at stroke-width 1.5.
   `.ig-scaler svg *` above already reads that width in device pixels, which is
   what keeps it a hairline as the stage scales. Sixteen is `size-4`, the
   default in these rows; the two exceptions state their own. */
.cx-shr__i {
  flex: none;
  width: 16px;
  height: 16px;
}

.cx-shr__chev {
  color: var(--cx-shr-sec); /* ChevronDown className="size-4 text-secondary" */
}

/* ------------------------------------------------------------------ *
 * #share card 2 — the job's page tabs over its items, switching to the
 * schedule (2026-09-10, at the owner's direction)
 *
 * IS NOT IN THIS SHEET. It lives in src/styles/custom-apps-handoff.css
 * (`cx-ho-`), paired with tools/fragments/custom-apps-handoff.mjs; the
 * document header it replaced (`cx-pg-`) is deleted with its rules rather
 * than left dormant. The one lesson that block carried is worth keeping
 * here: a base class in this sheet must be one drawing's alone — `.cx-doc`
 * belongs to #build's returned document card at the top of this file, and a
 * second root under that name took its width and its column direction.
 * ------------------------------------------------------------------ */
/* ------------------------------------------------------------------ *
 * /custom-apps #share card 3 — a field guest's phone, open on the apps a
 * team shared with them (2026-09-10, at the owner's direction)
 *
 * Pairs with tools/fragments/custom-apps-phone.mjs, whose header carries the
 * argument and names every source file; what follows is the drawing. Prefixes:
 * `cx-ph-` is the DEVICE (the captured phone, restated), `cx-tp-` is the
 * SCREEN — the account root's Apps tab, `features/teams`' own page.
 *
 * TWO SPACES, AND ONLY ONE SCALE BETWEEN THEM. The phone body is 300 x 650
 * CARD pixels — the captured phone `manage-surfaces.css` already draws, its
 * 44px corner, its 4px bezel and its 48px notch included — and everything
 * inside the screen is the APP's own number at the app's own 320px viewport,
 * brought down by one `transform: scale(0.9125)` on `.cx-tp`. That scale is
 * `PHONE.screen.w / PHONE_APP.w`, solved in the fragment rather than restated
 * — it was `manage-surfaces.mjs`' imported `SCALE_PHONE` (0.75) at a 390px
 * viewport until 2026-09-10, which drew the app's `text-sm` at 10.50; the
 * phones on this site render the app at one size. Nothing in either space is
 * scaled by hand.
 *
 * THE PHONE ITSELF TAKES A SECOND, CONTAINER-DRIVEN SCALE, and it is the one
 * this card owns. `--cx-ph-z` is `min(1, (100cqw - 52px) / 300)`: at the
 * 391.66px plate a 1440 viewport gives it caps at 1 and the phone stands 300
 * wide on 45.8 of air a side; below that it shrinks so the air never falls
 * under 26 — the row's own gap, and card 1's to a tenth. `.ig-stage` is not in
 * this card's chain, so the container query is declared here. THE DIVISOR
 * MUST BE A LENGTH: `/ 300` silently falls back to the stated 0.85.
 *
 * IT BLEEDS OFF THE BOTTOM AND HOLDS AIR AT THE TOP, which is why it does not
 * go through `.ig-scaler` at all: that scaler centres on both axes. The phone
 * starts on `--cx-ph-top` (26) and runs 676 into a 391.66 plate, so the plate
 * cuts it — the treatment /manage's three device cards already take.
 *
 * THE SHADOW IS ON THE DEVICE, NOT THE BODY. `.cx-ph__body` clips itself with
 * `clip-path: inset(0 round 44px)` and `contain: paint` (the capture's own
 * pair, which is what stops the screen bleeding past the bezel in Safari) —
 * and both clip a box's own shadow with its overflow. A shadow declared there
 * is a shadow nobody sees, which is what this card shipped for a day. The
 * wrapper clips nothing, so it carries the shadow. Its VALUES are the
 * paragraph below's; they were the hero frame's until 2026-09-10.
 *
 * 2026-09-10, LATER: THE SHADOW IS `.cx-win`'s, THE SCREEN IS PADDED 20, THE
 * CARDS FILL TWO COLUMNS AND EACH CARRIES A HUE. The fragment's header names
 * the six departures; here is where each lands. The device's shadow is the
 * two drop layers `.cx-win` (custom-apps-cards.css) carries — the windows in
 * the section directly above — with that sheet's own dark restatement at
 * 0.3 and without its 1px ring. `--cx-tp-pad` (20) replaces the app's 12 on
 * the header and the page column. `.cx-tp__card` is `calc(50% - 8px)` wide.
 * And the twelve `--cx-tp-<hue>` / `--cx-tp-<hue>-muted` tokens are the
 * app's own `--<c>-foreground` / `--<c>-badge` (light — see the token block
 * for why the banner is the badge step and not `--<c>-accent`) and
 * `--<c>-primary` / `--<c>-accent` (dark), converted from their oklch and copied
 * rather than shared — the same pairs `bid-hero-mockup.css` converts for its
 * badges, at the app's own values. A card reads them off `data-hue`.
 *
 * TOKENS ARE DECLARED ON `.cx-ph`, NOT ON `.cx-tp`: the screen and the notch
 * band are the phone's boxes and sit ABOVE the app element, and a custom
 * property does not reach a box's ancestors. The app's `--background` is the
 * site's page ground, its `--muted` (the card's fill) the site's card token
 * one step off it, and its `--hover` (the banner) a 6 % wash of the foreground
 * over that — `color-mix` so one rule serves both themes. The two grey inks
 * are FLATTENED the way `bid-surfaces.css` flattens its own: a lucide glyph is
 * several paths sharing one stroke, and at an alpha every crossing composites
 * a step darker than the lines running into it.
 * ------------------------------------------------------------------ */

.cx-ph {
  grid-area: 1 / 1 / -1 / -1;
  position: relative;
  overflow: hidden;
  container-type: inline-size;
  /* The row's own air, and the floor the container query holds the sides to
     when shrinking — see the header. */
  --cx-ph-top: 26px;
  --cx-tp-bg: var(--color-theme-bg); /* --background */
  --cx-tp-muted: var(--color-theme-card-hex); /* --muted */
  --cx-tp-hover: color-mix(in srgb, var(--color-theme-fg) 6%, var(--cx-tp-muted)); /* --hover */
  --cx-tp-line: color-mix(in srgb, var(--color-theme-fg) 16%, var(--cx-tp-bg)); /* --border */
  /* `border-border/50` — --border at half alpha, i.e. the foreground at 8 %,
     mixed against the CARD's ground rather than the page's: an element's
     background runs under its border box, so that is what it composites on. */
  --cx-tp-line-50: color-mix(in srgb, var(--color-theme-fg) 8%, var(--cx-tp-muted));
  --cx-tp-text: var(--color-theme-product-text); /* --foreground */
  --cx-tp-text-muted: color-mix(in srgb, var(--color-theme-fg) 55%, var(--cx-tp-bg)); /* --muted-foreground(-opaque) */
  --cx-tp-text-dim: color-mix(in srgb, var(--color-theme-fg) 40%, var(--cx-tp-bg)); /* --secondary-opaque */
  --cx-tp-accent: #2483e2; /* --primary, the app's own fixed hue */
  --cx-tp-on-accent: #fff; /* --primary-foreground, white in both themes */
  /* The screen's inline padding — over the app's own px-3 (12). */
  --cx-tp-pad: 20px;
  /* The app's scale, stated once and read by `.cx-tp`'s transform AND by the
     tab pill, so the drawing and the pill's rendered box cannot drift. */
  --cx-tp-scale: 0.9125;
  --cx-tp-px: calc(1px / var(--cx-tp-scale)); /* one RENDERED pixel */
  /* The six icon hues: `--<c>-foreground` (the glyph) and, for the banner,
     `--<c>-badge` composited over the card's own fill, converted from
     app/globals.css.

     THE BANNER IS THE BADGE STEP AND NOT `--<c>-accent`, AND THAT IS A LIGHT-
     MODE DEPARTURE (2026-09-10, at the owner's direction — "way too washed
     out"). `colorCss(c, "muted")` resolves to `--<c>-accent`, which in LIGHT
     is oklch L ~0.95: measured against this card's own ground (#f2f1ed) that
     is a contrast ratio of **1.01-1.02**, i.e. no visible tint at all, so six
     hued cards drew as six grey ones. It is right in the app, where that band
     is 144px wide on a full page; it is not readable at the 133px a card is
     here. DARK IS UNTOUCHED — its `--<c>-accent` is L 0.25-0.43, a deep tone
     that already reads, which is exactly the asymmetry that was reported.
     `--<c>-badge` is the palette's own next step and the one it uses wherever
     a hue has to READ on a small surface; composited it lands at **1.21-1.34**
     against the card. The alpha is the token's own (0.27, or 0.39/0.40 on the
     two lightest hues) and the ground stays a variable, so the two cannot
     drift. The GLYPH does not move: it is still `--<c>-foreground`, the app's
     own icon ink, whose contrast on the deeper band is 1.90-4.31 against
     2.41-5.49 before — orange and yellow are the weak pair either way, and
     they are the app's own pairing. Re-derive from `--<c>-badge`, never nudge
     a hex. */
  --cx-tp-blue: #0067a5; /* oklch(0.4962 0.1286 244.16) */
  --cx-tp-blue-muted: color-mix(in srgb, #418bc6 27%, var(--cx-tp-muted)); /* --blue-badge */
  --cx-tp-green: #036e4c; /* oklch(0.4775 0.102 163.29) */
  --cx-tp-green-muted: color-mix(in srgb, #509e72 27%, var(--cx-tp-muted)); /* --green-badge */
  --cx-tp-orange: #e48900; /* oklch(0.7059 0.1958 75.66) */
  --cx-tp-orange-muted: color-mix(in srgb, #e29212 27%, var(--cx-tp-muted)); /* --orange-badge */
  --cx-tp-purple: #885fa6; /* oklch(0.5566 0.1153 309.83) */
  --cx-tp-purple-muted: color-mix(in srgb, #9b78c9 27%, var(--cx-tp-muted)); /* --purple-badge */
  --cx-tp-yellow: #c98c00; /* oklch(0.6831 0.1458 79.01) */
  --cx-tp-yellow-muted: color-mix(in srgb, #f3ab1c 39%, var(--cx-tp-muted)); /* --yellow-badge */
  --cx-tp-red: #e1584d; /* oklch(0.6367 0.1731 27.57) */
  --cx-tp-red-muted: color-mix(in srgb, #ffab9e 40%, var(--cx-tp-muted)); /* --red-badge */
}

:root[data-theme='dark'] .cx-ph {
  --cx-tp-bg: var(--color-theme-card-02-hex);
  --cx-tp-muted: var(--color-theme-card-03-hex);
  /* `--<c>-primary` / `--<c>-accent` under `.dark`. */
  --cx-tp-blue: #2575c7; /* oklch(0.5586 0.148 252.7) */
  --cx-tp-blue-muted: #00254b; /* oklch(0.253 0.0973 239.28) */
  --cx-tp-green: #008055; /* oklch(0.5261 0.1215 162.38) */
  --cx-tp-green-muted: #1c3d2f; /* oklch(0.3296 0.0469 164.08) */
  --cx-tp-orange: #e28600; /* oklch(0.7005 0.1835 72.15) */
  --cx-tp-orange-muted: #69401a; /* oklch(0.4118 0.0771 60.29) */
  --cx-tp-purple: #8560b3; /* oklch(0.5623 0.1298 302.94) */
  --cx-tp-purple-muted: #332541; /* oklch(0.2933 0.0537 307.19) */
  --cx-tp-yellow: #d18600; /* oklch(0.6783 0.1696 76.72) */
  --cx-tp-yellow-muted: #674a26; /* oklch(0.4332 0.0651 71.33) */
  --cx-tp-red: #dd594e; /* oklch(0.6336 0.1674 27.91) */
  --cx-tp-red-muted: #582b26; /* oklch(0.3488 0.0671 28.31) */
}

.cx-ph__device {
  position: absolute;
  top: var(--cx-ph-top);
  left: 50%;
  width: 300px;
  height: 650px;
  transform-origin: top center;
  /* The fallback for an engine without length-over-length division; the
     line under it is what actually runs. */
  transform: translateX(-50%) scale(0.85);
  transform: translateX(-50%) scale(min(1, (100cqw - 52px) / 300px));
  /* The 44px corner is what the shadow follows; the body underneath draws
     the same radius, so the two edges are one edge. */
  border-radius: 44px;
  /* THE SECTION ABOVE'S OWN LIFT: `.cx-win`'s two drop layers, verbatim from
     custom-apps-cards.css — `.bx-popover`'s shadow, the elevation this site
     gives a surface resting on a plate — less that rule's 1px ring, this
     phone drawing its own bezel. The hero frame's own far larger pool
     shipped for a day and was reported as far too prominent. The dark half is restated
     below at that sheet's own 0.3. */
  box-shadow:
    0 10px 15px -3px rgb(0 0 0 / 0.1),
    0 4px 6px -4px rgb(0 0 0 / 0.1);
}

:root[data-theme='dark'] .cx-ph__device {
  box-shadow:
    0 10px 15px -3px rgb(0 0 0 / 0.3),
    0 4px 6px -4px rgb(0 0 0 / 0.3);
}

/* The captured phone's four side buttons, drawn on the device wrapper and
   before the body so the body's own clip does not cut them. */
.cx-ph__btn {
  position: absolute;
  display: block;
  width: 3px;
  border-radius: 2px;
  background: var(--color-theme-border-02);
}

.cx-ph__body {
  position: relative;
  display: block;
  width: 300px;
  height: 650px;
  padding: 4px; /* PHONE_BEZEL */
  border-radius: 44px;
  overflow: hidden;
  /* `overflow: hidden` alone leaves the screen's corners bleeding past the
     bezel in Safari — the capture's own pair. Both clip this box's own
     shadow too, which is why the device above carries it. */
  clip-path: inset(0 round 44px);
  contain: paint;
  /* A LITERAL in both themes: hardware is the colour hardware is, so a token
     that inverts would draw a white handset in dark mode. It is
     `manage-surfaces.mjs`' value to the byte. */
  background: rgb(58, 58, 60);
}

.cx-ph__screen {
  position: relative;
  display: block;
  width: 292px; /* 300 - 2 * PHONE_BEZEL */
  height: 642px;
  border-radius: 40px; /* 44 - PHONE_BEZEL */
  overflow: hidden;
  border: 1px solid var(--color-theme-border-02); /* the captured screen's own hairline */
  background: var(--cx-tp-bg);
}

/* The notch strip is painted the PAGE's own ground, so the band and the
   header under it are one surface. */
.cx-ph__notch {
  position: relative;
  display: block;
  height: 48px;
  background: var(--cx-tp-bg);
}

.cx-ph__island {
  position: absolute;
  display: block;
  top: 8px;
  left: 50%;
  width: 96px;
  height: 28px;
  margin-left: -48px;
  border-radius: 9999px;
  background: #000;
}

/* Everything from here down is inside the app: every length is the app's own
   number at the app's own 320px viewport. */
.cx-ph__app {
  position: absolute;
  display: block;
  top: 48px;
  left: 0;
  right: 0;
  bottom: 0;
  overflow: hidden;
}

.cx-tp {
  display: flex;
  flex-direction: column;
  width: 320px; /* PHONE_APP.w — a real iPhone SE viewport; see the fragment */
  height: 651px; /* PHONE_APP.h */
  transform: scale(var(--cx-tp-scale)); /* PHONE_APP.scale (0.9125) — 292 / 320 */
  transform-origin: top left;
  font-family: var(--font-system);
  font-size: 16px; /* the app's body size, which the avatar's initials inherit */
  line-height: 1.5;
  background: var(--cx-tp-bg);
  color: var(--cx-tp-text);
}

/* `<header class="grid grid-cols-[auto_auto] items-center gap-4 pb-3
   pt-[calc(0.75rem+env(safe-area-inset-top))] px-3 bg-background">`. The
   safe-area inset is the notch strip above, already spent. */
.cx-tp__head {
  display: flex;
  flex: none;
  align-items: center;
  gap: 16px;
  padding: 12px var(--cx-tp-pad);
  background: var(--cx-tp-bg);
}

/* `BidloLogo`: `flex items-center gap-2 shrink-0 relative`. */
.cx-tp__logo {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
}

/* `flex size-9 items-center justify-center rounded-lg border`. */
.cx-tp__markbox {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 10px; /* rounded-lg = --radius */
  border: 1px solid var(--cx-tp-line);
}

/* `BidloIcon className="size-5.5 text-foreground"`. */
.cx-tp__mark {
  display: block;
  width: 22px;
  height: 22px;
  color: var(--cx-tp-text);
}

/* `BidloText className="h-6 w-auto text-foreground mt-0.5"`: 142 x 47 at 24
   tall is 72.51 wide. */
.cx-tp__word {
  display: block;
  height: 24px;
  width: 72.51px;
  margin-top: 2px;
  color: var(--cx-tp-text);
}

/* The "Beta" badge: `Badge variant="secondary"` (a `rounded-full border
   px-2 py-0.5 text-xs font-medium` pill) overridden to `outline-1
   outline-background absolute top-0 -right-7 text-[11px] uppercase py-0 px-1
   bg-background text-primary` with an `after:bg-primary/20` overlay — i.e. a
   20 % wash of the accent over the page ground, drawn here as one mix. The
   line box is `text-xs`' own 1.3333 at 11px. */
.cx-tp__beta {
  position: absolute;
  top: 0;
  right: -28px;
  display: inline-flex;
  align-items: center;
  padding: 0 4px;
  border: 1px solid transparent;
  border-radius: 9999px;
  outline: 1px solid var(--cx-tp-bg);
  font-size: 11px;
  line-height: 1.3333;
  font-weight: 500;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--cx-tp-accent);
  background: color-mix(in srgb, var(--cx-tp-accent) 20%, var(--cx-tp-bg));
}

/* THE ACCOUNT AVATAR, `user.tsx`'s own header trigger: a `size-9` `Avatar`,
   i.e. `relative flex shrink-0 overflow-hidden rounded-full`, pushed to the
   header's right edge by the layout's `ml-auto`. Its button carries only
   `ring-0 hover:ring-4`, which paints nothing at rest, so there is no ring
   here. The fill is `AvatarFallback`'s own `bg-primary`, kept BEHIND the
   photograph so a lost image reads as the app's avatar chip rather than as a
   hole in the header; the fallback letter is not drawn under an opaque image.
   36px is the app's own number at the app's own scale — the same box the
   lockup's markbox takes, so the header's height does not move.

   THE RING IS THIS CARD'S OWN AND IS NOT THE APP'S, at the owner's direction
   (2026-09-10): `user.tsx`' trigger draws `ring-0` at rest and only rings on
   hover, and a still is never hovered, so a photograph sat on the header's
   own ground with no edge at all. It is the app's `--border` token, i.e. the
   hairline every surface in this window already draws, and it is stated as
   ONE RENDERED PIXEL divided back out by the sheet's own scale — the idiom
   `bid-capabilities.css`' `--bk-type-lg` and `manage-change-order.css`'
   `--mc-ask-px` both use, so the ring stays a hairline whatever the scale
   becomes. It is a `box-shadow` and not a `border`: a border would eat 2px
   off the 36px box and shrink the photograph inside it, where a shadow sits
   outside the box, costs the layout nothing and cannot be clipped by this
   element's own `overflow: hidden` (that clips children, not the box). */
.cx-tp__avatar {
  display: flex;
  flex: none;
  margin-left: auto;
  align-items: center;
  justify-content: center;
  width: 36px; /* size-9 */
  height: 36px;
  border-radius: 9999px;
  overflow: hidden;
  background: var(--cx-tp-accent);
  box-shadow: 0 0 0 calc(1px / 0.9125) var(--cx-tp-line);
}

/* `AvatarImage`: `aspect-square size-full`. `display: block` because this
   sheet's rules do not reach an inline replaced box's baseline, and
   `object-fit: cover` because the source is square and the box is square —
   it is the guard, not the crop. */
.cx-tp__avatar-img {
  display: block;
  width: 100%;
  height: 100%;
  aspect-ratio: 1;
  object-fit: cover;
}

/* The route's own column: `px-3 sm:px-4 …` around `mx-auto w-full pb-12`. */
.cx-tp__page {
  display: flex;
  flex: 1;
  min-height: 0;
  flex-direction: column;
  padding: 0 var(--cx-tp-pad); /* the app's own px-3 is 12 — see the header */
}

/* `TeamsHeader` on a phone: `flex flex-row-reverse items-center
   justify-between gap-2 py-2` — tabs on the left, the icon buttons right. */
.cx-tp__bar {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 0;
}

/* `GalleryTabs`: `flex items-center gap-0.5`. */
.cx-tp__tabs {
  display: flex;
  align-items: center;
  gap: 2px;
}

/* `Tab`: `Button variant="ghost" size="lg"` — `h-10 rounded-full px-3 gap-1.5
   text-sm font-medium`, `text-muted-foreground-opaque` at rest.

   THE PILL'S BOX IS STATED IN RENDERED PIXELS AND IS SHARED WITH CARD 2'S
   (2026-09-10, at the owner's direction). Both cards draw a tab strip and each
   drew its own component's app numbers under its own scale — `GalleryTabs`'
   `h-10 px-3 text-sm` at 0.9125 here against `PageTabBar`'s `h-9 px-3` at
   0.83332 there — so on the built page the two rendered 36.5 tall on 10.95 of
   pad beside 30.0 on 10.0, one row carrying two pill sizes. The shared spec is
   the MIDPOINT of the two: **33 tall, 10.5 of side pad, 5.25 of gap, the label
   at 13** — the row's own type ladder, which the app's 14 already rendered at
   12.78 here. Every length is divided back out by `--cx-tp-scale`, so the two
   pills land on one set of numbers whatever either scale becomes.
   `custom-apps-handoff.css`' `.cx-ho__tab` carries the same three; MOVE THEM
   TOGETHER, and re-measure on the built page rather than nudging one. The
   leading is the app's own 20 / 14 ratio said unitless, so it follows. */
.cx-tp__tab {
  display: inline-flex;
  align-items: center;
  gap: calc(5.25 * var(--cx-tp-px));
  height: calc(33 * var(--cx-tp-px)); /* h-10 (40) until the shared spec */
  padding: 0 calc(10.5 * var(--cx-tp-px)); /* px-3 (12) until the shared spec */
  border-radius: 9999px;
  font-size: calc(13 * var(--cx-tp-px)); /* text-sm (14) until the shared spec */
  font-weight: 500;
  line-height: 1.4286; /* the app's 20 / 14 */
  white-space: nowrap;
  color: var(--cx-tp-text-muted);
}

/* The open tab: `bg-foreground/10 text-foreground`. */
.cx-tp__tab--on {
  background: color-mix(in srgb, var(--color-theme-fg) 10%, transparent);
  color: var(--cx-tp-text);
}

/* `SharedAppsGrid`: `flex flex-wrap gap-4`; the column under it is `pb-12`. */
.cx-tp__grid {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  padding-bottom: 48px;
}

/* `DocumentCard`: `relative flex size-36 shrink-0 flex-col overflow-hidden
   rounded-xl border border-border/50 bg-muted`. Two things are this card's
   own and both are named in PHONE_DEPARTURES.

   THE WIDTH: two columns filling the sheet rather than the app's fixed
   size-36, which leaves a 46px strip down the right of a 350px sheet.

   THE BORDER IS DRAWN AND `SharedAppsGrid` TURNS IT OFF. That grid passes
   `className="border-0"`, so the app really draws no edge on THIS surface —
   it gets away with it because there `bg-muted` sits on `bg-background` in a
   full-width viewport. Here the card sits on a 300px phone on a marketing
   plate, and the two grounds are one step apart in both themes (#f7f7f4 over
   #f2f1ed light, #2a2a2a over #242424 dark), so with no edge the body below
   the banner dissolved into the screen and only the tint read as a card.
   What is drawn is `DocumentCard`'s OWN hairline, not an invented one. */
.cx-tp__card {
  position: relative;
  display: flex;
  flex: none;
  flex-direction: column;
  width: calc(50% - 8px); /* two columns on the grid's own 16px gap */
  /* SOLVED, not the app's `size-36`: with the footer off, 144 leaves ~24 of
     dead ground under a two-line title. 121 is the content's own sum —
     44 banner + 16 mb + 10 pt + two 19.25 lines + 12 pb = 120.5 — rounded up,
     so a one-line title and a two-line one still land on one height, and the
     stated 123 carries both borders: `box-sizing: border-box` is on site-wide,
     so 121 with a border would clip the second line. Re-solve, do not nudge. */
  height: 123px;
  overflow: hidden;
  border: 1px solid var(--cx-tp-line-50);
  border-radius: 14px; /* rounded-xl = --radius + 4 */
  background: var(--cx-tp-muted);
}

/* "Banner: 144×44 with 16px bottom margin for icon overlap" — `relative mb-4
   h-11 w-full shrink-0 bg-hover`, bare because these apps carry no banner
   and no icon colour; a card carrying `data-hue` paints it the hue's muted
   tint, `getCardBannerPresentation`'s own answer for a coloured icon. */
.cx-tp__banner {
  position: relative;
  display: block;
  flex: none;
  width: 100%;
  height: 44px;
  margin-bottom: 16px;
  background: var(--cx-tp-hover);
}

/* `absolute left-4 top-full z-10 -translate-y-1/2 drop-shadow-sm`. */
.cx-tp__icon {
  position: absolute;
  z-index: 1;
  left: 16px;
  top: 100%;
  display: flex;
  transform: translateY(-50%);
  filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.15)); /* drop-shadow-sm */
}

/* `AppIcon` at `size-7`: `text-secondary-opaque` for an uncoloured glyph;
   a preset with a colour draws in `colorCss(color, "primary")`. */
.cx-tp__icon-i {
  display: block;
  width: 28px;
  height: 28px;
  color: var(--cx-tp-text-dim);
}

.cx-tp__card[data-hue='blue'] { --cx-tp-hue: var(--cx-tp-blue); --cx-tp-hue-muted: var(--cx-tp-blue-muted); }
.cx-tp__card[data-hue='green'] { --cx-tp-hue: var(--cx-tp-green); --cx-tp-hue-muted: var(--cx-tp-green-muted); }
.cx-tp__card[data-hue='orange'] { --cx-tp-hue: var(--cx-tp-orange); --cx-tp-hue-muted: var(--cx-tp-orange-muted); }
.cx-tp__card[data-hue='purple'] { --cx-tp-hue: var(--cx-tp-purple); --cx-tp-hue-muted: var(--cx-tp-purple-muted); }
.cx-tp__card[data-hue='yellow'] { --cx-tp-hue: var(--cx-tp-yellow); --cx-tp-hue-muted: var(--cx-tp-yellow-muted); }
.cx-tp__card[data-hue='red'] { --cx-tp-hue: var(--cx-tp-red); --cx-tp-hue-muted: var(--cx-tp-red-muted); }

.cx-tp__card[data-hue] .cx-tp__banner {
  background: var(--cx-tp-hue-muted);
}

.cx-tp__card[data-hue] .cx-tp__icon-i {
  color: var(--cx-tp-hue);
}

/* "Content: 144×84 with padding 10px 16px 12px" — `flex min-h-0 flex-1
   flex-col px-4 pb-3 pt-2.5`. */
.cx-tp__body {
  display: flex;
  flex: 1;
  min-height: 0;
  flex-direction: column;
  padding: 10px 16px 12px;
}

/* `line-clamp-2 text-sm font-medium leading-snug text-foreground`. */
.cx-tp__title {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.375;
  color: var(--cx-tp-text);
}

/*
 * /custom-apps — #share card 2, "Department handoffs". Hand-maintained; pairs
 * with tools/fragments/custom-apps-handoff.mjs, imported by /custom-apps alone.
 *
 * WHAT THIS IS: two page-tab pills over a table whose non-primary column is,
 * for most of a 14-second loop, the app's timeline canvas. The table half is
 * bid-activities.css FORKED under `cx-ho-` (the `Aa Name` header, the 36px
 * app pitch, the inset header rule, the badge tone); the timeline half is
 * manage-timeline.css forked the same way (the tick tier, the today line and
 * its pill, the bar, the strip, the two cell treatments). Per the fork rule a
 * change that belongs to two files is made twice, on purpose, and a test fails
 * if a `ba-`, `mt-` or any other sibling prefix appears here. The fragment's
 * header carries the argument; this is the drawing and the score.
 *
 * EVERY LENGTH IS THE APP'S OWN NUMBER, drawn 1:1 and fitted to the square
 * plate by `artCard`'s own `.ig-scaler` — `--ig-w` / `--ig-fit` are solved in
 * the fragment (`HANDOFF_FIT`), which is how card 1 on this row is fitted too.
 * Nothing in here is scaled by hand, and the one thing that divides the scale
 * back out is the pointer, which is on the page rather than inside the app.
 *
 * DRAWN BARE ON THE PLATE, as bid-activities' table and manage-timeline's
 * canvas both are: rows on a canvas take the plate's own token
 * (media-plate.mjs), so there is no surface box and the secondary ink is
 * flattened against the plate rather than against a page ground.
 *
 * THE SCORE IS AT THE FOOT OF THE FILE and follows automate-mockup.css' rules,
 * which are load-bearing: every state is ADDED by the animation and never
 * removed, so THE REST STATE IS THE TABLE with Estimating open and the pointer
 * on it — what `prefers-reduced-motion` shows through one `animation: none`
 * block, and what the 100% keyframe holds so `pnpm run verify`'s collapsed
 * frame is the still. The row stagger is a NEGATIVE delay measured back from
 * the last row.
 */

.cx-ho {
  /* bid-activities.css' token set, the timeline's two beside it. */
  --cx-ho-line: var(--color-theme-border-02);
  --cx-ho-text: var(--color-theme-product-text);
  /* `--muted-foreground` is the foreground at an alpha, and a lucide line
     glyph is several paths sharing one stroke — flattened against the PLATE
     this table lies on, which is what `text-secondary-opaque` is for. */
  --cx-ho-text-sec: color-mix(in srgb, var(--color-theme-fg) 60%, var(--color-theme-card-03-hex));
  /* `--popover`, the bar's fill: a step lighter than the plate in light, so a
     bar reads as lifted off the canvas; restated in dark. */
  --cx-ho-bar: var(--color-theme-bg);
  /* `--red`, the today line's ink in light, from manage-timeline.css — the
     app's own oklch converted there and copied per the fork rule. */
  --cx-ho-red: #e1584d;
  /* `bg-foreground/10`, the open tab's fill. */
  --cx-ho-on: color-mix(in srgb, var(--color-theme-fg) 10%, transparent);
  /* `--hover`, the app's own wash — the foreground at 6 %, which is what an
     INACTIVE tab takes under the pointer (`hover:!bg-hover`). It needs no dark
     restatement: it reads `--color-theme-fg`, which inverts on its own, and
     the app's dark `--hover` is white at 5.5 %, i.e. the same step. */
  --cx-ho-hover: color-mix(in srgb, var(--color-theme-fg) 6%, transparent);

  /* The cycle. There is no stagger: the switch is a step, so every row and
     the canvas' own overlays trade on one clock — see THE SCORE below. */
  --cx-ho-cycle: 14s;

  /* THE TYPE IS STATED IN RENDERED PIXELS AND DIVIDED BACK OUT BY THE SCALE,
     AND IT IS THE ONE DEPARTURE IN THIS CARD — every other length here is the
     app's own number. As of 2026-09-10 this site draws every graphic at
     13 / 11 rendered (`/bid` #capabilities' `--bk-type-lg`, #context's
     `--bx-fit`, and both rows one section up on this page), and this card drew
     the app's `text-sm` at 11.67, its `text-xs` at 10.0 and its strip at 9.17
     — three sizes, none of them the row's, which is what the row was reported
     for. The other two cards in this row came to the ladder by SCALE (card 1's
     panel narrowed 380 -> 323, card 3's phone viewport 390 -> 320); this one
     cannot, and the arithmetic is why: the drawn width is 340 at the owner's
     direction, so a 13/14 scale wants a 366px block, and the block is
     `NAME_W` 156 + three whole 84px week buckets = 408. The Name column is
     already one pixel over DRILL SHAFT (36 IN)'s own ink and the two number
     columns are already at their heading's floor, so there is no 42px to give
     back and a 2.5-week canvas would cut a bucket's figure in half.
     `--cx-ho-fit` is the drawn scale at the widths this row is read at
     (391.66 / `HANDOFF_FIT.fit` 470); re-solve both steps against it rather
     than typing a size. WHAT IT COSTS is one more truncated name: at 15.6 the
     five ink 151.5 / 263 / 133.7 / 182.7 / 131.5 against the column's 140, so
     DRILL SHAFT (36 IN) joins the two that already ellipsise, which is the
     app's own `truncate` on a title cell. */
  --cx-ho-fit: 0.83332;
  --cx-ho-type-lg: calc(13px / var(--cx-ho-fit)); /* 15.60 — the app's text-sm */
  --cx-ho-type-sm: calc(11px / var(--cx-ho-fit)); /* 13.20 — its text-xs / [11px] */
  /* One RENDERED pixel, for the tab pill — see `.cx-ho__tab`. */
  --cx-ho-px: calc(1px / var(--cx-ho-fit));

  position: relative;
  display: block;
  width: 100%;
  box-sizing: border-box;
  color: var(--cx-ho-text);
  font-size: var(--cx-ho-type-lg); /* text-sm, at the row's rendered 13 */
  line-height: 1.4286;
}

.cx-ho * {
  box-sizing: border-box;
}

/* A `<span>` is inline; every box here is one, so the default is undone once.
   `:where()` contributes no specificity, so this cannot outrank the
   single-class layout rules under it. */
.cx-ho :where(span) {
  display: block;
}

.cx-ho svg {
  display: block;
  flex: 0 0 auto;
}

/* -------------------------------------------------------------------------
   The page tabs — `PageTabBar`: `flex items-center gap-[2px]`. The strip's
   `-ml-1.5` cancels a pill's `px-3` against a title's `px-1.5` in the app;
   here the line under it is the table's `p-2`, so -4 lands the first pill's
   label on the Name cells' own text line.
   ------------------------------------------------------------------------- */

.cx-ho__tabs {
  display: flex;
  align-items: center;
  gap: 2px;
  margin: 0 0 16px -4px; /* TABS_GAP below */
}

/* `getTabTriggerClass`: `inline-flex items-center justify-center
   whitespace-nowrap px-3 gap-[6px] h-9 rounded-full !text-sm !font-medium`,
   inactive `!bg-transparent !text-muted-foreground`. Both pills carry the
   fill and the ink as animatable properties; the rule says which is open at
   REST and the score trades them.

   THE PILL'S BOX IS STATED IN RENDERED PIXELS AND IS SHARED WITH CARD 3'S
   (2026-09-10, at the owner's direction). This card and the phone one both
   draw a tab strip and each drew its own component's app numbers under its
   own scale — `PageTabBar`'s `h-9 px-3` at 0.83332 here against `GalleryTabs`'
   `h-10 px-3` at 0.9125 there — so on the built page the two pills rendered
   30.0 x pad 10.0 beside 36.5 x pad 10.95, i.e. one row carrying two pill
   sizes, which is what it was reported as. The shared spec is the MIDPOINT of
   the two, which is what was asked for: **33 tall, 10.5 of side pad, 5.25 of
   gap**, with the label on the row's own 13 (`--cx-ho-type-lg`; the phone's
   14 rendered 12.78, so that end barely moved). Every length is divided back
   out by the card's own scale — the idiom `manage-change-order.css`'
   `--mc-ask-px` and `bid-capabilities.css`' `--bk-type-lg` already use — so
   the two pills land on one set of numbers whatever either scale becomes.
   `custom-apps-phone.css`' `.cx-tp__tab` carries the same three; MOVE THEM
   TOGETHER, and re-measure on the built page rather than nudging one. */
.cx-ho__tab {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  gap: calc(5.25 * var(--cx-ho-px));
  height: calc(33 * var(--cx-ho-px)); /* h-9 (36) until the shared spec */
  padding: 0 calc(10.5 * var(--cx-ho-px)); /* px-3 (12) until the shared spec */
  border-radius: 9999px;
  background-color: transparent;
  font-size: var(--cx-ho-type-lg); /* text-sm */
  line-height: 1.4286;
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
  color: var(--cx-ho-text-sec);
}

/* The open tab is a FILL and not an underline:
   `data-[state=active]:!bg-foreground/10 data-[state=active]:!text-foreground`. */
.cx-ho__tab--on {
  background-color: var(--cx-ho-on);
  color: var(--cx-ho-text);
}

.cx-ho__ti {
  width: 16px; /* size-4 */
  height: 16px;
}

/* -------------------------------------------------------------------------
   The table: the header row and five rows, each a Name cell beside the cell
   that switches. The grid rules and the today line are two overlays on the
   canvas' own column, so they can run down every row.
   ------------------------------------------------------------------------- */

.cx-ho__table {
  position: relative;
  width: 100%;
}

.cx-ho__row {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: stretch;
  height: 36px; /* VIEW_TABLE_ROW_HEIGHT; TABLE_HEADER_HEIGHT is the same */
}

/* The header's rule is an inset shadow rather than a border, as the app draws
   it, so the row keeps its exact height. */
.cx-ho__head {
  box-shadow: inset 0 -1px 0 0 var(--cx-ho-line);
}

/* THE NAME COLUMN'S RIGHT EDGE IS THE NAME CELL'S OWN, AS OF 2026-09-10, and
   that is what stops a bar crossing it. It was an absolutely positioned
   `.cx-ho__divide` overlay at `left: NAME_W` — the same x the canvas cell
   starts at — so the rule and the bar's clip began on one pixel and the bar's
   fill (`--popover`, LIGHTER than the plate) painted a sliver on the Name
   side of the line: measured at 1 device px at 3x, and reported. A pinned
   column's boundary is a border on the PANE in the app (`sortable-header.tsx`
   gives a head a right edge only where a column is pinned; a timeline pane
   carries `border-r` down its whole side), so it belongs to the Name cell —
   and drawn there it occupies the column's LAST pixel, with the canvas
   beginning after it, which is a seam a bar cannot reach.

   IT IS AN INSET SHADOW RATHER THAN A BORDER because `box-sizing` is
   `border-box` and these cells carry a stated width: a real border would take
   a pixel off the 140 of ink the Name column has, and the header row already
   draws its own rule this way. It is on BOTH `th` and `td`, so it runs through
   the header exactly as the overlay did, and it carries no animation — the
   Name column is the one thing the two states share, so a boundary that came
   and went would say the opposite. */
.cx-ho__th,
.cx-ho__td {
  position: relative;
  display: flex;
  flex: none;
  align-items: center;
  gap: 8px; /* gap-2 */
  min-width: 0;
  padding: 0 8px; /* p-2 */
  overflow: hidden;
  white-space: nowrap;
  box-shadow: inset -1px 0 0 0 var(--cx-ho-line);
}

/* `font-medium text-muted-foreground`, at the body's own `text-sm`. */
.cx-ho__th {
  color: var(--cx-ho-text-sec);
  font-weight: var(--font-weight-medium);
}

.cx-ho__row:not(.cx-ho__head) .cx-ho__td,
.cx-ho__row:not(.cx-ho__head) .cx-ho__cell {
  border-bottom: 1px solid var(--cx-ho-line);
}

.cx-ho__ell {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* `text-muted-foreground-opaque`, the same colour as the label; `TitleIcon`
   is a filled glyph. */
.cx-ho__mark {
  width: 18px; /* size-4.5 */
  height: 18px;
  fill: currentColor;
  color: var(--cx-ho-text-sec);
}

/* The switching cell: the canvas' column, clipping what a bar runs past. Its
   two children are stacked on it, one for each state. */
.cx-ho__cell {
  position: relative;
  flex: none;
  overflow: hidden;
}

/* The table half — the two columns beside Name, stacked on the same cell the
   canvas fills. It is the layer that fades, so it carries no padding of its
   own: each sub-cell is the app's own `p-2`. Present at rest. */
.cx-ho__act {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: stretch;
  overflow: hidden;
  white-space: nowrap;
}

/* A cell in either of those columns — `flex min-w-0 shrink-0 items-center
   overflow-hidden px-2`, the same box `.cx-ho__td` is. */
.cx-ho__sub {
  display: flex;
  flex: none;
  align-items: center;
  gap: 8px; /* gap-2 */
  min-width: 0;
  padding: 0 8px; /* p-2 */
  overflow: hidden;
  white-space: nowrap;
}

/* The head's own ink, which `.cx-ho__th` gives the Name column. A head is NOT
   right-aligned even over a number column — `sortable-header.tsx` sets no
   alignment at all, and only the cell under it does. */
.cx-ho__head .cx-ho__sub {
  color: var(--cx-ho-text-sec);
  font-weight: var(--font-weight-medium);
}

/* A number cell: `flex w-full items-center justify-end` (`columns.tsx`), so
   the figures line up on the column's right edge under a left-aligned head.
   Both columns beside Name are number columns — a quantity and a percent. */
.cx-ho__sub--num {
  justify-content: flex-end;
}

/* THE COLUMN RULE, which is the view's own `show_vertical_lines: true`. In the
   app it is scoped to CELLS — `div[role="table"][data-vertical-lines]
   div[role="cell"]` — so it rules the body and never the head, a
   `columnheader` taking a right edge only as a pinned column's boundary. It is
   a border on the cell rather than an overlay, so it fades with the table half
   and the timeline's own week grid replaces it; and the LAST column carries
   none, its right edge being the block's own where in the app the
   flex-growing add-field column follows it. */
.cx-ho__row:not(.cx-ho__head) .cx-ho__sub:not(:last-child) {
  border-right: 1px solid var(--cx-ho-line);
}

/* The value itself, on tabular figures: a quantity with no unit (Unit is its
   own column in the app and is outside this crop) or a percent, which the
   field's own `decimal_places` prints to one place. */
.cx-ho__num {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* -------------------------------------------------------------------------
   The timeline half — every rule manage-timeline.css' own, forked. Hidden at
   rest (`opacity: 0`); the score shows it.
   ------------------------------------------------------------------------- */

.cx-ho__tl {
  opacity: 0;
}

/* The tick tier, drawn IN the header row: `text-xs text-muted-foreground`,
   one per week at the `quarter` zoom, labelled with its Sunday's day of the
   month. */
.cx-ho__ticks {
  position: absolute;
  inset: 0;
}

.cx-ho__tick {
  position: absolute;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--cx-ho-text-sec);
  font-size: var(--cx-ho-type-sm); /* text-xs, at the row's rendered 11 */
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* The today pill: `size-[22px] rounded-full bg-red text-[11px] font-medium
   text-white`, centred on the line. */
.cx-ho__pill {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: translateX(-50%);
}

.cx-ho__pill > span {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 9999px;
  background: var(--cx-ho-red);
  color: #fff; /* text-white, in both themes */
  font-size: var(--cx-ho-type-sm); /* the today pill */
  font-weight: var(--font-weight-medium);
  line-height: 1;
}

/* The week grid: one rule per bucket edge INSIDE the crop, never at either
   cut, running down the rows and not the header — under the bars, as the
   app's grid is. */
.cx-ho__grid {
  position: absolute;
  bottom: 0;
  z-index: 0;
  pointer-events: none;
}

.cx-ho__gx {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--cx-ho-line);
}

/* The today line, `bg-red`, 1px — drawn OVER the bars (manage-timeline.css'
   own call; the app's z-10 under z-15 turns it into a column of dashes). It
   starts under the header, where the pill marks it, as the app's `.mt-rows`
   line does; the fragment states its `top`. */
.cx-ho__today {
  position: absolute;
  bottom: 0;
  z-index: 3;
  width: 1px;
  background: var(--cx-ho-red);
  pointer-events: none;
}

/* A row's plot: the bar at the app's own `(ROW_HEIGHT - BAR_HEIGHT) / 2`, cut
   by the cell where it runs past the crop. */
.cx-ho__plot {
  position: absolute;
  inset: 0;
}

/* `rounded-lg border border-border/60 bg-popover shadow-sm overflow-hidden`. */
.cx-ho__bar {
  position: absolute;
  z-index: 2;
  height: 34px;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--cx-ho-line) 60%, transparent);
  border-radius: 8px;
  background: var(--cx-ho-bar);
  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); /* shadow-sm */
}

/* The strip: `text-[11px] leading-none tabular-nums`, one cell per bucket at
   the bar's own offsets. */
.cx-ho__strip {
  position: relative;
  width: 100%;
  height: 100%;
  font-size: var(--cx-ho-type-sm); /* text-[11px], at the row's rendered 11 */
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* A bucket: `flex items-center justify-center px-0.5`, and by default the
   ESTIMATE treatment — `italic text-foreground/50`. */
.cx-ho__pcell {
  position: absolute;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: 0 2px;
  font-style: italic;
  color: color-mix(in srgb, var(--cx-ho-text) 50%, transparent);
}

/* A period pin — an actual: `font-medium text-foreground/85`, upright. */
.cx-ho__pcell[data-manual] {
  font-style: normal;
  font-weight: var(--font-weight-medium);
  color: color-mix(in srgb, var(--cx-ho-text) 85%, transparent);
}

/* -------------------------------------------------------------------------
   The pointer — the product's own demo cursor, at a FIXED SIZE ON SCREEN.
   It is a child of the scaled block, so its travel is written in app px and
   lands on the pills wherever the plate puts them; its box divides
   `.ig-scaler`'s own transform back out, a pointer being on the page rather
   than inside the app. The stated `26px` is the fallback for an engine
   without length-over-length division, which is the scaler's own fallback
   case. The tip is at 76/512 of the box, hence the two 0.1484 corrections;
   the stops are the two pills' centres, measured on the built page.
   ------------------------------------------------------------------------- */

.cx-ho__cursor {
  --cx-ho-ptr: 26px;
  --cx-ho-ptr: calc(22px / min(1, 100cqw / var(--ig-fit, 470px)));
  /* The two stops, in app px: a little inside each pill's label, off the
     centres measured on the built page (Estimating 54 x 18, Project
     Management 205 x 18, the pills 116 and 183 wide). */
  --cx-ho-est: 62px 24px;
  --cx-ho-pm: 215px 24px;

  position: absolute;
  z-index: 4;
  left: calc(0px - var(--cx-ho-ptr) * 0.1484);
  top: calc(0px - var(--cx-ho-ptr) * 0.1484);
  width: var(--cx-ho-ptr);
  height: var(--cx-ho-ptr);
  translate: var(--cx-ho-est);
  transform-origin: 14.84% 14.84%;
  /* The asset's own `feDropShadow dx=7 dy=9 stdDeviation=7` in #4B5563 at
     22%, restated at the size it is drawn. */
  filter: drop-shadow(0 1px 1.5px rgb(75 85 99 / 0.22));
  pointer-events: none;
}

.cx-ho__cursor svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* The asset's own paint, as bid-context.css restates it: a black fill under
   two rings — `#C9CDD3` at 38 under `#FAFAFA` at 30, both user units on its
   512 viewBox. The same in both themes; a pointer is the OS's, not the page's. */
.cx-ho__cursor path {
  fill: #000;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: none;
}

.cx-ho__cursor path:first-child {
  stroke: #c9cdd3;
  stroke-width: 38;
}

.cx-ho__cursor path:last-child {
  stroke: #fafafa;
  stroke-width: 30;
}

/* -------------------------------------------------------------------------
   THE SCORE.

   One fourteen-second cycle:

       0 - 10 %   the table, Estimating open, the pointer resting on it
      10 - 18 %   the pointer travels to Project Management
      12.48 %     it crosses onto that pill, which takes the HOVER wash
      19 %        PRESS — the tabs trade their fill AND the content changes,
                  on the same hundredth of a percent: the table half goes and
                  the timeline half arrives in one step, never a crossfade
      19 - 77 %   the timeline holds (8.1s, the loop's longest phase)
      68 - 76 %   the pointer travels back to Estimating
      71.23 %     it crosses onto that pill, which takes the HOVER wash
      77 %        PRESS — everything trades back, the same way
      77 - 100 %  the table again, which with 0 - 10 % is 4.7s of table

   THE SWITCH IS INSTANT, at the owner's direction (2026-09-10). It crossfaded
   over four percentage points, sequenced so a markup never drew through a
   phased cell, with each row a beat behind the one above it; all of that is
   DELETED, the stagger's four custom properties included. A step needs no
   sequencing and cannot half-draw, so the two halves simply trade at `press`
   and `press + 0.01` — the idiom the tab fill already used, which is why the
   fill and the content now move together.

   THE 100% KEYFRAME IS THE REST STATE — the table, Estimating open — so the
   frame `pnpm run verify` collapses to is the still, and every property the
   score touches is at the value its own rule declares there. Move the
   percentages and `--cx-ho-cycle` together; the fragment restates the
   percentages as `HANDOFF_SCORE` and a test reads both.
   ------------------------------------------------------------------------- */

.cx-ho__cell .cx-ho__act {
  animation: cx-ho-act var(--cx-ho-cycle) linear infinite;
}

.cx-ho__cell .cx-ho__tl,
.cx-ho__grid,
.cx-ho__today {
  animation: cx-ho-tl var(--cx-ho-cycle) linear infinite;
}

.cx-ho__tab--est {
  animation: cx-ho-tab-est var(--cx-ho-cycle) linear infinite;
}

.cx-ho__tab--pm {
  animation: cx-ho-tab-pm var(--cx-ho-cycle) linear infinite;
}

.cx-ho__cursor {
  animation:
    cx-ho-travel var(--cx-ho-cycle) infinite,
    cx-ho-press var(--cx-ho-cycle) linear infinite;
}

@keyframes cx-ho-act {
  0%,
  19% {
    opacity: 1;
  }

  19.01%,
  77% {
    opacity: 0;
  }

  77.01%,
  100% {
    opacity: 1;
  }
}

@keyframes cx-ho-tl {
  0%,
  19% {
    opacity: 0;
  }

  19.01%,
  77% {
    opacity: 1;
  }

  77.01%,
  100% {
    opacity: 0;
  }
}

/* The open tab's fill and ink trade at each press — AND AN INACTIVE TAB TAKES
   THE HOVER WASH WHILE THE POINTER IS ON IT (2026-09-10, at the owner's
   direction: the pointer walked onto a pill and the pill did not answer).
   `getTabTriggerClass` is three states and this draws all three:

       inactive          `!bg-transparent !text-muted-foreground`
       inactive, hovered `hover:!bg-hover hover:!text-foreground`
       active            `!bg-foreground/10 !text-foreground`

   An ACTIVE tab's hover is the app's own no-op (`data-[state=active]:hover:
   !bg-foreground/10`), which is why only two windows exist here and why the
   rest state — the pointer sitting on the open Estimating pill — is unmoved.

   THE TWO WINDOWS ARE SOLVED AGAINST THE TRAVEL, NOT PICKED. The tip runs
   62 -> 215 app px on `cubic-bezier(0.4, 0, 0.2, 1)` over 10-18 % and back
   over 68-76 %, and the pills measure -4..120.14 and 122.14..320.11 on the
   built page — so the tip crosses onto Project Management at **12.48 %** and
   back onto Estimating at **71.23 %**, which is where each wash begins. It
   ends at that tab's own press, where the active fill takes over. The 1 % ramp
   is the app's `transition-colors` (140ms of a 14s cycle against its 150ms);
   the press itself stays a step, as the fill and the content already are.
   Re-solve both against the travel if a stop or a pill's width moves. */
@keyframes cx-ho-tab-est {
  0%,
  19% {
    background-color: var(--cx-ho-on);
    color: var(--cx-ho-text);
  }

  19.01%,
  71.23% {
    background-color: transparent;
    color: var(--cx-ho-text-sec);
  }

  72.23%,
  77% {
    background-color: var(--cx-ho-hover);
    color: var(--cx-ho-text);
  }

  77.01%,
  100% {
    background-color: var(--cx-ho-on);
    color: var(--cx-ho-text);
  }
}

@keyframes cx-ho-tab-pm {
  0%,
  12.48% {
    background-color: transparent;
    color: var(--cx-ho-text-sec);
  }

  13.48%,
  19% {
    background-color: var(--cx-ho-hover);
    color: var(--cx-ho-text);
  }

  19.01%,
  77% {
    background-color: var(--cx-ho-on);
    color: var(--cx-ho-text);
  }

  77.01%,
  100% {
    background-color: transparent;
    color: var(--cx-ho-text-sec);
  }
}

/* The travel eases out on the way and settles on the pill; the loop's linear
   clock is what places the two stops. */
@keyframes cx-ho-travel {
  0%,
  10% {
    translate: var(--cx-ho-est);
    animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  }

  18%,
  68% {
    translate: var(--cx-ho-pm);
    animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  }

  76%,
  100% {
    translate: var(--cx-ho-est);
  }
}

/* The press: a dip on the pill at each click, on `scale` so it cannot fight
   the travel for `translate`. */
@keyframes cx-ho-press {
  0%,
  18.5% {
    scale: 1;
  }

  19% {
    scale: 0.88;
  }

  20%,
  76.5% {
    scale: 1;
  }

  77% {
    scale: 0.88;
  }

  78%,
  100% {
    scale: 1;
  }
}

/* A reader who has asked for less motion gets the table as it rests —
   Estimating open, the pointer on it, no bar. One block, no second set of
   values, because nothing above changed a resting property. */
@media (prefers-reduced-motion: reduce) {
  .cx-ho__cell .cx-ho__act,
  .cx-ho__cell .cx-ho__tl,
  .cx-ho__grid,
  .cx-ho__today,
  .cx-ho__tab--est,
  .cx-ho__tab--pm,
  .cx-ho__cursor {
    animation: none;
  }
}

/* The dark half: the bar's `--popover` maps to card-04 (a step ABOVE the
   plate, as in light it is a step above it), and the today line takes the
   app's lighter red. */
:root[data-theme='dark'] .cx-ho {
  --cx-ho-bar: var(--color-theme-card-04-hex);
  --cx-ho-red: #ee685a;
}

