/* ============================================================
   clapyo Resend-lean skin, Phase 1: the frame.
   Loaded from the forked layouts/main.twig. Tokens live in
   snippets/css-variables.twig. This file owns the sidebar frame
   + the content-column width; page interiors ride core
   markup and get reskinned in Phase 2.
   ============================================================ */

/* ============================================================
   SHAPES DECLARED TWICE. Measured by scripts/audit-design-system.py.
   Six pairs below declare the SAME property set with different values, so none of
   them can be folded into one rule: every fold would repaint something. They are one
   component rendered twice, and they are listed here so the next person to touch any
   of them changes both, and so Phase 1 can give each a single name with a modifier.

     the round control    .pbtn / .clp-vr-play                      fill, size, border differ
     the sprite glyph     .pbtn svg.ic / .act svg.ic                --icon-md / 13px
     the search row       .pick-search / .clp-menu-search           52px / 40px tall
     the pill             .tchip / .t-actions .apill                28px / 30px tall
     the thumbnail        .clp-io-thumb / .clp-att                  76px / 50px
     the full-bleed hit   .clp-vr-play button / .clp-att-hit        inherit / --text-3
   ============================================================ */

/* ---- content column: the mock's .view (padding) + .wrap (centered 1080).
   Replaces core's left-aligned .container so mx-auto centers in the FULL
   content area (not within a left-aligned 1280 box). ---- */
@media (min-width: 1024px) {
  #content:not([data-layout="side"]) .clp-view { padding: 32px; }
  #content:not([data-layout="side"]) .clp-content { max-width: 1080px; }
}

/* ---- the frame's layout (layouts/main.twig): the phone header above the page, the scrolling
   page area (#content) with its column, and the admin's right-hand panel (#sidebar). In core's
   utilities layer, where the classes it replaces sat, so every plugin rule still wins over it.
   1024px is the frame's breakpoint, 768px the page's. ---- */
@layer utilities {
  .clp-frame { overflow: hidden; }
  .clp-mhead { position: sticky; top: 0; z-index: 30; display: block; background-color: var(--bg); }
  :root[data-mobile-menu] .clp-mhead { border-style: solid; border-width: 0; }
  .clp-mhead-col { margin-inline: auto; width: 100%; max-width: 896px; }
  #content { width: 100%; height: calc(100dvh - 64px); display: flex; flex-direction: column; }
  .clp-toolbar { position: relative; }
  .clp-view { flex-grow: 1; display: flex; flex-direction: column; }
  .clp-content { display: flex; flex-direction: column; flex-grow: 1; width: 100%; margin-inline: auto; }
  :root[data-mobile-menu] .clp-content { display: none; }
  .clp-main { display: flex; flex-direction: column; flex-grow: 1; }
  #sidebar { position: sticky; top: 0; z-index: 10; display: none; flex-direction: column; gap: 16px; flex-shrink: 0;
    width: 384px; height: 100vh; margin-inline-end: -384px; overflow-y: auto; background-color: var(--bg);
    border-inline-start-style: solid; border-inline-start-width: 1px; border-color: var(--border-strong);
    transition: all .15s cubic-bezier(.4, 0, 1, 1); }
  :root[data-mode="dark"] #sidebar { border-color: var(--color-line-dimmed); }
  :root[data-sidebar] #sidebar { margin-inline-end: 0; }
  /* the page scrolls and its main column spaces its parts, except a side column page from 1024px, whose parts do */
  @media (max-width: 767px) {
    #content { overflow-y: auto; }
    .clp-main { gap: 20px; padding-bottom: 20px; }
  }
  @media (min-width: 768px) and (max-width: 1023px) {
    #content { overflow-y: auto; }
    .clp-main { gap: 32px; padding-bottom: 32px; }
  }
  @media (min-width: 1024px) {
    #content:not([data-layout="side"]) { overflow-y: auto; }
    #content:not([data-layout="side"]) .clp-main { gap: 32px; padding-bottom: 32px; }
  }
  @media (min-width: 1024px) {
    .clp-frame { display: flex; align-items: flex-start; min-height: 100vh; }
    .clp-mhead { display: none; }
    #content { height: 100vh; }
    :root[data-mobile-menu] .clp-content { display: flex; }
    #sidebar { display: flex; }
  }
}

/* ---- sidebar shell ----
   TWO LEVELS. `aside.sidebar` owns the layout slot and nothing else; `.sb-in` is the
   painted column. The split is what lets the collapsed rail hold a 64px slot while the
   panel floats back to full width over the page on hover, so nothing on the page reflows
   when the pointer crosses the edge. */
aside.sidebar {
  width: 252px;
  flex: none;
  position: sticky;
  top: 0;
  height: 100vh;
  transition: width .16s ease;

  /* THE COLUMN'S ONE INSET. The four blocks that stack here each picked their own
     (brand 8, nav item 10, credits card 13, account row 8) so nothing in the column
     lined up. The nav item is the most-repeated row, so its 10px sets the grid and the
     other three ride this property. */
  --pad-row: 10px;
}
.sidebar .sb-in {
  width: 100%;
  height: 100%;
  background: var(--sunken);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  padding: 18px 13px;
  /* no overflow clip here: the bottom account dropdown opens upward and must not be clipped */
}

/* THE SIDEBAR IS A DESKTOP OBJECT. Below core's lg breakpoint (1024px) the frame is
   sections/mobile-nav.twig, which layouts/main.twig already renders under `lg:hidden`.
   Without this rule both frames render at once: the 252px column fills a phone screen
   top to bottom and pushes the page below the fold. That is what shipped from July until
   now, because there is no .sidebar rule anywhere in core's stylesheet to fall back on. */
@media (max-width: 1023px) {
  aside.sidebar { display: none; }
  /* The page shell halves its inset below the desktop breakpoint. At 32px a side a 390px
     phone was left with 326px of content, which is too narrow for a record row or a
     receipt. One rule, every page in the app. */
  .clp-view { padding: 16px; }
}

/* ---- THE SIDE COLUMN PAGE (a page's `page_layout` 'side', which #content carries as `data-layout`), 1024px and wider:
   the page fills the page area under the top bar with no content column. Its side column is on the left, `--side-col-w`
   wide (the page sets it), `sunken` with a `border` hairline on its right and the full height; its main part takes the
   rest with the content column's 32px sides. Each scrolls inside itself. ---- */
@media (min-width: 1024px) {
  #content[data-layout="side"] :is(.clp-view, .clp-content, .clp-main) { min-height: 0; }
  #content[data-layout="side"] .side-col { flex: none; width: var(--side-col-w); min-height: 0; display: flex; flex-direction: column;
    background: var(--sunken); border-right: 1px solid var(--border); }
  #content[data-layout="side"] .side-col-in { flex: 1; min-height: 0; overflow-y: auto; }
  #content[data-layout="side"] .side-main { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; padding: 0 32px 32px; }
  /* the pick bar centres over the main part (`--pick-x`, the pick bar's centre on the screen) */
  #content[data-layout="side"] .side-main { --pick-x: calc(50% + var(--side-col-w) / 2); }
}
@supports (animation-timeline: scroll()) and (mask-image: none) {
  @media (min-width: 1024px) {
    /* the column's scroll fade, at its foot only */
    #content[data-layout="side"] .side-col-in { mask-image: linear-gradient(to bottom, #000 calc(100% - var(--fade-end)), transparent);
      animation: fade-end linear both; animation-timeline: scroll(self); animation-range: calc(100% - 22px) 100%; }
  }
}

/* ---- the app's top bar (sections/app-bar.twig), 1024px and wider ----
   The page area below it is the screen less --bar-h; #content keeps scrolling on its own. */
.clp-bar { display: none; }
@media (min-width: 1024px) {
  .clp-bar {
    position: relative; z-index: 40; display: flex; align-items: stretch; gap: 4px;
    height: var(--bar-h); padding: 0 20px; box-sizing: border-box;
    border-bottom: 1px solid var(--border); background: var(--topbar-bg);
    backdrop-filter: blur(var(--overlay-blur)); -webkit-backdrop-filter: blur(var(--overlay-blur));
  }
  .clp-bar ~ .clp-frame { min-height: calc(100dvh - var(--bar-h)); }
  .clp-bar ~ .clp-frame #content,
  .clp-bar ~ .clp-frame #sidebar { height: calc(100dvh - var(--bar-h)); }
}
.clp-bar .brand { display: flex; align-items: center; gap: 10px; margin-right: 22px; }
.clp-bar .brand-mark { display: block; width: 23px; height: 23px; flex: none; }
.clp-bar .brand-mark img { display: block; width: 23px; height: 23px; }
.clp-bar .brand-name { display: block; flex: none; line-height: 0; }
.clp-bar .brand-name img { width: auto; height: 26px; }
.clp-bar .bar-nav { display: flex; align-items: stretch; gap: 2px; }
.clp-bar .bar-item { display: flex; align-items: center; }
.clp-bar .bar-link {
  height: 34px; display: flex; align-items: center; gap: 6px; padding: 0 11px; border-radius: var(--r);
  font-size: var(--fs-body); font-weight: 450; color: var(--text-2); white-space: nowrap; transition: background .14s, color .14s;
}
.clp-bar .bar-link .chev { font-size: var(--icon-xs); color: var(--text-4); }
.clp-bar .bar-item:hover .bar-link,
.clp-bar .bar-item:focus-within .bar-link { background: var(--hover); color: var(--text); }
/* the current page in the blue wash, the picked state */
.clp-bar .bar-link.on,
.clp-bar .bar-item:hover .bar-link.on { background: var(--accent-fill); color: var(--text); }
.clp-bar .bar-end { margin-left: auto; display: flex; align-items: center; gap: 9px; }
.clp-bar .bar-icon {
  width: 34px; height: 34px; border-radius: var(--r); display: grid; place-items: center;
  color: var(--text-3); font-size: var(--icon-md); transition: background .14s, color .14s;
}
.clp-bar .bar-icon:hover { background: var(--hover); color: var(--text); }
.clp-bar .bar-icon.on { background: var(--accent-fill); color: var(--accent-hi); }
.clp-bar mode-switcher { display: grid; place-items: center; }
/* DRAWN IN ONE COLOUR MODE: the wordmark's two pictures, and the moon and the sun of every light and
   dark switch. In core's utilities layer, where the classes they replace sat. */
@layer utilities {
  :root:not([data-mode="dark"]) .only-dark, :root[data-mode="dark"] .only-light { display: none; }
  :root[data-mode="dark"] .only-dark { display: block; }
  .brand-name img { display: block; }
}

/* the panels: Tools beside every model, opened on hover or keyboard focus. `minmax(0, 1fr)` makes the
   one row the panel's own capped height; an `auto` row grows to the model list's full height, so the
   list never scrolls and the panel cuts it off. */
.clp-bar .mp {
  position: absolute; top: calc(var(--bar-h) + 8px); left: 120px; z-index: 41;
  display: none; background: var(--card); border: 1px solid var(--border-strong); border-radius: var(--r-lg);
  box-shadow: var(--lift-dialog); max-height: calc(100dvh - var(--bar-h) - 24px); overflow: hidden;
  grid-template-rows: minmax(0, 1fr);
}
/* the 8px between the bar and the panel stays part of the hover */
.clp-bar .mp::before { content: ''; position: absolute; left: 0; right: 0; top: -9px; height: 9px; }
.clp-bar .bar-item.has-panel:hover .mp,
.clp-bar .bar-item.has-panel:focus-within .mp { display: grid; }
.clp-bar .mp.two { grid-template-columns: 250px minmax(0, 1fr); width: 900px; }
.clp-bar .bar-item:has(.mp.tools-only) { position: relative; }
.clp-bar .mp.tools-only { grid-template-columns: 250px; width: 252px; left: 0; top: calc(100% + 8px); }
@media (max-width: 1099px) { .clp-bar .mp.two { left: 16px; width: calc(100vw - 32px); } }
.clp-bar .mp .col { padding: 16px 10px 12px; min-height: 0; display: flex; flex-direction: column; }
.clp-bar .mp .col + .col { border-left: 1px solid var(--border); }
.clp-bar .mp .clp-label { flex: none; padding: 0 10px 10px; }
/* the models fill two columns top to bottom; the browser balances them, so no row count is passed */
.clp-bar .mp .models { min-height: 0; overflow: auto; padding-bottom: 4px; }
.clp-bar .mp .cols { columns: 2; column-gap: 4px; }
.clp-bar .mp .cols .clp-menu-row { break-inside: avoid; }
/* The scroll fade, at the top and the foot: the top bar's model list, the window's prompt and the floating bar's
   grown part (which scrolls only below 1024px; a box that does not scroll never fades). */
@supports (animation-timeline: scroll()) and (mask-image: none) {
  .clp-bar .mp .models,
  .clp-lb .lb-ptext,
  .clp-tool .side-col .bar-grow,
  .clp-tool .t-models .clp-menu-list { mask-image: linear-gradient(to bottom, transparent, #000 var(--fade-start), #000 calc(100% - var(--fade-end)), transparent);
    animation: fade-start linear both, fade-end linear both; animation-timeline: scroll(self), scroll(self); animation-range: 0px 22px, calc(100% - 22px) 100%; }
}

/* credits (snippets/clp/credits-button.twig), in the top bar and the phone header: the balance in mono with a mini bar, opening the balance block, Billing and Orders */
.bar-credits { position: relative; }
.bar-credits .cred-btn {
  height: 32px; display: flex; align-items: center; gap: 9px; padding: 0 11px; border-radius: var(--r);
  border: 1px solid var(--border-strong); background: var(--hover); color: var(--text-3); cursor: pointer;
}
.bar-credits .cred-btn:hover, .bar-credits .cred-btn.open { border-color: var(--border-struct); }
.bar-credits .cred-btn .n { font-family: var(--mono); font-size: var(--fs-sm); font-weight: 500; color: var(--text); font-variant-numeric: tabular-nums; }
/* the button is filled with `hover`, so its meter's track takes the deeper step */
.bar-credits .cred-btn .clp-meter { width: 34px; background: var(--deep); }
.bar-credits .cred-btn .chev { font-size: var(--icon-xs); color: var(--text-4); }
.bar-credits .cred-menu { position: absolute; right: 0; top: calc(100% + 8px); width: 248px; padding: 5px; }
.bar-credits .cred-menu .credits-mini { padding: 9px 9px 12px; margin: 0 0 5px; border-bottom: 1px solid var(--border); }

/* the account button and its menu, opening below it */
.clp-bar .bar-av { width: 34px; height: 34px; display: grid; place-items: center; border-radius: var(--r-pill); cursor: pointer; }
.clp-bar .bar-av .ident-av { width: 30px; height: 30px; }
.clp-bar .acct-menu { position: absolute; right: 0; top: calc(100% + 8px); left: auto; bottom: auto; }

/* brand */
.sidebar .brand { display: flex; align-items: center; gap: 10px; padding: 8px var(--pad-row) 20px; }
/* the brand lockup, drawn identically by both frames: the sidebar on desktop and the
   mobile header below 1024px. The mark (brand/mark.svg) at 23px and the wordmark
   (brand/wordmark.svg, wordmark-dark.svg in dark mode) at 26px tall, 10px apart, are the
   approved logo's own proportions; they stay two pieces so the collapsed rail can show the
   mark alone. */
.sidebar .brand-mark,
.clp-mnav .brand-mark { display: block; width: 23px; height: 23px; flex: none; }
.sidebar .brand-mark img,
.clp-mnav .brand-mark img { display: block; width: 23px; height: 23px; }
.sidebar .brand-name,
.clp-mnav .brand-name { display: block; flex: none; line-height: 0; }
.sidebar .brand-name img,
.clp-mnav .brand-name img { width: auto; height: 26px; }
.sidebar .brand mode-switcher { margin-left: auto; display: grid; place-items: center; }
.sidebar .theme-tog,
.clp-auth .theme-tog {
  width: 30px; height: 30px; border-radius: var(--r-sm); color: var(--text-3);
  display: grid; place-items: center; font-size: var(--icon-md);
  transition: background .14s, color .14s;
}
.sidebar .theme-tog:hover { background: var(--hover); color: var(--text); }

/* nav */
.sidebar .nav {
  display: flex; flex-direction: column; gap: 1px; margin-top: 2px;
  /* Scroll the nav when it outgrows the sidebar height (e.g. the long admin nav),
     restoring core's own mechanism: core's <nav> carries overflow-y-auto overflow-x-hidden.
     Shared across app + admin (this is the only nav container). The account block sits
     OUTSIDE .nav, so its upward-opening dropdown is never clipped. */
  flex: 0 1 auto; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
}
/* THE GROUPS ARE CORE'S OWN SECTIONS, surfaced. Core registers Home and Library under
   `app.primary` and the generation surfaces under `app.apps`; navigation.twig renders one
   group per section and skips any section whose rows are all hidden, so a separator never
   draws over nothing. The last group closes itself, because a rule under the final row
   would fence the nav off from the credits block below it. */
/* Written once for both frames: navigation.twig is the single renderer, so the desktop
   column and the phone drawer draw the same groups. */
.sidebar .navgroup,
.clp-mnav .navgroup { padding-bottom: 9px; margin-bottom: 9px; border-bottom: 1px solid var(--border); }
.sidebar .navgroup:last-child,
.clp-mnav .navgroup:last-child { border-bottom: 0; padding-bottom: 0; margin-bottom: 0; }
/* the section name, at the quietest register in the app: only the group that needs
   naming carries one, since a label on every group is noise rather than structure.

   THE VOICE MENU IS THE THIRD CONSUMER, so the TYPE is shared by extending the selector rather
   than by writing a second rule somewhere else.

   THE INSET CANNOT BE SHARED, AND THAT IS WHY THIS IS TWO RULES. The inset reads
   `padding: 0 var(--pad-row)` and that token is declared on `aside.sidebar`. Inside a menu the
   token does not resolve, so the whole padding declaration would be dropped and the label would
   sit flush against the panel edge. Each consumer states its own inset, which is the rule
   `.clp-drop` already states for its colours: what is shared is shared, what belongs to the
   consumer sits on the consumer. */
.sidebar .grouplab,
.clp-mnav .grouplab,
.clp-tool .clp-menu .grouplab {
  font-size: var(--label-size); font-weight: 600; letter-spacing: .11em; text-transform: uppercase;
  color: var(--text-4); margin: 4px 0 6px; white-space: nowrap;
}
.sidebar .grouplab,
.clp-mnav .grouplab { padding: 0 var(--pad-row); }
/* THE VOICE MENU'S OWN INSET, and it is measured rather than borrowed. Its rows begin 11px in:
   `.clp-menu-list` pads 5px and `.clp-tool .clp-vmenu .clp-menu-row` adds 6px of its own. The
   label takes that same edge, the way the sidebar's label takes the nav item's.

   NOT THE EDGE OF THE VOICE NAME, which is 54px in, because a voice row leads with a 30px play
   button and the name sits after it. A label lines up with the row it names, not with one span
   inside it. */
.clp-tool .clp-vmenu .grouplab { padding: 0 6px; }
/* the second group and every one after it needs air above it that the first does not */
.clp-tool .clp-menu .grouplab + .clp-menu-row { margin-top: 0; }
.clp-tool .clp-menu .clp-menu-row + .grouplab { margin-top: 10px; }
.sidebar .nav-item {
  display: flex; align-items: center; gap: 12px; padding: 8px var(--pad-row);
  border-radius: var(--r); color: var(--text-2); font-size: var(--fs-body); font-weight: 450;
  white-space: nowrap; transition: background .14s, color .14s;
}
.sidebar .nav-item:hover { background: var(--hover); color: var(--text); }
/* THE CURRENT ROW is an accent fill rather than a rail. With the groups
   doing the work of orientation, a rail outside the column was a second signal saying the
   same thing, and it was the one mark in the frame that lived outside its own row. */
.sidebar .nav-item.active,
.clp-mnav .nav-item.active { background: var(--accent-fill); color: var(--text); }
.sidebar .nav-item i, .sidebar .nav-item svg, .sidebar .nav-item img {
  color: var(--text-3); font-size: var(--icon-md); width: var(--icon-md); height: var(--icon-md); flex: none;
}
.sidebar .nav-item.active i,
.clp-mnav .nav-item.active i { color: var(--accent-hi); }
.sidebar .nav-item:hover i { color: var(--text); }
.sidebar .nav-item span,
.clp-mnav .nav-item span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.sidebar .spacer { flex: 1; }

/* credits-mini: the balance block, at the top of the credits button's menu
   (snippets/clp/credits-button.twig). */
.credits-mini .row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 9px; }
.credits-mini .n {
  font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w); font-size: var(--fig-sm); line-height: 1;
  letter-spacing: var(--display-ls); color: var(--text);
}
.credits-mini .lbl { font-size: var(--fs-xs); color: var(--text-3); }
.credits-mini .foot { margin-top: 9px; font-size: var(--fs-xs); color: var(--text-3); }

/* THE ACCOUNT TRIGGER, the row that opens the account menu. It carries a name in the
   forked account-menu.twig instead of being addressed as `.sidebar .relative > button`,
   which was the one place in this file that styled an element by where it sat rather than
   by what it is. It rides the column inset, so its three dots get the same room every
   other trailing glyph in the app gets, and its two lines take .nm and .em, the same two
   rules the menu panel above it draws its own identity row with. */
.sidebar .acct-row {
  display: flex; align-items: center; gap: 11px; width: 100%;
  padding: 8px var(--pad-row);
  border-radius: var(--r); text-align: start; color: var(--text-2);
  transition: background .14s, color .14s;
}
.sidebar .acct-row:hover { background: var(--hover); color: var(--text); }
/* while the panel is open the row stays lit, so the trigger and the menu read as one
   object. The panel is the trigger's preceding sibling inside core's .relative wrapper. */
.sidebar .menu[data-open] ~ .acct-row { background: var(--hover); color: var(--text); }
.sidebar .acct-row .who { min-width: 0; flex: 1; }
.sidebar .acct-row .dots { margin-inline-start: auto; font-size: var(--icon-md); flex: none; color: var(--text-4); transition: color .14s; }
.sidebar .acct-row:hover .dots,
.sidebar .menu[data-open] ~ .acct-row .dots { color: var(--text-2); }

/* THE GRAB HANDLE. Core's own control, forked from sections/aside.twig with its click
   handler and its localStorage key untouched, restyled from core's 6px slab to the
   app's hairline register. It sits outside the column's edge, vertically centred. */
.sidebar .rail-tog {
  position: absolute; top: 50%; left: 100%; margin-left: 8px; transform: translateY(-50%);
  width: 5px; height: 44px; border-radius: var(--r-pill); background: var(--border-strong);
  border: 0; padding: 0; cursor: pointer; z-index: 4;
  transition: background .14s, transform .14s;
}
.sidebar .rail-tog:hover { background: var(--text-4); transform: translateY(-50%) scaleX(1.4); }

/* THE HIT AREA, and the reason the collapsed rail could not be reopened.

   A 5px hairline is not a click target on its own, so the button carries a transparent pad.
   The pad belongs to the button, which is the whole point: hovering it counts as hovering
   the button, and therefore as hovering the column, so the floating panel stays open for
   the whole trip to the click.

   Sized to clear the page. The collapsed slot is 64px and .clp-view insets the page a
   further 32px, so nothing interactive sits before 96px. The resting pad reaches 83px. */
.sidebar .rail-tog::before {
  content: '';
  position: absolute;
  inset: -12px -6px -12px -6px;
}
/* The handle is anchored to .sidebar, the layout slot, and `left: 100%` is therefore the
   slot's right edge. When the collapsed rail is hovered the slot STAYS 64px while .sb-in
   floats out to 252px, so the handle stayed at 72px and sat a third of the way inside the
   expanded panel. It has to follow the panel, not the slot. 252px is the open column's
   width, restated here for the same reason the hover block restates every other open
   value: this rule describes where the panel's edge is. */
html[data-collapsed] .sidebar:hover .rail-tog { left: 252px; }
/* THE 8px THAT BROKE IT. Following the handle out to 252px leaves its own `margin-left: 8px`
   between the panel's edge and the button, and that strip belonged to neither: not to .sb-in,
   which stops at 252, not to the button, which starts at 260, and not to the 64px slot. A
   pointer crossing it hovered nothing, so the column un-hovered, the panel collapsed and the
   handle snapped back to 72px, out from under the cursor. The rail could never be reopened by
   pointer at all. The pad reaches back across that margin and the path is continuous.

   It stops at 248px, 9px clear of the panel's own 13px inset, so it covers no nav row. */
html[data-collapsed] .sidebar:hover .rail-tog::before { left: -12px; }

/* ============================================================
   THE RAIL. `data-collapsed` on <html> is core's own state, written by the handle above
   and remembered in localStorage by layouts/base.twig. Core used it to slide the whole
   column off screen; clapyo narrows it to 64px instead, so the nav is always reachable.

   Every label steps out rather than being clipped, which is why the column needs no
   overflow rule and the account dropdown stays unclipped in both states.
   ============================================================ */
html[data-collapsed] .sidebar { width: 64px; }
html[data-collapsed] .sidebar .sb-in { padding: 18px 8px; }
html[data-collapsed] .sidebar .brand { padding: 8px 0 20px; justify-content: center; }
html[data-collapsed] .sidebar .brand-name,
html[data-collapsed] .sidebar .brand mode-switcher,
html[data-collapsed] .sidebar .grouplab,
html[data-collapsed] .sidebar .nav-item span,
html[data-collapsed] .sidebar .acct-row .who,
html[data-collapsed] .sidebar .acct-row .dots { display: none; }
html[data-collapsed] .sidebar .nav,
html[data-collapsed] .sidebar .navgroup { align-items: center; }
html[data-collapsed] .sidebar .navgroup { display: flex; flex-direction: column; }
html[data-collapsed] .sidebar .nav-item { width: 40px; justify-content: center; padding: 11px 0; gap: 0; }
html[data-collapsed] .sidebar .acct-row { padding: 8px 0; justify-content: center; gap: 0; }

/* ON HOVER the column returns to full width OVER the page instead of pushing it. The 64px
   slot stays in the layout and the panel floats on the app's menu shadow, so no page
   reflows when the pointer crosses the edge. Every rule here restates the open column's
   own value rather than inventing a second set. */
/* The column is `position: sticky`, which makes it its own stacking context, so a z-index on
   the panel inside it cannot lift the panel above anything outside the column: a page's
   sticky part is a later sibling and paints over it on document order alone. The lift has
   to sit on the column itself, and it belongs at .menu's level, because an expanded rail is
   a floating panel over the page exactly as a menu is. Scoped to the hover, so the column
   claims no layer while it is simply sitting in the layout. */
html[data-collapsed] .sidebar:hover { z-index: 20; }
html[data-collapsed] .sidebar:hover .sb-in {
  position: absolute; top: 0; left: 0; width: 252px; height: 100%;
  padding: 18px 13px; border-right: 1px solid var(--border-strong); box-shadow: var(--lift-menu);
}
html[data-collapsed] .sidebar:hover .brand { padding: 8px var(--pad-row) 20px; justify-content: flex-start; }
html[data-collapsed] .sidebar:hover .brand-name,
html[data-collapsed] .sidebar:hover .grouplab,
html[data-collapsed] .sidebar:hover .nav-item span,
html[data-collapsed] .sidebar:hover .acct-row .who,
html[data-collapsed] .sidebar:hover .acct-row .dots { display: block; }
html[data-collapsed] .sidebar:hover .brand mode-switcher { display: grid; }
html[data-collapsed] .sidebar:hover .nav,
html[data-collapsed] .sidebar:hover .navgroup { align-items: stretch; }
html[data-collapsed] .sidebar:hover .nav-item { width: auto; justify-content: flex-start; padding: 8px var(--pad-row); gap: 12px; }
html[data-collapsed] .sidebar:hover .acct-row { padding: 8px var(--pad-row); justify-content: flex-start; gap: 11px; }

/* ============================================================
   THE MOBILE FRAME. sections/mobile-nav.twig, forked from core and reskinned to the
   desktop column's vocabulary: the same brand lockup, the same nav row at touch size,
   and the account panel drawn as .menu, which is the object the desktop dropdown already
   is. Core's three-state machine on <html data-mobile-menu> (closed / nav / account) and
   every @click that drives it are untouched; only the classes it toggles changed.

   This block sits with the desktop frame because it IS the frame below 1024px. Where it
   restates a .menu rule it names the panel as well as the row, so the override does not
   depend on which of the two blocks the file happens to declare first.
   ============================================================ */
.clp-mnav { background: var(--sunken); --pad-row: 10px; }
.clp-mnav .head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  /* 64px, because the shell sizes the content column at calc(100dvh - 4rem) and the two
     have to agree or the page ends short of the viewport */
  min-height: 64px; padding: 0 14px;
}
.clp-mnav .brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
.clp-mnav .acts { display: flex; align-items: center; gap: 4px; }
/* the header's controls take .theme-tog's shape, the square icon button the sidebar
   already ships, one size up for a thumb */
.clp-mnav .mnav-btn,
.menu .mnav-btn {
  width: 34px; height: 34px; border-radius: var(--r-sm); color: var(--text-3);
  display: grid; place-items: center; font-size: var(--icon-lg); flex: none;
  transition: background .14s, color .14s;
}
.menu .mnav-btn:hover,
.clp-mnav .mnav-btn:hover { background: var(--hover); color: var(--text); }
/* the open panel lights its own trigger. data-mobile-menu lives on <html>. */
html[data-mobile-menu="nav"] .clp-mnav .nav-t,
html[data-mobile-menu="account"] .clp-mnav .acct-t { background: var(--hover); color: var(--text); }
.clp-mnav > hr { border: 0; border-top: 1px solid var(--border); margin: 0; }

/* the nav panel: the sidebar's rows, sized for a thumb. No active rail here, because the
   drawer has no column edge for it to sit against. */
.clp-mnav .nav-panel { padding: 8px; }
.clp-mnav .nav { display: flex; flex-direction: column; gap: 2px; }
.clp-mnav .nav-item {
  display: flex; align-items: center; gap: 12px; padding: 10px;
  border-radius: var(--r); color: var(--text-2); font-size: var(--fs-md); font-weight: 450;
  transition: background .14s, color .14s;
}
.clp-mnav .nav-item:hover { background: var(--hover); color: var(--text); }
.clp-mnav .nav-item i, .clp-mnav .nav-item svg, .clp-mnav .nav-item img {
  color: var(--text-3); font-size: var(--icon-lg); width: var(--icon-lg); height: var(--icon-lg); flex: none;
}

/* the app's phone menu (snippets/clp/phone-menu.twig): Image, Video and Audio open in place
   to the top bar's Tools and Models */
.clp-mnav .pm-sec > summary { list-style: none; cursor: pointer; }
.clp-mnav .pm-sec > summary::-webkit-details-marker { display: none; }
.clp-mnav .pm-sec > summary .chev { margin-left: auto; font-size: var(--icon); width: 16px; height: 16px; color: var(--text-4); transition: transform .14s; }
.clp-mnav .pm-sec[open] > summary .chev { transform: rotate(180deg); }
.clp-mnav .pm-body { margin: 2px 0 8px 20px; padding-left: 20px; border-left: 1px solid var(--border); display: flex; flex-direction: column; gap: 2px; }
.clp-mnav .pm-body .clp-label { padding: 10px 10px 4px; }
/* the phone's rows at touch size */
.clp-mnav .clp-menu-row.nav .nm { font-size: var(--fs-md); }
.clp-mnav .bar-credits { margin-right: 4px; }
/* the narrowest phones: the credits button keeps its balance and caret and drops its mini bar, so the logo fits whole */
@media (max-width: 374px) { .clp-mnav .bar-credits .cred-btn .clp-meter { display: none; } }

/* THE ACCOUNT PANEL IS .menu, drawn in place instead of floating. Core's .menu is an
   absolutely-positioned dropdown (position:absolute, top:100%, inset-inline-end:0,
   overflow:auto), so the first half of this rule un-floats it and the second takes off the
   floating panel's surface, border, radius and lift. Its rows are untouched: they are the
   same .menu-item the desktop dropdown draws, one size up for a thumb. Its display is the
   phone menu's open state, below. */
.clp-mnav .acct-panel {
  position: static; top: auto; inset-inline-end: auto; margin-top: 0; z-index: auto;
  overflow: visible;
  border: 0; border-radius: 0; box-shadow: none; background: none;
  padding: 8px; gap: 0;
}
.clp-mnav .acct-panel .menu-item { padding: 11px 10px; font-size: var(--fs-md); }
/* the phone menu's states, read from <html data-mobile-menu>: nav opens the menu and turns its
   button into Close, account opens the account panel and rings the avatar. In core's utilities
   layer, where the classes they replace sat. */
@layer utilities {
  .clp-mnav .nav-t .ti-x, .clp-mnav .nav-panel, .clp-mnav .acct-panel { display: none; }
  .clp-mnav .nav-t .ti-menu-deep { display: block; }
  .clp-mnav .acct-panel { flex-direction: column; }
  :root[data-mobile-menu="nav"] .clp-mnav .nav-t .ti-menu-deep { display: none; }
  :root[data-mobile-menu="nav"] .clp-mnav .nav-t .ti-x { display: inline; }
  :root[data-mobile-menu="nav"] .clp-mnav .nav-panel { display: block; }
  :root[data-mobile-menu="account"] .clp-mnav .acct-panel { display: flex; }
  :root[data-mobile-menu="account"] .clp-mnav .acct-t .ident-av { outline-style: solid; outline-width: 1px;
    outline-color: var(--border-strong); outline-offset: 2px; }
}
.clp-mnav .acct-panel .menu-item i { font-size: var(--icon-lg); }
.clp-mnav .acct-panel .menu-foot { padding: 11px 10px 4px; }
/* an identity row with a control beside it: the row takes the width, the control keeps
   its own hit area outside the link */
.menu .who { min-width: 0; }
.menu .ident { display: flex; align-items: center; gap: 4px; }
.menu .ident .menu-item { flex: 1; min-width: 0; }

/* ============================================================
   Phase 2: interior components. Overrides core's shared
   component classes (index.css loads BEFORE this file, so equal-
   specificity rules here win) to the Resend-lean shape: hairline
   borders, no drop shadows, mock radii + proportions, mono
   numerals. Core's color semantics stay (accent = signal, not
   blanket decoration); per-surface display sizing is a later phase.
   ============================================================ */

/* ---- buttons: proportion + radius, keep neutral/accent semantics ---- */
.button { height: 36px; padding: 0 14px; border-radius: var(--r); font-size: var(--fs-body); font-weight: 500; gap: 7px; }
.button i { font-size: var(--icon-md); }
.button-sm, .button-xs { height: 32px; padding: 0 10px; border-radius: var(--r-sm); font-size: var(--fs-sm); }
.button-xs { height: 26px; padding: 0 8px; }
/* a button holding one icon and no label, as wide as it is tall */
.button-square { width: 36px; padding: 0; justify-content: center; }
.button-sm.button-square { width: 32px; }
.button-xs.button-square { width: 26px; }
.button-outline { border-color: var(--border-strong); }
.button-failure { background-color: var(--red); outline-color: var(--red); color: var(--on-fill); }
/* a payment provider's own button: its colours come from the provider, passed as `--brand-bg` and `--brand-fg` */
.button-brand { background-color: var(--brand-bg); outline-color: var(--brand-bg); color: var(--brand-fg); }
/* the blue and grey buttons sit in core's layer, where its own rules for them sat, so a disabled one still turns grey */
@layer components {
  .button-accent { background-color: var(--color-button-accent); color: var(--color-button-accent-content); outline-color: var(--color-button-accent); }
  .button-dimmed { background-color: var(--color-button-dimmed); color: var(--color-button-dimmed-content); outline-color: var(--color-button-dimmed); }
}
@layer utilities {
  .button-full { width: 100%; }
}
.button-outline:hover, .button-outline:focus:not(:focus-visible) { box-shadow: none; background: var(--hover); border-color: var(--border-struct); }

/* ---- inputs / selects / textareas ---- */
.input {
  height: 40px; border-radius: var(--r); border-color: var(--border-control);
  padding: 0 13px; font-size: var(--fs-md); transition: border-color .14s, box-shadow .14s;
}
textarea.input { min-height: 60px; padding: 10px 13px; line-height: 1.55; }
.input-sm, .input-xs { height: 32px; border-radius: var(--r-sm); }
/* ---- THE SELECT'S GLYPH. Core's is the Tailwind forms arrow: a fixed blue-grey that
   does not move between modes, at stroke 1.5, sitting 8px from the edge while the field's
   own text stops at 32px. This one is the app's chevron, on --text-4, at the 1.6 every
   svg.ic in this file draws, ending where .input's padding says the content ends.
   A data URI cannot resolve a token, so the dark value is the one place in this file that
   restates a colour; both are --text-4's exact value in their own mode. ---- */
select.input { padding-inline-end: 34px; background-repeat: no-repeat;
  background-position: right 13px center; background-size: 16px 16px;
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='%23686C71' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.6' d='M6 9l6 6 6-6'/%3e%3c/svg%3e"); }
:root[data-mode="dark"] select.input {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3e%3cpath stroke='%237B7F81' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.6' d='M6 9l6 6 6-6'/%3e%3c/svg%3e"); }

/* ============================================================
   FOCUS, one treatment for every field in the app
   ============================================================
   Core styles no focus state at all, and our own rule used to be written
   against .input, so every field WITHOUT that class fell through to the browser's
   default ring: the template palette search, the model picker, voice search, the
   Imagine and voiceover prompts, negative prompt, the result titles.

   THE DEFAULT RULE DOES NOT LIVE HERE. It sits with the token in
   snippets/css-variables.twig, because that snippet is included from core's
   base.twig and therefore reaches every page core draws, whichever layout wraps it.

   What lives here is the shell layer: a field inside a bordered control (a
   searchbar, a composer, a menu search) hands the ring to that control and gives up
   its own, so the ring is drawn exactly once around the thing that looks like the
   control. Those elements exist only on skinned app pages, so this is their home.

   Every ring reads --focus-ring. No literal focus box-shadow exists in this file or
   in the token snippet, so the treatment moves in one edit and cannot drift apart.
   ------------------------------------------------------------ */

/* Shells: the visible control takes the ring. A field whose own border outranks the default rule
   is listed with them. */
#content .searchbar:focus-within,
#content .palette .pick-search:focus-within,
.pb-shell:has(textarea:focus),
.clp-menu-search:focus-within,
.cr-search:focus-within,
.cr-rate:focus-within,
.clp-bill .co .coup:focus-within,
.clp-bill .stripe-el:focus-within,
.clp-tool .clp-io-in:focus,
.clp-tool .clp-range-val:focus {
  border-color: var(--accent); box-shadow: var(--focus-ring);
}

/* Under the pointer a field's edge firms, unless it holds the blue focus edge or a live search. */
.input:hover:not(:focus-within),
#content .searchbar:hover:not(:focus-within, .is-live),
.pb-shell:hover:not(:has(textarea:focus)),
.cr-search:hover:not(:focus-within),
.cr-rate:hover:not(:focus-within),
.clp-bill .co .coup:hover:not(:focus-within),
.clp-bill .stripe-el:hover:not(:focus-within),
.clp-tool .clp-io-in:hover:not(:focus) {
  border-color: var(--border-control-hover);
}

/* and their inner field gives it up, so the ring never doubles. */
#content .searchbar:focus-within input,
#content .palette .pick-search:focus-within input,
.pb-shell textarea:focus,
.clp-menu-search:focus-within input,
.cr-search:focus-within input,
.cr-rate:focus-within input {
  outline: none; box-shadow: none; border-color: transparent;
}

/* An item's borderless editable name (`snippets/clp/item-title.twig`): no shell to hand the ring to, so it takes it
   directly, with a radius so the ring reads as a halo rather than a box. */
textarea.r-title:focus { box-shadow: var(--focus-ring); border-radius: var(--r-sm); }

/* A slider: its own thumb is invisible, so the range takes focus and the chosen mark or the knob wears the ring. */
.clp-tool .clp-io-rail:has(.clp-range:focus-visible) :is(.clp-tk.now, .clp-io-knob) { box-shadow: var(--focus-ring); }

/* ---- boxes / cards ---- */
.box { border-radius: var(--r-lg); border-color: var(--border-strong); background: var(--card); padding: 18px; }
a.box:hover, button.box:hover, .box.cursor-pointer:hover { border-color: var(--border-struct); background: var(--raised); }

/* ---- editorial button: the small pair that closes a form or a dialog.
   App-scoped rather than page-scoped because two surfaces share it: the chat
   edit form and the confirm dialog. `primary` is the affirmative action,
   `danger` the destructive one; the two are the same shape, filled with the
   token that matches the consequence. ---- */
.ebtn { display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: 30px; padding: 0 13px; border: 1px solid var(--border-strong); border-radius: var(--r);
  background: var(--card); font-size: var(--fs-sm); color: var(--text-2); cursor: pointer; transition: background .14s, color .14s, border-color .14s; }
/* core owns the spinner's visibility inside a button: `button .spinner` is display:none until
   `button[processing]` flips it. Both rules sit in core's cascade layer and this rule does not, so
   a display declared here would override both and the spinner would stop following [processing].
   Only the flex behaviour is ours. */
.ebtn .spinner { flex: none; }
.ebtn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-fill); }
.ebtn.primary:hover,
#content .t-actions .apill.is-go:hover { background: var(--accent); border-color: var(--accent); color: var(--on-fill); outline: 1px solid var(--accent); outline-offset: 2px; }
.ebtn.danger { background: var(--red); border-color: var(--red); color: var(--on-fill); }
.ebtn.danger:hover { background: var(--red); border-color: var(--red); color: var(--on-fill); outline: 1px solid var(--red); outline-offset: 2px; }
.ebtn[disabled] { opacity: .5; cursor: default; }

/* ---- badges -> Resend-lean pills ----
   The state fills carry literal fallbacks. The tokens live in the plugin's
   css-variables.twig, which Twig compiles into a cache that can lag a deploy, so a
   stylesheet that lands before the token file would otherwise paint these transparent. */
.badge {
  height: 25px; border-radius: var(--r-pill); border-color: var(--border-strong);
  background: transparent; color: var(--text-2); padding: 0 10px; font-size: var(--fs-xs);
}
.badge:hover { box-shadow: none; }
.badge-success { background: var(--success-fill); color: var(--success-hi); border-color: var(--success-bd); }
.badge-accent { background: var(--accent-fill); color: var(--accent-hi); border-color: var(--accent-bd); }
.badge-failure { background: var(--red-fill, rgba(223,22,59,.10)); color: var(--red-hi); border-color: var(--red-bd, rgba(223,22,59,.30)); }
.badge-alert   { background: var(--amber-fill, rgba(255,197,61,.10)); color: var(--amber-hi); border-color: var(--amber-bd, rgba(255,197,61,.30)); }
/* A BADGE HUGS ITS WORD. Core's `.badge` ends `display: flex` inside `@layer components`, so without
   this a badge in a block parent stretches across its whole column, as it does in the order and
   receipt heads.
   THE DISPLAY IS DECLARED INSIDE CORE'S OWN LAYER, AND THAT PLACEMENT IS THE FIX. A rule outside
   every layer outranks every layered rule whatever its specificity, core's utilities included, so
   it would show the badges core hides with `hidden md:flex` on its workspace page and with
   `hidden ... group-hover:flex` in the workspace switcher on every app page. Inside `components`
   it comes after core's own badge rule, which it overrides, and before `utilities`, which
   still win. */
@layer components {
  .badge { display: inline-flex; vertical-align: top; }
}

/* SKELETON LINE: a pulsing stand-in for text that is still loading, sized by `--w` and `--h`, which the rule for the
   place it stands in sets; a 16 pixel line as wide as its column unless told otherwise. */
@layer components {
  .clp-skel { width: var(--w, auto); height: var(--h, 16px); border-radius: var(--r); background-color: var(--color-line-dimmed);
    animation: clp-skel-pulse 2s cubic-bezier(.4, 0, .6, 1) infinite; }
}
@keyframes clp-skel-pulse { 50% { opacity: .5; } }
/* the writing placeholder's last line is two thirds wide */
.clp-wb .wb-skel .clp-skel:last-child { --w: 66.6667%; }

/* ---- THE EMPTY LINE, every surface that can be empty without being a page: the
   dashboard's recent list, the imagine / video / voiceover galleries, the template
   palette, the voice menu, each credits group and chat's history drawer. One centred
   quiet line, so a surface with nothing in it says so the same way everywhere.
   A whole PAGE that is empty is the other case and has no block of its own: it speaks
   through its page opening, so the line lives on `.greet-sub` and the action on
   `.open-act`. This quiet line never draws an icon; that action may.
   `tall` exists for the tool gallery, which sits alone under a section head with 40px
   of air above it; every other consumer sits inside a list or a panel. ---- */
/* the whole row when its list is a grid, so the line never squeezes into one cell */
.clp-empty { grid-column: 1 / -1; padding: 20px 14px; text-align: center; font-size: var(--fs-body); color: var(--text-4); }
.clp-empty.tall { padding: 40px 0 8px; }

/* ---- THE SCROLL FADE, every box whose content scrolls inside it: the prompt box and the top bar's
   model list. Content fades out over 22px at an edge only while more lies past it: `--fade-start`
   grows over the first 22px of scrolling and `--fade-end` shrinks over the last 22px, so content at
   rest is never faded. Each consumer draws its own mask from these two lengths, fenced by
   `@supports (animation-timeline: scroll())`: a browser without scroll timelines would run the
   animations to their ends and fade the box at rest. A mask reads only opacity, so its black is not
   a colour of the design. ---- */
/* ---- THE DIVIDER inside a row of controls: a 1 by 18 pixel line in the strong border, between groups that act on
   different things (the selection bar's Select all and its actions, the media window's arrows and Close). ---- */
.clp-vsep { flex: none; width: 1px; height: 18px; background: var(--border-strong); }

/* ---- THE LABEL, the design's Label type: the small mono eyebrow over a group or a card. Each
   consumer adds only its own spacing. ---- */
/* every mono eyebrow is this one rule: a group's name, a column or a step's number and state, a code block's language */
.clp-label,
.clp-tool .clp-io-title,
.clp-tempbar .k,
.clp-wb :is(x-markdown, .clp-rv) pre .actions .lang,
.clp-wb .clp-shead .s-n,
.clp-wb .clp-shead .s-state { font-family: var(--mono); font-size: var(--label-size); letter-spacing: var(--label-ls); text-transform: uppercase; color: var(--text-4); }

/* ---- LABEL AND VALUE: every list of named values, a label on the left and its value on the right (checkout's
   and the order card's lines, the media window's facts). A number is in the mono face; a long value takes its own
   line under its label. ---- */
.clp-kv { display: flex; flex-direction: column; gap: 8px; margin: 0; font-size: var(--fs-body); color: var(--text-2); }
.clp-kv .kv { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.clp-kv :is(dt, dd) { margin: 0; }
.clp-kv .v { min-width: 0; text-align: right; color: var(--text); }
.clp-kv .v.is-num { font-family: var(--mono); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.clp-kv .q { font-size: var(--fs-sm); color: var(--text-4); }
.clp-kv .kv.is-long { flex-direction: column; align-items: stretch; gap: 8px; }
.clp-kv .kv.is-long .v { text-align: left; }

/* ---- LINE BREAKS: running text never leaves one word alone on its last line, a heading breaks into balanced
   lines, and a text box keeps the browser's own wrapping, so what is being typed never reflows. ---- */
body { text-wrap-style: pretty; }
:is(h1, h2, h3, .greet) { text-wrap-style: balance; }
:is(input, textarea, [contenteditable], .autogrow-textarea) { text-wrap-style: auto; }

/* ---- THE METER, every bar that shows how much is left or how far along: a 4px track with the blue
   fill. The template passes only the amount, as `--fill`; a consumer sets the width when it is not
   the whole line. `.is-wait` holds no fill and runs the page loader's sweep instead. ---- */
.clp-meter { display: block; height: 4px; border-radius: var(--r-pill); background: var(--raised); overflow: hidden; }
.clp-meter > i { display: block; width: var(--fill); height: 100%; border-radius: var(--r-pill); background: var(--accent); }
.clp-meter.is-wait { position: relative; }
/* THE KNOB: the one round handle a person drags, on the moving bar and the options slider; each places it. */
.clp-knob { width: 12px; height: 12px; border-radius: var(--r-pill); background: var(--accent); box-shadow: var(--ring-knob); }
/* THE MOVING BAR (`snippets/clp/seek-bar.twig`): the meter while a line plays, pressed or dragged to jump. Its box is
   6px with the 4px line drawn inside, so growing to 6px with the 12px handle, under the pointer, on keyboard focus and
   while dragging, moves nothing around it. It sits over the row's link, like the play control. */
.clp-meter.clp-seek { position: relative; z-index: 2; height: 6px; overflow: visible; border-block: 1px solid transparent;
  background-clip: padding-box; cursor: pointer; touch-action: pan-y; user-select: none; }
.clp-seek:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.clp-seek::before { content: ''; position: absolute; inset: -5px 0; }
.clp-seek > b { display: none; position: absolute; left: var(--fill); top: 50%; transform: translate(-50%, -50%); }
.clp-seek:is(:hover, :focus-visible, [data-drag]) { border-block-width: 0; }
.clp-seek:is(:hover, :focus-visible, [data-drag]) > b { display: block; }
/* a touch screen has no pointer to bring the handle up, so it shows while the line plays, the press area is 24px, and
   the hover a tap leaves behind does not grow the bar; a drag does */
@media (hover: none) {
  .clp-seek > b { display: block; }
  .clp-seek:hover:not([data-drag]) { border-block-width: 1px; }
  .clp-seek::before { inset: -9px 0; }
}

/* ---- THE BRAND SURFACE, Clapyo Blue under one page's hero: the billing balance, the plans page's featured
   card. It re-points the text, line and card tokens, so its own fill and edge and every word, tick, mark,
   hairline and meter track inside follow; its badge and its one button are white with the colour's light-mode word. ---- */
.clp-brand { --text: var(--on-fill); --body: var(--on-fill); --text-2: var(--on-fill); --text-3: var(--on-fill);
  --text-4: var(--on-fill); --accent-hi: var(--on-fill); --border: var(--on-fill-line); --raised: var(--on-fill-line);
  --card: var(--accent); --border-strong: var(--accent);
  background: var(--accent); border-color: var(--accent); color: var(--on-fill); }
.clp-brand .clp-meter > i { background: var(--on-fill); }
.clp-brand .badge { background: var(--on-fill); border-color: var(--on-fill); color: var(--brand-word-blue); }
.clp-brand .badge-success { color: var(--brand-word-green); }
.clp-brand .button { background: var(--on-fill); border-color: var(--on-fill); outline-color: var(--on-fill); color: var(--brand-word-blue); }

/* ---- THE MAKER MARK, a model maker's logo at 16 pixels in its row's colour; the template passes the file as `--m`. ---- */
.clp-mark { display: block; width: 16px; height: 16px; flex: none; background: currentColor; mask: var(--m) center / contain no-repeat; }

/* ---- WHAT CREDITS MAKE (snippets/clp/plan-buys.twig): a mark, the count in the mono face, the model and what it
   makes; `.is-row` sets the rows side by side on a wide screen. ---- */
.clp-buys { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.clp-buys li { display: grid; grid-template-columns: 16px 40px 1fr; align-items: center; gap: 6px; font-size: var(--fs-sm); color: var(--text-2); }
.clp-buys .clp-mark { color: var(--text-3); }
.clp-buys b { font-family: var(--mono); font-variant-numeric: tabular-nums; font-weight: 500; color: var(--text); }
@media (min-width: 901px) {
  .clp-buys.is-row { flex-direction: row; gap: 24px; }
  .clp-buys.is-row li { grid-template-columns: 16px auto auto; }
}

/* ---- THE COMPARE TABLE, one card holding a table: the row's name on the left, the columns' values
   right-aligned in the mono face, a tick in the blue text step and a cross in the quietest grey. ---- */
.clp-ctable { border: 1px solid var(--border-strong); border-radius: var(--r-lg); background: var(--card); overflow-x: auto; }
.clp-ctable table { width: 100%; border-collapse: collapse; }
.clp-ctable :is(th, td) { padding: 10px 18px; border-top: 1px solid var(--border); text-align: end; }
.clp-ctable thead th { border-top: 0; vertical-align: bottom; padding-block: 18px 16px; }
.clp-ctable thead th:first-child, .clp-ctable tbody th { text-align: start; }
.clp-ctable tbody th { font-size: var(--fs-body); font-weight: 400; color: var(--text-2); }
.clp-ctable td { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: var(--fs-sm); color: var(--text); }
.clp-ctable td svg.ic { display: inline-block; vertical-align: middle; color: var(--text-4); }
.clp-ctable td.on svg.ic { color: var(--accent-hi); }
@media (max-width: 640px) { .clp-ctable table { min-width: 600px; } }

/* ---- THE SPLIT CARD, one card whose direct children are its parts with a hairline between each, in a column or,
   with `.is-row`, side by side above 900 pixels. The consumer sets the parts' padding as `--split-pad`. Alpine's
   <template> elements are skipped, so a list drawn by x-for takes no hairline over its first row. ---- */
.clp-split { display: flex; flex-direction: column; border: 1px solid var(--border-strong); border-radius: var(--r-lg); background: var(--card); }
.clp-split > :not(template) { padding: var(--split-pad, 18px); }
.clp-split > :not(template) ~ :not(template) { border-top: 1px solid var(--border); }
@media (min-width: 901px) {
  .clp-split.is-row { flex-direction: row; }
  .clp-split.is-row > :not(template) { flex: 1 1 0; min-width: 0; }
  .clp-split.is-row > :not(template) ~ :not(template) { border-top: 0; border-inline-start: 1px solid var(--border); }
}

@property --fade-start { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --fade-end { syntax: '<length>'; inherits: false; initial-value: 0px; }
@keyframes fade-start { to { --fade-start: 22px; } }
@keyframes fade-end { from { --fade-end: 22px; } to { --fade-end: 0px; } }

/* ---- audio player, every surface that plays one ----
   Shared because snippets/audio.twig is shared: chat plays speech replies and
   dictation, voiceover plays its previews, and the two off tools would play
   theirs. The shell is .clp-recording's, the live dictation bar in the composer,
   so recording and playback are the same object in two states. ---- */
.clp-audio { display: flex; align-items: center; gap: 6px; padding: 6px 8px;
  border: 1px solid var(--border-strong); border-radius: var(--r-xl); background: var(--card); }
.clp-audio .clp-audio-w { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; }
.clp-audio a.pbtn:hover, .clp-audio button.pbtn:hover { color: var(--text); background: var(--hover); }
.clp-audio .pbtn svg.ic { width: var(--icon-md); height: var(--icon-md); }
.clp-audio .mono,
#content .vcard .vp-t { font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-3); width: 38px; flex: none; text-align: center; }
/* core's <x-wave> paints its own canvas into the [wave] element; it only needs the box */
.clp-audio .wave { flex: 1; min-width: 0; height: 28px; }
.clp-audio .conv { flex: 1; min-width: 0; height: 28px; display: flex; align-items: center; justify-content: center; position: relative; }
.clp-audio .conv .rule { position: absolute; left: 0; right: 0; height: 1px; background: var(--border); }
.clp-audio .conv .t { position: relative; padding: 0 10px; background: var(--card); font-family: var(--mono);
  font-size: var(--label-size); letter-spacing: .04em; color: var(--text-4); }
.clp-audio.is-loading { opacity: .6; }

/* ---- confirm dialog, every page that deletes something ----
   chat, imagine, video, writer and voiceover all render the same question, so
   they all render this. The panel is .clp-menu's surface with the anchoring
   dropped, the tinted square is the .err state treatment, and the buttons are
   the shared .ebtn pair. ---- */
.modal.clp-modal { padding: 0; background: var(--card); border: 1px solid var(--border-strong); border-bottom-width: 0;
  box-shadow: var(--lift-dialog); overflow: hidden; text-align: left;
  border-radius: var(--r-xl) var(--r-xl) 0 0; }
/* core draws a grab handle on its bottom sheet; nothing in the app drags one */
.modal.clp-modal::before { display: none; }
/* core anchors the panel as a bottom sheet under 48rem and a centred card above it,
   in that exact media query. The surface follows the same fork and nothing else: the sheet
   sits on the screen's bottom edge with no border there, the card has all four. */
@media (width >= 48rem) {
  .modal.clp-modal { max-width: 404px; border-radius: var(--r-lg); border-bottom-width: 1px; }
  .modal.clp-modal.wide { max-width: 512px; }
}
/* THE PHONE SHEET · `snippets/clp/phone-sheet.twig`: a 44 pixel head over a hairline, then 44 pixel rows */
.modal.clp-modal.clp-sheet { padding: 6px 10px 24px; }
.clp-sheet .sht-head { height: 44px; line-height: 44px; margin: 0 -10px 6px; padding: 0 20px; border-bottom: 1px solid var(--border);
  font-size: var(--fs-md); font-weight: 600; letter-spacing: -.01em; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.clp-sheet .sht-rows { max-height: 62vh; overflow-y: auto; }
.clp-sheet .sht-row { width: 100%; min-height: 44px; display: flex; align-items: center; gap: 12px; padding: 0 10px;
  border: 0; border-radius: var(--r); background: none; font: inherit; font-size: var(--fs-md); color: var(--text);
  text-align: start; cursor: pointer; }
.clp-sheet .sht-row svg.ic { color: var(--text-3); }
.clp-sheet .sht-row:hover { background: var(--hover); }
.clp-sheet .sht-row.is-danger, .clp-sheet .sht-row.is-danger svg.ic { color: var(--red-hi); }
/* core's copy control around a row is no box of its own */
.clp-sheet x-copy { display: contents; }
.clp-modal .m-body { padding: 20px; }
.clp-modal .m-icon { width: 36px; height: 36px; border-radius: var(--r); display: grid; place-items: center;
  border: 1px solid var(--red-bd); background: var(--red-fill); color: var(--red-hi); }
.clp-modal .m-icon svg.ic { width: var(--icon-md); height: var(--icon-md); }
.clp-modal .m-t { margin-top: 14px; font-family: var(--font); font-size: var(--fs-md); font-weight: 600;
  letter-spacing: -.01em; color: var(--text); }
.clp-modal .m-d { margin-top: 5px; font-size: var(--fs-body); line-height: 1.6; color: var(--text-3); }
.clp-modal .m-acts { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
/* THE ONE DIALOG THAT ASKS FOR A VALUE RATHER THAN FOR A YES. Every other caller runs title, line,
   actions with nothing between them, so a field has no room of its own until it is given some. The
   title, or the line under a panel head, drops its offset when it comes first in the body. */
/* ---- THE VOICE PASS DIALOG. The shell, the chip and the wall are the app's own; what is added
   here is the rhythm of a list the app did not have before, and nothing else.

   THE MENU OPENS UPWARD, exactly as the chat composer's does and for a harder reason: a dialog
   clips its own overflow, so a menu dropped below the chip would be cut off by the card's edge.

   THE LIST SCROLLS AGAINST THE VIEWPORT, not against a pixel guess: six scenes at tile height run
   past the bottom of a laptop screen, and the title, the line and the actions take the rest. The
   command palette caps itself the same way. */
.clp-vmodal .clp-vm-scenes { margin-top: 14px; max-height: 46vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 14px; }
.clp-vmodal .clp-vm-h { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.clp-vmodal .clp-vm-n { flex: none; font-family: var(--mono); font-size: var(--label-size); letter-spacing: var(--label-ls);
  text-transform: uppercase; color: var(--text-4); }
.clp-vmodal .clp-vm-m { overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: var(--fs-sm); color: var(--text-3); }
.clp-vmodal .clp-vm-acts { position: relative; align-items: center; }
/* A scene the press skips says Voiced and fades. */
.clp-vmodal .clp-vm-s { margin-left: auto; flex: none; display: inline-flex; align-items: center; gap: 5px; font-family: var(--mono); font-size: var(--label-size); letter-spacing: var(--label-ls); text-transform: uppercase; color: var(--text-4); }
.clp-vmodal .clp-vm-s svg.ic { width: var(--icon-xs); height: var(--icon-xs); fill: var(--success-hi); }
.clp-vmodal .clp-vm-scene.is-done .gal { opacity: .55; }
.clp-vmodal .clp-vm-acts .clp-msel { margin-right: auto; }
.clp-modal .m-body > .input { margin-top: 14px; }
.clp-modal .m-d + .fld { margin-top: 14px; }
.clp-modal .m-body > :is(.m-t, .m-d):first-child { margin-top: 0; }
/* the dialog header reuses the history drawer's header, the app's only panel head */
.clp-modal .drawer-head .t, .clp-drawer .drawer-head .t { font-size: var(--fs-body); font-weight: 500; color: var(--text); }
.clp-modal .pbtn:hover { color: var(--text); background: var(--hover); }
.clp-modal .pbtn svg.ic { width: var(--icon-md); height: var(--icon-md); }

/* ---- THE CLONE DIALOG, the first dialog that is a form rather than a question. Every piece is an
   existing component reaching a second page, or the one control the app did not have, drawn as the
   round Hatim chose on 2026-09-12. ---- */

/* ONE FIELD FROM THE NEXT, AND THE TWO-COLUMN ROW. Inside the row the second field is still an adjacent
   sibling, so the field-to-field offset would push it 14px below the first; the row's own `gap` is
   what separates those two. Folded to one column, the row keeps the same 14px between them. */
.clp-modal .fld + .fld { margin-top: 14px; }
.clp-modal .fld-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 14px; }
.clp-modal .fld-2 > .fld + .fld { margin-top: 0; }
.clp-modal .fld-2 + .fld { margin-top: 14px; }
@media (max-width: 560px) { .clp-modal .fld-2 { grid-template-columns: 1fr; row-gap: 14px; } }

/* CHOOSE A FILE OR RECORD ONE, on one line. Core put the record button inside a 180px drop box behind
   an OR rule; the app's file control is a 38px row, so the two are siblings. */
.clp-pick { display: flex; align-items: center; gap: 8px; }
.clp-pick .clp-drop { flex: 1; min-width: 0; }
.clp-pick .ebtn { flex: none; height: 38px; }

/* THE LIVE RECORDING ROW is `.clp-recording`'s shell holding the level bars the chat composer draws,
   so recording in a dialog and dictation in the composer are one object. Core sizes each bar from 20
   to 32px in both places; the row stays at the round's 26px and the tallest bars sit in the shell's
   own padding. THE BARS SPAN THE ROW, Hatim's pick on 2026-09-29: the dialog gives core's recorder one
   bar for every 6px of the row's width and they spread across it, so the row fills at any width. */
.clp-rec-live { display: flex; align-items: center; gap: 10px; }
.clp-rec-live .bars { display: flex; align-items: center; justify-content: space-between; gap: 3px; height: 26px; flex: 1; }
/* The composer gives the shell a bottom margin to clear its bar; inside a field the hint does that. */
.clp-modal .clp-recording { margin: 0; }

/* THE CONSENT BLOCK opens a new part of the dialog rather than continuing the form, so it takes more
   air than one field takes from the next. */
.clp-modal .fld + .clp-chk { margin-top: 18px; }

/* THE CHECKBOX THE APP DID NOT HAVE. Its only checkbox-derived control is the account page's switch,
   and a switch reads as a reversible setting where consent is a one-time affirmation.

   THE MECHANISM IS CORE'S, KEPT WHOLE: a real checkbox does the work, so `required` still blocks, the
   label still toggles it and a keyboard still reaches it. Only the painted box is ours. The input is
   moved out of sight rather than set to `display: none`, which would take it off the tab order and
   kill the focus ring the box borrows.

   INLINE-FLEX, NOT FLEX, SO THE LABEL HUGS ITS CONTENT. A full-width label would toggle a legal
   affirmation when the dead space beside the sentence is clicked. `vertical-align: top` keeps the
   inline box off the text baseline, which would otherwise leave a descender's worth of space under it.

   A CHECKED BOX TAKES THE ACCENT: the switch takes it when on and the primary button is filled with
   it, so consent reads as chosen in the same colour every other control uses. */
/* the hidden checkbox is placed against its own row, so it never adds to the height of the page behind an inner scroller */
.clp-chk, :has(> .peer ~ .clp-sw) { position: relative; }
.clp-chk { display: inline-flex; vertical-align: top; align-items: flex-start; gap: 10px; cursor: pointer; }
.clp-chk .peer, .peer:has(~ .clp-sw) { position: absolute; width: 1px; height: 1px; opacity: 0;
  margin: 0; padding: 0; border: 0; clip-path: inset(50%); }
.clp-chk .bx { position: relative; flex: none; width: 18px; height: 18px; margin-top: 1px;
  border: 1px solid var(--border-control); border-radius: var(--r-sm); background: var(--card);
  transition: background .14s, border-color .14s; }
.clp-chk .bx svg.ic { position: absolute; inset: 0; margin: auto; width: var(--icon-xs); height: var(--icon-xs);
  opacity: 0; transition: opacity .14s; }
.clp-chk:hover .bx { border-color: var(--border-control-hover); }
.clp-chk .lb { font-size: var(--fs-body); font-weight: 400; line-height: 1.55; color: var(--text-2); }
.clp-chk .peer:focus-visible ~ .bx { outline: 2px solid var(--accent); outline-offset: 2px; }
.clp-chk .peer:checked ~ .bx { background: var(--accent); border-color: var(--accent); }
.clp-chk .peer:checked ~ .bx svg.ic { opacity: 1; color: var(--on-fill); }
/* a row's pick box has no label, so its box sits on the row's centre line · `snippets/clp/pick.twig` */
.clp-chk.is-bare { align-items: center; }
.clp-chk.is-bare .bx { margin-top: 0; }

/* THE SWITCH, the one on and off control: a button carrying `.on`, or a checkbox (`.peer`) ahead of
   it inside a label, kept reachable by keyboard the way the tick box keeps its own. */
.clp-sw { position: relative; display: block; flex: none; width: 38px; height: 22px; padding: 0; border: 0;
  border-radius: var(--r-pill); background: var(--border-control); cursor: pointer; transition: background .16s; }
.clp-sw::after { content: ""; position: absolute; top: 3px; inset-inline-start: 3px; width: 16px; height: 16px;
  border-radius: var(--r-pill); background: var(--on-fill); transition: inset-inline-start .16s; }
.clp-sw.on, .peer:checked ~ .clp-sw { background: var(--accent); }
.clp-sw.on::after, .peer:checked ~ .clp-sw::after { inset-inline-start: 19px; }
.peer:disabled ~ .clp-sw { opacity: .45; cursor: default; }
.clp-sw:focus-visible, .peer:focus-visible ~ .clp-sw { outline: 2px solid var(--accent); outline-offset: 2px; }

/* THE TERMS are a plain list in the quiet register, not a `.box`: a bordered panel of terms under the
   checkbox would be two frames saying one thing. The 28px inset is the box and its gap, so the list
   starts under the sentence. */
.clp-terms { margin: 9px 0 0 28px; padding: 0; list-style: disc; }
.clp-terms li { font-size: var(--fs-xs); line-height: 1.6; color: var(--text-4); margin-top: 5px; }
.clp-terms li::marker { color: var(--text-4); }

/* WHAT IT COSTS, SAID BEFORE THE PRESS, beside the actions because it is about the press and not about
   the form. `margin-inline-end: auto` sends it to the start of the row. */
.clp-modal .m-cost { font-size: var(--fs-sm); color: var(--text-3); margin-inline-end: auto; }
.clp-modal .m-cost b { font-weight: 500; color: var(--text-2); }

/* ---- THE SEGMENTED SWITCH. Core's own `.tabs`. The shape is core's, one bordered container
   with the segments inside it; what "chosen" says is ours, the accent fill every other
   chosen thing in the app already wears (the nav's current row, the writer's tone pill,
   the model menu's row, the duration rail, the effort menu's check).

   FIVE PROPERTIES, NOT A COMPONENT. Core still owns the layout, the alignment and the border
   width, so what follows is an override and breaks if read as a whole control.

   The segments sit INSET in a track rather than flush to the container, so the chosen
   one reads as an object floating on the track rather than as one half of a divided bar.

   The unchosen label is --text-3, NOT --text-2. A flush or near-flush pair of labels
   carries its state by direct contrast between the two, and at --text-2 the unchosen
   label is brighter than the chosen accent one, so the control points at the wrong
   answer. .clp-seg never had this problem because its pills each carry a border, so
   chosen reads from the fill rather than from the neighbour. Found in a render.

   IT SITS WITH THE SHARED COMPONENTS BECAUSE IT IS ONE. Two billing pages wear it today, the
   plans page's cycle switch and the receipt, and the voiceover page's mode switch is the third.
   The selector is unscoped, so declaring it inside one page's section applied it everywhere
   regardless while reading as though it belonged to that page. ---- */
.tabs { border-color: var(--border-strong); border-radius: var(--r);
  background: var(--raised); padding: 3px; gap: 2px; }
.tabs button { height: 28px; padding: 0 16px; border: 0; border-radius: var(--r-sm);
  background: transparent; font-family: var(--font); font-size: var(--fs-sm); color: var(--text-3);
  transition: background .14s, color .14s; }
.tabs button:hover { background: var(--hover); color: var(--text); }
.tabs button.active { background: var(--accent-fill); color: var(--accent-hi);
  box-shadow: var(--ring-chosen); }
.tabs button.active:hover { background: var(--accent-fill); color: var(--accent-hi); }
/* THE SELECTED TAB TAKES THE FOCUS RING. Its inset shadow outranks the ring every button gets on
   keyboard focus, and the focus outline covers that same 1px line, since a tab has no border. */
.tabs button.active:focus-visible { box-shadow: var(--focus-ring); }
/* Who sees it (`snippets/audience.twig`): the two choices at their own width, or sharing their column's (`is-fill`). */
.clp-aud .tabs { display: inline-flex; }
.clp-aud.is-fill .tabs { display: flex; width: 100%; }
.clp-aud.is-fill .tabs button { flex: 1; }
.menu .clp-aud { padding: 3px 3px 0; }

/* ---- THE MENU. Ten dropdown panels across eight templates render this: the account
   menu, the writer's download menu, chat's reasoning-effort menu, the sort menu and the
   row context menu on documents and on speeches, the tile context menu on images and on
   videos, and the locale menu on 404. It is written once and unscoped, the way .input
   and .button are, so all ten carry one vocabulary.

   The panel is .clp-menu's surface and the row is .clp-menu-row's row, because the model
   command menu is this same object with a search field on top. Two of core's own values
   have to be overridden rather than extended: it paints the panel --color-main, which IS
   the page on a black canvas, and it hovers a row to --color-intermediate, which is
   #FBFBFC on a white card. ---- */
.menu { border-radius: var(--r-lg); border-color: var(--border-strong); box-shadow: var(--lift-menu);
  background: var(--card); padding: 5px; font-size: var(--fs-body); }
/* the item owns the corner: a radius on the wrapper rounds a different box than the one
   that paints the hover, so only one of the two can ever be the corner you see */
.menu li { border-radius: 0; overflow: visible; }
.menu hr { border-top: 1px solid var(--border); margin: 5px 0; }
.menu .menu-item { border-radius: var(--r); gap: 10px; padding: 8px 9px; font-size: var(--fs-body);
  color: var(--text-2); transition: background .14s, color .14s; }
.menu .menu-item:hover { background: var(--hover); color: var(--text); }
/* the rest of the row's layout, in core's components layer where core's own rule for it sits */
@layer components {
  .menu .menu-item { display: flex; align-items: center; width: 100%; text-align: start; overflow: hidden; }
}
/* the row's glyph, dimmed like the nav item's three pixels above it in the same sidebar */
.menu .menu-item i { color: var(--text-3); font-size: var(--icon-md); flex: none; }
.menu .menu-item:hover i { color: var(--text); }
/* THE CURRENT ROW. Stated for hover as well, because the row's own hover colour would
   otherwise take the mark with it. Same accent .clp-mr-chk paints in the model menu. */
.menu .menu-item .now,
.menu .menu-item:hover .now { margin-inline-start: auto; color: var(--accent-hi); font-size: var(--fs-lg); }
/* the row that destroys something, on the treatment .r-act.del already uses */
.menu .menu-item.danger:hover,
.menu .menu-item.danger:hover i { color: var(--red-hi); }
/* the second line a row can carry, at .clp-mr-txt .pv's size */
.menu .menu-item .sub { display: block; margin-top: 2px; font-size: var(--fs-xs); color: var(--text-4); white-space: normal; }
/* the identity row: .clp-mr-av's avatar, the model row's name, a quieter second line.
   The sidebar's account trigger draws the same two lines, because it is the closed state
   of this panel. */
.menu .menu-item .nm,
.sidebar .acct-row .nm { display: block; font-size: var(--fs-body); color: var(--text); overflow: hidden; text-overflow: ellipsis; }
.menu .menu-item .em,
.sidebar .acct-row .em { display: block; font-size: var(--fs-xs); color: var(--text-4); overflow: hidden; text-overflow: ellipsis; }
/* the trigger is one row in a fixed-width column, so its two lines clip instead of wrapping */
.sidebar .acct-row .nm, .sidebar .acct-row .em { white-space: nowrap; }
/* the small print, on .clp-menu-foot's rule and quiet register. It draws its own top
   border, so the panel above it carries no separator of its own. */
.menu .menu-foot { border-top: 1px solid var(--border); margin-top: 5px; padding: 9px 9px 4px;
  display: flex; flex-direction: column; gap: 7px; }
.menu .menu-foot a { display: inline-flex; align-items: center; gap: 5px; font-size: var(--fs-xs);
  color: var(--text-4); transition: color .14s; }
.menu .menu-foot a:hover { color: var(--text-3); }
.menu .menu-foot a i { font-size: var(--icon-xs); }

/* ---- numerals: Geist Mono, tabular ---- */
x-money, x-credit, x-currency, x-uuid, code.mono, .font-mono {
  font-family: var(--mono); font-variant-numeric: tabular-nums; letter-spacing: 0;
}
/* ---- THE DISPLAY FIGURE, a hero number in the display face; the numerals inside it follow, and the place sets its size. ---- */
.clp-figure { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w); }
.clp-figure :is(x-money, x-credit) { font: inherit; font-variant-numeric: inherit; letter-spacing: inherit; }

/* ============================================================
   Phase 4b: writer page, browse state + command palette.
   Scoped under .clp-browse / .palette-wrap so generic class names
   never touch Tailwind utilities. Shared mock tokens; light + dark.
   ============================================================ */
[x-cloak] { display: none !important; }

.kbd { font-family: var(--mono); font-size: var(--label-size); color: var(--text-4); border: 1px solid var(--border-strong); border-radius: 5px; padding: 2px 6px; line-height: 1; }

/* feather icon sizing inside writer surfaces */

/* ---- browse state (v3 search-first: hero searchbar + category grid + recently used) ---- */
/* WIDTH 100% IS LOAD-BEARING, not redundant with max-width. This is a flex item in the column at
   layouts/main.twig:104, and `margin: 0 auto` puts auto margins on the cross axis, which cancels
   the stretch a flex item would otherwise get. Without this line the block is sized to its own
   contents and centred, so the column never reaches 820 and a page of narrow cards collapses to
   the width of one card. `.clp-page` and `.clp-tool` carry the same three properties for the same
   reason; this rule was copied from them without it. */
.clp-browse { max-width: 820px; margin: 0 auto; width: 100%; padding: 12px 0 40px; }
/* Category section: a serif heading with its count over a hairline, then that category's
   templates as cards, two across. Grouped live from list('presets'). */
.clp-browse .cat-head { display: flex; align-items: baseline; gap: 9px; margin: 30px 0 10px; padding-bottom: 7px; border-bottom: 1px solid var(--border); }
.clp-browse .cat-head h2 { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-size: var(--fs-xl); font-weight: var(--display-w); letter-spacing: var(--display-ls); color: var(--text); }
/* Two across, not three. Counts are 6,4,4,4,4,3,3,2,2: against 3 columns six of the nine sections
   end ragged and four strand a lone card; against 2 columns only Hooks and Utility do. */
.clp-browse .tpl-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
@media (max-width: 720px) { .clp-browse .tpl-grid { grid-template-columns: 1fr; } }
/* THE TEMPLATE CARD · `snippets/clp/template-card.twig`, Writer's browse grid. Its description is the reason it exists, so
   it is never clamped. */
.clp-browse .qk { border: 1px solid var(--border-strong); border-radius: var(--r-lg); background: var(--card); padding: 16px; cursor: pointer; text-align: left; transition: background .16s, border-color .16s; }
.clp-browse .qk .qn { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w); font-size: var(--fs-xl); letter-spacing: var(--display-ls); color: var(--text); margin-bottom: 6px; }
.clp-browse .qk .qd { font-size: var(--fs-sm); color: var(--text-3); line-height: 1.5; }
/* ITS FOOT · `snippets/clp/template-card.twig`: the platforms it posts to, or its step in a chain, held to the card's bottom
   so the feet of a row line up. */
.qk:has(> .qk-foot) { display: flex; flex-direction: column; }
.qk .qk-foot { display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 14px; color: var(--text-3); }
.qk .qk-foot svg.ic { width: var(--icon-md); height: var(--icon-md); }

/* ---- command palette ---- */
.palette-wrap { position: fixed; inset: 0; z-index: var(--z-overlay); display: flex; align-items: flex-start; justify-content: center; padding-top: 12vh; background: var(--scrim); backdrop-filter: blur(var(--overlay-blur)); -webkit-backdrop-filter: blur(var(--overlay-blur)); }
/* core's dialog backdrop takes the one overlay too; it drew its own wash, which lightened the page in dark mode. Its dialogs and
   its preview take the layer order (`--z-` in css-variables.twig) here, unlayered, so they beat its own layered rules. */
modal-element { background-color: var(--scrim); z-index: var(--z-dialog); }
x-preview { z-index: var(--z-overlay); }
modal-element.open { backdrop-filter: blur(var(--overlay-blur)); -webkit-backdrop-filter: blur(var(--overlay-blur)); }
/* full-viewport backdrop, but center the palette over the content column, offset past the 252px sidebar where there is one (admin) */
@media (min-width: 1024px) { html:has(aside.sidebar) .palette-wrap { padding-left: 252px; } }

/* ============================================================
   Phase 4c: writer form (with-preset). Restyles the core dynamic
   placeholder form to the mock: serif template head, flat fields,
   slop note, generate bar. Rides the engine markup + bindings, so
   only wrappers/typography change, never the form logic.
   ============================================================ */
/* the form section rides core markup; drop the boxed chrome so fields sit flat like the mock */
.clp-wb section { border: none; background: none; padding: 0; }
.clp-wb form label { font-size: var(--fs-sm); font-weight: 500; color: var(--text-2); }
/* the tooltip glyph beside a label reads at label scale; core sizes it at body scale (16px) */
.clp-wb form label .help { font-size: var(--fs-md); color: var(--text-3); }
.clp-wb .slop-note { display: flex; gap: 7px; align-items: flex-start; padding: 11px 13px; border: 1px dashed var(--border-strong); border-radius: var(--r); color: var(--text-3); font-size: var(--fs-sm); line-height: 1.5; }
.clp-wb .slop-note svg.ic { width: var(--icon); height: var(--icon); fill: var(--accent); flex: none; margin-top: 1px; }
.palette { width: min(600px, 92vw); max-height: 64vh; display: flex; flex-direction: column; border: 1px solid var(--border-strong); border-radius: var(--r-xl); background: var(--card); box-shadow: var(--lift-palette); overflow: hidden; }
.palette .pick-search { display: flex; align-items: center; gap: 9px; height: 52px; padding: 0 14px; border-bottom: 1px solid var(--border); color: var(--text-3); }
.palette .pick-search input { border: none; background: none; outline: none; width: 100%; font-size: var(--fs-md); color: var(--text); }
.palette .pick-search input::placeholder { color: var(--text-4); }
.palette .pick-list { overflow-y: auto; padding: 8px; }
.palette .pick { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--r); color: var(--text-2); font-size: var(--fs-md); }
.palette .pick:hover { background: var(--hover); color: var(--text); }
.palette .pick .tk { width: 5px; height: 5px; border-radius: var(--r-pill); background: var(--text-4); flex: none; }
.palette .pick .pnm { color: var(--text); font-weight: 450; }
.palette .pick .pd { margin-left: auto; font-size: var(--fs-xs); color: var(--text-4); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 55%; }
.palette .pfoot { display: flex; align-items: center; gap: 16px; padding: 10px 14px; border-top: 1px solid var(--border); font-size: var(--fs-xs); color: var(--text-4); }

/* ============================================================
   Phase 5: content pages (Home + Library hub + Library list).
   The approved content-pages-v1 system. Scoped under .clp-page so
   the mock's generic component names (.hero / .searchbar /
   .qk / .mbtn / .rechead ...) never clobber Tailwind utilities
   or the writer's .clp-browse. Rides the shared command palette
   (snippets/clp/template-search.twig + its .palette CSS above).
   Shared mock tokens; light + dark.
   ============================================================ */
.clp-page { max-width: 840px; margin: 0 auto; width: 100%; }
/* a page as wide as the content column: the billing pages and Home's showroom */
.clp-page:is(.clp-bill, .clp-home) { max-width: 1080px; }
/* the sprite's glyphs on every content-page surface */

/* ============================================================
   THE PAGE OPENING. Gate 1 variant A, approved 2026-08-03.
   One treatment on every forked page: h1 in the hero, an optional
   one-line subhead, an optional template-search button. Emitted by
   snippets/clp/page-opening.twig, so no page owns its own opening.
   Scoped to #content, which every app page sits inside. Replaces
   the three systems that used to live under .clp-tool, .clp-page
   and .clp-browse.
   The searchbar shares the composer measure via --composer-w
   rather than repeating 600px (BACKLOG:67).
   The opening owns the space under it (--opening-gap) as a margin, so the block below adds none
   of its own: touching margins merge and the larger wins. A block asking for more would widen it.
   ============================================================ */
#content .hero { text-align: center; padding: 44px 0 0; margin-bottom: var(--opening-gap); }
#content .hero .greet { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-size: var(--fs-3xl); font-weight: var(--display-w); letter-spacing: var(--display-ls); line-height: 1.08; color: var(--text); margin-bottom: 22px; }
#content .hero .greet:has(+ .greet-sub) { margin-bottom: 6px; }
#content .hero .greet-sub { color: var(--text-3); font-size: var(--fs-md); margin-bottom: 22px; }
/* THE OPENING'S ACTION. An empty page holds ONE stack, so the action that used to sit in a
   second block below the heading belongs to the opening itself.

   The element is inline-flex, so it follows the opening's centred text and needs no centring of its own.

   Hover gives the app's one filled accent action its feedback; it reads --accent-hi like every
   other filled control. */
#content .hero .open-act { display: inline-flex; align-items: center; gap: 8px; margin-top: 18px; height: 40px; padding: 0 18px; border: 0; border-radius: var(--r); background: var(--accent); color: var(--on-fill); font-family: var(--font); font-size: var(--fs-body); font-weight: 500; cursor: pointer; text-decoration: none; transition: background .14s; }
#content .hero .open-act svg.ic { width: var(--icon); height: var(--icon); fill: var(--on-fill); }
#content .hero .open-act:hover { background: var(--accent); outline: 1px solid var(--accent); outline-offset: 2px; }
#content .pagetop .uuid { font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-4); margin-top: 5px; }
/* THE PAGE TOP (snippets/clp/page-top.twig): the back link over the title, the page's actions at its right,
   moving under it at 640 pixels and narrower. The centred opening's back link is the same link. */
#content .pagetop { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  margin: 8px 0 var(--opening-gap); }
#content :is(.pagetop, .hero) .back { display: inline-flex; align-items: center; gap: 4px; margin-bottom: 10px; font-size: var(--fs-body); color: var(--text-3); }
#content :is(.pagetop, .hero) .back:hover { color: var(--text); }
#content .pagetop .sub { margin-top: 8px; font-size: var(--fs-md); color: var(--text-3); }
#content .pagetop :is(h1, .wb-title) { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w);
  letter-spacing: var(--display-ls); font-size: var(--fs-3xl); line-height: 1.08; color: var(--text); }
#content :is(.pagetop, .convhead) .acts { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* the centred opening's buttons, one row under it */
#content .hero .hero-acts { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
#content :is(.pagetop, .convhead) .acts:empty { display: none; }
@media (max-width: 640px) {
  #content .pagetop { flex-direction: column; align-items: flex-start; }
  #content .button .kbd { display: none; }
  #content .pagetop :is(h1, .wb-title) { font-size: var(--fs-2xl); }
}
#content .searchbar { display: flex; align-items: center; gap: 13px; height: 56px; width: 100%; max-width: var(--composer-w); margin: 0 auto; padding: 0 18px; border: 1px solid var(--border-control); border-radius: var(--r-lg); background: var(--card); color: var(--text-3); cursor: text; transition: border-color .16s, box-shadow .16s; text-align: left; }
/* Hover is the quiet step, focus keeps the ring. They used to be the same declaration, so
   passing the mouse over a search field looked exactly like typing in one. */
#content .searchbar .ph { flex: 1; font-size: var(--fs-md); color: var(--text-4); }
/* a phone says the short words, so the button stays one line */
#content .searchbar .ph-s { display: none; }
@media (max-width: 640px) {
  #content .searchbar .ph-l { display: none; }
  #content .searchbar .ph-s { display: inline; }
}

/* library search: the SAME .searchbar shell carrying a real input instead of the palette
   button, so the two surfaces sit at identical size and position and the page does not
   shift when a library page swaps one for the other. Only the inner element and the focus
   treatment differ. .is-live keeps the accent ring while a query is active but unfocused,
   so a filtered list always looks filtered. */
#content .searchbar.is-input { cursor: text; }
#content .searchbar.is-input input {
  flex: 1; min-width: 0; border: 0; background: none; outline: none; padding: 0;
  font-family: var(--font); font-size: var(--fs-md); color: var(--text);
}
#content .searchbar.is-input input::placeholder { color: var(--text-4); }
#content .searchbar.is-input input::-webkit-search-cancel-button,
#content .searchbar.is-input input::-webkit-search-decoration { -webkit-appearance: none; display: none; }
#content .searchbar.is-input:focus-within,
#content .searchbar.is-live { border-color: var(--accent); box-shadow: var(--focus-ring); }
#content .searchbar .sb-clr {
  flex: none; font-size: var(--fs-sm); color: var(--text-4); cursor: pointer;
  background: none; border: 0; padding: 0 2px; font-family: var(--font);
}
#content .searchbar .sb-clr:hover { color: var(--text); }


/* ============================================================
   THE SHELF: the library hub, one section per content type, each showing what is inside it.
   ============================================================
   THE HUB CARRIES NO CARDS, because the card was the problem.
   EVERY OTHER BROWSE SURFACE IN THIS APP SHOWS A REAL THING: the niches page puts the
   artwork in the card and says so in its own comment ("THE ART IS THE CARD"), the media wall is
   the media, the ledger row carries the document's own first line, the billing tile carries the
   number the customer came to check. The hub was the one surface showing a label and an icon,
   so it made you open a page to discover whether anything was in it. Hatim 2026-08-08, on being
   shown a version where only the padding had changed: "NO CREAIVE WHTF BE CREATIVE LOOK AT
   OTHER PAGES".

   So each type is a shelf: its name, its real count, and its most recent items. Text types show
   rows, media types show their pictures. The hub stops pointing at four pages and previews all four.

   THE ROWS ARE THE TEXT ROW (`#content .trow`), in the list card, opening their item only.

   THE PICTURES ARE THE MEDIA WALL (`#content .gal`), one row high (`.gal-row`), each at its own
   shape and opening in the result window. ------------------------------------------------------------ */
.clp-page .shelf + .shelf { margin-top: 30px; }
.clp-page .sh-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 11px; }
.clp-page .sh-head h3 { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w); font-size: var(--fs-lg);
  letter-spacing: var(--display-ls); color: var(--text); margin: 0; }
/* every count beside a list's name or over a list, and a queue row's time */
.clp-page .sh-head .ct,
#content :is(.list-bar, .sec-head) .lb-count,
#content .clp-q .q-time { font-family: var(--mono); font-size: var(--fs-sm); color: var(--text-4);
  font-variant-numeric: tabular-nums; }
.clp-page .sh-head a { margin-inline-start: auto; display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-sm); color: var(--text-3); transition: color .14s; }
.clp-page .sh-head a:hover { color: var(--text); }
.clp-page .sh-head a svg.ic { width: var(--icon-sm); height: var(--icon-sm); }

.clp-page .rec .t .nm, #content .clp-q .q-prompt { font-size: var(--fs-body); color: var(--text); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }

#content .trow-pic img,
:is(.clp-tool, .clp-lb) .clp-fk video,
.clp-chat .files .imgf img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* a type with nothing in it yet still gets its shelf, so the hub does not change shape */
.clp-page .sh-none { font-size: var(--fs-sm); color: var(--text-4); padding: 10px 2px 2px; }

/* ---- a card lifts by filling to --raised, not by moving ---- */
.clp-browse .qk:hover { background: var(--raised); border-color: var(--border-struct); }

/* The bar that sits over a list: what it holds on the left, the control that acts on it on the
   right. SCOPED #content, NOT .clp-page, for the reason `.led` states where it is declared: a
   component pinned to one page family grows a duplicate the first time a second family needs it.
   Every library list wears this, billing's orders wear it, and the template page wears it over
   that template's own runs, which is a .clp-wb and not a .clp-page. */
#content .list-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 20px 0 14px; }
/* the menu button: a labelled control that opens a dropdown, on the library list bar.
   Named for the family it belongs to (.ebtn / .pbtn), not `chip`, which is the
   content token a chat message carries and is a different component entirely. */
#content .mbtn { display: inline-flex; align-items: center; gap: 7px; height: 34px; padding: 0 12px; border: 1px solid var(--border-strong); border-radius: var(--r); font-size: var(--fs-sm); color: var(--text-2); cursor: pointer; transition: background .14s, color .14s, border-color .14s; }
#content .mbtn .ic { width: var(--icon-sm); height: var(--icon-sm); }

/* THE PAGE-LEVEL EMPTY STATE HAS NO MARKUP OF ITS OWN. A page with nothing on it holds one
   centred stack, not a heading followed by a second centred block restating it, so the state
   lives in the page opening: `#content .hero .greet-sub` swaps its line and
   `#content .hero .open-act` carries the action. A page with a left title says it under its
   title, in the page top's `under` block.

   The quiet line for a surface that is empty WITHOUT being a page is still `.clp-empty`,
   declared above. That distinction is the whole vocabulary: a shelf inside a page whispers
   one line, a page speaks through its own opening. Nothing draws a tile. */

/* THE SORT CHIP, on every list page that offers an order. Its dropdown is core's `.menu`,
   which is absolutely positioned, so the chip has to be the thing it measures from.
   SCOPED #content, NOT .clp-page, for the reason `.list-bar` gives above: the library pages and
   the orders page are .clp-page, and the admin voice list, which carries the same control, is not. */
#content .clp-sort { position: relative; }
#content .clp-sort .menu { left: auto; right: 0; top: calc(100% + 6px); }

/* THE LIBRARY HAS NO GRID OF ITS OWN. Images and videos ride `#content .gal`, the same
   justified media wall the imagine and video pages show, because a wall of generated media is
   one object. There is no column-masonry block here and no use of core's `masonry` component
   anywhere in the skin. */

/* load more */
.clp-page .clp-more { display: flex; justify-content: center; margin-top: 20px; }
.clp-page .clp-more button { height: 36px; padding: 0 16px; border: 1px solid var(--border-strong); border-radius: var(--r); font-size: var(--fs-body); color: var(--text-2); transition: background .14s, color .14s, border-color .14s; }
.clp-page .clp-more.is-loading { opacity: .55; pointer-events: none; }

/* ============================================================
   THE TEXT ROW · `snippets/clp/text-row.twig`, in the list card (`.clp-list`): the item on the left, its name over its
   first line, the date alone on the right. The row is a link; everything above it lets a press through to it.
   ============================================================ */
#content .trow { --trow-acts-w: calc(3 * var(--icon-md) + 28px); position: relative; display: flex; align-items: center;
  gap: 14px; padding: 12px 14px; transition: background .12s; }
#content .trow + .trow { border-top: 1px solid var(--border); }
/* the card does not clip, so a row's fill rounds its own corners inside the card's border */
#content .trow:first-of-type { border-top-left-radius: calc(var(--r-lg) - 1px); border-top-right-radius: calc(var(--r-lg) - 1px); }
#content .trow:last-of-type { border-bottom-left-radius: calc(var(--r-lg) - 1px); border-bottom-right-radius: calc(var(--r-lg) - 1px); }
#content .trow:hover { background: var(--hover); }
#content .trow.is-on { background: var(--accent-fill); }
#content .trow-slot { flex: none; width: 36px; height: 36px; display: grid; place-items: center; }
/* a speech's play control sits over the row's link, so a press on it plays and a press anywhere else opens */
#content .trow-slot:has(> .clp-vr-play) { position: relative; z-index: 2; }
#content .trow-mark, #content .trow-pic { width: 100%; height: 100%; box-sizing: border-box; border: 1px solid var(--border); border-radius: var(--r); }
#content .trow-mark { display: grid; place-items: center; background: var(--raised); color: var(--text-3); }
#content .trow-pic { position: relative; display: block; overflow: hidden; }
#content .trow-pic .clp-fk-play svg.ic { width: var(--icon-xs); height: var(--icon-xs); }
#content .trow-t { flex: 1; min-width: 0; }
#content .trow-nm, #content .trow-ln { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#content .trow-nm { font-size: var(--fs-md); font-weight: 500; color: var(--text); }
/* a name or text standing in for a missing one reads quieter */
#content .trow-nm.is-untitled, #content .led-row .nm.is-untitled, .clp-drawer .drawer-row .nm.is-untitled,
.clp-acct .mrow .mtxt.is-untitled { color: var(--text-3); }
#content .trow-ln { margin-top: 2px; font-size: var(--fs-body); color: var(--text-3); }
/* a chat's first line is the person's own words, so it is set as a quotation */
#content .trow-ln.q::before { content: '\201C'; }
#content .trow-ln.q::after { content: '\201D'; }
/* the date's box is as wide as the widest set of row buttons, so the buttons take exactly its place */
#content .trow-date { display: block; flex: none; width: var(--trow-acts-w); text-align: end; font-family: var(--mono);
  font-size: var(--fs-xs); letter-spacing: .02em; color: var(--text-4); white-space: nowrap; }
/* WHILE A SPEECH PLAYS its first line becomes the moving bar on `deep`, its 6px box set in a box of the line's own height
   so the row keeps its height, and the time, played over length, takes the date's place, at least as wide as the date.
   Paused or finished, the row is at rest again. */
#content .trow-t > .clp-seek { display: none; font-size: var(--fs-body); background-color: var(--deep);
  margin-block: calc(2px + (1lh - 6px) / 2) calc((1lh - 6px) / 2); }
#content .trow-clock { display: none; width: auto; min-width: var(--trow-acts-w); }
#content .trow:has(.clp-vr-play[state="playing"]) .trow-ln,
#content .trow:has(.clp-vr-play[state="playing"]) > x-time.trow-date { display: none; }
#content .trow:has(.clp-vr-play[state="playing"]) .trow-t > .clp-seek,
#content .trow:has(.clp-vr-play[state="playing"]) > .trow-clock { display: block; }
#content .trow-acts { position: absolute; right: 14px; top: 50%; transform: translateY(-50%); z-index: 2;
  width: var(--trow-acts-w); display: flex; align-items: center; justify-content: flex-end; gap: 14px; visibility: hidden; }
#content .trow-more { display: flex; align-items: center; gap: 14px; }
/* Under the pointer or the keyboard (the focus a mouse press leaves does not count) the buttons take the date's place.
   While anything is picked, every row shows its pick box there, and the other buttons only on the row under the pointer.
   A row that cannot be picked keeps its date. */
#content .trow:is(:hover, :has(:focus-visible)) > .trow-acts,
#content .clp-list.is-picking .trow-acts { visibility: visible; }
#content .trow:is(:hover, :has(:focus-visible)):has(> .trow-acts) > .trow-date,
#content .clp-list.is-picking .trow:has(> .trow-acts) > .trow-date { visibility: hidden; }
#content .clp-list.is-picking .trow:not(:hover, :has(:focus-visible)) .trow-more { display: none; }
/* the More button, 32 pixels, which a phone or a touch screen shows in place of the buttons */
#content .trow-dots { display: none; flex: none; width: 32px; height: 32px; position: relative; z-index: 2; }
#content .trow-dots .r-act { width: 32px; height: 32px; justify-content: center; }
/* A PHONE OR A TOUCH SCREEN has no pointer to bring the buttons up, so they fold into the More button, and a tap's
   leftover hover brings nothing up either. While anything is picked, the pick box shows in the More button's place. */
@media (hover: none), (max-width: 640px) {
  #content .trow-dots { display: block; }
  #content .trow-more { display: none; }
  #content .trow:is(:hover, :has(:focus-visible)) > .trow-acts { visibility: hidden; }
  #content .trow:is(:hover, :has(:focus-visible)):has(> .trow-acts) > .trow-date { visibility: visible; }
  #content .clp-list.is-picking .trow-dots { display: none; }
  #content .clp-list.is-picking .trow-acts { visibility: visible; }
}
/* A PHONE: the name and the date on one line over the first line, the slot beside both, the More button at the end. */
@media (max-width: 640px) {
  #content .trow { display: grid; grid-template-columns: 36px minmax(0,1fr) auto; grid-template-areas: "slot nm date" "slot ln ln";
    column-gap: 12px; row-gap: 2px; }
  #content .trow:has(> .trow-dots) { grid-template-columns: 36px minmax(0,1fr) auto 32px; grid-template-areas: "slot nm date dots" "slot ln ln dots";
    padding-right: 6px; }
  #content .trow-slot { grid-area: slot; }
  #content .trow-t { display: contents; }
  #content .trow-nm { grid-area: nm; }
  #content .trow-ln { grid-area: ln; margin-top: 0; }
  #content .trow-t > .clp-seek { grid-area: ln; margin-block: calc((1lh - 6px) / 2); }
  #content .trow-date { grid-area: date; width: auto; }
  #content .trow-dots { grid-area: dots; }
  #content .trow-acts { grid-area: dots; position: static; transform: none; width: auto; justify-self: center; }
  #content .clp-list.is-picking .trow:has(> .trow-acts) > .trow-date { visibility: visible; }
}
/* A SPEECH OPEN IN PLACE (`snippets/clp/speech-open.twig`): the row on the `hover` tint, its open part on a line of its own
   lined up under the name, above the row's link, with its name the editable one in the name's own type. While it is
   open its first line steps aside, since the whole script is right below, and so do the hover buttons and the More
   button; the pick box stays. */
#content .trow.is-open { flex-wrap: wrap; background: var(--hover); }
#content .trow.is-open .trow-ln,
#content .trow.is-open .trow-more { display: none; }
#content .trow.is-open > .trow-dots { visibility: hidden; }
/* the pick box keeps to the middle of the row's own line, under the row's 12 pixels of padding: the 36 pixel slot's while
   it is open, the name over the moving bar's (both at the page's 1.5 line height) while it also plays; the name stays on
   one line while it is edited */
#content .trow.is-open > .trow-acts { top: calc(12px + 36px / 2); }
#content .trow.is-open:has(.clp-vr-play[state="playing"]) > .trow-acts { top: calc(12px + (1.5 * var(--fs-md) + 2px + 1.5 * var(--fs-body)) / 2); }
#content .trow-t > .autogrow-textarea { position: relative; z-index: 2; }
#content .trow textarea.r-title,
#content .trow .autogrow-textarea:has(> textarea.r-title)::after { font-size: var(--fs-md); font-weight: 500; line-height: inherit;
  white-space: nowrap; overflow: hidden; }
#content .trow-body { flex: 0 0 100%; position: relative; z-index: 2; padding: 4px 0 6px calc(36px + 14px); cursor: auto; }
#content .trow-body .clp-facts { margin-bottom: 12px; }
#content .trow-script { max-width: 68ch; font-size: var(--fs-md); line-height: 1.65; color: var(--body); white-space: pre-wrap; }
#content .trow-body .t-actions { justify-content: flex-start; flex-wrap: wrap; margin-top: 14px; }
/* on a phone the name's place is the grid's, and the open part takes the row's whole width */
@media (max-width: 640px) {
  #content .trow-t > .autogrow-textarea { grid-area: nm; }
  #content .trow-body { grid-column: 1 / -1; padding-left: 0; }
}

/* THE FACTS LINE: an item's facts in the mono face, each after its glyph, wrapping · the open speech, the speech window. */
.clp-facts { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 16px; font-family: var(--mono); font-size: var(--fs-xs);
  letter-spacing: .03em; color: var(--text-4); }
.clp-facts > span { display: inline-flex; align-items: center; gap: 6px; }
.clp-facts svg.ic { width: var(--icon-xs); height: var(--icon-xs); }
.clp-facts .f-voice { text-transform: capitalize; }

/* ============================================================
   THE LEDGER: Voiceover's Recent, the one list that does not draw the text row.
   No card chrome: a header rule, hairlines between rows, the title beside one actions track.
   Scoped #content, which every skinned surface sits inside.
   ------------------------------------------------------------ */
#content .led-head,
#content .led-row { display: grid; grid-template-columns: minmax(0,1fr) 62px; gap: 16px; }

#content .led-head { padding: 0 12px 9px; font-size: var(--fs-xs); letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-4); font-weight: 500;
  border-bottom: 1px solid var(--border-strong); }

/* THE ROW IS A LINK, the same full-bleed overlay the app already uses: navigable, and an
   agent can find the href instead of guessing at a click handler. The trailing actions sit
   above the overlay so their own clicks still land. */
#content .led-row { position: relative; align-items: start; padding: 14px 12px;
  border-bottom: 1px solid var(--border); transition: background .12s; cursor: pointer; }
#content .led-row:last-of-type { border-bottom: 0; }
#content .led-row:hover { background: var(--hover); }
#content .led-row .row-open { position: absolute; inset: 0; z-index: 1; border-radius: var(--r); }
#content .led-row > .t { position: relative; z-index: 0; pointer-events: none; }

/* THE ITEM: its name. `min-width: 0` is load-bearing rather than decorative: the name is a nowrap
   line that ellipses, so its min-content contribution is the whole untruncated title, and without
   the floor a long title widens the row past the page. */
#content .led-row .t { min-width: 0; }
#content .led-row .nm { display: block; font-size: var(--fs-md); color: var(--text);
  letter-spacing: -.008em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* THE ACTIONS KEEP THEIR OWN COLUMN, and the width is reserved on every row whether or not
   it is hovered, so the row never reflows under the cursor. `.r-act` itself is declared once
   further down and worn by imagine, video, voiceover and the writer; a library row is the same
   on-surface control. */
#content .led-row .led-acts { position: relative; z-index: 2; justify-self: end;
  display: flex; align-items: center; gap: 14px; padding-top: 1px;
  opacity: 0; transition: opacity .16s; }
#content .led-row:hover .led-acts,
#content .led-row:focus-within .led-acts { opacity: 1; }

/* NARROW. Below 640px the header goes, and the actions stay visible, because a touch device has
   no hover to reveal them with. */
@media (max-width: 640px) {
  #content .led-head { display: none; }
  #content .led-row { grid-template-areas: "item acts"; gap: 5px 14px; }
  #content .led-row > .t { grid-area: item; }
  #content .led-row > .led-acts { grid-area: acts; align-self: center; opacity: 1; }
}

/* ============================================================
   Loading state (clapyo-coherent). Shown while a data-driven surface's
   list() is in 'initial' (pre-fetch); the content reveals in one pass
   once state flips to 'loaded'/'empty', so the page no longer paints the
   shell and then pops the cards in. A caption in the display treatment
   + a hairline track with an accent fill sweeping L->R. The
   dots cycle inside a reserved-width slot so the caption never reflows.
   ============================================================ */
.clp-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 24px; width: 100%; min-height: 360px; padding: 56px 16px; }
.clp-loading .lcap { display: inline-flex; align-items: baseline; font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w); font-size: var(--fs-3xl); letter-spacing: var(--display-ls); color: var(--text); }
.clp-loading .lcap .dots { display: inline-block; width: 1.1em; text-align: left; }
.clp-loading .lcap .dots::after { content: ''; animation: clp-dots 1.5s steps(4, end) infinite; }
.clp-loading .lbar { position: relative; display: block; width: 100%; max-width: 460px; height: 2px; background: var(--border-strong); overflow: hidden; border-radius: var(--r-pill); }
.clp-loading .lbar::before,
.clp-meter.is-wait::before { content: ''; position: absolute; top: 0; left: -30%; width: 30%; height: 100%; background: var(--accent); border-radius: var(--r-pill); animation: clp-sweep 1.8s linear infinite; }
@keyframes clp-dots { 0% { content: ''; } 25% { content: '.'; } 50% { content: '..'; } 75% { content: '...'; } 100% { content: ''; } }
@keyframes clp-sweep { 0% { left: -30%; } 100% { left: 100%; } }
@media (prefers-reduced-motion: reduce) {
  .clp-loading .lcap .dots::after { animation: none; content: '...'; }
  .clp-loading .lbar::before,
  .clp-meter.is-wait::before { animation: none; left: 0; width: 100%; opacity: .5; }
}

/* ============================================================
   Boot overlay (centralized, full-viewport). ONE loader for the whole
   app: rendered once in the shell (layouts/main.twig) and shown by DEFAULT
   via this CSS, so it masks the entire viewport (sidebar included) the
   instant any page's HTML paints. Core does full-page navigation, so
   this runs on every page load. The boot controller in main.twig fades it
   out once the page's data fetches settle (the network goes idle), with a
   hard timeout fallback. Replaces the old per-page .clp-loading blocks;
   individual pages carry no loader markup. Theme-aware background.
   ============================================================ */
#clp-boot {
  position: fixed; inset: 0; z-index: var(--z-cover);
  display: flex; align-items: center; justify-content: center;
  background: var(--bg);
  opacity: 1; visibility: visible;
  transition: opacity .34s ease, visibility 0s linear .34s;
}
#clp-boot[data-done] { opacity: 0; visibility: hidden; pointer-events: none; }
@media (prefers-reduced-motion: reduce) { #clp-boot { transition: none; } }

/* ============================================================
   Phase 6: tool pages (Imagine / Video / Voiceover) - "Studio hero".
   Approved direction B (design/redesign/tool-pages-explore.html):
   Instrument Serif prompt hero + inline controls at top, results
   gallery below. The gallery REUSES the library grid/rows via a
   nested .clp-page wrapper (no duplicate gallery CSS). Rides the
   core imagine()/video()/voiceover() Alpine bindings - markup +
   type only, never the generation logic. Scoped under .clp-tool so
   utility names never touch Tailwind. Light + dark via shared tokens.
   ============================================================ */
.clp-tool { width: 100%; }
/* Voiceover's reading measure; Chat's bar spans the content area and Image and Video take the side column page */
#content:not([data-layout="side"]) .clp-tool:not(.clp-chat) { max-width: 840px; margin: 0 auto; }
/* ---- THE ICON. Every glyph the skin draws from the sprite is an `--icon-md` filled shape;
   `.ic-sm` is the small one. `.modal svg.ic`, further down, restates only the size so a
   modal's icons outrank the menu rules that shrink an icon; the search field and the row
   meta inside a modal carry their own sizes, declared there at modal specificity. ---- */
svg.ic { width: var(--icon-md); height: var(--icon-md); fill: currentColor; flex: none; }
svg.ic-sm { width: var(--icon-sm); height: var(--icon-sm); }
/* the icon sheet (snippets/clp/icons.twig): out of the flow, so it takes no room */
.clp-sprite { position: absolute; }

/* the composer, relocated from the stock bottom bar. The hero above it is the shared opening. */

/* composer: rides the core prompt textarea + submit binding; only wrappers/type change */
.clp-tool .composer { position: relative; }
/* Voiceover's prompt box keeps the prompt's measure */
.clp-tool > .composer { max-width: var(--composer-w); margin: 0 auto; }

/* WHICH STEP THIS PAGE IS WORKING INSIDE, a box over the prompt box in the tool column and the floating bar alike, shown
   only while the page works inside a run step. */
.clp-tool .t-onstep { display: flex; align-items: center; gap: 10px; padding: 7px 8px 7px 15px; border: 1px solid var(--border-strong); border-radius: var(--r-lg); background: var(--raised); }
.clp-tool .t-onstep .lab { flex: 1; min-width: 0; font-family: var(--mono); font-size: var(--label-size); letter-spacing: var(--label-ls); text-transform: uppercase; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clp-tool .t-onstep .apill { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 11px; border: 1px solid var(--border-strong); border-radius: var(--r-pill); font-size: var(--fs-sm); color: var(--text-2); background: var(--card); cursor: pointer; transition: border-color .14s, background .14s, color .14s; }
.clp-tool .t-onstep .apill:hover { border-color: var(--border-struct); color: var(--text); }
.clp-tool .t-onstep .apill svg.ic { width: var(--icon-sm); height: var(--icon-sm); }
/* THE PROMPT BOX, on every tool page: the text on top, the button row pinned under it. The whole
   inside scrolls as one, so the scrollbar runs the box's full height in its own lane right of the Generate or Send button.
   Only `.pb-scroll` scrolls; a height cap on the text area as well leaves blank space under it. */
.clp-tool .promptbar { padding: 4px; }
/* THE PROMPT'S FIELD SHELL (`.pb-shell`): the edge, fill and corners a prompt is typed in, and the focus ring its prompt
   hands it while being typed in (a button inside keeps its own). It is the prompt box on Voiceover, Chat and the tool
   column, and the whole card of the floating bar below 1024px. */
.pb-shell { border: 1px solid var(--border-control); border-radius: var(--r-xl); background: var(--card); transition: border-color .16s, box-shadow .16s; }
/* `--pb-row` is the height of the button row inside the prompt, declared only where the prompt draws one (Voiceover,
   Chat, the tool column's Image); the bottom scroll padding adds the 22px fade above it, so the line being typed stays
   clear of both. With no row it is 0. */
#content:not([data-layout="side"]) .clp-tool .pb-scroll { --pb-row: 44px; }
.clp-tool .pb-scroll { max-height: 216px; overflow-y: auto; scrollbar-gutter: stable; scroll-padding-bottom: calc(var(--pb-row, 0px) + 22px); padding: 4px 0 0 4px; }
/* The scroll fade, at the top and just above the button row. */
@supports (animation-timeline: scroll()) and (mask-image: none) {
  .clp-tool .pb-scroll { mask-image: linear-gradient(to bottom, transparent, #000 var(--fade-start), #000 calc(100% - var(--pb-row, 0px) - var(--fade-end)), transparent calc(100% - var(--pb-row, 0px)), #000 calc(100% - var(--pb-row, 0px)));
    animation: fade-start linear both, fade-end linear both; animation-timeline: scroll(self), scroll(self); animation-range: 0px 22px, calc(100% - 22px) 100%; }
}
.clp-tool .promptbar .field { padding: 0 4px 0 8px; grid-template-columns: minmax(0, 1fr); }
.clp-tool .promptbar textarea { width: 100%; border: none; background: none; outline: none; resize: none; font-family: var(--font); font-size: var(--fs-md); color: var(--text); line-height: 1.5; padding: 8px 0; }
.clp-tool .promptbar textarea::placeholder { color: var(--text-4); }
/* The autogrow copy must match the text area's box and wrapping exactly, or the box is sized wrong. */
.clp-tool .promptbar .autogrow-textarea::after { font-family: var(--font); font-size: var(--fs-md); line-height: 1.5; padding: 8px 0; overflow-wrap: break-word; }
.clp-tool .pb-tools { position: sticky; bottom: 0; display: flex; align-items: center; gap: 6px; min-height: var(--pb-row, 0px); padding: 2px 4px 4px 0; background: var(--card); }
.clp-tool .pb-end { margin-left: auto; display: flex; align-items: center; gap: 6px; }
/* ---- THE ROUND ICON BUTTON, 38px: the composer's controls and the audio player's transport. ---- */
.clp-audio .pbtn,
.clp-tool .pbtn { flex: none; width: 38px; height: 38px; border-radius: 11px; display: flex; align-items: center;
  justify-content: center; color: var(--text-3); background: none; border: none; cursor: pointer;
  transition: color .14s, background .14s; }
.clp-tool .pbtn:hover { color: var(--text); background: var(--hover); }

/* control chips row (model / options / aspect / count) under the bar */
.clp-tool .barmeta { display: flex; align-items: center; justify-content: center; gap: 9px; margin-top: 12px; flex-wrap: wrap; }
.clp-tool .tchip,
.clp-vmodal .tchip { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 11px; border: 1px solid var(--border-strong); border-radius: var(--r-pill); font-size: var(--fs-sm); color: var(--text-2); background: var(--card); cursor: pointer; transition: border-color .14s, background .14s, color .14s; }
.clp-tool .tchip:hover,
.clp-vmodal .tchip:hover,
.clp-drop:hover, .clp-drop.over { border-color: var(--border-struct); background: var(--hover); }
.clp-tool .tchip .tchip-av,
.clp-vmodal .tchip .tchip-av { width: 18px; height: 18px; border-radius: 5px; flex: none; margin-left: -2px; overflow: hidden; }
.clp-tool .tchip .tchip-av .mask { width: 12px; height: 12px; }
.clp-tool .tchip .tchip-av > img, .clp-tool .tchip .tchip-av > canvas { border-radius: 5px; }
.clp-tool .tchip .mono,
.clp-vmodal .tchip .mono { font-family: var(--mono); font-size: var(--fs-xs); }
.clp-tool .tchip svg.ic,
.clp-vmodal .tchip svg.ic { width: var(--icon-sm); height: var(--icon-sm); }
.clp-tool .tchip.ghost { border-color: transparent; color: var(--text-3); }
.clp-tool .tchip.ghost:hover { color: var(--text); background: var(--hover); }
.clp-tool .tchip.active { border-color: var(--accent); color: var(--text); }

/* ---- THE TOOL COLUMN, 1024px and wider (Image and Video on the side column page). Top to bottom: the step row, the
   model as a field, the prompt box with only the page's round buttons, the attachments, then the options panel always
   open with no box of its own; at the foot, fixed under the scrolling part behind a hairline, the price strip and
   Generate at the foot's full width. A control drawn for the floating bar (`bar-only`) is not drawn here, and one drawn
   for the column (`col-only`) is not drawn below 1024px. The results take the rest: the queue, Recent and the days. ---- */
.clp-tool { --side-col-w: var(--tool-col-w); }
@media (min-width: 1024px) {
  #content[data-layout="side"] .clp-tool { flex: 1; min-height: 0; display: flex; }
  [data-layout="side"] .clp-tool .bar-only { display: none; }
  .clp-tool .side-col-in { padding: 16px 16px 0; }
  .clp-tool .side-col .composer { display: flex; flex-direction: column; gap: 12px; }
  .clp-tool .side-col .clp-inline-opts { margin-top: 16px; }
  .clp-tool .side-col .promptbar textarea { min-height: 118px; }
  .clp-tool .t-surprise { margin-left: auto; }
  /* a prompt box with no round button of its own (Video) draws no button row */
  .clp-tool .side-col .pb-tools:not(:has(> :not(template))) { display: none; }
  .clp-tool .side-col .pb-scroll:has(> .pb-tools > :not(template)) { --pb-row: 44px; }
  .clp-tool .t-foot { flex: none; display: flex; flex-direction: column; gap: 12px; padding: 16px; border-top: 1px solid var(--border); }
  .clp-tool .t-why { font-size: var(--fs-sm); color: var(--red-hi); }
  .clp-tool .side-main { display: flex; flex-direction: column; }
  /* the results scroll, so each part keeps its own height; a part that clips its corners (the run queue) would else shrink flat */
  .clp-tool .side-main > * { flex: none; }
  /* the opening, shown while nothing has finished, stands in the middle of the results */
  .clp-tool .side-main:has(> .hero) { justify-content: center; }
}
@media (max-width: 1023px) {
  [data-layout="side"] .clp-tool .col-only { display: none; }
}
/* the model as a field: its mark, its name and the select chevron; its menu opens under it at its width */
.clp-tool .clp-msel.is-field { position: relative; display: block; }
.clp-tool .msel-field { display: flex; align-items: center; gap: 10px; width: 100%; height: 40px; padding: 0 12px;
  border: 1px solid var(--border-strong); border-radius: var(--r); background: var(--card);
  font-size: var(--fs-md); font-weight: 500; color: var(--text); text-align: left; cursor: pointer;
  transition: border-color .14s, background .14s; }
.clp-tool .msel-field > span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clp-tool .msel-field .tchip-av { width: 22px; height: 22px; border-radius: var(--r-sm); flex: none; overflow: hidden; }
.clp-tool .msel-field .tchip-av .mask { width: 14px; height: 14px; }
.clp-tool .msel-field svg.ic { width: var(--icon); height: var(--icon); color: var(--text-4); }
/* the results by day: each day's label over its wall */
.day-grp + .day-grp { margin-top: 26px; }
.day-head { display: block; margin-bottom: 12px; }

/* ---- THE SECTION HEAD, one row: a title on the left, an optional action on the right. ---- */
.sec-head { display: flex; align-items: center; justify-content: space-between; margin: 34px 0 14px; }
/* ---- its title, in the UI face on every page: the #content prefix beats the page-wide heading face in css-variables ---- */
#content .sec-head h2 { font-family: var(--font); font-size: var(--fs-md); font-weight: 600; letter-spacing: -.01em; color: var(--text); }
/* ---- its trailing action ---- */
#content .sec-head a { font-size: var(--fs-sm); color: var(--text-3); display: inline-flex; align-items: center; gap: 4px; cursor: pointer; }
#content .sec-head a:hover { color: var(--text); }

.clp-wb .r-meta { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-3); }
/* THE ICON BUTTON (`snippets/clp/act.twig`), declared once and scoped `#content`, which every skinned surface sits
   inside: the result window, the writer's document, the text rows and the speech and voiceover rows. */
#content .r-act { color: var(--text-3); cursor: pointer; transition: color .14s; background: none; border: none; display: inline-flex; align-items: center; padding: 0; }
#content .r-act:hover { color: var(--text); }
#content .r-act.del:hover { color: var(--red-hi); }
#content .r-act svg.ic { width: var(--icon-md); height: var(--icon-md); }

/* ------------------------------------------------------------
   THE LEDGER'S SELECT BOX, `.led-sel`: the ledger's rows (Voiceover's Recent) and Chat's
   history drawer. Every other row draws the app's one checkbox (`snippets/clp/pick.twig`).
   It lives IN the actions cell, on the right, beside Delete.

   THE BOX ITSELF IS COMPONENT-NEUTRAL, scoped by its own class rather than by a component.
   Only the REVEAL and PICKED rules below are per-family, because only those differ.

   AND IT CARRIES NO `#content` PREFIX, which is what makes the third family possible at all. The
   sidebar is a SIBLING of `#content` in the layout, not a child (`layouts/main.twig`, the div
   closes before the aside opens), so a `#content` rule reaches nothing inside a drawer. Every
   `.clp-drawer` rule in this file is unprefixed for the same reason. Core's own stylesheets carry
   none of these class names, so there is nothing for the shorter selector to lose to.
   ------------------------------------------------------------ */
.led-sel {
  position: relative; z-index: 2; align-self: center;
  width: 18px; height: 18px; display: grid; place-items: center; padding: 0;
  border-radius: var(--r-sm); cursor: pointer;
  background: transparent; border: 1.5px solid var(--border-struct);
  opacity: 0; transition: background .14s, border-color .14s, opacity .16s;
}
.led-sel svg.ic { width: 11px; height: 11px; fill: var(--on-fill); opacity: 0; transition: opacity .12s; }
.led-sel:hover { border-color: var(--text-3); }

/* THE CELL STOPS HIDING, ITS CONTENTS DO NOT. The rule above at `.led-row .led-acts` fades the
   whole cell in on hover. Left alone, a box living in that cell would be invisible until each
   row was hovered in turn, so a selection could never be seen. The cell stays up on any list
   that carries a box, and each control inside decides for itself. */
#content .led.has-sel .led-row .led-acts { opacity: 1; }

/* Up when you point at the row, when it is picked, and once anything on the surface is picked.
   That last clause is what makes a right-hand box legible: a selection you cannot see is not a
   selection you can trust. */
#content .led .led-row:hover .led-sel,
#content .led .led-row.is-on .led-sel,
#content .led.is-picking .led-sel { opacity: 1; }

#content .led-row.is-on .led-sel { background: var(--accent); border-color: var(--accent); }
#content .led-row.is-on .led-sel svg.ic { opacity: 1; }

/* THE DRAWER IS THE THIRD FAMILY. Unprefixed, because the sidebar is not inside `#content`.
   Same three reveal clauses the other two families get: under the pointer, when picked, and
   once anything on the surface is picked. */
.clp-drawer .drawer-row:hover .led-sel,
.clp-drawer .drawer-row.is-on .led-sel,
.clp-drawer .drawer-list.is-picking .led-sel { opacity: 1; }
.clp-drawer .drawer-row.is-on .led-sel { background: var(--accent); border-color: var(--accent); }
.clp-drawer .drawer-row.is-on .led-sel svg.ic { opacity: 1; }
/* Writer's drawer draws the app's one checkbox, with the same reveals and the keyboard's. */
.clp-drawer .drawer-row .clp-chk { opacity: 0; transition: opacity .16s; }
.clp-drawer .drawer-row:is(:hover, :focus-within, .is-on) .clp-chk,
.clp-drawer .drawer-list.is-picking .clp-chk { opacity: 1; }

/* PICKED IS A RAIL, AND IT BRINGS NO BACKGROUND WITH IT.
   `.on` and `.is-on` are two different facts about one drawer row and both can be true at once:
   `.on` is the run open on screen and already owns `--accent-fill`, `.is-on` is picked. Giving
   picked a tint of its own put it in the same channel as hover and it LOST: `--raised` sits one
   step short of `--hover`, so a picked row read fainter than a row under the pointer. The rail is
   a channel nothing else uses, so a row that is open and picked shows both without either
   winning, and no `.on.is-on` rule is needed to arbitrate. `--r-sm` on a 2px bar rounds its ends
   completely. */
.clp-drawer .drawer-row.is-on::before {
  content: ""; position: absolute; inset-block: 6px; inset-inline-start: 0; width: 2px;
  border-radius: var(--r-sm); background: var(--accent); z-index: 3;
}
#content .led-row.is-on { background: var(--accent-fill); }

/* Delete keeps the shipped behaviour on a list that carries a box: the row under the pointer
   only. Picking a row is not a reason to put a trash can on every line. */
#content .led.has-sel .led-row .r-act.del { opacity: 0; transition: opacity .16s, color .14s; }
#content .led.has-sel .led-row:hover .r-act.del,
#content .led.has-sel .led-row:focus-within .r-act.del { opacity: 1; }

/* A TOUCH DEVICE HAS NO HOVER TO REVEAL ANYTHING WITH, so both controls stay up. The media wall
   already makes exactly this exception for its own action row. Without this, the two rules above
   would hide the delete on every phone.

   THE BOX RULE IS UNPREFIXED, like the appearance it belongs to. "Every select box stays up on a
   touch device" is true of all three families, so it is stated once here rather than repeated per
   family. Only the DELETE half is per-family, because each family names its delete differently. */
@media (hover: none) {
  .led-sel,
  #content .led.has-sel .led-row .r-act.del { opacity: 1; }
}

/* ---- Imagine flagship additions (plan Task 3): control-chip parts, toolbar buttons, recent
   gallery (flat square tiles from the history binding), result focus view, New/Edit action pills.
   All scoped under .clp-tool, light+dark via tokens. ---- */

/* icon+label inline pair, used in the options summary chip and the result meta row */
.clp-tool .cpart { display: inline-flex; align-items: center; gap: 5px; }
.clp-tool .tchip .cdot { width: 5px; height: 5px; border-radius: var(--r-pill); background: var(--red); flex: none; }

/* ---- model command menu (spans the composer column and drops below it; replaces the stock models modal) ---- */
.clp-tool .clp-msel,
.clp-vmodal .clp-msel { position: static; display: inline-flex; }
/* The shadow is the dialog height from the tokens, not a literal, so it deepens on black
   the way every other floating surface in the app does. It was the one menu that did not. */
/* a menu under its picker drops below it as a floating card */
.clp-tool .clp-msel > .clp-menu,
.clp-vmodal .clp-msel > .clp-menu { position: absolute; top: calc(100% + 8px); left: 0; right: 0; z-index: 40; background: var(--card); border: 1px solid var(--border-strong); border-radius: var(--r-lg); box-shadow: var(--lift-dialog); overflow: hidden; text-align: left; }
.clp-menu-search { display: flex; align-items: center; gap: 9px; height: 40px; padding: 0 13px; border-bottom: 1px solid var(--border); color: var(--text-3); }
.clp-menu-search svg.ic { width: var(--icon); height: var(--icon); }
.clp-menu-search input { flex: 1; min-width: 0; border: none; background: none; outline: none; font-size: var(--fs-body); color: var(--text); }
.clp-menu-search input::placeholder { color: var(--text-4); }
.clp-menu-list { overflow-y: auto; padding: 5px; }
/* a list in a dropdown or a dialog stops at 296px; in the floating bar's card it takes the card */
.clp-msel > .clp-menu .clp-menu-list,
.clp-modal .clp-menu-list { max-height: 296px; }
.clp-menu-row { position: relative; display: flex; align-items: center; border-radius: var(--r); }
.clp-menu-row:hover { background: var(--hover); }
.clp-menu-row.on { background: var(--accent-fill); }
/* WITHHELD BY THE PLAN, greyed so the list reads as a list. It lifts on hover so the name stays
   readable, draws the lock, and sends the click to billing. It is the only unchoosable row state:
   a model the health monitor has found broken is drawn exactly like a healthy one and is fully
   selectable, because nothing takes a model away from a customer any more. */
.clp-menu-row.locked:not(.on) { opacity: .6; }
.clp-menu-row.locked:not(.on):hover { opacity: 1; }
/* THE NAVIGATION ROW (snippets/clp/nav-row.twig), the menu row in two lines: the name with its tag
   over its line. The top bar's panels and the phone menu draw every tool and model with it; `ic`
   puts the tool's glyph beside the text. */
.clp-menu-row.nav { display: block; min-width: 0; padding: 8px 10px; }
/* a model row sits a step tighter than a tool row, in the top bar and the phone menu alike */
.clp-menu-row.nav.model { padding: 7px 10px; }
.clp-menu-row.nav.ic { display: flex; gap: 12px; align-items: flex-start; }
.clp-menu-row.nav.ic > i { font-size: var(--icon-md); line-height: 1; color: var(--text-3); margin-top: 1px; }
.clp-menu-row.nav.ic:hover > i { color: var(--text); }
.clp-menu-row.nav .l1 { display: flex; align-items: center; gap: 8px; }
.clp-menu-row.nav .nm { font-size: var(--fs-body); font-weight: 500; color: var(--text); white-space: nowrap; }
.clp-menu-row.nav .ln { display: block; margin-top: 1px; font-size: var(--fs-sm); color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.clp-mr-hit { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; padding: 8px 9px; background: none; border: none; cursor: pointer; text-align: left; color: inherit; }
.clp-mr-av { width: 28px; height: 28px; border-radius: 7px; flex: none; overflow: hidden; }
.menu .menu-item .ident-av,
.sidebar .acct-row .ident-av,
.clp-mnav .ident-av { width: 28px; height: 28px; flex: none; }
.clp-mr-av .mask { width: 17px; height: 17px; }
.clp-mr-av > img, .clp-mr-av > canvas { border-radius: 7px; }
/* THE IDENTITY AVATAR, everywhere a person or a workspace is shown: Clapyo Blue with white initials,
   under any photo, in the logo mark's shape at every size. The whole look is this rule; core's <x-avatar>
   only writes the initials or the photo. A place sets the size; the initials are 3/7 of it. */
.ident-av { position: relative; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 40px; height: 40px; container-type: inline-size; border-radius: var(--r-mark); overflow: hidden;
  background-color: var(--accent); color: var(--on-fill);
  font-weight: 700; line-height: calc(1 / .75); text-transform: uppercase; white-space: nowrap; }
.ident-av > span { font-size: calc(100cqi * 3 / 7); }
.ident-av > img, .ident-av > canvas { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  border-radius: inherit; }
.ident-av > img { z-index: 1; }
.clp-mr-txt { min-width: 0; flex: 1; display: flex; align-items: baseline; gap: 7px; }
.clp-mr-txt .nm { font-size: var(--fs-body); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.clp-mr-txt .pv { font-size: var(--fs-xs); color: var(--text-4); white-space: nowrap; flex: none; }
.clp-mr-meta { display: flex; align-items: center; gap: 7px; flex: none; color: var(--text-4); }
.clp-mr-meta svg.ic { width: var(--icon-sm); height: var(--icon-sm); }
/* the model tag, one dress for the model picker, the top bar's panels and the phone menu; TOP takes the accent step and edge */
.mtag { font-family: var(--mono); font-size: var(--label-size); letter-spacing: .03em; color: var(--text-3); border: 1px solid var(--border-strong); border-radius: 4px; padding: 1px 5px; }
.mtag.top { color: var(--accent-hi); border-color: var(--accent-bd); }
.clp-mr-chk { color: var(--accent-hi); }
.clp-mr-tools { position: absolute; right: 7px; top: 50%; transform: translateY(-50%); display: none; align-items: center; gap: 4px; padding-left: 10px; background: var(--hover); }
.clp-menu-row:hover .clp-mr-tools { display: flex; }
.clp-mr-tools a, .clp-mr-tools button { display: grid; place-items: center; width: 24px; height: 24px; border-radius: var(--r-sm); color: var(--text-4); background: none; border: none; cursor: pointer; }
/* a button on a row under the pointer fills to `raised`, so it still shows on the row's `hover` fill */
.clp-mr-tools a:hover, .clp-mr-tools button:hover,
#content :is(.rec, .mrow, .cr-row) :is(.kebab, .iconbtn):hover { color: var(--text); background: var(--raised); }
.clp-mr-tools button.on { color: var(--text); }
.clp-menu-foot { display: flex; align-items: center; gap: 14px; padding: 8px 13px; border-top: 1px solid var(--border); font-family: var(--mono); font-size: var(--label-size); color: var(--text-4); }
.clp-menu-foot .kbd { border: 1px solid var(--border-strong); border-radius: 4px; padding: 0 4px; color: var(--text-3); }
/* the keyboard hints mean nothing on a touch screen */
@media (hover: none) { .clp-menu-foot { display: none; } }

/* ---- inline options (unfolds in the composer flow; replaces the stock options modal) ---- */
/* ---- THE PANEL TAKES THE TOOL COLUMN, NOT THE PROMPT'S MEASURE.

   840 is not a new number: it is `.clp-tool`'s own max-width, the column this page is laid out in.
   The panel had asked for 640 since it shipped, which is already wider than --composer-w's 600, and
   never got it, because `.composer` wrapped it and capped it. It is now a sibling of that form
   inside `.clp-composer`, which owns the Alpine state both of them read.

   NO NEGATIVE MARGIN AND NO BREAKPOINT. Escaping the composer in CSS alone would need a literal 840
   to offset against, and that offset overflows every viewport where `.clp-tool` is narrower than
   840, which starts at 1156px (840 + the 252 sidebar + the 64 inset) and moves again at 1023px
   where the sidebar is replaced. As a child of `.clp-tool` the panel is simply as wide as the
   column is, at every width, with nothing to keep in sync. ---- */
/* ONE GRID FOR THE WHOLE PANEL: a name column as wide as the card's longest setting name, and the
   controls. Every part spans both columns and every row takes them through `subgrid`, so each name
   stays on one line and each control starts at the same place, whichever band the row sits in. A
   row is a name and one control. Every part also carries an `order` in the cost-leads block below,
   which is what lets the upload rows sit in either band without being written twice. */
.clp-tool .clp-inline-opts { container: opts / inline-size; display: grid; grid-template-columns: max-content minmax(0,1fr); column-gap: 14px; width: 100%; text-align: left; }
/* ---- THE FLOATING BAR, the side column below 1024px: one `card` with the strong border, 16px corners and the dialog
   shadow, at most --column-w wide and 12px in from the screen's sides and bottom. At rest it is the prompt over its
   button row: the add files button on Video, the model chip, the settings chip and Surprise me on the left, the cost
   and Generate on the right; on a phone the model chip is its mark alone and Surprise me is not drawn. Settings grow it
   upward into one card over the one overlay, the price strip and the options panel on top, scrolling inside with the
   22px edge fade; a tap outside or Escape closes it. While anything on the wall is picked, the pick bar takes its place.
   ---- */
@media (max-width: 1023px) {
  [data-layout="side"] .side-col { position: fixed; z-index: var(--z-pickbar); left: 50%; bottom: 12px; transform: translateX(-50%);
    width: min(var(--column-w), calc(100vw - 24px)); max-height: calc(100dvh - 64px - 24px); display: flex; flex-direction: column;
    border-radius: var(--r-xl); box-shadow: var(--lift-dialog); }
  [data-layout="side"] .side-col.is-open { z-index: var(--z-overlay); }
  [data-layout="side"] .clp-tool:has(> .side-col.is-open)::before { content: ''; position: fixed; inset: 0; z-index: var(--z-overlay);
    background: var(--scrim); backdrop-filter: blur(var(--overlay-blur)); -webkit-backdrop-filter: blur(var(--overlay-blur)); }
  [data-layout="side"] .clp-tool:has(.clp-selbar.on) > .side-col { display: none; }
  /* the results leave room under them for the bar */
  [data-layout="side"] .side-main { padding-bottom: 128px; }
  [data-layout="side"] .side-col-in,
  .clp-tool .t-models .clp-menu { flex: 1; min-height: 0; display: flex; flex-direction: column; }
  .clp-tool .side-col .composer { flex: none; }
  /* the model list fills the card above the prompt, its rows scrolling inside */
  .clp-tool .t-models { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; border-bottom: 1px solid var(--border); }
  .clp-tool .t-models .clp-menu-list { flex: 1; min-height: 0; }
  /* the grown part (`.bar-grow`): what the bar's card opens above the bar, the settings or a template's setup */
  .clp-tool .side-col .bar-grow { order: -1; flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 16px 18px; border-bottom: 1px solid var(--border); border-radius: var(--r-xl) var(--r-xl) 0 0; }
  .clp-tool .side-col .clp-inline-opts > .clp-io-price { margin: -16px -18px 0; padding: 15px 18px 14px; border-bottom: 1px solid var(--border); }
  .clp-tool .clp-io-band { margin: 0 -18px; }
  /* the bar's button row, under the prompt: it wraps only when it does not fit, the cost and Generate then taking a row
     of their own */
  .clp-tool .pb-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-height: 44px; padding: 2px 4px 4px; white-space: nowrap; }
  .clp-tool .pb-chips { display: flex; align-items: center; gap: 6px; }
}
/* the template page's setup chip in the floating bar: it takes the row's room left of Generate, its answers fading at the
   edge while more lies past them */
.clp-tool .t-setup-slot { flex: 1 1 0; min-width: 0; display: flex; }
.clp-tool .t-setup-chip { min-width: 0; max-width: 100%; }
.clp-tool .t-setup-parts { display: flex; gap: 6px; min-width: 0; overflow: hidden; }
@supports (animation-timeline: scroll()) and (mask-image: none) {
  .clp-tool .t-setup-parts { mask-image: linear-gradient(to right, #000 calc(100% - var(--fade-end)), transparent);
    animation: fade-end linear both; animation-timeline: scroll(self inline); animation-range: calc(100% - 22px) 100%; }
}
@media (max-width: 640px) {
  .clp-tool .pb-chips .clp-msel .tchip > span,
  .clp-tool .pb-chips .t-shuffle,
  .clp-tool .t-cost-word { display: none; }
}
.clp-tool .clp-inline-opts > * { grid-column: 1 / -1; }
.clp-tool .clp-io-paid,
.clp-tool .clp-io-ups,
.clp-tool .clp-io-free { display: grid; grid-template-columns: subgrid; }
/* THE MICRO-LABEL on the inline options panel, the 10px mono eyebrow. It labels the floor line in
   the price strip; the panel head it used to label is gone, replaced by that strip. */
.clp-tool .clp-io-reset { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 9px; font-family: var(--mono); font-size: var(--label-size); letter-spacing: .04em; color: var(--text-3); background: var(--card); border: 1px solid var(--border-strong); border-radius: var(--r-sm); cursor: pointer; }
.clp-tool .clp-io-reset:hover { color: var(--text); background: var(--hover); }
.clp-tool .clp-io-reset svg.ic { width: var(--icon-xs); height: var(--icon-xs); }
.clp-tool .clp-io-grp { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: start; padding: 12px 0; border-top: 1px solid var(--border); }
.clp-tool .clp-io-grp:first-of-type { border-top: none; }
.clp-tool .clp-io-lab { display: flex; align-items: center; gap: 8px; padding-top: 6px; font-size: var(--fs-sm); color: var(--text-2); }
.clp-tool .clp-io-lab svg.ic { width: var(--icon-sm); height: var(--icon-sm); color: var(--text-3); }
.clp-tool .clp-io-segs { display: flex; flex-wrap: wrap; gap: 7px; flex: 1; }
.clp-tool .clp-seg,
.radio-button + span { display: inline-flex; align-items: center; height: 30px; padding: 0 12px; border: 1px solid var(--border-strong); border-radius: var(--r-sm); background: var(--card); color: var(--text-2); font-size: var(--fs-sm); cursor: pointer; transition: border-color .14s, background .14s, color .14s; }
/* ---- ONE hover treatment. Nine surfaces raise the same way: the fill lifts to --hover,
   the label goes full strength, and the border firms to --border-struct. It was
   written nine times across five page scopes, in two different property orders. ---- */
.ebtn:hover,
#content .mbtn:hover,
.clp-page .clp-more button:hover,
.clp-tool .tchip.open,
.clp-vmodal .tchip.open,
.clp-tool .msel-field:hover,
.clp-tool .msel-field.open,
.clp-tool .clp-seg:hover,
#content .t-actions .apill:hover,
.clp-chat .chip.src:hover,
.clp-chat .files .chip:hover,
.radio-button + span:hover,
.clp-bill .off-row:hover { background: var(--hover); color: var(--text); border-color: var(--border-struct); }
/* ---- THE SINGLE-CHOICE PILL: the writer's Tone (12 marks). It asks
   the question the options panel above asks, so it wears that answer. The three rules the
   two share are folded into .clp-seg's own, in .clp-seg's order, so a chosen mark does not
   lift when the pointer crosses it. Core fills the chosen mark with --color-button, which
   is what this app means by text; the accent is what it means by chosen, and the model menu's
   row, the duration rail and the effort menu's check all already say so.

   The input is moved out of sight rather than out of the document. Core sets display:none,
   and a control that is not rendered cannot take focus, so all 12 marks on the writer form
   are unreachable by keyboard. Hidden this way they keep their place in the tab order, and
   the visible mark wears the app's focus ring for them. ---- */
.radio-button,
.clp-drop-in { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.radio-button:checked + span { cursor: default; }
.radio-button:focus-visible + span { border-color: var(--accent); box-shadow: var(--focus-ring); }
.radio-button:disabled + span { opacity: .5; cursor: not-allowed; }
/* Duration: a TICK RAIL, one mark per second the model actually sells. The pills beside it are
   discrete choices and a track is continuous, which is why a slider read as a second interaction
   language here no matter how it was coloured; a rail of marks is discrete too, while still being
   one drag.

   The native range is kept and made transparent on top, so dragging, keyboard stepping, the clamp
   and the step grid are exactly what they were, and no pricing behaviour moves. The visible mark
   is driven by the VALUE rather than by thumb geometry, so the highlight can never drift by a
   pixel from the duration that is quoted and charged.

   Marks flex, so a 9-stop model and grok's 25-stop range both fill the same width; they thin out
   rather than overflow. */
.clp-tool .clp-io-slider { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; height: 30px; }
/* the rail: the marks or the continuous track, with the range over them */
.clp-tool .clp-io-rail { position: relative; flex: 1; min-width: 0; height: 30px; display: flex; align-items: center; }
.clp-tool .clp-ticks { flex: 1; min-width: 0; height: 30px; display: flex; align-items: center; justify-content: space-between; gap: 3px; }
/* Capped so the control looks the same on every model. Without it the mark width is just the
   rail divided by the stop count, and a 9-stop model (2 to 10s) renders 60px slabs that read as a
   segmented bar while grok's 25 stops render as fine ticks: the same control, two shapes. Capped,
   the extra room becomes gap instead of mark. */
.clp-tool .clp-tk { flex: 1 1 0; min-width: 2px; max-width: 24px; height: 14px; border-radius: var(--r-xs); background: var(--border-strong); transition: background .14s, height .14s; }
.clp-tool .clp-tk.past { background: var(--accent-bd); }
.clp-tool .clp-tk.now { height: 22px; background: var(--accent); }
/* Hover lifts only the unchosen marks, so the selection stays the one accent thing on the rail. */
.clp-tool .clp-io-rail:hover .clp-tk { background: var(--border-struct); }
.clp-tool .clp-io-rail:hover .clp-tk.past { background: var(--accent-bd); }
.clp-tool .clp-io-rail:hover .clp-tk.now { background: var(--accent); }
/* The input carries the focus ring for the whole control, because it is the thing that takes
   focus and its own thumb is invisible. Same ring as .clp-seg and .input. */
.clp-tool .clp-range { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; background: none; border: none; outline: none; cursor: pointer; opacity: 0; -webkit-appearance: none; appearance: none; }
.clp-tool .clp-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 14px; height: 30px; }
.clp-tool .clp-range::-moz-range-thumb { width: 14px; height: 30px; border: none; }
.clp-tool .clp-range-val { flex: none; width: 56px; height: 30px; padding: 0 6px; text-align: center; font-family: inherit; outline: none; border: 1px solid var(--accent-bd); border-radius: var(--r-sm); background: var(--accent-fill); color: var(--accent-hi); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.clp-tool .clp-io-in { flex: 1; min-width: 0; height: 38px; padding: 0 13px; border: 1px solid var(--border-control); border-radius: var(--r); background: var(--card); color: var(--text); font-size: var(--fs-body); outline: none; }
.clp-tool .clp-io-in::placeholder { color: var(--text-4); }

/* THE THREE CONTROL KINDS THE PANEL GAINED when the generator started declaring every setting kie
   documents. Ported from the design round Hatim approved, not re-invented. All three read existing
   tokens and follow the shapes already above: the 30px control height of .clp-seg, the panel's
   shared name column, the accent as the only colour. */

/* ON/OFF: the app's one switch, 4 pixels down so it sits centred in .clp-seg's 30px height and a
   toggle row lines up with a choice row. */
.clp-tool .clp-io-swrow { display: flex; align-items: flex-start; gap: 10px; flex: 1; min-width: 0; }
.clp-tool .clp-io-swrow .clp-sw { margin-top: 4px; }

/* PAST 25 STOPS THE RAIL IS ONE TRACK: it carries the value as `--pos`, a percentage; the fill runs to it and the knob
   sits on it. */
.clp-tool .clp-io-track { position: relative; flex: 1; min-width: 0; height: 4px; border-radius: var(--r-pill); background: var(--border-strong); }
.clp-tool .clp-io-fill { position: absolute; left: 0; top: 0; bottom: 0; width: var(--pos); border-radius: var(--r-pill); background: var(--accent-bd); }
.clp-tool .clp-io-knob { position: absolute; top: 50%; left: var(--pos); transform: translate(-50%, -50%); }
/* a slider with no value yet: no fill and no knob, its box reading Auto */
.clp-tool .clp-io-track.is-unset > :is(.clp-io-fill, .clp-io-knob) { display: none; }

/* NUMBER, UNBOUNDED. A seed is an identifier, not a magnitude: no rail, a typed box and the one
   control that matters, a way to get a fresh value. */
.clp-tool .clp-io-seed { display: flex; align-items: center; gap: 7px; flex: 1; min-width: 0; }
.clp-tool .clp-io-seed .clp-io-in { font-family: var(--mono); font-size: var(--fs-sm); }

/* ---- COST LEADS. The panel's head is the run's cost, and the settings that move it are drawn above the ones that do
   not. ---- */

/* THE ORDER OF THE PANEL, declared once here rather than by source position, because the upload
   rows are written once and belong in a different band depending on the card. 4 and 7 are the two
   places that block can sit; everything else is fixed. */
.clp-inline-opts > .clp-io-price { order: 1; }
.clp-inline-opts > .clp-io-band.paid { order: 2; }
.clp-inline-opts > .clp-io-paid { order: 3; }
.clp-inline-opts > .clp-io-ups { order: 4; }
.clp-inline-opts > .clp-io-band { order: 5; }
.clp-inline-opts > .clp-io-free { order: 6; }
.clp-inline-opts > .clp-io-ups.free { order: 7; }

.clp-tool .clp-io-price { display: flex; flex-direction: column; gap: 8px; }
.clp-tool .clp-io-price-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.clp-tool .clp-io-price .amt {
  font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w); letter-spacing: var(--display-ls);
  font-size: var(--fig-md); line-height: 1; color: var(--text); font-variant-numeric: tabular-nums;
}
.clp-tool .clp-io-price .amt em { font-style: normal; font-size: var(--fs-body); font-weight: 450; color: var(--text-3); margin-left: 6px; letter-spacing: 0; }
/* the chips run from the left under the floor line, as many as the selection earns, and none takes a row */
.clp-tool .clp-io-price .why { display: flex; flex-wrap: wrap; gap: 6px; }
.clp-tool .clp-io-price .why:not(:has(.wchip)) { display: none; }
.clp-tool .clp-io-price .wchip {
  display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 9px;
  border: 1px solid var(--accent-bd); border-radius: var(--r-pill); background: var(--accent-fill);
  font-size: var(--fs-xs); color: var(--accent-hi);
}
.clp-tool .clp-io-price .wchip .x { font-family: var(--mono); font-size: var(--label-size); color: var(--accent-hi); }

.clp-tool .clp-io-band {
  display: flex; align-items: center; gap: 9px; padding: 9px 18px;
  border-bottom: 1px solid var(--border); background: var(--sunken);
  font-family: var(--mono); font-size: var(--label-size); letter-spacing: var(--label-ls); text-transform: uppercase; color: var(--text-4);
}
.clp-tool .clp-io-band.paid { color: var(--accent-hi); background: var(--accent-fill); }
.clp-tool .clp-io-band svg.ic { width: var(--icon-xs); height: var(--icon-xs); }

/* AN UPLOAD ROW KEEPS ITS RULE wherever the block lands. It is never the top of its band: in the
   paid position it follows the settings that move the cost, and in the free position it follows the
   other settings. The panel-wide `:first-of-type` reset would strip it in both. */
.clp-tool .clp-io-ups > .clp-io-grp { border-top: 1px solid var(--border); }

/* AN ASPECT RATIO SHOWS ITS SHAPE: an outline of the ratio in a square frame (`snippets/clp/shape.twig`),
   in the colour of the words beside it, dashed for Auto. HD is a mono tag after the ratio. */
/* an aspect ratio's outline (snippets/clp/shape.twig): the template passes the frame as `--box`, the
   outline as `--w` and `--h` */
.clp-shape { display: inline-flex; align-items: center; justify-content: center; flex: none; width: var(--box); height: var(--box); }
.clp-shape i { display: block; width: var(--w); height: var(--h); border: 1.5px solid currentColor; border-radius: var(--r-xs); opacity: .8; }
.clp-shape.auto i { border-style: dashed; }
.clp-tool .clp-seg .clp-shape { margin-right: 7px; }
.clp-tool .clp-seg .segtag { margin-left: 5px; font-family: var(--mono); font-size: var(--label-size); letter-spacing: .04em; color: var(--text-4); }
.clp-tool .clp-seg.on .segtag { color: var(--accent-hi); }
/* A NARROW PANEL stacks each setting: its name on top, its choices across the full width, and an on/off setting keeping
   its switch beside its name. Narrow is the panel's own width: 578px is its content at a 640px window (579 at 641,
   measured), so a phone and a 640px window stack as before; the tool column's 347 always does. */
@container opts (max-width: 578px) {
  .clp-tool .clp-io-grp { display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
  .clp-tool .clp-io-lab { padding-top: 0; }
  .clp-tool .clp-io-grp > :not(.clp-io-lab) { flex: none; }
  .clp-tool .clp-io-grp:has(> .clp-io-swrow) { flex-direction: row; align-items: center; }
  .clp-tool .clp-io-grp:has(> .clp-io-swrow) .clp-io-lab { flex: 1; }
  .clp-tool .clp-io-swrow .clp-sw { margin-top: 0; }
}
/* `min-width: 0` IS WHAT KEEPS THE ROW INSIDE THE PANEL. A flex item defaults to `min-width: auto`
   and refuses to shrink below its own content, so anything long inside this box widens it past the
   panel and the dashed control leaves the right edge of the page. Nothing in the row is long enough
   to do that today; this is what says it cannot start. */
.clp-tool .clp-io-uploads { display: flex; flex-wrap: wrap; gap: 8px; flex: 1; min-width: 0; }
.clp-tool .clp-io-thumb { width: 76px; height: 76px; border-radius: var(--r); border: 1px solid var(--border); background: var(--raised); position: relative; overflow: hidden; }
.clp-tool .clp-io-thumb img { width: 100%; height: 100%; object-fit: cover; }
.clp-tool .clp-io-thumb-x { position: absolute; top: 5px; right: 5px; width: 22px; height: 22px; display: grid; place-items: center; border-radius: var(--r-pill); background: var(--scrim); color: var(--on-fill); border: none; cursor: pointer; }
.clp-tool .clp-io-add { width: 76px; height: 76px; display: flex; flex-direction: column; align-items: flex-start; justify-content: space-between; padding: 12px; border: 1px dashed var(--border-strong); border-radius: var(--r); background: none; color: var(--text-3); font-size: var(--fs-xs); cursor: pointer; }
.clp-tool .clp-io-add:hover, .clp-tool .clp-io-add.over { background: var(--hover); color: var(--text); }
.clp-tool .clp-io-add svg.ic { width: var(--icon); height: var(--icon); }
.clp-tool .clp-io-thumb .clp-io-tag { position: absolute; bottom: 5px; left: 5px; padding: 1px 6px; font-family: var(--mono); font-size: var(--label-size); line-height: 1.5; border-radius: 5px; background: var(--scrim-deep); color: var(--on-fill); letter-spacing: .02em; }
/* ---- A NAMED INPUT'S OWN ROW. A card that takes several files names each one, and each
   named input takes the same row every other setting in this panel takes: kie's own name in
   the name column, one control on the right, one row per thing being set. The picker
   belongs here and not under the prompt box, because Options is where every other card's
   upload has always lived and the strip below the composer only ever showed what was already
   attached.

   THE CONTROL FILLS THE ROW rather than sitting in the 76px `.clp-io-add` square, so the row
   reads as one line per input against the label column beside it.

   IT CARRIES NO DESCRIPTION. The label names the input and the picker accepts only the types the
   slot declares, which leaves a description with nothing true left to add: kie's own is written
   for somebody calling the API, tells the reader to enter a URL where the control takes a file,
   and names kie's `asset://` scheme. A real constraint, a size or a count, is short text written
   from the numbers the slot already carries.

   IT IS THE TEXT FIELD'S SHAPE, NOT A NEW ONE. `.clp-io-in`, the negative-prompt field, is the
   panel's other full-width control in this same column, so this takes its height, its padding,
   its radius and its border weight exactly: 38px, `0 13px`, `--r`, 1px `--border-strong`. The
   one difference is the dash, which is what says drop a file here rather than type here.

   EVERY SIZE AND COLOUR IS ONE THIS PANEL ALREADY USES. 12.5px on the action, the same as
   `.clp-io-lab` and `.clp-seg` sitting directly above it; 11px on the description, the same as
   `.clp-io-add`; `--text-2`, `--text-3` and `--text-4` in the panel's own order of emphasis.
   Nothing is introduced for this row, so the panel reads as one thing at every card. ---- */
/* ---- IT IS UNSCOPED, AND THAT IS THE POINT. Every place a file goes into this app wears this one
   control: the named slot rows on Imagine and Video, and the reference picture on a writer form.
   It was written inside `.clp-tool` when the tool panels were its only home, which is why the form
   was left showing the browser's own `Choose File` widget, the one surface in the whole app that
   did. A second control drawn for the form would have been a second thing to keep in step.

   EVERY COLOUR SITS ON THE CHILD THAT USES IT, never inherited from this row. A form sets its own
   `label` colour, weight and size (`.clp-wb form label`), and it out-specifies a single class, so a
   row that leant on inheritance would read one way in the panel and another in the form. Stating
   them here costs three declarations and makes the control identical wherever it lands. ---- */
.clp-drop { width: 100%; height: 38px; display: flex; align-items: center; gap: 11px; padding: 0 13px; border: 1px dashed var(--border-strong); border-radius: var(--r); background: none; cursor: pointer; text-align: left; font-family: inherit; transition: border-color .14s, background .14s; }
/* a named slot's file input is out of sight but takes the keyboard, and its control wears the focus ring for it */
.clp-drop:has(> .clp-drop-in:focus-visible) { border-color: var(--accent); box-shadow: var(--focus-ring); }
/* A FILE HELD OVER THE CONTROL READS AS A HOVER, because that is what it is: the pointer is on the
   control and about to act on it. Each control takes ITS OWN hover values and not the other's, so
   dragging introduces no appearance of its own and neither control's hover moves. */
.clp-drop svg.ic { width: var(--icon); height: var(--icon); flex: none; color: var(--text-3); }
.clp-drop .lab { flex: none; font-size: var(--fs-sm); font-weight: 400; color: var(--text-2); }

/* ---- THE PICTURE SITS IN THE ROW IT WAS ADDED FROM. A row that only said "saved" left the one
   question a reference picture raises unanswered: which picture. On a rerun the answer was nowhere
   on the page at all, so the only way to check was to open the file again.

   THE ROW STOPS BEING A TARGET ONCE IT IS FULL. Solid rather than dashed, because a dashed edge
   invites a drop and this row no longer takes one; the cross is what changes the answer. ---- */
.clp-drop.filled { border-style: solid; border-color: var(--border-strong); padding-left: 5px; gap: 9px; cursor: default; }
.clp-drop.filled:hover { border-color: var(--border-strong); background: none; }
.clp-drop .shot { width: 28px; height: 28px; border-radius: var(--r-sm); flex: none; display: block; object-fit: cover; object-position: 50% 14%; background: var(--raised); }
.clp-drop.filled .lab { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.clp-drop-x { width: 22px; height: 22px; flex: none; display: grid; place-items: center; padding: 0; border: none; background: none; border-radius: var(--r-sm); cursor: pointer; }
.clp-drop-x:hover { background: var(--hover); }
.clp-drop-x svg.ic { width: var(--icon-xs); height: var(--icon-xs); color: var(--text-4); }
.clp-drop-x:hover svg.ic { color: var(--text-2); }
/* ---- ONE TREATMENT FOR AN INPUT KIE REQUIRES AND THE USER HAS NOT GIVEN, declared once and
   worn by every surface that has to say it. Four surfaces say it, and they have to agree:
   the full-width row a NAMED input uses, the 76px square a card with ONE input uses, the star
   beside either one's name, and the options chip, which is the only one visible while the
   panel is shut. 34 cards declare a required upload with one input and 15 declare several, so
   this is most of the catalogue, not an edge.

   IT IS THE APP'S OWN RED, NOT A NEW ONE. `--red-fill` and `--red-bd` are the pair every
   other failure surface reads, so this deepens on black with them rather than carrying its
   own alpha, and its words read `--red-hi`, the step every red word reads.

   WHY IT IS WORTH COLOURING AT ALL: kie refuses the request at its door when a required file
   is missing, and that refusal reads as our bug unless the gap is visible before the user
   presses send. Optional and empty stays grey, a resting state and not a warning. ---- */
.clp-drop.need,
.clp-tool .clp-io-add.need { border-color: var(--red-bd); background: var(--red-fill); color: var(--red-hi); }
.clp-drop.need:hover,
.clp-tool .clp-io-add.need:hover { border-color: var(--red); background: var(--red-fill); color: var(--red-hi); }
.clp-drop.need .lab,
.clp-drop.need svg.ic,
.clp-tool .clp-io-req.need,
.clp-tool .tchip .cpart.need { color: var(--red-hi); }
/* The chip is the same shape it always was, wearing the same border colour the rows wear. It
   keeps its own `.cdot`, which means something else entirely on this chip (the settings differ
   from the model's defaults), so the count beside it is what carries this meaning and the dot
   is left alone. */
.clp-tool .tchip.need { border-color: var(--red-bd); }
.clp-tool .tchip.need:hover { border-color: var(--red); }

/* attached-image strip under the prompt (small preview of `images`; the promptbar icon stays as the count cue) */
:is(.clp-tool, .clp-lb) .clp-attached { display: flex; flex-wrap: wrap; gap: 8px; }
:is(.clp-tool, .clp-lb) .clp-att { position: relative; display: block; width: 50px; height: 50px; border-radius: var(--r); border: 1px solid var(--border); background: var(--raised); overflow: hidden; }
:is(.clp-tool, .clp-lb) .clp-att img { width: 100%; height: 100%; object-fit: cover; }
.clp-tool .clp-att-x { position: absolute; top: 2px; right: 2px; width: 16px; height: 16px; display: grid; place-items: center; border-radius: var(--r-pill); background: var(--scrim); color: var(--on-fill); border: none; cursor: pointer; padding: 0; }
.clp-tool .clp-att-x svg.ic { width: var(--icon-xs); height: var(--icon-xs); }

/* THE FILE THIS MODEL CANNOT USE. An attachment is never thrown away when the model changes, so a
   file the selected card will not send stays in the strip and says so, and the run is refused until
   it is resolved · `window.clpFits` in snippets/clp/named-slots.twig.

   Direction B of four, rendered at 50px on real output and chosen by Hatim 2026-08-26: the border
   the app already wears when an input is wrong, and the file itself switched off. Greyed rather than
   washed or hidden BECAUSE THE PICTURE HAS TO STAY RECOGNISABLE: the whole point of the state is
   choosing which of several files to remove, and a red wash over the picture takes away the thing
   you are choosing between.

   THE MEDIA IS NAMED, NOT THE TILE, so the remove control keeps its full contrast and stays
   pressable. Greying the tile would grey the one control that resolves the problem. All three
   shapes a thumbnail can take are covered, because `snippets/clp/file-thumb.twig` draws a bare
   `img` for a picture, a `.clp-fk` for a clip and a `.clp-fk-kind` for anything else.

   `.clp-att-hit` IS THE FOURTH SHAPE AND IT IS NOT DECORATION. Where the strip is asked for a
   preview, the thumbnail is wrapped in that control, which makes the picture a grandchild rather
   than a child and takes it out of a `>` selector: the file kept its full colour under a red border
   and only half the state was drawn. Naming the wrapper greys everything inside it and still leaves
   the remove control, a sibling, at full contrast. Found by screenshotting the state, not by
   reading it · `snippets/clp/attachments.twig`. */
.clp-tool .clp-att.wont-send { border-color: var(--red-bd); }
.clp-tool .clp-att.wont-send > img,
.clp-tool .clp-att.wont-send > .clp-fk,
.clp-tool .clp-att.wont-send > .clp-fk-kind,
.clp-tool .clp-att.wont-send > .clp-att-hit { filter: grayscale(1); opacity: .45; }

/* ONE SENTENCE FOR THE WHOLE SET, in the app's own small print at the size every other caption
   uses, wearing the muted red destructive text already reads in rather than `--red`, which is too
   hot at this size. Each thumbnail carries its own mark and its own tooltip, so this says the CAUSE
   and what clears it, once, rather than the same words under every file.

   IT WRAPS, AND IS MEANT TO. Naming the cause costs a line at this width on the longer two, which is
   the trade taken deliberately: the short version fitted on one line and told nobody which of four
   problems they had · `snippets/clp/attachments.twig`. */
.clp-tool .clp-att-why { margin: 8px 2px 0; font-size: var(--fs-xs); line-height: 1.5; color: var(--red-hi); }

.clp-tool .clp-io-req { margin-left: 2px; color: var(--text-3); }

/* ---- NAMED UPLOAD SLOTS, WHAT IS ATTACHED. A card that takes more than ONE file cannot use
   the anonymous strip above it: kie names each input and refuses the wrong shape, so
   `ideogram/character-edit` needs the picture, the mask and the character reference told
   apart. The catalogue carries the answer per card in `config.images.slots` /
   `config.frames.slots`, each with the label and hint written from kie's own schema, so this
   is drawn from data rather than from a list of card names.

   IT SHOWS, IT DOES NOT PICK. Filling a named input happens on its own row in Options, where
   every other card's upload has always lived, so a slot with no file draws nothing here. That
   is what the single-input strip above does and this does the same, with a name added under
   each cell because several thumbnails cannot say which input they belong to.

   IT RIDES THAT STRIP RATHER THAN REPLACING IT. Same flex row, same 8px rhythm, same radius
   and the same scrim on the corner controls, so a one-slot card keeps the 50px thumbnail it
   ships today and gains nothing.

   FIVE CELLS FIT ON ONE ROW BY CONSTRUCTION: 5 x 76 plus 4 x 8 is 412 inside the 600px
   `--composer-w`, and five is the most any card in the catalogue declares (the three
   seedance video cards). Nothing wraps, so the row height is stable while files land. ---- */
:is(.clp-tool, .clp-lb) .clp-slot { width: 76px; }
:is(.clp-tool, .clp-lb) .clp-slot .clp-att { width: 76px; height: 76px; display: grid; place-items: center; }
/* The label is the whole point of the direction: it turns an anonymous bucket into a named
   one, so it has to READ. Eight of the seventeen labels the catalogue writes are longer than
   one 76px line, `Reference images` and `First frame image` among them, so the label takes up
   to two lines and reserves the height of both. Reserving it is what keeps the row still: a
   one-word label and a two-word one leave the strip exactly as tall, whatever the card, so
   nothing shifts as files land. Two lines clear every label the catalogue writes today,
   measured rather than estimated; the clamp is what a longer one would meet, and the full name
   rides the title attribute for that case. */
:is(.clp-tool, .clp-lb) .clp-slotlab { margin-top: 5px; font-size: var(--fs-xs); line-height: 1.35; color: var(--text-3); text-align: center; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: 30px; }
/* A slot taking several files carries its count BOTTOM right, where the remove button never
   sits, so a multi-file slot can show both without the two colliding. */
.clp-tool .clp-att-n { position: absolute; bottom: 3px; right: 3px; min-width: 17px; height: 17px; padding: 0 5px; display: grid; place-items: center; border-radius: var(--r-pill); background: var(--scrim); color: var(--on-fill); font-size: var(--label-size); line-height: 1; font-weight: 500; }

/* ---- AN ATTACHED FILE THAT IS NOT A PICTURE. Direction A, picked from rendered variants
   2026-08-20. Every thumbnail on Video drew an `<img>`, which is all a slot could hold until each
   slot came to carry its own accepted types; a video or a sound drew a broken image. A file shows
   itself where it can, so a video holds its own first frame in the same box at the same crop an
   image gets, and only sound, which has no frame, falls back to naming itself. Markup is
   views/snippets/clp/file-thumb.twig, drawn into all four places Video shows an attachment. ---- */
:is(.clp-tool, .clp-lb) .clp-fk { display: block; width: 100%; height: 100%; }
/* The mark that says this frame is the start of something, not a still. It sits on the same scrim
   the corner controls use, laid on as a gradient so the frame stays readable under it. */
:is(.clp-tool, .clp-lb, .trow-pic) .clp-fk-play { position: absolute; inset: 0; display: grid; place-items: center; color: var(--on-fill); background: linear-gradient(transparent 40%, var(--scrim)); }
:is(.clp-tool, .clp-lb) .clp-fk-kind { display: grid; place-items: center; width: 100%; height: 100%; gap: 3px; color: var(--text-3); }
/* The filename is the whole reason a sound gets more than a glyph: two voice references are the
   same picture otherwise. Mono because it is a filename, clipped because the box is 76px. */
:is(.clp-tool, .clp-lb) .clp-fk-name { font-family: var(--mono); font-size: var(--label-size); line-height: 1; color: var(--text-4); max-width: 66px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The 50px strip is the single-input one, and it is a direct child of .clp-attached where the named
   strip nests a .clp-slot first. At that size a filename cannot be read and the play mark crowds
   the frame, so the name goes and the mark takes the small glyph. */
:is(.clp-tool, .clp-lb) .clp-attached > .clp-att .clp-fk-name { display: none; }
:is(.clp-tool, .clp-lb) .clp-attached > .clp-att .clp-fk-play svg.ic { width: var(--icon); height: var(--icon); }
/* THE REMOVE CONTROL ASSUMED A PICTURE BEHIND IT. It is --scrim over whatever it sits on, and until
   a slot could hold a sound there was always an image there to darken. On a cell drawing a glyph
   there is nothing to darken and the control all but disappears.
   IT DROPS THE SCRIM RATHER THAN DEEPENING IT. --scrim and --scrim-deep are declared once, in the
   light block, and the dark block does not redefine them, so both stay rgba(0,0,0,...) in dark: a
   deeper scrim on a #101012 cell is darker paint on a dark wall. A control that is not floating
   over a picture does not need a scrim at all, so it takes the bare-icon treatment, which reads
   in both modes because --text-3 is declared in both. */
.clp-tool .clp-io-thumb:has(.clp-fk-kind) .clp-io-thumb-x,
.clp-tool .clp-att:has(.clp-fk-kind) .clp-att-x { background: none; color: var(--text-3); }
.clp-tool .clp-io-thumb:has(.clp-fk-kind) .clp-io-thumb-x:hover,
.clp-tool .clp-att:has(.clp-fk-kind) .clp-att-x:hover { background: var(--hover); color: var(--text); }

/* ---- THE RUN QUEUE, above the wall on all four surfaces that can hold one ----
   SCOPED TO #content FOR THE SAME REASON `.gal` IS. The queue is one component and the two page
   families both render it: `.clp-tool` on imagine and video, `.clp-page` in the two libraries.
   Scoping it to either one is exactly what made the library grow a second media wall.

   IT READS AS ONE CARD, NOT AS TILES. The wall below is the loud object on these pages, so the
   queue states itself in the panel's language instead: a sunken card with a hairline, mono meta,
   the same tokens the options panel above it uses. What is running should be legible at a glance
   and never compete with the finished work it is about to join.

   THE WIDTH FOLLOWS THE PAGE: it spans whatever holds it, like the wall does. */
/* THE TOP GAP IS `.sec-head`'s OWN 34px, not a number picked for this card. The queue sits in the
   slot the Recent heading otherwise occupies, so it takes that heading's spacing and the composer
   keeps the same air above whatever follows it. No bottom margin: whatever comes next brings its
   own, and doubling them pushed the wall down the page. */
#content .clp-q { border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--sunken); margin: 34px 0 0; overflow: hidden; }
#content .clp-qrow { display: grid; grid-template-columns: 40px minmax(0,1fr) auto; gap: 13px; align-items: center; padding: 12px 14px; }
#content .clp-qrow + .clp-qrow { border-top: 1px solid var(--border); }
#content .clp-q .q-thumb { width: 40px; height: 40px; display: grid; place-items: center; border-radius: var(--r-sm); background: var(--raised); border: 1px solid var(--border); color: var(--text-3); }
/* `minmax(0,1fr)` above plus this is what lets a long prompt end in an ellipsis instead of
   widening the grid: a track sized to content refuses to shrink below it, and so does the cell. */
#content .clp-q .q-body { min-width: 0; }
#content .clp-q .q-meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 11px; margin-top: 3px; font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-3); font-variant-numeric: tabular-nums; }
/* The held figure is the only part of the line that is about money, so it is the only part that
   takes the accent, as the paid band does. */
#content .clp-q .q-meta x-credit { color: var(--accent-hi); }
@media (max-width: 640px) {
  #content .clp-qrow { grid-template-columns: 32px minmax(0,1fr) auto; gap: 10px; padding: 11px 12px; }
  #content .clp-q .q-thumb { width: 32px; height: 32px; }
}

/* recent gallery: flat square-tile grid from the imagine() history binding (reuses the library tile language) */
/* TRUE JUSTIFIED ROWS. Every row is filled to the full container width and each row computes its own
   height. Never force an aspect ratio here: the catalogue ships 22 of them and a square crop cuts
   57% off a 21:9.

   How it works. Each tile carries its own aspect ratio as the number `--r`, set inline from the
   generation's real file dimensions. Both flex-grow and flex-basis are proportional to `--r`, so
   within any row  width_i = R_i * (H0 + leftover/sumR)  and therefore
   height_i = width_i / R_i = H0 + leftover/sumR, which is IDENTICAL for every item in that row.
   The row lands on one shared height and consumes the width exactly. Different rows resolve to
   different heights, which is the point of a justified gallery.

   The `.gspace` items exist so the LAST row is not stretched. They sit at the end of the flex order,
   carry no width of their own (flex-basis 0, height 0) and a grow weight so large that they absorb
   essentially all of the final row's slack, leaving its real tiles at the base height and
   left-aligned. Full rows contain no spacers and are untouched by them.

   Do NOT add a max-height or max-width cap to bound tall rows: measured 2026-08-04, a cap clamps
   genuine full rows at some viewport widths and drops them to 63-69% fill. The spacers solve the
   only real problem (a sparse final row) without that cost.

   column-gap only, with the vertical rhythm on the tile's own margin-bottom, so a spacer-only row
   contributes exactly zero height.

   SCOPED TO #content, NOT TO A PAGE FAMILY. This is THE media wall: the tool pages' recent
   strip and the image and video libraries are the same object and must not be two. It used to
   be written as `.clp-tool .gal`, and since every library page is `.clp-page`, that one
   selector is the entire reason a second layout engine (core's column masonry) existed on the
   library at all. A component belongs to no family, exactly as `.clp-empty` and the page
   opening do. Verified before widening: no core template and no core stylesheet uses `gal`,
   `gtile`, `gspace`, `g-state`, `g-grad` or `g-dur`, so nothing else can be caught by it. */
#content .gal { display: flex; flex-wrap: wrap; column-gap: 8px; --gal-h: 116px; }
@media (max-width: 640px) { #content .gal { --gal-h: 92px; } }
/* THE LIBRARY WALL IS THE PAGE, THE RECENT STRIP IS A GLANCE. The base above is tuned for the
   strip that sits under a section head on a tool page, and the library inherited it when the two
   became one component: at full width that packed nine tiles into a row 119px tall, which is what
   "the image is too small" was. A modifier rather than a `.clp-page` selector, so the caller asks
   for the scale it wants and the component stays unowned by either page family. */
#content .gal.gal-lg { --gal-h: 200px; }
@media (max-width: 640px) { #content .gal.gal-lg { --gal-h: 140px; } }
/* TWO LEVELS, and the split is load-bearing rather than decorative. `.gtile` is the flex item
   and the positioning context; `.gt-link` is the navigation filling it. They are separate
   because the action row holds a Download ANCHOR, and an <a> nested in an <a> is invalid HTML
   that the parser unnests, taking the tile apart. So the actions are the link's sibling. */
/* THE TILE IS ITS OWN CONTAINER, and only the tile can answer for its width. A 0.66 portrait and
   a 2.4 panorama sit in the same justified row at the same moment at 77px and 278px, so no media
   query and no page-level class can tell a narrow tile from a wide one. NAMED, not anonymous: an
   unnamed `@container` binds to the nearest ancestor container, so the day anything else on a page
   becomes one, the fold rules below would silently retarget. */
#content .gal .gtile { position: relative; box-sizing: border-box; flex-grow: var(--r, 1.7778); flex-shrink: 1; flex-basis: calc(var(--r, 1.7778) * var(--gal-h)); aspect-ratio: var(--r, 1.7778); height: auto; margin-bottom: 8px; border-radius: var(--r-sm); overflow: hidden; border: 1px solid var(--border-strong); background: var(--raised); transition: border-color .16s; container-type: inline-size; container-name: gtile; }
#content .gal .gt-link { display: block; position: absolute; inset: 0; }
#content .gal .gspace { flex-grow: 999; flex-shrink: 1; flex-basis: 0; height: 0; margin: 0; padding: 0; border: 0; }
/* A ONE-ROW WALL (`row`) draws only the tiles its row holds; when more wait, the push takes a line of its own under them,
   so their row is not the last and stretches to the width. One row has no row under it to space from. */
#content .gal.gal-row .gtile { margin-bottom: 0; }
#content .gal.gal-row .g-push { flex-basis: 100%; height: 0; }
#content .gal .gtile:hover { border-color: var(--border-struct); }
/* MARGIN IS STATED, and it is not defensive noise. A tile's media is absolutely positioned against
   the tile, so any inherited margin moves it off its own corner: `top` and `bottom` cannot both be
   honoured, `bottom` loses, and the picture is pushed down and clipped at the foot of the tile.
   Core's prose stylesheet gives every `img` and `video` inside `.editor` a 2em margin, and the run
   result IS `.editor`, so a wall placed in a result was showing a photograph shifted off its tile
   while the colour behind it filled the gap. Declared here rather than on that one wall, because
   this is what the component needs wherever it is put, and prose is not the only thing that sets a
   margin on a picture. */
#content .gal .gtile img, #content .gal .gtile canvas, #content .gal .gtile video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; margin: 0; }
/* ---- THE TILE OVERLAY STATE, centred over a thumbnail. ---- */
#content .gal .gtile .g-state { position: absolute; inset: 0; display: grid; place-items: center; z-index: 2; color: var(--text-3); }
#content .gal .gtile .g-state svg.ic { width: var(--icon-xl); height: var(--icon-xl); }
/* THE FAILURE MARK IS A COMPONENT, NOT A WALL FEATURE, and it is written here once for the same
   reason `.gal` itself is unscoped: a failed run looks the same wherever it is drawn. Two surfaces
   render it, the media wall's tile and the library hub's shelf frame, and they belong to different
   page families, so the mark belongs to neither. The CALLER owns the placement, which is the only
   part that genuinely differs: the wall lays it over an absolutely positioned tile, the hub frame
   simply is the box.

   A failed run has no media, so its whole frame is empty and the centre is where the eye already
   goes. Its words read `--red-hi`, the step every red word reads.

   A HAIRLINE, NOT A FILL. A quarter of the shipped image library is failures, and a red wash on
   every one of them would make the wall read as an error page. The border states it once.

   THE WORDS CLIP RATHER THAN WRAP. A justified wall clamps tiles as narrow as 0.66 of their row
   height, which at the mobile strip size is about 61px, and two words cannot fit that. The icon
   above them carries the meaning on its own when they are cut. */
#content .clp-fail { width: 100%; height: 100%; display: grid; grid-auto-flow: row; gap: 5px; align-content: center; justify-items: center; text-align: center; color: var(--red-hi); padding: 0 6px; }
#content .clp-fail svg.ic { width: var(--icon-md); height: var(--icon-md); }
#content .clp-fail span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: var(--label-size); letter-spacing: .06em; text-transform: uppercase; }
/* placement, per caller */
#content .gal .gtile .clp-fail { position: absolute; inset: 0; z-index: 2; }
#content .gal .gtile.g-fail, #content .gal .gtile.g-fail:hover { border-color: var(--red-bd); }
#content .gal .gtile .g-grad { position: absolute; inset: 0; z-index: 1; background: linear-gradient(to top, var(--scrim-soft), transparent 45%); opacity: 0; transition: opacity .16s; pointer-events: none; }
/* --mono, not --font-mono. `--font-mono` was never declared anywhere, so this chip has been
   silently inheriting the sans face since it shipped. The declared token is --mono. */
#content .gal .gtile .g-dur { position: absolute; left: 6px; bottom: 6px; z-index: 2; font-family: var(--mono); font-size: var(--label-size); letter-spacing: .02em; color: var(--on-fill); background: var(--scrim-deep); border-radius: var(--r-sm); padding: 1px 5px; }
/* THE VOICED MARK: a mic inside the length label, on a voiced take, a voiced clip and a voiced copy. */
#content .gal .gtile .g-dur.has-voice { display: inline-flex; align-items: center; gap: 3px; }
#content .gal .gtile .g-dur.has-voice svg.ic { width: var(--icon-xs); height: var(--icon-xs); fill: var(--on-fill); }
#content .gal .gtile:hover .g-grad { opacity: 1; }

/* ---- THE TILE ACTION ROW, every media tile on every surface.
   Hover-revealed, top right, clear of `.g-dur` which sits bottom left. Built from the app's
   existing parts rather than a new language: the 28px scrim button with backdrop blur is the
   one the library grid's overflow menu already used, and the actions themselves are the ones
   the imagine result already offers.

   Kept visible on touch, where there is no hover to reveal them: a pointer:coarse device would
   otherwise have no way to reach download or delete at all. ---- */
#content .gal .gtile .g-acts { position: absolute; top: 6px; right: 6px; z-index: 3; display: flex; gap: 4px; opacity: 0; transition: opacity .16s; }
#content .gal .gtile:hover .g-acts,
#content .gal .gtile:focus-within .g-acts { opacity: 1; }
@media (hover: none) { #content .gal .gtile .g-acts { opacity: 1; } }
/* THE SCRIM CONTROL, one declaration and three consumers. A tile's hover actions, the full-page
   viewer's corner row and the lightbox's arrows are the same button: a blurred scrim chip carrying
   an `--on-fill` glyph, which is what this app puts on top of media. Declared once with all three
   scopes rather than copied, the rule the header of this file states and the reason it counts its
   pairs. Only SIZE is a consumer's business, and each one that wants a different size says so
   directly below its own scope. */
#content .gal .gtile .g-act,
#content .clp-lb .g-act,
.clp-viewer .v-acts .g-act { display: grid; place-items: center; width: 26px; height: 26px; border: 0; border-radius: var(--r-sm); background: var(--scrim); color: var(--on-fill); cursor: pointer; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); transition: background .14s; }
#content .gal .gtile .g-act:hover,
#content .clp-lb .g-act:hover,
.clp-viewer .v-acts .g-act:hover { background: var(--scrim-deep); }
#content .gal .gtile .g-act.del:hover { background: var(--red); }
#content .gal .gtile .g-act svg.ic,
#content .clp-lb .g-act svg.ic,
.clp-viewer .v-acts .g-act svg.ic { width: var(--icon-sm); height: var(--icon-sm); fill: var(--on-fill); }

/* ------------------------------------------------------------
   THE FOLD, on a tile too narrow to carry both control groups.

   THE WIDTHS ARE ARITHMETIC AND THEY WERE MEASURED. A tile hands its absolutely positioned
   children its own width less its 2px of border. The select box takes 6 + 22 on the left, the
   action row takes 6 plus its own width on the right, and that row is 26px a chip with 4px
   between them. So the two groups touch at 122px carrying three chips, 92px carrying two and
   62px carrying one, measured on this stylesheet at each of those widths rather than reasoned
   about.

   THE COUNT IS PER TILE, NOT PER WALL, and that is what makes it three widths instead of one. A
   wall declares its actions once, but `x-show` decides per tile: an image carries Download, Copy
   and Delete, a video hides Copy, and a run that failed has no output file so it carries Delete
   alone. The dashboard draws all three kinds in ONE wall. Hatim's rule, 2026-08-25: fold only
   when they cannot all be shown. A single width would fold a failed tile at 128px and hide its
   one remaining button behind a button, which is the bug wearing a different hat.

   EACH WIDTH CARRIES 6px OF HEADROOM over the width where the groups touch, because a justified
   row adds flex slack on top of the basis width. 128, 98 and 68.

   IT OPENS A COLUMN, NOT A PANEL, and that is forced rather than chosen. Core's `.menu` is
   `position: absolute` and `.gtile` is `overflow: hidden`, so a panel drawn in a tile is cut at
   the tile's edge; `position: fixed` does not escape either, because `container-type` applies
   `contain: layout` and that makes the tile the containing block for fixed descendants too. The
   column is the same chips turned ninety degrees, inside the tile, with nothing to position.

   22px WHEN OPEN, AND THAT SIZE IS LOAD-BEARING. Every tile in a justified row shares the row's
   height, so the shortest tile any wall produces is the phone strip at 92px. Three 22px chips
   with 4px gaps is 74px and clears; at 26px the column needs 98px and does not fit. Only SIZE is
   a consumer's business, per the note on `.g-act` above, which is why this says so here.

   THREE ACTIONS IS THE CEILING. A fourth needs 112px against 92px of tile.

   EVERY OPEN-STATE RULE LIVES INSIDE ITS OWN QUERY ON PURPOSE. `acts-open` is written by Alpine
   and survives a resize, so a tile opened at 92px and then widened past its width would keep
   drawing a 22px vertical column on a wide tile.
   ------------------------------------------------------------ */
#content .gal .gtile .g-fold { display: none; }

/* THREE BUTTONS. They touch at 122px. */
@container gtile (max-width: 128px) {
  #content .gal .gtile.acts-3 .g-act.wide { display: none; }
  #content .gal .gtile.acts-3 .g-fold { display: grid; width: 22px; height: 22px; }
  #content .gal .gtile.acts-3 .g-fold svg.ic { width: var(--icon-xs); height: var(--icon-xs); }

  #content .gal .gtile.acts-3.acts-open .g-act.wide { display: grid; }
  #content .gal .gtile.acts-3.acts-open .g-fold { display: none; }
  #content .gal .gtile.acts-3.acts-open .g-acts { flex-direction: column; opacity: 1; }
  #content .gal .gtile.acts-3.acts-open .g-act { width: 22px; height: 22px; }
  #content .gal .gtile.acts-3.acts-open .g-act svg.ic { width: var(--icon-xs); height: var(--icon-xs); }
}

/* TWO BUTTONS, a video tile. They touch at 92px, so it keeps both chips down to 98px while an
   image tile beside it on the dashboard has already folded. That difference IS the rule, not a
   defect: each tile shows the most it can show correctly. Same eight rules as above, at its own
   width; the reasoning is written once, up there. */
@container gtile (max-width: 98px) {
  #content .gal .gtile.acts-2 .g-act.wide { display: none; }
  #content .gal .gtile.acts-2 .g-fold { display: grid; width: 22px; height: 22px; }
  #content .gal .gtile.acts-2 .g-fold svg.ic { width: var(--icon-xs); height: var(--icon-xs); }

  #content .gal .gtile.acts-2.acts-open .g-act.wide { display: grid; }
  #content .gal .gtile.acts-2.acts-open .g-fold { display: none; }
  #content .gal .gtile.acts-2.acts-open .g-acts { flex-direction: column; opacity: 1; }
  #content .gal .gtile.acts-2.acts-open .g-act { width: 22px; height: 22px; }
  #content .gal .gtile.acts-2.acts-open .g-act svg.ic { width: var(--icon-xs); height: var(--icon-xs); }
}

/* ONE BUTTON, a run that failed. IT DOES NOT FOLD, AND NOT FOLDING IS THE POINT: a control that
   opens a column of one is a button hidden behind a button. It touches at 62px against the 61px
   phone portrait, the narrowest tile the app draws, so the overlap is 1px in one place. The chip
   takes the 22px the open column already uses and clears by 3px. No control, no state, no
   listener: a smaller chip. */
@container gtile (max-width: 68px) {
  #content .gal .gtile.acts-1 .g-act { width: 22px; height: 22px; }
  #content .gal .gtile.acts-1 .g-act svg.ic { width: var(--icon-xs); height: var(--icon-xs); }
}

/* ------------------------------------------------------------
   THE TILE SELECT BOX
   Top LEFT, because `.g-acts` already owns top right. Same scrim chip family as `.g-act`: same
   scrim, same blur, same radius, so it reads as one set rather than a control borrowed from
   somewhere else.
   ------------------------------------------------------------ */
#content .gal .gtile .g-sel {
  position: absolute; top: 6px; left: 6px; z-index: 4;
  width: 22px; height: 22px; display: grid; place-items: center;
  border-radius: var(--r-sm); cursor: pointer; padding: 0;
  background: var(--scrim); border: 1.5px solid var(--scrim-edge);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  opacity: 0; transition: opacity .16s, background .14s, border-color .14s;
}
#content .gal .gtile .g-sel svg.ic { width: var(--icon-xs); height: var(--icon-xs); fill: var(--on-fill); opacity: 0; transition: opacity .12s; }

/* Revealed on hover, exactly as `.g-acts` is, and always up where there is no hover to use. */
#content .gal .gtile:hover .g-sel,
#content .gal .gtile:focus-within .g-sel { opacity: 1; }
@media (hover: none) { #content .gal .gtile .g-sel { opacity: 1; } }

/* ONCE ANYTHING IS PICKED, EVERY BOX STAYS UP. You are choosing a set, so the other boxes are
   the rest of the choice. */
#content .gal.is-picking .gtile .g-sel { opacity: 1; }
#content .gal .gtile.is-on .g-sel { opacity: 1; background: var(--accent); border-color: var(--accent); }
#content .gal .gtile.is-on .g-sel svg.ic { opacity: 1; }

/* OUTLINE, NOT BORDER. `.gtile` carries a 1px border that is part of its box, so thickening it
   would move every tile in the justified row. An outline is drawn outside layout. */
#content .gal .gtile.is-on { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ------------------------------------------------------------
   THE SELECTION BAR
   Fixed, so the wall never moves while you are still picking. No ancestor in this app carries a
   transform, filter, backdrop-filter, contain, perspective or will-change on `body`, `#content`,
   `.clp-page` or `.clp-tool`, so this is positioned against the viewport.

   UNPREFIXED, so it reaches the two history drawers as well. The sidebar is a SIBLING of
   `#content`, so a `#content` rule never lands inside a drawer. Core's stylesheets carry none of
   `.clp-selbar`, `.sb-act`, `.sb-plain`, `.clp-vsep` or `.sb-n`, so the shorter selector has
   nothing to lose a specificity contest to.
   ------------------------------------------------------------ */
.clp-selbar {
  position: fixed; left: var(--pick-x, 50%); bottom: 26px; transform: translateX(-50%) translateY(6px);
  z-index: var(--z-pickbar); display: flex; align-items: center; gap: 14px; padding: 10px 14px;
  font-size: var(--fs-body); color: var(--text);
  background: var(--card); border: 1px solid var(--border-strong); border-radius: var(--r-lg);
  box-shadow: var(--lift-dialog);
  opacity: 0; pointer-events: none; transition: opacity .16s, transform .16s;
}
.clp-selbar.on { opacity: 1; transform: translateX(-50%) translateY(0); pointer-events: auto; }
.clp-selbar .sb-n { font-family: var(--mono); font-size: var(--fs-sm); color: var(--text-2); white-space: nowrap; }
.clp-selbar .sb-act {
  appearance: none; border: 1px solid var(--border-strong); background: var(--card); color: var(--text);
  font: inherit; font-size: var(--fs-sm); cursor: pointer; padding: 7px 12px; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; gap: 7px;
  transition: background .14s, border-color .14s, color .14s;
}
.clp-selbar .sb-act:hover { background: var(--hover); }
.clp-selbar .sb-act[disabled] { opacity: .5; cursor: default; }
.clp-selbar .sb-act.del { color: var(--red-hi); border-color: var(--red-bd); }
.clp-selbar .sb-act.del:hover:not([disabled]) { background: var(--red-fill); border-color: var(--red); color: var(--red-hi); }
.clp-selbar .sb-plain {
  appearance: none; border: 0; background: none; color: var(--text-3); font: inherit; font-size: var(--fs-sm);
  cursor: pointer; padding: 4px 2px; transition: color .14s; white-space: nowrap;
}
.clp-selbar .sb-plain:hover { color: var(--text); }
/* On a narrow screen the bar has to fit the viewport before it has to look roomy. */
@media (max-width: 640px) {
  .clp-selbar { left: 12px; right: 12px; bottom: 14px; transform: translateY(6px); gap: 10px; padding: 9px 12px; }
  .clp-selbar.on { transform: translateY(0); }
  /* "1 selected": the item word goes, so Select all, Delete and Clear fit a 390 pixel phone */
  .clp-selbar .sb-what { display: none; }
}

/* THE DRAWER'S BAR BELONGS TO THE DRAWER, and that is a layout necessity before it is a
   preference. `/app/writer` carries TWO selections, the page's own recent list and the drawer's,
   and both can be live at once, so two bars centred on the same point stack exactly on top of
   each other. This one stops centring instead: it stays `fixed`, and because the sidebar is
   flush to the right edge of the viewport, `right` plus the panel width lands it over the panel
   with no positioned ancestor needed. 268px is the panel, 10px a side leaves 248px.

   THE LIST GIVES IT ROOM. Unlike every other bar in the app this one draws over its own surface
   rather than over a page, so the scroller reserves the height instead of hiding a row under it. */
.clp-drawer .clp-selbar {
  left: auto; right: 10px; width: 248px; bottom: 10px;
  transform: translateY(6px); box-sizing: border-box;
  gap: 8px; padding: 8px 10px; justify-content: space-between;
}
.clp-drawer .clp-selbar.on { transform: translateY(0); }
.clp-drawer .clp-selbar .sb-act { padding: 6px 10px; }
.clp-drawer:has(.clp-selbar.on) .drawer-list { padding-bottom: 52px; }

/* An item's editable name, bare: the place it sits in sets its type, on the name and on the hidden copy that sizes its box */
textarea.r-title { width: 100%; border: none; background: none; outline: none; resize: none; padding: 0; color: var(--text); }
textarea.r-title::placeholder { color: var(--text-4); }
/* the window's, in the display face */
.clp-lb textarea.r-title,
.clp-lb .autogrow-textarea:has(> textarea.r-title)::after,
.clp-lb .lb-name { color: var(--text); font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-size: var(--fs-md); line-height: 1.5; font-weight: var(--display-w); letter-spacing: var(--display-ls); }
.clp-lb .lb-err { border: 1px solid var(--border-strong); border-radius: var(--r-lg); background: var(--raised); padding: 22px; text-align: center; }
.clp-lb .lb-err .e-t { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w); font-size: var(--fs-xl); color: var(--text); margin-bottom: 6px; letter-spacing: var(--display-ls); }
.clp-lb .lb-err .e-r { margin-top: 10px; font-size: var(--fs-sm); color: var(--red-hi); }

/* New / Edit action pills (shown under a result or the generating state) */
#content .t-actions { display: flex; justify-content: center; gap: 8px; margin-top: 4px; }
/* actions stacked at a narrow column's full width: the result window's facts column, a version's setup */
#content .t-actions.is-stack { flex-direction: column; align-items: stretch; }
#content .t-actions.is-stack .apill { justify-content: center; }
#content .t-actions .apill { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px; border: 1px solid var(--border-strong); border-radius: var(--r-pill); font-size: var(--fs-sm); color: var(--text-2); background: var(--card); cursor: pointer; transition: border-color .14s, background .14s, color .14s; }
#content .t-actions .apill svg.ic { width: var(--icon-sm); height: var(--icon-sm); }
/* the pill that destroys something, its words and glyph in red, on hover too */
#content .t-actions .apill.is-danger, #content .t-actions .apill.is-danger:hover { color: var(--red-hi); }
/* THE GO PILL (`is-go`): the action that makes something, a run step still to be carried out or Make this on an example,
   filled with the accent like `.ebtn.primary`, its glyph filled too. */
#content .t-actions .apill.is-go {
  background: var(--accent); border-color: var(--accent); color: var(--on-fill); font-weight: 500;
}
#content .t-actions .apill.is-go svg.ic { fill: var(--on-fill); }

/* THE NEXT-STEP ROW, on the same pill atom as New / Edit and deliberately not on the same alignment.
   A result carries both rows at once, so two centred rows separated only by the prompt would read as
   one toolbar repeated. This one is flush to the frame's right edge because it belongs to the step
   directly above it; New / Edit stays centred because it belongs to the page. It carries the control
   and nothing else: how the step stands is said once, in the step's own head. */
#content .t-actions.t-next { justify-content: flex-end; align-items: center; }

/* EVERY ATTEMPT THE STEP MADE, on the app's own media wall rather than a strip of its own. A run
   is the page where the work IS the content, so its attempts get the wall the library and both
   tool pages already give generated media.

   THE STEP'S WALL IS THE SMALL ONE. What a step made sits under that step's prompt while you read
   down the page, so it reports rather than presents: bigger than the 44px strip it replaces,
   smaller than the row that ends the run. 92px is the height the base wall already takes when the
   viewport is small, so the size is one the stylesheet declares rather than one chosen here. The
   run's closing row keeps the base 116px, which is what Imagine and Video give their latest work.

   EVERYTHING ELSE THE WALL NEEDS IS ALREADY DECLARED ABOVE: the justified row, the tile and its
   ratio, the duration chip, the hover gradient and the hover action.

   14px is the space this app puts between the last thing on the page and a wall, declared once by
   `.sec-head`'s bottom margin and reused here.

   THE CHOSEN ATTEMPT TURNS THE TILE'S OWN BORDER TO THE ACCENT, which is how this app marks a thing that
   is live or chosen, at the same one pixel the live search row uses. Scoped to this wall so the
   four walls that came first cannot be reached by it, and one class deeper than `.gtile:hover` so
   the mark survives the pointer.

   THE ACTION SHRINKS WITH THE TILE IT SITS ON. A base wall tile is a picture you are looking at
   and carries the 26px control; a step's attempt is 61px wide, where that same control covers
   nearly half the picture. 16px with a 10px glyph is the pairing this app already declares for a
   control laid over a small picture, on the attachment thumbnail, so the small tile takes the
   small control rather than a size invented for it. */
#content .clp-sgal { --gal-h: 92px; }
/* the tile opens the picture, so it says so */
#content .clp-sgal .gtile { cursor: pointer; }
#content .clp-wb .clp-sgal { margin-top: 14px; }
/* THE CHOSEN MARK BELONGS TO BOTH WALLS THIS PAGE DRAWS, the attempts under a step and the row
   that ends the run, so it is scoped to the page rather than to one of them. The four walls that
   came first are outside `.clp-wb` and cannot be reached by it. */
#content .clp-wb .gtile.on { border-color: var(--accent); }
#content .clp-sgal .gtile .g-act { width: 16px; height: 16px; }
#content .clp-sgal .gtile .g-act svg.ic { width: var(--icon-xs); height: var(--icon-xs); }
#content .clp-wb .gtile .g-mark {
  position: absolute; top: 6px; right: 6px; z-index: 3; display: grid; place-items: center;
  width: 16px; height: 16px; border-radius: var(--r-sm); background: var(--scrim);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
}
#content .clp-wb .gtile .g-mark svg.ic { width: var(--icon-xs); height: var(--icon-xs); fill: var(--accent-hi); }
#content .clp-sgal .gtile:not(.on) img,
#content .clp-sgal .gtile:not(.on) video,
#content .clp-sgal .gtile:not(.on) canvas { opacity: .45; transition: opacity .14s; }
#content .clp-sgal .gtile:not(.on):hover img,
#content .clp-sgal .gtile:not(.on):hover video,
#content .clp-sgal .gtile:not(.on):hover canvas { opacity: .85; }
/* THE VOICE STEP'S WALL, one tile per scene. A scene with no voiced clip shows its original under a veil,
   with its number and what happened; the wall has nothing to choose, so no clip on it is faded. */
#content .gal .gtile .g-veil { position: absolute; inset: 0; z-index: 1; background: var(--scrim-deep); }
#content .gal .gtile .g-state.on-veil { color: var(--on-fill); }
#content .gal .gtile .g-word { position: absolute; inset: 0; z-index: 2; display: grid; align-content: center; justify-items: center; gap: 5px; text-align: center; color: var(--on-fill); padding: 0 6px; }
#content .gal .gtile .g-word span { font-family: var(--mono); font-size: var(--label-size); letter-spacing: .06em; text-transform: uppercase; }
#content .gal .gtile .g-word.is-failed { color: var(--red-hi); }
#content .gal .gtile .g-num { position: absolute; left: 6px; top: 6px; z-index: 3; font-family: var(--mono); font-size: var(--label-size); color: var(--on-fill); background: var(--scrim-deep); border-radius: var(--r-sm); padding: 1px 5px; }
#content .clp-sgal.no-pick .gtile:not(.on) img,
#content .clp-sgal.no-pick .gtile:not(.on) video,
#content .clp-sgal.no-pick .gtile:not(.on) canvas { opacity: 1; }

/* ---- Voice Over: voice-picker command menu, recent speeches rows, script i/o ---- */
.clp-tool .composer .vo-script textarea { min-height: 44px; }
.clp-tool .tchip .vchip-av,
.clp-vmodal .tchip .vchip-av { width: 18px; height: 18px; border-radius: 5px; flex: none; margin-left: -2px; display: grid; place-items: center; background: var(--raised); border: 1px solid var(--border-strong); color: var(--text-3); }
.clp-tool .tchip .vchip-av svg.ic,
.clp-vmodal .tchip .vchip-av svg.ic { width: var(--icon-xs); height: var(--icon-xs); }
.clp-tool .clp-vmenu .clp-menu-row,
.clp-vmodal .clp-vmenu .clp-menu-row { gap: 4px; padding-left: 6px; }
/* THE PLAY CONTROL IS SHARED, so its rules are scoped to `#content`, which every skinned surface
   sits inside: the voice menu wears it and so does the Voices card. Scoped to one page it would
   grow the duplicate the row menu control's note warns against. */
#content .clp-vr-play { width: 30px; height: 30px; border-radius: var(--r); flex: none; display: flex; align-items: center; justify-content: center; background: var(--raised); border: 1px solid var(--border-strong); color: var(--text-2); cursor: pointer; transition: color .14s, border-color .14s, background .14s; }
#content .clp-vr-play:hover { color: var(--text); border-color: var(--border-struct); }
#content .clp-vr-play button { display: flex; align-items: center; justify-content: center; background: none; border: none; color: inherit; cursor: pointer; padding: 0; width: 100%; height: 100%; }
#content .clp-vr-play svg.ic { width: var(--icon-xs); height: var(--icon-xs); }
#content .clp-vr-play [wave] { display: none; }
#content .clp-vr-noplay { cursor: default; }
/* EVERY PLAYER'S GLYPHS FOLLOW ITS STATE, which core's `x-wave` stamps on itself (`snippets/clp/play.twig`,
   `snippets/audio.twig`): play at rest, pause while it plays, and in a player with a spinner, the spinner in play's place
   while its sound loads and no time until it has loaded */
x-wave .wv-pause, x-wave .wv-load { display: none; }
x-wave[state="playing"] .wv-pause, x-wave:is([state="loading"], [state="loaded"]) .wv-load { display: block; }
x-wave[state="playing"] .wv-play, x-wave:has(.wv-load):is([state="loading"], [state="loaded"]) .wv-play,
x-wave[state="loading"] .wv-time { display: none; }
/* playing state: x-wave stamps state="playing" on itself -> accent the control + tint the row it plays in, the voice
   menu's row and the text row alike; a chosen or picked row keeps its blue */
#content .clp-vr-play[state="playing"] { color: var(--accent-hi); border-color: var(--accent-bd); background: var(--accent-fill); }
.clp-menu-row:has(.clp-vr-play[state="playing"]),
#content .trow:has(.clp-vr-play[state="playing"]) { background: var(--hover); }
.clp-menu-row.on:has(.clp-vr-play[state="playing"]),
#content .trow.is-on:has(.clp-vr-play[state="playing"]) { background: var(--accent-fill); }
.clp-tool .clp-vr-hit,
.clp-vmodal .clp-vr-hit { flex: 1; min-width: 0; }
.clp-tool .clp-vr-meta,
.clp-vmodal .clp-vr-meta { display: flex; gap: 5px; text-transform: capitalize; }
/* A VOICE OF THE CUSTOMER'S OWN CARRIES ITS SHARING LABEL ON THAT LINE, the voice card's words and icon
   (`.vshare`), written as they are: the capitals are for core's lowercase age, accent and gender, and
   "Only me" is a phrase (Hatim's pick P1, 2026-09-30). */
.clp-tool .clp-vr-share { text-transform: none; }
.clp-tool .clp-vr-share > span { display: inline-flex; align-items: center; gap: 5px; }
.clp-tool .clp-vr-share i { font-size: var(--icon-xs); }
.clp-mr-meta .clp-vr-flag { width: 15px; height: 11px; border-radius: var(--r-xs); object-fit: cover; flex: none; box-shadow: var(--ring-hairline); }
/* ---- MONO METADATA, list scale: the "N found" count in a list bar. ---- */

/* Imagine modals render OUTSIDE .clp-tool (as direct <main> children, so the fixed .modal centers),
   so size their sprite icons here instead of relying on the .clp-tool svg.ic scope. */
/* NOT folded into the shared icon rule above: written after the two menu rules that
   shrink an icon, it outranks them, so the assistant-list modal renders at 18px what
   the model picker renders at 16px. Only the size: colour and flex come from the shared rule. */
.modal svg.ic { width: var(--icon-md); height: var(--icon-md); }
.modal svg.ic-sm { width: var(--icon-sm); height: var(--icon-sm); }
/* The search field and the row meta keep their own sizes inside a modal. The assistant
   list is the model picker's field on another surface, so it has to read the same. */
.modal .clp-menu-search svg.ic { width: var(--icon); height: var(--icon); }
.modal .clp-mr-meta svg.ic { width: var(--icon-sm); height: var(--icon-sm); }

/* ============================================================
   Phase 7: toast. Core renders one <toast-message> in
   layouts/base.twig and core's api.js fills it on every API error,
   so the plumbing already works and only the treatment is ours.
   Styling only, never a template override: if core redesigns that
   element these rules stop matching and it degrades to core's own
   toast rather than breaking.

   The `:root` prefix carries (0,1,1), which beats core's single-class
   Tailwind utilities (bg-content, text-main, rounded-lg, px-4, py-3,
   gap-3) regardless of stylesheet order.

   Shape is the approved "hairline card": the same floating surface as
   .menu and .palette, with the state in a small tinted square, which is
   how .badge-success and .badge-failure already carry state.
   ============================================================ */
:root toast-message {
  z-index: var(--z-notice);
  gap: 11px;
  padding: 11px 15px;
  border-radius: var(--r-lg);
  border: 1px solid var(--border-strong);
  background: var(--card);
  color: var(--text);
  box-shadow: var(--lift-menu);
  font-size: var(--fs-body);
  line-height: 1.45;
}

/* Centre in the CONTENT column, not the viewport. Core offsets the toast by half of
   ITS sidebar (7.5rem = 120px): on a page with the 252px sidebar (admin) the offset is half
   of that, and under the app's top bar there is no column to clear. */
@media (min-width: 1024px) {
  :root toast-message.lg\:ms-\[7\.5rem\] { margin-inline-start: 0; }
  html:has(aside.sidebar) toast-message.lg\:ms-\[7\.5rem\] { margin-inline-start: 126px; }
}

/* The icon slot becomes the state chip. Success is the default because core omits the
   icon argument on toast.success(); the failure class repaints it. */
:root toast-message > i {
  width: var(--icon-xl); height: var(--icon-xl); flex: none;
  display: grid; place-items: center;
  border-radius: var(--r-sm);
  border: 1px solid var(--success-bd);
  background: var(--success-fill);
  color: var(--success-hi);
  font-size: var(--icon-sm); line-height: 1;
  /* Core spins the glyph in from 45deg. A bordered square reads wrong spinning, and
     `rotate` is its own property in Tailwind v4, so cancelling it leaves core's rise
     and fade untouched. */
  rotate: 0deg;
}
:root toast-message > i.ti-square-rounded-x-filled {
  border-color: var(--red-bd, rgba(223,22,59,.30)); background: var(--red-fill, rgba(223,22,59,.10)); color: var(--red-hi);
}
/* Core picks FILLED Tabler glyphs; every other icon in the skin is a thin stroke, so
   swap the codepoints inside the toast only. Scoped to toast-message, so the filled
   icons keep their own shape everywhere else in the app. */
:root toast-message > i.ti-square-rounded-check-filled::before { content: "\ea5e"; }
:root toast-message > i.ti-square-rounded-x-filled::before { content: "\ea06"; }

/* ============================================================
   The sign-in frame (layouts/minimal.twig): sign-in, policy and 404 pages. No box around
   the page: the mark above a centered column, the light and dark switch and the account menu
   in the top right corner. A box keeps its chrome only inside a form, which is the captcha.
   ============================================================ */
.clp-auth { position: relative; min-height: 100vh; display: flex; flex-direction: column; background: var(--bg); color: var(--text); }
.clp-auth .acts { position: absolute; top: 14px; inset-inline-end: 14px; z-index: 5; display: flex; align-items: center; gap: 6px; }
.clp-auth .theme-tog:hover { background: var(--hover); color: var(--text); }
.clp-auth .corner-acct { display: flex; align-items: center; }
.clp-auth .corner-acct .ubtn { display: flex; align-items: center; gap: 6px; height: 30px; padding: 0 6px 0 3px;
  border-radius: var(--r-sm); color: var(--text-3); }
.clp-auth .corner-acct .ubtn:hover,
.clp-auth .corner-acct:has(.menu[data-open]) .ubtn { background: var(--hover); color: var(--text); }
.clp-auth .corner-acct .ubtn .ident-av { width: 24px; height: 24px; }
@layer utilities {
  .clp-auth .corner-acct .ubtn i { font-size: var(--icon); line-height: 1.5; }
}
.clp-auth .corner-acct .menu { top: calc(100% + 6px); inset-inline-end: 0; }
.clp-auth .main { flex: 1; display: flex; align-items: center; justify-content: center; padding: 72px 16px 24px; }
.clp-auth .col { width: 100%; max-width: 360px; }
.clp-auth .col:has(.message) { max-width: 680px; }
.clp-auth .mark { display: block; width: 40px; margin: 0 auto 24px; }
.clp-auth .mark img { display: block; width: 40px; height: 40px; }
.clp-auth .box:not(form .box) { border: 0; background: transparent; padding: 0; border-radius: 0; }
.clp-auth form h1,
.clp-auth .ttl { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w);
  letter-spacing: var(--display-ls); font-size: var(--fs-2xl); line-height: 1.15; color: var(--text); text-align: center; }
.clp-auth .message :is(h1, h2, h3) { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz);
  font-weight: var(--display-w); letter-spacing: var(--display-ls); }
.clp-auth .lede { text-align: center; font-size: var(--fs-md); line-height: 1.6; color: var(--text-2); }
.clp-auth fieldset > label { display: block; font-size: var(--fs-sm); font-weight: 400; color: var(--text-2); margin-bottom: 7px; }
/* every field on these forms is required, so the required star says nothing */
.clp-auth label::before { display: none; }
.clp-auth .alt { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px; font-size: var(--fs-body); color: var(--text-3); text-align: center; }
.clp-auth .alt a { font-weight: 600; color: var(--text); }
.clp-auth .alt a:hover { text-decoration: underline; }
/* THE SIGN-IN FORMS, the four pages' one set of parts: the form's stack, a field pair, the password
   field's show button, the remember-and-forgot row, the submit with its last-used badge, the phone
   field's flag, the note list and the fine print. */
.clp-auth .af-stack { display: flex; flex-direction: column; gap: 4px; }
.clp-auth .af { display: flex; flex-direction: column; gap: 20px; }
.clp-auth .af-pair { display: grid; gap: 24px; }
@media (width >= 48rem) { .clp-auth .af-pair { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.clp-auth .pw { position: relative; }
.clp-auth .pw .input { padding-inline-end: 44px; }
.clp-auth .pw .eye { position: absolute; inset-inline-end: 12px; top: 50%; translate: 0 -50%;
  font-size: var(--icon-xl); line-height: calc(2 / 1.5); color: var(--text-3); }
.clp-auth .pw .eye i { display: block; }
.clp-auth .af-row { display: flex; justify-content: space-between; align-items: center; }
.clp-auth .af-link { font-size: var(--fs-md); line-height: calc(1.25 / .875); font-weight: 600; }
.clp-auth .af-go { position: relative; }
.clp-auth .af-go .last { position: absolute; inset-inline-end: 12px; top: 50%; translate: 0 -50%;
  pointer-events: none; opacity: .75; }
.clp-auth .af-phone { display: block; position: relative; }
.clp-auth .af-phone .flag { position: absolute; left: 12px; top: 50%; translate: 0 -50%; display: flex;
  justify-content: center; align-items: center; width: 24px; height: 24px;
  font-size: var(--icon-xl); line-height: calc(2 / 1.5); color: var(--text-3); }
.clp-auth .af-phone .flag x-avatar { width: 24px; height: 24px; }
.clp-auth .af-phone .input { padding-left: 48px; }
.clp-auth .af-info { display: flex; flex-direction: column; gap: 4px; padding: 4px 12px;
  font-size: var(--fs-sm); line-height: calc(1 / .75); color: var(--text-3); list-style: disc inside; }
.clp-auth .af-fine { font-size: var(--fs-sm); line-height: calc(1 / .75); color: var(--text-3); }
.clp-auth .af-fine a { color: var(--text); }
.clp-auth .af-gap { margin-top: 16px; }
.clp-auth .af-cred { font-family: var(--mono); font-size: var(--fs-md); line-height: calc(1.25 / .875); }
.clp-auth .af-cred > div + div { margin-top: 4px; }
.clp-auth .af-cred .k { font-weight: 600; text-transform: uppercase; }
@media (hover: hover) {
  .clp-auth .af-fine a:hover { text-decoration-line: underline; }
  .clp-auth .af-go:hover .last { opacity: 1; }
}
@media (width >= 48rem) and (hover: hover) { .clp-auth .af-link:hover { text-decoration-line: underline; } }
/* a ticked box is a choice, so it takes the accent */
.clp-auth i.ti-square-rounded-check-filled { color: var(--accent); }
.clp-auth .nf { display: flex; flex-direction: column; align-items: center; gap: 14px; }
.clp-auth .foot { flex: none; padding: 22px 16px; font-size: var(--fs-xs); line-height: calc(1 / .75); color: var(--text-4); background: transparent; text-align: center; }

/* ============================================================
   Admin credit ratios (/admin/settings/credits).
   The page is plugin-owned (views/templates/admin/settings/credits.twig),
   so the whole surface is authored here: one table per model category,
   a search box, and the four tier margins on every row. Grid columns are
   shared by the head and the rows so every number lines up down the page.

   THE LIST PARTS ARE SHARED, NOT THE CREDITS PAGE'S ALONE. The admin voice
   list (views/templates/admin/voices.twig) is built in this page's form, so
   the search bar, the folding group, the column head and row, the name cell,
   the tag and the mono cell are declared once without the `.clp-credits`
   scope and both pages match one rule for each part. What only this page
   has stays under `.clp-credits`: its note, health pills, column tracks,
   dirty row, rate field, margin cells, save bar and narrow-screen tracks.
   Where one of those and a shared rule set the same property on the same
   element, the scoped rule is the more specific, so it wins.
   ============================================================ */
/* the page column both lists stand in: a flex column, so the list bar's and the groups' margins
   add up instead of collapsing */
.clp-credits,
.clp-voices { display: flex; flex-direction: column; gap: 0; }

.clp-terms a { color: var(--text-2); text-decoration: underline; text-underline-offset: 2px; }
.clp-terms a:hover { color: var(--text); }

/* search: sticky at the top of the page as it scrolls */
.cr-bar { position: sticky; top: 0; z-index: 6; padding: 14px 0 12px; background: var(--bg); }
.cr-search {
  display: flex; align-items: center; gap: 10px; height: 40px; padding: 0 14px;
  border: 1px solid var(--border-control); border-radius: var(--r); background: var(--card);
  transition: border-color .16s ease, box-shadow .16s ease;
}
.cr-search input { flex: 1; border: 0; background: none; outline: none; font-size: var(--fs-md); color: var(--text); }
.cr-search input::placeholder { color: var(--text-4); }
.cr-search .kbd {
  font-family: var(--mono); font-size: var(--label-size); color: var(--text-4);
  border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px;
}
.cr-search svg { width: var(--icon); height: var(--icon); fill: currentColor; color: var(--text-4); }

/* THE ONE FOLDING GROUP. Rendered by the admin pages the plugin owns and built onto a core
   page by snippets/clp/admin-model-groups.twig, from these rules either way.

   group: no overflow:hidden, it would make the group its own scrollport and the sticky
   column head would then resolve against the group box instead of the scroll container */
.cr-group { border: 1px solid var(--border-strong); border-radius: var(--r-lg); background: var(--card); margin-top: 14px; }
.cr-head {
  display: flex; align-items: center; gap: 11px; width: 100%; padding: 14px 16px;
  background: none; border: 0; cursor: pointer; text-align: start;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  /* ignored unless the section is a grid, which a core page's is */
  grid-column: 1 / -1;
}
.cr-head:hover { background: var(--hover); }
.cr-group[data-open="false"] .cr-head { border-radius: var(--r-lg); }
/* every direct child but the head, so it folds a section that renders `.cr-body` itself and
   one whose children the plugin never wrote; !important because a core page paints those
   children with utility classes that set display */
.cr-group[data-open="false"] > *:not(.cr-head) { display: none !important; }
#content .cr-head h2 { font-family: var(--font); font-size: var(--fs-md); font-weight: 600; letter-spacing: -.01em; color: var(--text); margin: 0; }
.cr-head .cr-caret { width: 16px; height: 16px; flex: none; color: var(--text-4); fill: currentColor; transition: transform .16s ease; }
.cr-group[data-open="false"] .cr-caret { transform: rotate(-90deg); }
.cr-head .cr-count { font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-4); }
.clp-credits .cr-head .cr-health { margin-inline-start: auto; display: flex; gap: 6px; font-family: var(--mono); font-size: var(--fs-xs); }
.clp-credits .cr-pill { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: var(--r-pill); border: 1px solid var(--border); color: var(--text-3); }
.clp-credits .cr-pill[data-band="low"] { border-color: var(--red-bd); color: var(--red-hi); background: var(--red-fill); }
.clp-credits .cr-pill[data-band="good"] { border-color: var(--success-bd); color: var(--success-hi); background: var(--success-fill); }

/* table */
/* One 56px track per margin column. The track list comes from the page, which sets
   `--cr-tier-cols` from the tier list in registry/margin-basis.json, so the columns the grid
   reserves and the columns the markup fills are the same number by construction. The fallback is
   the two plans shipping today, and only ever applies if that list arrives empty. */
.cr-cols,
.cr-row { display: grid; align-items: center; gap: 14px; padding: 0 16px; }
.clp-credits .cr-cols,
.clp-credits .cr-row { grid-template-columns: minmax(200px, 1fr) 148px 128px var(--cr-tier-cols, 56px 56px); }
.cr-cols {
  height: 32px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
  /* parks directly under the search bar; measured at runtime because the bar is one
     element whose height changes with the viewport, and a wrong offset floats the head
     into the middle of the table */
  background: var(--sunken); position: sticky; top: var(--cr-stick, 66px); z-index: 4;
  font-family: var(--mono); font-size: var(--label-size); letter-spacing: var(--label-ls); text-transform: uppercase; color: var(--text-4);
}
.cr-cols .r { text-align: end; }
.cr-row { min-height: 52px; border-bottom: 1px solid var(--border); transition: background .12s ease; }
.cr-row:last-child { border-bottom: 0; border-radius: 0 0 var(--r-lg) var(--r-lg); }
.cr-row:hover { background: var(--hover); }
.clp-credits .cr-row[data-dirty="true"] { background: var(--accent-fill); }
.cr-row[hidden] { display: none; }

.cr-nm { min-width: 0; }
.cr-nm .t { display: flex; align-items: center; gap: 8px; }
.cr-nm .n { color: var(--text); font-weight: 450; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cr-nm .meta { font-size: var(--fs-xs); color: var(--text-4); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cr-tag {
  flex: none; font-family: var(--mono); font-size: var(--label-size); letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-4); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px;
}
.cr-tag[data-off] { color: var(--red-hi); border-color: var(--red-bd); }
/* the other end of the same tag: a provider clapyo actually sells through, on the model
   settings page, where snippets/clp/admin-model-groups.twig writes it */
.cr-tag[data-on] { color: var(--success-hi); border-color: var(--success-bd); }

.cr-rate {
  display: flex; align-items: center; height: 34px; overflow: hidden;
  border: 1px solid var(--border-control); border-radius: var(--r-sm); background: var(--card);
  transition: border-color .16s ease, box-shadow .16s ease;
}
.cr-rate input, .cr-rate .v {
  width: 100%; min-width: 0; border: 0; outline: none; background: none; padding: 0 9px;
  text-align: end; font-family: var(--mono); font-size: var(--fs-body); color: var(--text);
}
.cr-rate .u {
  flex: none; height: 100%; display: flex; align-items: center; padding: 0 9px;
  border-inline-start: 1px solid var(--border); background: var(--sunken);
  font-size: var(--label-size); color: var(--text-4); white-space: nowrap;
}
.cr-buys { font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.clp-credits .cr-m { font-family: var(--mono); font-size: var(--fs-sm); text-align: end; font-variant-numeric: tabular-nums; color: var(--text-2); }
.clp-credits .cr-m[data-band="low"] { color: var(--red-hi); }
.clp-credits .cr-m[data-band="good"] { color: var(--success-hi); }
.clp-credits .cr-m[data-band="none"] { color: var(--text-4); }

/* save bar: 146 rows put the native buttons a long way down the page */
.clp-credits .cr-save {
  position: sticky; bottom: 0; z-index: 6; display: flex; align-items: center; gap: 14px;
  margin-top: 18px; padding: 12px 0; background: var(--bg); border-top: 1px solid var(--border-strong);
}
.clp-credits .cr-save .st { font-size: var(--fs-sm); color: var(--text-3); }
.clp-credits .cr-save .st b { font-family: var(--mono); font-weight: 500; color: var(--text); }
.clp-credits .cr-save .actions { margin-inline-start: auto; display: flex; gap: 8px; }

@media (max-width: 900px) {
  .clp-credits .cr-cols { display: none; }
  .clp-credits .cr-row { grid-template-columns: 1fr 130px; row-gap: 8px; padding: 12px 16px; }
  .clp-credits .cr-buys { grid-column: 1 / -1; }
  .clp-credits .cr-m { text-align: start; }
}

/* ---- the admin voice list (/admin/voices), views/templates/admin/voices.twig.
   Built from the list parts above; these are what only it has. Its row is the voice, the
   creation date in the shared mono cell, the Status cell and the row menu, which is the app's
   `.kebab` hanging from `.x` as on the order rows. A cloned voice's Status cell stays empty. ---- */
.clp-voices .cr-cols,
.clp-voices .cr-row { grid-template-columns: minmax(200px, 1fr) 148px 128px 32px; }
.clp-voices .vr-status { display: flex; align-items: center; font-size: var(--fs-body); }
.clp-voices .x { position: relative; justify-self: end; }
/* the drag handle, shown only while the list is sorted Manual */
.clp-voices .vr-grip { color: var(--text-4); font-size: var(--icon); cursor: grab; }
/* the status filter and the sort, two of the app's sort controls on the list bar's right */
.clp-voices .vr-ctls { display: flex; gap: 8px; }

/* the phone fold, as Hatim approved it: the column heads and the date go; a catalogue row keeps
   its name, its Status in the credits page's 130px track and its menu; a cloned row, whose Status
   cell is empty, gives that width to its name. */
@media (max-width: 900px) {
  .clp-voices .cr-cols { display: none; }
  .clp-voices .cr-row { grid-template-columns: minmax(0, 1fr) 130px 32px; padding: 12px 16px; }
  .clp-voices .cr-buys { display: none; }
  .clp-voices [data-group="cloned"] .cr-row { grid-template-columns: minmax(0, 1fr) 32px; }
  .clp-voices [data-group="cloned"] .vr-status { display: none; }
}

/* ---- the three-dot processing spinner on writer's Generate button.
   Core ships this as an inline <style> inside the SVG, and the fork inherited it.
   The rules are the plugin's to load once here rather than to re-emit inside every
   render of that button. Class names are core's own and stay until writer is
   converted, so the markup keeps working untouched. ---- */
.spinner_S1WN { animation: spinner_MGfb 0.8s linear infinite; animation-delay: -0.8s; }
.spinner_Km9P { animation-delay: -0.65s; }
.spinner_JApP { animation-delay: -0.5s; }
@keyframes spinner_MGfb { 93.75%, 100% { opacity: .2 } }


/* ============================================================
   Phase 4 stage 5: chat composer + the two layout states.
   The composer is the shared .promptbar / .barmeta system, so
   nothing here restyles it; these rules only place it and cover
   the affordances chat has that no other tool page does (quote,
   dictation waveform, attachments, thinking effort).
   ============================================================ */

/* ---- measures: the bar spans the content area, the thread keeps a reading column ---- */
.clp-chat .clp-thread { width: 100%; }
.clp-chat .clp-quote, .clp-chat .clp-recording, .clp-chat .clp-usagenote, .clp-chat .clp-attached { max-width: none; }

/* ---- placement: the bar is docked at the bottom, empty and in-thread alike.
   `margin-top: auto` is unconditional, which is what pins it when the thread is
   short or absent. The empty-state hero keeps its own `my-auto` and centres in
   whatever space is left above the bar. ---- */
.clp-chat .clp-chatbar { position: sticky; bottom: 0; z-index: 40; width: 100%; margin-top: auto; padding: 14px 0 18px; background: linear-gradient(to top, var(--bg) 72%, transparent); }

/* Chat is the one page whose own last element is the bottom edge of the screen, so
   it drops the shared bottom gutter that every other page keeps below its content.
   Without this the sticky bar stops one gutter short of the viewport floor. */
main:has(.clp-chat) { padding-bottom: 0; }

/* Chips sit under a full-width bar here, not the capped one imagine and video use,
   so they align to the bar's leading edge instead of floating in its centre. */
.clp-chat .barmeta { justify-content: flex-start; }

/* ---- scroll-to-bottom, floating above the bar ---- */
.clp-scrollend { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); margin-bottom: 10px;
  z-index: 50; width: 34px; height: 34px; border-radius: var(--r-pill); display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border-strong); background: var(--card); color: var(--text-3); cursor: pointer;
  transition: color .14s, border-color .14s; }
.clp-scrollend:hover { color: var(--text); border-color: var(--border-struct); }
.clp-scrollend svg.ic { width: var(--icon); height: var(--icon); fill: currentColor; transform: rotate(90deg); }

/* ---- session usage note, above the composer ---- */
.clp-usagenote { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin: 0 auto 10px;
  padding: 11px 14px; border: 1px solid var(--border-strong); border-radius: var(--r-lg); background: var(--raised);
  font-size: var(--fs-body); color: var(--text-2); }

/* ---- the turn the composer is replying to ---- */
.clp-quote { display: flex; align-items: center; gap: 10px; margin: 0 auto 8px; padding: 8px 8px 8px 14px;
  border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--raised);
  font-size: var(--fs-sm); color: var(--text-3); }
.clp-quote > div { flex: 1; min-width: 0; max-height: 76px; overflow-y: auto; }
.clp-quote svg.ic { flex: none; }

/* ---- dictation: playback row and the live level bars ---- */
.clp-recording { margin: 0 auto 8px; padding: 6px 8px; border: 1px solid var(--border-strong);
  border-radius: var(--r-xl); background: var(--card); }
.clp-recording .mono { font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-3); width: 38px; }
/* one level bar of the live recording row; the template passes its height as `--h` */
.clp-vizbar { width: 3px; height: var(--h); border-radius: var(--r-pill); background: var(--border-strong); transition: height .1s ease-out; }
.clp-vizbar.on { background: var(--text); }

/* ---- attachments on the composer ---- */
.clp-tool .composer .clp-attached { margin: 10px auto 0; }
.clp-att-hit { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;
  padding: 0; border: none; background: none; color: var(--text-3); cursor: pointer; }
.clp-att-hit img { width: 100%; height: 100%; object-fit: cover; }

/* ---- thinking effort: the menu anchors to its chip, like the model picker ---- */
.clp-reason { position: relative; display: inline-flex; }
.clp-reason .menu { top: auto; bottom: calc(100% + 8px); box-shadow: var(--lift-menu-up); }
/* a menu that opens above its control: at its start edge, or at its end edge with `.end`. The
   placement sits in core's layer, where core's own placements sat, so any plugin rule still wins. */
@layer components {
  .menu.menu-up { margin-top: 0; margin-bottom: 4px; inset-inline-start: 0; inset-inline-end: auto; top: auto; bottom: 100%; }
  .menu.menu-up.end { inset-inline-start: auto; inset-inline-end: 0; }
}
.menu.menu-up { box-shadow: var(--lift-menu-up); }
/* the account menu's size, 240 pixels wide with its height following its rows, the column its rows
   stack in, and the anchor its trigger and menu share; in core's utilities layer, as above */
@layer utilities {
  .menu.menu-wide { width: 240px; max-height: max-content; }
  .menu-stack { display: flex; flex-direction: column; gap: 4px; }
  .acct-wrap { position: relative; }
}



/* ============================================================
   Chat message = a TURN. Approved design: chat-message-v2 option A.
   Every value below is lifted from that file. Built from atoms the
   app already had: a --border hairline separates turns, the role label
   is .sec-head weight, the model name is .cm mono metadata, and the
   reasoning panel is the bordered --raised note shape.
   ============================================================ */
.clp-chat .turn { padding: 14px 0; border-bottom: 1px solid var(--border); }
.clp-chat .turn:last-child { border-bottom: none; }
.clp-chat .turn-body { min-width: 0; }
.clp-chat .turn-h { display: flex; align-items: baseline; gap: 10px; margin-bottom: 6px; }
.clp-chat .turn-h .role { display: inline-flex; align-items: center; gap: 8px; font-family: var(--font); font-size: var(--fs-md); font-weight: 600; letter-spacing: -.01em; color: var(--text); }
.clp-chat .turn-h .role .is-deleted { color: var(--text-3); }
.clp-chat .turn-h .meta { font-family: var(--mono); font-size: var(--label-size); color: var(--text-4); white-space: nowrap; }
/* ---- who is speaking, as a mark on the header line. Same tile as .clp-mr-av in the
   model picker, one size down, so the provider logo you pick a model by is the logo
   on the answer it produced. Core's <x-avatar> supplies the image, the masked logo or
   the initials; only the box is ours. The sender's own mark is the identity avatar. ---- */
.clp-chat .clp-av { width: 20px; height: 20px; flex: none; overflow: hidden; }
.clp-chat .clp-av.logo { border-radius: var(--r-sm); color: var(--text-3);
  font-family: var(--mono); font-size: var(--label-size); font-weight: 400; letter-spacing: .02em; }
.clp-chat .clp-av .mask { width: 13px; height: 13px; }
.clp-chat .clp-av.logo > img, .clp-chat .clp-av.logo > canvas { border-radius: var(--r-sm); }
.clp-chat .turn .pulse { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: clp-pulse 1.4s ease-in-out infinite; flex: none; }
@keyframes clp-pulse { 0%, 100% { opacity: .25 } 50% { opacity: 1 } }

/* ---- message body. Core runs the output through Tailwind Typography on
   `x-markdown > div`, which caps it at 65ch and imposes its own type scale, so the
   overrides have to sit at that level: styling `.content` alone loses to prose.
   `.content` is also a CORE class (720px centred flex column), reset here too.
   Full width and the app's own scale, per Hatim. ---- */
.clp-chat .content { display: block; max-width: none; }
.clp-chat x-markdown > div {
  max-width: none;
  font-family: var(--font);
  font-size: var(--fs-md);
  line-height: 1.65;
  color: var(--body);
}
.clp-chat x-markdown > div :where(p) { margin: 0; font-size: inherit; line-height: inherit; }
.clp-chat x-markdown > div :where(p) + :where(p, ul, ol, pre, blockquote, table) { margin-top: 11px; }
.clp-chat x-markdown > div :where(ul, ol) { margin: 11px 0 0; padding-inline-start: 20px; }
.clp-chat x-markdown > div :where(li) { margin: 0; padding-inline-start: 2px; }
.clp-chat x-markdown > div :where(li) + :where(li) { margin-top: 5px; }
.clp-chat x-markdown > div :where(li) :where(p) { margin: 0; }
.clp-chat x-markdown > div :where(strong) { font-weight: 600; color: var(--text); }
.clp-chat x-markdown > div :where(a) { color: var(--accent-hi); font-weight: 400; text-underline-offset: 2px; }
.clp-chat x-markdown > div :where(h1, h2, h3, h4) {
  font-family: var(--font); font-weight: 600; letter-spacing: -.01em; color: var(--text); margin: 18px 0 6px; }
.clp-chat x-markdown > div :where(h1) { font-size: var(--fs-lg); }
.clp-chat x-markdown > div :where(h2) { font-size: var(--fs-md); }
.clp-chat x-markdown > div :where(h3, h4) { font-size: var(--fs-md); }
.clp-chat x-markdown > div :where(code) {
  font-family: var(--mono); font-size: var(--fs-sm); font-weight: 400; color: var(--text);
  background: var(--raised); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 1px 5px; }
.clp-chat x-markdown > div :where(code)::before, .clp-chat x-markdown > div :where(code)::after { content: none; }
.clp-chat x-markdown > div :where(pre) {
  font-family: var(--mono); font-size: var(--fs-sm); line-height: 1.6; color: var(--body);
  background: var(--raised); border: 1px solid var(--border-strong); border-radius: var(--r);
  padding: 12px 14px; margin-top: 11px; overflow-x: auto; }
.clp-chat x-markdown > div :where(pre code) { background: none; border: none; padding: 0; font-size: inherit; }
.clp-chat x-markdown > div :where(blockquote) {
  margin-top: 11px; padding-left: 13px; border-left: 2px solid var(--border-strong);
  color: var(--text-3); font-style: normal; font-weight: 400; }
.clp-chat x-markdown > div :where(blockquote p)::before, .clp-chat x-markdown > div :where(blockquote p)::after { content: none; }
.clp-chat x-markdown > div :where(table) { margin-top: 11px; font-size: var(--fs-body); border-collapse: collapse; }
.clp-chat x-markdown > div :where(th, td) { border: 1px solid var(--border); padding: 7px 10px; text-align: left; }
.clp-chat x-markdown > div :where(th) { font-weight: 600; color: var(--text); background: var(--raised); }
.clp-chat x-markdown > div :where(hr) { margin: 16px 0; border: none; border-top: 1px solid var(--border); }


/* ---- history drawer: core's own right-hand aside, restyled.
   #sidebar ships Tailwind's w-96 / -me-96 and slides on [data-sidebar] on <html>.
   The ID beats those utilities, so the width moves here without editing the shared
   layout. Chat is the only page that fills the sidebar block. ---- */
#sidebar:has(.clp-drawer) { width: 268px; margin-right: -268px; background: var(--sunken); border-left: 1px solid var(--border); }
:root[data-sidebar] #sidebar:has(.clp-drawer) { margin-right: 0; }
/* the drawer shows while core's SidebarController keys the sidebar to history, and is a column while the sidebar is open */
.clp-drawer { display: none; }
#sidebar[data-key="history"] .clp-drawer { display: block; }
:root[data-sidebar] #sidebar[data-key="history"] .clp-drawer { display: flex; flex-direction: column; height: 100%; min-height: 0; }
/* ---- THE PANEL HEAD. The app has one: the history drawer's, which the confirm dialog reuses. ---- */
.clp-modal .drawer-head,
.clp-drawer .drawer-head { flex: none; height: 58px; display: flex; align-items: center; justify-content: space-between;
  padding: 0 12px 0 18px; border-bottom: 1px solid var(--border); }
/* ---- and the 34px button that closes it ---- */
.clp-modal .pbtn,
.clp-drawer .pbtn { flex: none; width: 34px; height: 34px; border-radius: var(--r); display: grid; place-items: center;
  color: var(--text-3); background: none; border: none; cursor: pointer; }
.clp-drawer .pbtn:hover { color: var(--text); background: var(--hover); }
/* ---- the icon over a wall tile's word, and inside a drawer button that owns its own flex layout ---- */
#content .gal .gtile .g-word svg.ic,
.clp-drawer .pbtn svg.ic { width: var(--icon-md); height: var(--icon-md); fill: currentColor; }
/* THE ROWS STOP TOUCHING. The list was `display: block` and the row carries `margin: 0`, so
   there were zero pixels between rows and the row's own `border-radius` had nowhere to show.
   Nothing noticed while `.on` was the only row that could be painted; picking paints several
   at once and they ran together as one block. The gap goes on the LIST rather than as a margin
   on the row, so the last row leaves no dead space under it. */
.clp-drawer .drawer-list { flex: 1; min-height: 0; overflow-y: auto; padding: 8px;
  display: flex; flex-direction: column; gap: 3px; }
/* `position: relative` is load-bearing, not tidiness: the row holds a full-bleed link overlay and
   an absolutely placed delete control, and without it BOTH escape to whatever ancestor happens to
   be positioned. The control then floats at that box's midpoint instead of the row's, and the
   overlay covers the entire drawer, so every row opens whichever one is last in the markup. */
.clp-drawer .drawer-row { position: relative; display: flex; flex-direction: column; padding: 9px 11px; border-radius: var(--r);
  cursor: pointer; transition: background .12s; }
.clp-drawer .drawer-row:hover { background: var(--hover); }
.clp-drawer .drawer-row.on { background: var(--accent-fill); }
.clp-drawer .drawer-row .nm { font-size: var(--fs-sm); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* THE DRAWER ROW GAINS A DELETE, on both drawers, because `.drawer-row` is worn twice: the
   writer and niche history and the chat conversation history. Neither has ever carried a
   control of any kind.

   NOT SCOPED `#content`. The sidebar is a SIBLING of `#content` in the layout, not a child, so
   a `#content` prefix here would reach nothing. Every shipped `.clp-drawer` rule above is
   unprefixed for the same reason.

   THE CELL IS ABSOLUTE, THE CONTROLS ARE NOT. The row is a column holding one line of text, so a
   control added as a sibling would land on a second line. The cell sits at the right edge instead
   and lays its two controls out in a row, exactly as `.led-acts` does on every ledger row. The trash
   MUST NOT keep positioning itself: it did while it was the only control here, and left in place
   it escapes the cell and lands on top of the box. */
.clp-drawer .drawer-row .dr-open, #content .trow-open { position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
.clp-drawer .drawer-row > .nm { position: relative; z-index: 0; pointer-events: none; }
.clp-drawer .drawer-row .dr-acts {
	position: absolute; right: 8px; top: 50%; transform: translateY(-50%); z-index: 2;
	display: flex; align-items: center; gap: 8px;
}
.clp-drawer .drawer-row .dr-act {
	appearance: none; border: 0; background: none; padding: 0; cursor: pointer;
	color: var(--text-3); display: inline-flex; opacity: 0;
	transition: opacity .16s, color .14s;
}
.clp-drawer .drawer-row .dr-act svg.ic { width: var(--icon); height: var(--icon); fill: currentColor; }
.clp-drawer .drawer-row:hover .dr-act,
.clp-drawer .drawer-row:focus-within .dr-act { opacity: 1; }
.clp-drawer .drawer-row .dr-act:hover { color: var(--red-hi); }
/* THE TITLE CLEARS THE WHOLE CELL, not the part of it that happens to be visible. The trash keeps
   its flex slot at opacity 0, so even where the box is the only thing shown the box still sits
   41px in from the row edge. And the cell is up whenever the surface is picking, not only under
   the pointer, so hover alone is the wrong trigger. Measured at 268px: the title ellipsises at
   632, the box starts at 640, the trash ends at 681 and the row at 689. */
.clp-drawer .drawer-list.is-picking > .drawer-row > .nm,
.clp-drawer .drawer-row.is-on > .nm,
.clp-drawer .drawer-row:hover > .nm { padding-inline-end: 46px; }
/* A touch device has no hover to reveal anything with, so the delete and Writer's checkbox stay up
   and the title keeps its allowance; Chat's `.led-sel` box is handled by its own touch rule. */
@media (hover: none) {
	.clp-drawer .drawer-row :is(.dr-act, .clp-chk) { opacity: 1; }
	.clp-drawer .drawer-row > .nm { padding-inline-end: 46px; }
}
/* LOAD MORE, one rule for every list that pages by hand: the writer's and the chat's drawers and
   the admin voice list's groups. NO SCOPE AT ALL, because the drawers sit in the layout's #sidebar,
   outside #content, and the voice groups sit inside it. */
.drawer-more { width: 100%; margin-top: 6px; padding: 8px; border: none; background: none;
  font-size: var(--fs-sm); color: var(--text-3); cursor: pointer; }
.drawer-more:hover { color: var(--text); }

/* ---- the model menu opens UPWARD on chat: the bar is at the bottom of the screen,
   so core's downward menu opens off the edge. THE DIALOG OPENS ITS MENU UPWARD TOO, and for
   a harder reason: a dialog clips its own overflow, so a menu dropped below the chip is cut
   off by the card. Both cast their shadow upward with it. ---- */
.clp-chat .clp-menu,
.clp-vmodal .clp-menu { top: auto; bottom: calc(100% + 10px); box-shadow: var(--lift-dialog-up); }

/* ---- message interior states, chat-message-v2 option A.
   Reasoning is the bordered --raised note panel, tools and sources are .chip rows,
   the error is --red-fill, and the group disclosures read as mono activity lines. ---- */
/* THE DISCLOSURE, one declaration and three consumers. Chat folds an activity group behind it, a
   carried-out step folds its prompt behind it, and the lightbox's prompt box opens the rest of a
   clamped prompt with it. All three are the same control: a bare mono line and a chevron that
   turns. Declared once with every scope rather than copied, which is the rule the header of this
   file states and the reason it counts its pairs. */
.clp-chat .grp-toggle,
.clp-wb .grp-toggle,
.clp-lb .grp-toggle { display: inline-flex; align-items: center; gap: 8px; margin-top: 10px; padding: 0;
  background: none; border: none; cursor: pointer; font-family: var(--mono); font-size: var(--label-size);
  letter-spacing: .04em; color: var(--text-4); transition: color .14s; }
.clp-chat .grp-toggle:hover,
.clp-wb .grp-toggle:hover,
.clp-lb .grp-toggle:hover { color: var(--text); }
/* the same line used as a status readout rather than a disclosure */
.clp-chat .grp-toggle[role="status"] { cursor: default; }
.clp-chat .grp-toggle .chev,
.clp-wb .grp-toggle .chev,
.clp-lb .grp-toggle .chev { width: 13px; height: 13px; transition: transform .2s; }
.clp-chat .grp-toggle .chev.open,
.clp-wb .grp-toggle .chev.open,
.clp-lb .grp-toggle .chev.open { transform: rotate(180deg); }

.clp-chat .reason { margin-top: 10px; border: 1px solid var(--border); border-radius: var(--r);
  background: var(--raised); padding: 10px 13px; }
.clp-chat .reason-h { font-family: var(--mono); font-size: var(--label-size); letter-spacing: var(--label-ls); color: var(--text-4);
  text-transform: uppercase; margin-bottom: 5px; }
.clp-chat .reason-b, .clp-chat .reason-b .content, .clp-chat .reason-b p { font-size: var(--fs-body); color: var(--text-3); line-height: 1.6; }
.clp-chat .reason-b button { font-family: var(--mono); font-size: var(--label-size); letter-spacing: .04em; color: var(--text-4); }
.clp-chat .reason-b button:hover { color: var(--text); }

.clp-chat .chips { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 6px; }
.clp-chat .chip { display: inline-flex; align-items: center; gap: 7px; min-height: 31px; padding: 4px 11px;
  border: 1px solid var(--border-strong); border-radius: var(--r); font-size: var(--fs-sm); color: var(--text-2);
  max-width: 100%; transition: border-color .14s, background .14s, color .14s; }
.clp-chat .chip .n { font-family: var(--mono); font-size: var(--label-size); color: var(--text-4); flex: none; }
/* ---- one-line truncation ---- */
.clp-chat .grp-toggle .lbl,
.clp-chat .chip .ct { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clp-chat .chip .cd { color: var(--text-4); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clp-chat .chip.src { cursor: pointer; }

.clp-chat .grp-done { display: inline-flex; align-items: center; gap: 7px; margin-top: 10px;
  font-family: var(--mono); font-size: var(--label-size); letter-spacing: .04em; color: var(--text-4); }
.clp-chat .grp-done svg.ic { width: var(--icon-xs); height: var(--icon-xs); color: var(--success-hi); }

/* The app's one error treatment, reaching the clone dialog as well rather than being copied there. Its
   sentence wraps the way the dialog's hint does, so no refusal ends on one word alone. */
.clp-chat .err,
.clp-modal .err { margin-top: 10px; border: 1px solid var(--red-bd); background: var(--red-fill);
  border-radius: var(--r); padding: 11px 13px; font-size: var(--fs-body); color: var(--body);
  display: flex; gap: 10px; align-items: center; }
.clp-chat .err .ei,
.clp-modal .err .ei { color: var(--red-hi); font-weight: 600; flex: none; }
.acts-row { display: flex; gap: 3px; flex-wrap: wrap; }
/* one owner for the space under a turn body, so the row never stacks its margin on the wrapper's */
.clp-chat .turn-foot { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.act { display: inline-flex; align-items: center; gap: 5px; height: 27px; padding: 0 9px;
  border-radius: var(--r-sm); border: 1px solid transparent; background: none; cursor: pointer;
  font-family: var(--mono); font-size: var(--label-size); letter-spacing: .03em; color: var(--text-4);
  transition: background .14s, border-color .14s, color .14s; }
.act:hover { background: var(--hover); border-color: var(--border); color: var(--text); }
.act svg.ic { width: var(--icon-xs); height: var(--icon-xs); fill: currentColor; }
/* the shared player carries no outer spacing; each surface owns its own rhythm */
.clp-chat .clp-audio { margin-top: 10px; }
.act[disabled] { opacity: .35; cursor: default; }
.act[disabled]:hover { background: none; border-color: transparent; color: var(--text-4); }
/* the silent state: same object as the action row it replaces, carrying the pulse */
.act.is-live { border-color: var(--border); }

/* ---- branch stepper: editing forks the thread and these walk the versions.
   No atom of its own. It joins .turn-foot so a turn keeps ONE action line,
   and speaks the .act mono the rest of that line already speaks. ---- */
.clp-chat .clp-branch { display: inline-flex; align-items: center; gap: 2px; }
.clp-chat .clp-branch .cnt { font-family: var(--mono); font-size: var(--label-size); color: var(--text-4); padding: 0 3px; letter-spacing: .03em; }
.clp-chat .clp-branch .act { padding: 0 6px; }
.clp-chat .clp-branch .act svg.ic { width: 13px; height: 13px; }
.clp-chat .clp-branch .act .rot-r { transform: rotate(90deg); }
.clp-chat .clp-branch .act .rot-l { transform: rotate(-90deg); }

/* ---- temporary chat strip: the conversation is not being saved.
   Fill and border are .clp-usagenote's, the app's existing full-width in-flow
   notice. It adds a hairline bottom rule because the thread scrolls under it. ---- */
.clp-tempbar { display: flex; align-items: center; gap: 12px; padding: 9px 24px; background: var(--raised);
  border-bottom: 1px solid var(--border); }
.clp-tempbar .d { font-size: var(--fs-sm); color: var(--text-3); }
.clp-tempbar .sp { margin-left: auto; display: flex; align-items: center; gap: 4px; }
.clp-tempbar > svg.ic { width: var(--icon-sm); height: var(--icon-sm); fill: currentColor; color: var(--text-4); flex: none; }

/* ---- conversation header: title, id, state chips, delete. ---- */
.clp-chat .convhead { padding: 22px 0 16px; border-bottom: 1px solid var(--border); }
/* the chat's name with the page's buttons at its right, moving under it at 640 pixels and narrower, as the page top's do */
.clp-chat .convhead-top { display: flex; align-items: flex-start; gap: 16px; }
.clp-chat .convhead-top .t { flex: 1; min-width: 0; }
@media (max-width: 640px) {
  .clp-chat .convhead-top { flex-direction: column; }
}
/* ---- THE DISPLAY TITLE at 26px: a conversation's name. ---- */
.clp-chat .convhead .t { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-size: var(--fs-2xl); font-weight: var(--display-w); letter-spacing: var(--display-ls); color: var(--text); }
.clp-chat .convhead .t textarea { display: block; width: 100%; padding: 0; border: none; background: none;
  outline: none; resize: none; font: inherit; letter-spacing: inherit; color: inherit; }
.clp-chat .convhead .t textarea::placeholder { color: var(--text-4); }
.clp-chat .convhead .t.autogrow-textarea::after { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w); font-size: var(--fs-2xl); letter-spacing: var(--display-ls); }
.clp-chat .convhead .row { display: flex; align-items: center; gap: 12px; margin-top: 10px; flex-wrap: wrap; }
/* ---- MONO METADATA, small: a count beside a heading, an id beside a title. ---- */
.clp-browse .cat-head .cs-count,
.clp-chat .convhead .uuid { font-family: var(--mono); font-size: var(--label-size); color: var(--text-4); }
.clp-chat .convhead .chip { min-height: 28px; padding: 3px 10px; font-size: var(--fs-sm); }
.clp-chat .convhead .chip.warn { border-color: var(--amber-bd); background: var(--amber-fill); }
.clp-chat .convhead .chip svg.ic { color: var(--text-4); }
.clp-chat .convhead .sp { margin-left: auto; display: flex; gap: 3px; }
.clp-chat .convhead .act.del:hover { color: var(--red-hi); border-color: var(--red-bd); background: var(--red-fill); }
/* chat's hero sits in a flex column that also holds the docked bar, so it centres
   in the space above it rather than carrying the shared opening's fixed padding. */
#content .clp-chat .hero { padding: 0; margin: auto 0; }
#content .clp-chat .hero .greet-sub { margin-bottom: 20px; }
#content .clp-chat .hero.is-temp { filter: grayscale(1); }

/* ---- editing your own turn: the accent-ringed field from chat-message-v2 ---- */
.clp-chat .turn-edit { margin-top: 4px; }
.clp-chat .turn-edit .field { width: 100%; border: 1px solid var(--accent); border-radius: var(--r);
  background: var(--card); box-shadow: var(--focus-ring); padding: 11px 13px; }
.clp-chat .turn-edit .field textarea { display: block; width: 100%; border: none; background: none; outline: none;
  resize: none; padding: 0; font-family: var(--font); font-size: var(--fs-md); line-height: 1.6; color: var(--text); }
.clp-chat .turn-edit .field.autogrow-textarea::after { font-family: var(--font); font-size: var(--fs-md); line-height: 1.6; }
.clp-chat .turn-edit .note { margin-top: 8px; font-size: var(--fs-sm); color: var(--text-4); }
.clp-chat .turn-edit .acts { display: flex; gap: 8px; margin-top: 10px; }

/* ---- the turn a message is replying to ---- */
.clp-chat .turn-quote { display: flex; align-items: flex-start; gap: 9px; margin-bottom: 9px;
  border: 1px solid var(--border); border-radius: var(--r); background: var(--raised);
  padding: 8px 11px; font-size: var(--fs-sm); color: var(--text-3); }
.clp-chat .turn-quote svg.ic { flex: none; margin-top: 2px; }

/* ---- attached files: an image shows itself, anything else is a labelled chip ---- */
.clp-chat .files { margin-top: 11px; display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; }
.clp-chat .files .imgf { width: 96px; height: 96px; border-radius: var(--r-lg); border: 1px solid var(--border-strong);
  overflow: hidden; display: block; background: var(--raised); }
.clp-chat .files .chip { cursor: pointer; }
.clp-chat .files .chip .warn { color: var(--red-hi); font-weight: 600; }

/* ============================================================
   Phase B: plans and checkout, the money path.
   Both pages are forked into the plugin. Everything below is
   scoped under .clp-bill so no generic name reaches a Tailwind
   utility on a page we have not forked.
   ============================================================ */

/* ---- PLANS. One card per plan on a shared row grid, so each part of every card sits on the same line; the
   page is .clp-bill's, as wide as the content column. ---- */
.clp-bill .plan-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.clp-bill .pcard { display: grid; grid-template-rows: subgrid; grid-row: span 10; row-gap: 14px; align-content: start;
  border: 1px solid var(--border-strong); border-radius: var(--r-lg); background: var(--card); padding: 20px; color: var(--text); }
.clp-bill .pcard.cur { background: var(--accent-fill); border-color: var(--accent-bd); }
.clp-bill .p-name { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 25px; }
.clp-bill .p-name h3, .clp-bov .bcard h3, .clp-bill .bdoc .dc h3 { font-size: var(--fs-md); font-weight: 600; color: var(--text); }
.clp-bill .p-for { font-size: var(--fs-sm); color: var(--text-3); margin-top: -8px; }
.clp-bill .p-price { display: flex; align-items: baseline; gap: 4px; }
.clp-bill .p-price b { letter-spacing: -.028em; font-size: var(--fig-xl); line-height: 1; color: var(--text); }
.clp-bill .p-price span { font-size: var(--fs-sm); color: var(--text-4); }
.clp-bill .p-note { font-size: var(--fs-sm); color: var(--text-4); margin-top: -8px; }
.clp-bill .p-hero { display: flex; flex-direction: column; gap: 10px; padding-top: 14px; border-top: 1px solid var(--border); }
.clp-bill .p-cr { display: flex; align-items: baseline; gap: 6px; }
.clp-bill .p-cr b { letter-spacing: -.02em; font-size: var(--fig); line-height: 1; color: var(--text); }
.clp-bill .p-cr span { font-size: var(--fs-sm); color: var(--text-3); }
.clp-bill .pcard > .clp-label { align-self: end; }
.clp-bill .p-list { list-style: none; margin: -6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.clp-bill .p-list li { display: flex; align-items: center; gap: 8px; font-size: var(--fs-sm); color: var(--text-2); }
.clp-bill .p-list li svg.ic { color: var(--accent-hi); }
.clp-bill .p-top li span { flex: 1; }
.clp-bill .p-top li .clp-mark { color: var(--text-2); }
.clp-bill .p-list li.off, .clp-bill .p-list li.off :is(svg.ic, .clp-mark) { color: var(--text-4); }
.clp-bill .tabs .save { font-family: var(--mono); font-size: var(--label-size); letter-spacing: .02em; color: var(--text-3); margin-inline-start: 6px; }
.clp-bill .plan-fine { font-size: var(--fs-sm); color: var(--text-4); margin-top: 14px; }
.clp-bill .plan-compare .clp-ctable :is(th, td):not(:first-child) { width: 17%; }
.clp-bill .plan-compare thead .ct-n { font-size: var(--fs-body); font-weight: 600; color: var(--text); }
.clp-bill .plan-compare thead .ct-p { margin: 4px 0 10px; font-size: var(--fig-xs); letter-spacing: -.02em; color: var(--text); }
.clp-bill .plan-compare thead .ct-p span { font-family: var(--font); font-size: var(--fs-sm); font-weight: 400; letter-spacing: 0; color: var(--text-4); }

/* ---- CHECKOUT. The order summary is a box holding the plan card's parts, the coupon and the money lines; the
   payment column is not a box, since it holds Stripe's own form. ---- */
.clp-bill .co { display: grid; grid-template-columns: minmax(0, 400px) minmax(0, 1fr); gap: 20px; align-items: start; }
.clp-bill .co .sum { display: flex; flex-direction: column; gap: 14px; padding: 20px; }
.clp-bill .co .sum .p-list { margin-top: -6px; }
.clp-bill .tot { display: flex; align-items: baseline; justify-content: space-between; padding-top: 14px;
  border-top: 1px solid var(--border); font-size: var(--fs-md); color: var(--body); }
.clp-bill .tot b { font-size: var(--fig-md); letter-spacing: -.02em; line-height: 1; color: var(--text); }
/* the checkout's stand-ins while the taxes are worked out: the taxes line, then the total */
.clp-bill .kv .clp-skel { --w: 40px; }
.clp-bill .tot .clp-skel { --w: 40px; --h: 20px; margin-block: 4px; }
.clp-bill .co hr { border: 0; border-top: 1px solid var(--border); margin: 0; }
.clp-bill .co .sum-note { font-size: var(--fs-sm); line-height: 1.55; color: var(--text-3); }
/* the coupon field is one shell, so the input and its action share a border and a focus ring */
.clp-bill .co .coup { display: flex; align-items: center; gap: 8px; height: 40px; padding: 0 13px;
  border: 1px solid var(--border-control); border-radius: var(--r); transition: border-color .14s, box-shadow .14s; }
.clp-bill .co .coup input { flex: 1; min-width: 0; border: 0; background: transparent; outline: none;
  height: auto; padding: 0; font-size: var(--fs-body); color: var(--text); text-transform: uppercase; }
.clp-bill .co .coup input:focus { box-shadow: none; border-color: transparent; }
.clp-bill .co .coup input::placeholder { color: var(--text-4); text-transform: none; }
.clp-bill .co .coup :is(button, .coup-k) { font-size: var(--fs-sm); font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }
.clp-bill .co .coup button { color: var(--text-3); transition: color .14s; }
.clp-bill .co .coup button:hover, .clp-bill .co .coup .coup-k { color: var(--text); }
.clp-bill .co .secure { display: flex; align-items: center; justify-content: center; gap: 5px;
  font-size: var(--fs-xs); color: var(--text-3); margin-top: 9px; }
/* the round tile a summary or a limit panel opens with */
.clp-bill .av { width: 38px; height: 38px; border-radius: 10px; background: var(--raised);
  border: 1px solid var(--border-strong); display: grid; place-items: center;
  color: var(--text-2); flex: none; }
.clp-bill .av svg.ic { width: var(--icon-lg); height: var(--icon-lg); }

/* the sprite's glyphs on the billing and account pages, sized once for both scopes.
   fill: currentColor is what makes a Phosphor shape take the colour of the row it sits in. */
.clp-bill svg.ic,
.clp-acct svg.ic { width: var(--icon); height: var(--icon); fill: currentColor; flex: none; }

/* ---- the three limit-exceeded panels and the permission-denied screen. Live states,
   not dead branches: Alpine swaps them in when the plan you picked allows fewer seats,
   cloned voices or assistants than the workspace already holds. ---- */
.clp-bill .cap { display: flex; flex-direction: column; gap: 12px; }
.clp-bill .cap h2 { font-size: var(--fs-xl); }
.clp-bill .cap p, .clp-acct .note { font-size: var(--fs-body); line-height: 1.6; color: var(--text-2); }
.clp-bill .cap .fine { font-size: var(--fs-xs); color: var(--text-4); }

/* ---- phone. The grid stacks and the two checkout columns become one. ---- */
@media (max-width: 1100px) { .clp-bill .plan-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 1023px) { .clp-bill .co { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 640px) {
  .clp-bill .plan-grid { grid-template-columns: minmax(0, 1fr); }
  .clp-bill .p-price b { font-size: var(--fig-lg); }
}

/* ---- the payment column's own pieces. Most of these draw only when a gateway or the
   address step is switched on, which none are today; they are styled anyway so the day
   one is enabled it lands on the system rather than on stock Tailwind. ---- */
.clp-bill .pay { display: flex; flex-direction: column; gap: 20px; }
.clp-bill .pay h2, .clp-bill .pay .pay-h { font-size: var(--fs-xl); margin-bottom: 14px; }
.clp-bill .pay-h .n { font-family: var(--mono); color: var(--text-4); }
.clp-bill .addr { display: flex; flex-direction: column; }
.clp-bill .addr-saved { display: flex; align-items: center; justify-content: space-between;
  gap: 24px; width: 100%; text-align: start; }
.clp-bill .addr-saved address { font-style: normal; font-size: var(--fs-body); font-weight: 450;
  line-height: 1.55; color: var(--text-2); }
.clp-bill .addr-saved svg.ic { color: var(--text-4); }
.clp-bill .addr-saved:hover svg.ic { color: var(--text); }
.clp-bill .addr-form { display: flex; flex-direction: column; gap: 16px; }

/* the other payment methods: provider buttons on a grid of one, two or three, the crypto marks, the offline rows */
.clp-bill .pay-alt { display: flex; flex-direction: column; gap: 8px; }
.clp-bill .pay-grid { display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); gap: 8px; align-items: center; }
.clp-bill .pay-logo { display: inline-flex; align-items: center; }
.clp-bill .pay-logo :is(svg, img) { height: 16px; width: auto; }
.clp-bill .pay-coins { display: inline-flex; align-items: center; gap: 4px; }
.clp-bill .pay-coins img { width: 20px; height: 20px; }
.clp-bill .off-rows { display: flex; flex-direction: column; gap: 4px; }

/* the divider between two payment methods: a label sitting on a hairline */
.clp-bill .pay-or { position: relative; display: flex; justify-content: center;
  margin: 16px 0; font-size: var(--fs-sm); }
.clp-bill .pay-or::before { content: ""; position: absolute; left: 0; top: 50%;
  width: 100%; border-top: 1px solid var(--border); }
.clp-bill .pay-or span { position: relative; padding: 0 10px; background: var(--bg); color: var(--text-3); }

.clp-bill .pay-err { display: flex; align-items: center; gap: 5px; margin-top: 9px;
  font-size: var(--fs-xs); color: var(--red-hi); }
.clp-bill .pay-err svg.ic { color: var(--red-hi); }

/* an offline method: a selectable row, chosen in the same accent as everything else */
.clp-bill .off-row { display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 11px 13px; border: 1px solid var(--border-strong); border-radius: var(--r);
  font-size: var(--fs-body); color: var(--text-2); cursor: pointer;
  transition: background .14s, color .14s, border-color .14s; }
.clp-bill .off-row svg.ic { margin-inline-start: auto; }
/* THE PICKED STATE on an outline control: a chosen choice button (the options panel and the writer's Tone), billing's
   chosen row, and an outline button that switches something on while it is on. One rule after their hovers, so a
   picked control keeps its look under the pointer. */
.clp-tool .clp-seg.on,
.radio-button:checked + span,
.clp-bill .off-row.on,
.button-outline.on, .button-outline.on:hover { background: var(--accent-fill); border-color: var(--accent-bd); color: var(--accent-hi); }

/* the screen a member who does not own the workspace sees */
.clp-bill .denied { max-width: 384px; }

/* the Stripe form: the mount point, the button and the reassurance line stacked */
.clp-bill .stripe-pay { display: flex; flex-direction: column; gap: 8px; }
.clp-bill .stripe-card { display: flex; align-items: center; }
.clp-bill .stripe-mount { width: 100%; }
/* the Payment Element's own box. Stripe draws its fields inside; this only holds them. */
.clp-bill .stripe-el { border: 1px solid var(--border-control); border-radius: var(--r-lg);
  background: var(--card); padding: 14px 15px; }

/* ---- THE PLANS PAGE'S THREE STATES, read off `data-state`, which core's `plans` component writes: `initial`
   while the plans load, then `normal` or `empty`. A utility class cannot hide a card this sheet gives a display. ---- */
.clp-bill .pcard.skel { display: none; }
.clp-bill[data-state="initial"] .pcard.skel { display: grid; }
/* a plan card's stand-ins: the name, the price, the line, the button, two lines of what it holds */
.clp-bill .pcard.skel .clp-skel:nth-child(1) { --w: 80px; --h: 20px; }
.clp-bill .pcard.skel .clp-skel:nth-child(2) { --w: 96px; --h: 36px; }
.clp-bill .pcard.skel .clp-skel:nth-child(3) { --w: 128px; }
.clp-bill .pcard.skel .clp-skel:nth-child(4) { --w: 100%; --h: 36px; }
.clp-bill .pcard.skel .clp-skel:nth-child(5) { --w: 100%; }
.clp-bill .pcard.skel .clp-skel:nth-child(6) { --w: 66.6667%; }
.clp-bill[data-state="initial"] .pcard:not(.skel) { display: none; }
.clp-bill .bill-empty { display: none; }
.clp-bill[data-state="empty"] .bill-empty { display: block; }
.clp-bill[data-state="empty"] .plan-grid,
.clp-bill:is([data-state="initial"], [data-state="empty"]) :is(.plan-fine, .plan-compare) { display: none; }

/* ============================================================
   Phase C: the account pages. Six pages on one shell, all
   scoped under .clp-acct so no generic name reaches a page we
   have not forked.

   Measured against the live box before anything was designed:
   760 stock class tokens, 112 of them unreachable. Account
   deletion has no option row, so its two blocks never draw. The
   phone field has no policy row, so it never draws. The language
   picker is gone on Hatim's call: 30 locales ship and exactly one
   is enabled, so the control could never change anything.
   ============================================================ */

/* the account set reads as a form, not as a gallery, so it sits narrower than .clp-page */
.clp-page.clp-acct { max-width: 720px; }

/* ---- THE SHELL. A page top, and a stack of cards, one card per subject.
   Every one of the six pages is this shell with a different body. ---- */
.clp-acct .stack { display: flex; flex-direction: column; gap: 12px; }

/* ---- THE SECTION CARD: one subject, its heading, and its rows. ---- */
.clp-acct .sec { display: flex; flex-direction: column; gap: 16px; }
/* a section card placed on the page outside the stack keeps the stack's gap */
.clp-acct > .sec { margin-top: 12px; }
.clp-acct .sec > .sh { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.clp-acct .sec > .sh h2 { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w);
  letter-spacing: var(--display-ls); font-size: var(--fs-lg); color: var(--text); }
/* The clone dialog wears the same field. CHILD COMBINATORS THERE, because its file control is itself a
   `<label>` inside the field, and a descendant selector would repaint that row as a block of text. */
:is(.clp-acct, .clp-bill) .fld label,
.clp-modal .fld > label { display: block; font-size: var(--fs-sm); color: var(--text-2); margin-bottom: 7px; }
.clp-acct .hint,
.clp-modal .fld > .hint { font-size: var(--fs-xs); color: var(--text-4); margin-top: 7px; line-height: 1.55; }
:is(.clp-acct, .clp-bill) .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
/* a label with an action opposite it, which is how email and verification both read */
.clp-acct .lbl-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 7px; }
.clp-acct .lbl-row label { margin: 0; }
/* the quiet link: Change email beside the email, Change password at the profile's foot */
.clp-acct .qlink { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-sm); font-weight: 600; color: var(--text-3); }
.clp-acct .qlink:hover { color: var(--text); }
.clp-acct .qlink svg.ic { width: var(--icon-sm); height: var(--icon-sm); }
.clp-acct .foot { display: flex; align-items: center; justify-content: space-between; gap: 14px; }

/* ---- THE IDENTITY CARD. Core's header, reskinned. ---- */
.clp-acct .ident { display: flex; align-items: center; justify-content: space-between; gap: 16px; position: relative; }
.clp-acct .ident .who { display: flex; align-items: center; gap: 14px; min-width: 0; }
.clp-acct .ident .av { width: 52px; height: 52px; }
.clp-acct .ident .nm h2 { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w);
  letter-spacing: var(--display-ls); font-size: var(--fs-xl); color: var(--text); }
.clp-acct .ident .meta { display: flex; align-items: center; gap: 7px; font-size: var(--fs-sm);
  color: var(--text-3); margin-top: 4px; flex-wrap: wrap; }
.clp-acct .ident .meta .sep { width: 3px; height: 3px; border-radius: var(--r-pill); background: var(--text-4); flex: none; }

/* ---- THE MAP BEHIND THE IDENTITY CARD. Core's own treatment, translated one for one
   from its Tailwind: greyscale, three overlays fading the card colour in from the left so
   the map survives mainly in the right third, and a hard-light blend rather than an
   inversion on black. The state class is bound in Alpine, so a user whose coordinates are
   unknown gets the plain card with none of these rules reaching it.
   The wrapper takes no pointer events: this is a backdrop, not a map the user drives. ---- */
.clp-acct .mapbox { position: relative; overflow: hidden; }
.clp-acct .mapbox .mapwrap { position: absolute; inset: 0; filter: grayscale(1); pointer-events: none; }
/* THE FRAME IS TALLER THAN THE CARD ON PURPOSE, AND OFFSET UPWARD. OpenStreetMap's embed
   paints two pieces of its own interface over the map, and neither can be styled or switched
   off: the frame is another origin so no CSS of ours reaches inside it, and the embed accepts
   only four parameters (bbox, layer, marker, theme), none of which hides its controls.
     - TOP RIGHT, 68px tall: the zoom buttons. A white control on a card that has none.
     - BOTTOM: the credit, which also carries a "Report a problem" link and a donation appeal,
       so a third party asks a paying customer for money on their own account page.
   Both are clipped by overshooting the frame and letting the card's overflow cut them. Every
   number here was measured in a browser at both widths, not estimated: the control is 68px at
   any width, the bottom bar is 20px on a 720px card and wraps to 40px at phone width. 80 and
   72 clear both with room for a third wrapped line.
   The two overshoots are near enough equal that the location stays centred: the map's centre
   lands 4px above the card's own.
   The credit does not disappear, it MOVES. The licence requires attribution near the map, not
   inside it, and `.mapcred` below redraws it in our type. Removing it would breach the licence
   the map data is published under. */
.clp-acct .mapbox .mapwrap iframe { display: block; position: absolute; inset-inline: 0;
  top: -80px; width: 100%; height: calc(100% + 152px); border: 0; }
/* Our credit, in the corner where a map credit is expected. Sized and coloured to be read,
   not hidden: the licence asks for legible attribution, and a credit nobody can see is the
   same breach as no credit at all. */
.clp-acct .mapbox .mapcred { position: absolute; inset-block-end: 9px; inset-inline-end: 12px; z-index: 2;
  font-size: var(--fs-xs); line-height: 1; color: var(--text-3); text-decoration: none; transition: color .14s; }
.clp-acct .mapbox .mapcred:hover { color: var(--text); }
:root[data-mode="dark"] .clp-acct .mapbox .mapwrap { mix-blend-mode: hard-light; }
.clp-acct .mapbox .fade { position: absolute; inset: 0; }
.clp-acct .mapbox .fade.a { background: linear-gradient(to top right, var(--card), transparent); opacity: .5; }
.clp-acct .mapbox .fade.b { background: linear-gradient(to right, var(--card) 0%, transparent 20%, transparent 100%); }
.clp-acct .mapbox .fade.c { background: linear-gradient(to right, var(--card) 0%, transparent 80%, transparent 100%); }
.clp-acct .mapbox .ident { padding-top: 54px; }

/* ---- THE SWITCH. The one new component this phase adds. Nothing in the plugin had one;
   core builds it from a hidden checkbox and a styled span, and that mechanism is kept
   whole here so the change event still submits. Only the painted part is ours.
   The state is named in words beside it, because a switch alone is ambiguous at a glance. ---- */
.clp-acct .pref { display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 12px 14px; border-radius: var(--r); background: var(--raised); }
.clp-acct .pref .k { font-size: var(--fs-body); color: var(--text); }
.clp-acct .pref .side { display: flex; align-items: center; gap: 10px; }
.clp-acct .pref .st { font-size: var(--fs-sm); color: var(--text-4); }
.clp-acct .peer:checked ~ .st { color: var(--accent-hi); }
.clp-acct .why { font-size: var(--fs-xs); color: var(--text-4); line-height: 1.55; margin: 7px 0 0 2px; }

/* ---- THE SECRET ROW: a value that is mostly hidden, with its two actions inside the shell
   so the row reads as one object rather than a field beside two buttons. ---- */
.clp-acct .secret { display: flex; align-items: center; gap: 10px; height: 44px; padding: 0 6px 0 13px;
  border: 1px solid var(--border-strong); border-radius: var(--r); background: var(--card); }
.clp-acct .secret code { flex: 1; min-width: 0; font-family: var(--mono); font-size: var(--fs-sm);
  color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clp-acct .secret .none { color: var(--text-4); font-family: var(--font); font-size: var(--fs-sm); }
.clp-acct .iconbtn { width: 30px; height: 30px; border-radius: var(--r-sm); display: grid;
  place-items: center; color: var(--text-4); flex: none; transition: background .14s, color .14s; }
.clp-acct .iconbtn:hover { background: var(--hover); color: var(--text); }
.clp-acct .iconbtn svg.ic { width: var(--icon); height: var(--icon); }


/* ---- MEMORIES: the record row the library already ships, on this page's scope. The list card is shared with the order
   lists and every list of text rows (`.clp-list`). ---- */
.clp-acct .mlist, .clp-page .reclist, #content .clp-list { border: 1px solid var(--border-strong); border-radius: var(--r-lg); background: var(--card); }
.clp-acct .mrow { display: grid; grid-template-columns: 1fr 132px 28px; align-items: center; gap: 12px;
  padding: 13px 17px; border-bottom: 1px solid var(--border); transition: background .12s; }
.clp-acct .mrow:last-child { border-bottom: none; }
.clp-acct .mrow:hover { background: var(--hover); }
.clp-acct .mrow .mtxt { font-size: var(--fs-body); color: var(--text); line-height: 1.5; }
.clp-acct .mrow .tm { font-family: var(--mono); font-size: var(--fs-sm); color: var(--text-4); text-align: end; }
.clp-acct .mrow .x { position: relative; }

@media (max-width: 767px) {
  :is(.clp-acct, .clp-bill) .grid2 { grid-template-columns: minmax(0, 1fr); }
  .clp-acct .mrow { grid-template-columns: 1fr 28px; }
  .clp-acct .mrow .tm { display: none; }
}

/* ---- THE MEMORIES LIST'S THREE STATES, on core's own `state` (`initial`, then `loaded`
   or `empty`). Attribute selectors here rather than Tailwind's group-data variants, for
   the same reason the plans grid needed them: `.clp-acct .mrow` sets a display and scores
   two classes, so a single-class `.hidden` on a skeleton row loses to it and the
   placeholders never leave. That exact bug shipped on the plans page and was caught on the
   live site, not in review. ---- */
.clp-acct .mrow.skel { display: none; }
.clp-acct [data-state="initial"] .mrow.skel { display: grid; }
/* a memory row's stand-ins: its words, then its date */
.clp-acct .mrow.skel > :nth-child(1) .clp-skel { --w: 160px; }
.clp-acct .mrow.skel > :nth-child(2) .clp-skel { --w: 80px; --h: 12px; }
.clp-acct [data-state="initial"] .mrow:not(.skel) { display: none; }
.clp-acct .m-empty { display: none; }
.clp-acct [data-state="empty"] .m-empty { display: block; }
.clp-acct [data-state="empty"] .mlist { display: none; }
.clp-acct .mrow .who { display: flex; align-items: center; gap: 6px; font-size: var(--fs-sm); color: var(--text-4); margin-top: 4px; }


/* ============================================================================
   PHASE D · RECORDS AND RECEIPTS
   The three patterns approved on design/redesign/decisions-v1.html, questions
   11, 12 and 13.

   EVERYTHING IS SCOPED UNDER .clp-page, the same rule Phase 5 set for the
   content pages: generic component names stay inside our own shell so they can
   never clobber a Tailwind utility. .rec and .doc are exactly the kind of
   name that would.

   VISIBILITY IS DRIVEN FROM .reclist[data-state], NOT from core's Tailwind
   group-data variants. Those variants toggle `display` through a single-class
   utility; our .rec rule is also a single class and loads later, so ours won the
   cascade and the loading skeleton sat on screen next to the empty state. Any
   list state we care about is therefore an attribute rule here.
   ============================================================================ */

/* ---- THE ORDER LIST, one card: the column header, then the rows with a hairline between each. An empty
   list hides the whole card; the orders page says it is empty in its own opening. No overflow clip, so a
   row's menu can open past the card; the last row rounds its own foot instead. ---- */
.clp-page .reclist[data-state="empty"] { display: none; }
/* the image and video library's wall: an empty library says so in its opening instead */
.clp-page .lib-list[data-state="empty"] .lib-wall { display: none; }
.clp-page .reclist:not([data-state="initial"]) .rec-load { display: none; }
.clp-page .reclist[data-state="initial"] .rec-load { display: grid; }

/* ---- THE RECORD ROW. Date in the mono face so the column aligns down the list,
   then what was bought, then the numbers right-aligned, then the state in a badge.
   One grid definition, shared by the header and the row, so they cannot drift. ---- */
.clp-page .rechead,
.clp-page .rec { display: grid; grid-template-columns: 104px minmax(0,1fr) 96px 84px 92px 28px;
  align-items: center; gap: 14px; }
.clp-page .rec-nocred .rechead,
.clp-page .rec-nocred .rec { grid-template-columns: 104px minmax(0,1fr) 104px 92px 28px; }

.clp-page .rechead { padding: 12px 18px 10px; border-bottom: 1px solid var(--border); }
.clp-page .rechead .r,
.clp-page .rechead .e { text-align: end; }

.clp-page .rec { position: relative; padding: 13px 18px; border-bottom: 1px solid var(--border); }
.clp-page .rec:last-of-type { border-bottom: 0; border-radius: 0 0 calc(var(--r-lg) - 1px) calc(var(--r-lg) - 1px); }
.clp-page .rec:hover { background: var(--hover); }
.clp-page .rec .hit { position: absolute; inset: 0; border-radius: inherit; z-index: 0; }
.clp-page .rec .d { font-family: var(--mono); font-size: var(--fs-sm); color: var(--text-3);
  font-variant-numeric: tabular-nums; }
.clp-page .rec .t { min-width: 0; display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.clp-page .rec .t .cyc { color: var(--text-4); font-size: var(--fs-sm); }
.clp-page .rec .c,
.clp-page .rec .m { font-family: var(--mono); font-size: var(--fs-sm); text-align: end;
  font-variant-numeric: tabular-nums; }
.clp-page .rec .c { color: var(--text-2); }
.clp-page .rec .m { color: var(--text); }
.clp-page .rec .s { justify-self: end; }
.clp-page .rec .x { position: relative; z-index: 1; justify-self: end; }
/* THE ROW MENU CONTROL, RESCOPED RATHER THAN COPIED. It was born here because the billing record
   row was its only consumer; the voice card is the second. This stylesheet has been through that
   exact moment twice and wrote down what it cost: `.gal` scoped `.clp-tool` produced a duplicate
   layout engine and `.r-act` scoped `.clp-tool .result` produced a duplicate button that then
   disagreed with the first. So it moves to the scope every skinned surface sits inside.

   THE MOVE PRESERVES BEHAVIOUR AND THAT IS CHECKABLE: the declarations are unchanged, and the
   one consumer that exists today, `snippets/clp/order-row.twig`, renders inside `#content` like
   every other page, since `#content` is the app shell itself in `layouts/main.twig`. The record
   row's own `.rec .x { z-index: 1 }` keeps doing its job where a full-row overlay exists. */
#content .kebab { display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: var(--r-sm); color: var(--text-4); }
#content .kebab:hover { background: var(--hover); color: var(--text); }
#content .kebab i { font-size: var(--icon-md); }
/* the sprite glyph, for the consumers that draw one instead of a `ti-` icon */
#content .kebab svg.ic { width: var(--icon-md); height: var(--icon-md); }

/* the loading row. Same grid, so the list does not jump when the data lands. */
.clp-page .rec-load .ld { display: block; height: 11px; border-radius: var(--r-pill); background: var(--hover); }
.clp-page .rec-load .ld-d { width: 72px; }
.clp-page .rec-load .ld-t { width: 128px; }
.clp-page .rec-load .ld-n { width: 54px; margin-inline-start: auto; }
.clp-page .rec-load .ld-s { width: 74px; height: 22px; border-radius: var(--r-pill); margin-inline-start: auto; }

/* the phone fold, approved: two lines, what and when on the left, amount and
   state on the right. The credits column and the kebab drop; the row is a link
   already, and the only item in the kebab is the destination that link goes to. */
@media (max-width: 700px) {
  .clp-page .rechead { display: none; }
  /* with the header and every row menu gone, the card clips its first row's hover fill to its corners */
  .clp-page .reclist { overflow: hidden; }
  .clp-page .rec,
  .clp-page .rec-nocred .rec { grid-template-columns: minmax(0,1fr) auto; gap: 4px 12px; padding: 12px 14px; }
  .clp-page .rec .t { grid-column: 1; grid-row: 1; }
  .clp-page .rec .d { grid-column: 1; grid-row: 2; }
  .clp-page .rec .m { grid-column: 2; grid-row: 1; }
  .clp-page .rec .s { grid-column: 2; grid-row: 2; }
  .clp-page .rec .c,
  .clp-page .rec .x { display: none; }
}

/* ---- THE BILLING PAGE (templates/app/billing/overview.twig): the balance on the brand surface, then one split card
   with the plan beside the usage credits, then the latest orders. ---- */
.clp-bov .bbal { display: flex; flex-direction: column; gap: 14px; padding: 28px; }
.clp-bov .bcard { margin-top: 12px; --split-pad: 24px 28px; }
.clp-bov .bcard > * { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; }
.clp-bov .bcard > .is-off { opacity: .55; }
.clp-bov .bcard .p-price b { font-size: var(--fig-lg); }
.clp-bov .b-n { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.clp-bov .b-n b { font-size: var(--fig-lg); line-height: 1; letter-spacing: -.02em; color: var(--text); }
.clp-bov .bbal .b-n b { font-size: var(--fig-3xl); }
.clp-bov .b-n span { font-size: var(--fs-sm); color: var(--text-3); }
.clp-bov .b-ln { margin-top: -4px; font-size: var(--fs-sm); color: var(--text-4); }
.clp-bov .b-ln .nw { white-space: nowrap; }
.clp-bov .b-for { display: flex; flex-direction: column; gap: 8px; }
/* as tall as a badge, so the two parts' labels sit on one line whether or not a badge is beside them */
.clp-bov .b-head { align-self: stretch; min-height: 25px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.clp-bov .b-tip { display: inline-flex; color: var(--text-4); }
/* the small icon beside a page's back link, on a pick bar button and in a billing tip */
#content :is(.pagetop, .hero) .back svg.ic,
.clp-selbar .sb-act svg.ic,
.clp-bov .b-tip svg.ic { width: var(--icon-sm); height: var(--icon-sm); fill: currentColor; }
.clp-bov .b-acts { margin-top: auto; display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.clp-bov .b-quiet { font-size: var(--fs-sm); color: var(--text-3); background: none; cursor: pointer; }
.clp-bov .b-quiet:hover { color: var(--text); }
@media (max-width: 900px) {
  .clp-bov .bbal { padding: 20px; }
  .clp-bov .bcard { --split-pad: 20px; }
}
@media (max-width: 640px) { .clp-bov .bbal .b-n b { font-size: var(--fig-2xl); } }

/* ---- THE BUY-CREDITS WINDOW (snippets/usage-credits.twig): the packs in a split card, each its own Buy button,
   and the note on what usage credits are. ---- */
.clp-buy .clp-split { margin-top: 14px; --split-pad: 14px 16px; }
/* the Buy button sits on the pack's credits line, so the price reads with the amount */
.clp-buy .pk { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.clp-buy .pk .l { display: flex; flex-direction: column; gap: 9px; min-width: 0; }
.clp-buy .pk .button { flex: none; }
/* the pack's stand-ins while the packs load: the credits, the price line, the button */
.clp-buy .pk .l .clp-skel:first-child { --w: 96px; --h: 22px; }
.clp-buy .pk .l .clp-skel:last-child { --w: 160px; --h: 12px; }
.clp-buy .pk > .clp-skel { --w: 72px; --h: 32px; }
.clp-buy .pk-cr { display: flex; align-items: baseline; gap: 5px; }
.clp-buy .pk-cr b { font-size: var(--fig-sm); line-height: 1; letter-spacing: -.02em; color: var(--text); }
.clp-buy :is(.pk-cr span, .pk-ln) { font-size: var(--fs-sm); color: var(--text-3); }
.clp-buy .pk-custom { margin-top: 14px; display: flex; flex-direction: column; gap: 14px; }
.clp-buy .pk-custom :is(.fld-2, .err) { margin-top: 0; }
.clp-buy .pk-foot { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 8px; }
.clp-buy .pk-foot p { display: flex; gap: 6px; font-size: var(--fs-xs); line-height: 1.55; color: var(--text-4); }
.clp-buy .pk-foot svg.ic { width: var(--icon-sm); height: var(--icon-sm); flex: none; fill: currentColor; }
.clp-buy .pk-foot p svg.ic { margin-top: 1px; }
.clp-buy .pk-foot a { align-self: flex-start; display: inline-flex; align-items: center; gap: 5px; font-size: var(--fs-sm); font-weight: 500; color: var(--text-2); }
.clp-buy .pk-foot a:hover { color: var(--text); }

/* ---- THE ORDER CARD (snippets/clp/order-doc.twig), order details and the receipt: a head row over what the
   order gave beside how it was paid. Its lines are the label and value list (`.clp-kv`) and its total is checkout's (`.tot`). ---- */
.clp-bill .bdoc { padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.clp-bill .bdoc .dh { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; gap: 20px;
  padding: 24px 28px; border-bottom: 1px solid var(--border); }
.clp-bill .bdoc .dh .p-name { justify-content: flex-start; gap: 10px; }
.clp-bill .bdoc .dh img { height: 22px; width: auto; }
.clp-bill .bdoc .dh-total { font-size: var(--fig-2xl); line-height: 1; letter-spacing: -.02em; color: var(--text); }
.clp-bill .bdoc .dh-meta { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; font-size: var(--fs-sm); color: var(--text-3); }
.clp-bill .bdoc .dq { margin-top: 6px; font-size: var(--fs-sm); color: var(--text-3); }
.clp-bill .bdoc .id { font-family: var(--mono); }
.clp-bill .bdoc .dc { display: flex; flex-direction: column; gap: 14px; padding: 24px 28px; }
.clp-bill .bdoc .dc + .dc { border-inline-start: 1px solid var(--border); }
.clp-bill .bdoc .p-cr b { font-size: var(--fig-lg); }
.clp-bill .bdoc .fine { margin-top: -6px; font-size: var(--fs-sm); color: var(--text-4); }
@media (max-width: 900px) {
  .clp-bill .bdoc { grid-template-columns: minmax(0, 1fr); }
  .clp-bill .bdoc .dh { flex-direction: column; align-items: flex-start; padding: 20px; }
  .clp-bill .bdoc .dh-meta { align-items: flex-start; }
  .clp-bill .bdoc .dh-total { font-size: var(--fig-xl); }
  .clp-bill .bdoc .dc { padding: 20px; }
  .clp-bill .bdoc .dc + .dc { border-inline-start: 0; border-top: 1px solid var(--border); }
}

/* ---- a pending order's payment instructions under the receipt, bank transfer or manual payment ---- */
.clp-page .docpay { margin-top: 26px; padding: 20px 22px;
  border: 1px solid var(--border-strong); border-radius: var(--r-lg); background: var(--card); }
.clp-page .docpay .lead { font-size: var(--fs-sm); color: var(--text-3); line-height: 1.65; margin: -4px 0 14px; }
.clp-page .docpay .instr { font-size: var(--fs-body); color: var(--text-2); line-height: 1.7; margin-bottom: 14px; }
.clp-page .docpay .tabs { margin-bottom: 14px; }
.clp-page .paylines { display: flex; flex-direction: column; }
.clp-page .payline { display: flex; flex-direction: column; gap: 4px; align-items: flex-start;
  padding: 11px 0; border-bottom: 1px solid var(--border); }
.clp-page .payline:last-child { border-bottom: 0; }
.clp-page .docpay .button { margin-top: 14px; }

/* ============================================================
   NICHES. The catalogue surface: /app/niches, views/templates/app/niches.twig.

   THE ART IS THE CARD. Everywhere else in the app a card is a bordered box with text in it and
   a picture is decoration. Here the picture IS the thing being chosen, because a niche is a
   look, and no amount of description does the job one frame does. So the name sits ON the art
   under a scrim rather than beside it.

   THREE ACROSS, which is the count every browse-and-pick surface in the app already uses: the
   library galleries and the library type cards. Four appears once, on
   the plans page, and only because there are exactly four plans. Below the desktop breakpoint
   it drops to two, following the library gallery rather than inventing a third answer.

   3:4, settled with Hatim 2026-08-07 against 9:16. Tall enough to read as a frame rather than a
   thumbnail, short enough that three rows are reachable without scrolling past the fold.
   ============================================================ */
/* The 30px top is the gap the category heading used to supply. The headings went when one
   category came to hold one niche, and the grid was left sitting straight under the search bar
   with only the hero's own 6px beneath it. Same number as `.cat-head`'s top margin, so the page
   keeps the rhythm it already had rather than inventing a new one. */
.clp-niches .nch-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 30px; }
@media (max-width: 768px) { .clp-niches .nch-grid { grid-template-columns: repeat(2, 1fr); } }

/* THE MOVING CARD (DESIGN.md): its picture is a loop, a still or the empty field, all in `.nch-art`. It answers
   the pointer by firming its edge and never moves, as every card does. */
.nch {
  position: relative; display: block; overflow: hidden;
  border: 1px solid var(--border-strong); border-radius: var(--r-lg); background: var(--card);
  transition: border-color .16s;
}
.nch:hover { border-color: var(--border-struct); }

/* The 3:4 frame of a showcase picture: a moving card's and an example's on Home's walls. */
.nch-art, .ex-art { display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: cover; }
/* NO ARTWORK YET, AND HONEST ABOUT IT. Not a broken img, not a stand-in picture, and no longer a
   flat plate either: a drawn hatch field carrying the niche's own name, so a card without art still
   reads as a card. The 3:4 frame stays, because the day artwork exists it drops straight in.

   The hatch is a repeating gradient rather than an asset: two declarations, no file to ship, no
   request to make, and it takes the theme's own border colour in both modes. */
.nch-art.is-empty {
  background-color: var(--raised);
  background-image: repeating-linear-gradient(135deg, var(--border) 0 1px, transparent 1px 9px);
  display: grid; place-items: center; padding: 20px 16px 64px; text-align: center;
}
.nch-art.is-empty span {
  font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w); letter-spacing: var(--display-ls);
  font-size: var(--fs-xl); line-height: 1.2; color: var(--text-3);
}

/* The scrim is the app's own --scrim-deep, carried far enough up the frame that the caption stays
   readable over a bright image, the third line a phone gives the description included. It is a
   gradient because a solid bar would crop the picture, and the picture is the point. */
.nch-cap {
  position: absolute; inset: auto 0 0 0; padding: 52px 14px 14px;
  background: linear-gradient(to top, var(--scrim-deep) 30%, transparent);
}
.nch-n {
  font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w); letter-spacing: var(--display-ls);
  font-size: var(--fs-md); line-height: 1.25; color: var(--on-fill);
}
.nch-d {
  font-size: var(--fs-xs); line-height: 1.45; color: var(--on-fill-2); margin-top: 4px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* THREE LINES ON A PHONE, TWO EVERYWHERE ELSE. The two-column card on a phone is narrow enough
   that two lines stop a description mid-sentence, so the phone gets one more line rather than a
   sentence that never finishes. Settled with Hatim 2026-09-14 against rendered cards. */
@media (max-width: 768px) { .nch-d { -webkit-line-clamp: 3; } }
/* The caption paints on a dark scrim in BOTH modes, so it reads the fixed light values above
   rather than --text. That is the same reason .cap on a library media tile does. */

/* THE WIDE MOVING CARD, Home's featured row: the same card at 16:10, its kind in the Label type on a scrim pill over its
   top left corner. */
.nch.is-wide .nch-art { aspect-ratio: 16 / 10; }
.nch .nch-eb {
  position: absolute; top: 12px; left: 12px; padding: 4px 8px;
  border-radius: var(--r-pill); background: var(--scrim-deep); color: var(--on-fill);
}

/* HOME'S SHOWROOM: the featured row, then one wall per model under a section head with the model's line and its blue Try
   link. Each example is a 3:4 loop that always plays and opens the example window. */
.show-feat { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.sec-head .sec-t { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.sec-head .sec-line { font-size: var(--fs-sm); color: var(--text-3); }
#content .sec-head a.sec-try,
#content .sec-head a.sec-try:hover { color: var(--accent-hi); }
#content .sec-head a.sec-try:hover { text-decoration: underline; text-underline-offset: 2px; }
.ex-wall { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; }
.ex-tile { display: block; padding: 0; border: 0; border-radius: var(--r); overflow: hidden; background: var(--raised); cursor: pointer; }
.ex-tile:focus-visible { box-shadow: var(--focus-ring); }
@media (max-width: 768px) {
  .show-feat { grid-template-columns: minmax(0, 1fr); }
  .ex-wall { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 640px) { .sec-head .sec-line { display: none; } }

/* ============================================================
   THE TEMPLATE PAGE: the workbench, the prose result, the prompt block.
   views/templates/app/writer.twig. Direction 2, approved by Hatim 2026-08-07.

   WHY THIS PAGE IS THE ONE EXCEPTION TO THE CENTRED COLUMN. Every other surface in the app
   centres a single column, because every other surface is read once. A template is RUN, and run
   again with one field changed, and compared. Hiding the form to read the result and reopening
   it to change one word is the whole friction, so here the form holds a column of its own and
   the result fills the rest. Hatim chose this over the composer shape knowing the cost.
   ============================================================ */
/* THE FORM MOVED INTO A DIALOG, Hatim 2026-08-07, replacing the two-column workbench.
   `.clp-wb` is now a pure scope rather than a layout: it is carried by BOTH the result page and
   the form dialog, so every field, label, prose and prompt-block rule below applies to whichever
   surface the markup happens to sit on. Nothing here positions anything. */
/* THE PAGE KEEPS THE APP'S SHAPE. Imagine, video and voiceover are centred columns at 840px, and
   for a while this one ran the full width of the window: right when a form held a second column
   beside it, wrong the moment the form moved into a dialog. A full-width page beside three centred
   ones is the loudest way for one product to look like two, so the column comes back.

   840px is `.clp-tool`'s width, restated rather than shared because these are two different pages
   agreeing, not one page borrowing the other's rule.

   THE CONTENT FILLS THAT COLUMN. It briefly sat at 640px, copied from imagine's single-result
   view, which was the wrong precedent: 640 is where imagine settles ONE IMAGE, while its gallery
   fills the whole column. Text is the gallery case, not the single-image case, and 640 inside 840
   reads as a narrow page rather than a considered measure. */
.clp-wdoc { max-width: var(--column-w); margin: 0 auto; width: 100%; padding: 26px 0 60px; }
@media (max-width: 1023px) { .clp-wdoc { padding: 20px 0 48px; } }

/* THE RESULT'S META ROW, the app's own mono row: what the run cost, then its three icon buttons (`#content .r-act`). */
.clp-wb .r-meta .sp { margin-left: auto; }

/* A RUN IN FLIGHT. The form is held `inert` for the duration, which blocks every click and every
   keystroke without touching what the browser submits; the setup greys while it waits. */
.clp-wb .wb-fields.is-running { opacity: .45; transition: opacity .15s ease; }


/* A FIELD THIS ANSWER DOES NOT CALL FOR. Set by the field layer, never written in markup. It has
   to win over `.wb-fields > div { display: flex }` on the same element, which is why it carries
   the override the rest of this sheet does without. */
.clp-wb .wb-fields .clp-off { display: none !important; }

/* THE SUGGESTION PICKER. Sits above the text box it fills and reads as subordinate to it, because
   the text box is the field and this is only an offer. Hidden until it has something to offer, and
   stated explicitly because `.input` sets a display of its own that would outrank the attribute. */
.clp-wb .clp-picker { color: var(--text-2); }
.clp-wb .clp-picker[hidden] { display: none; }

/* THE GENERATED-OPTIONS CONTROL. Sits under a field whose list comes from a call rather than a
   fixed set. Built on the writer's own small-control values, mono at 11px, so it reads as help
   beside the field rather than a second primary action. It says it spends credits, because a
   control that spends credits never hides that. */
/* a row that wraps, its items centred 8px apart: the account's name with its badge, a credits control with its note */
.clp-acct .ident .nm,
.clp-wb .wb-gen { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* a field's suggestion row sits 6px under its control */
.clp-wb .wb-fields > div > .wb-gen { margin-top: 6px; }
.clp-wb .wb-gen-btn { color: var(--text-2); cursor: pointer; background: none; border: 1px solid var(--border-struct); border-radius: var(--r); padding: 4px 9px; font-family: var(--mono); font-size: var(--fs-xs); transition: color .14s, border-color .14s, background .14s; }
.clp-wb .wb-gen-btn:hover { color: var(--text); border-color: var(--border-strong); background: var(--hover); }
.clp-wb .wb-gen-btn[disabled] { opacity: .45; cursor: default; }
.clp-wb .wb-gen-note { font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-3); }
.clp-wb .wb-gen-note.is-failed { color: var(--red-hi); }
/* THE SETUP'S HEAD ROW: Randomize all, and the price strip's small Reset at its right. A version's setup is its label,
   the label and value list, then Edit setup. */
.clp-wb .t-head { display: flex; align-items: center; gap: 8px; }
.clp-wb .t-head .clp-io-reset { margin-left: auto; }
.clp-wb .t-answers { display: flex; flex-direction: column; gap: 12px; }
/* The voice step's line under its wall, naming the scenes a press could not voice. */
.clp-wb .clp-vlines { margin: 2px 0 0; display: grid; gap: 4px; }
.clp-wb .clp-vlines .wb-gen-note { display: block; line-height: 1.5; }

/* ONE BODY AT A TIME. Three things can fill a result: the page the server draws for a run whose
   template declares one, core's markdown holding the same run as prose, and the skeleton that
   stands in while there is no text at all. Only one of them is ever right.

   WHICH ONE IS A CLASS ON THE COLUMN, NOT A PROPERTY ON THE ELEMENT, and that is structural rather
   than a preference. Core's markdown sits inside an `x-if`, so Alpine destroys and rebuilds it on
   every change: a new one arrives visible and would have to be hidden again by whichever call
   reached it next. The column is server-rendered and lives for the life of the page, so an answer
   put on it is already true when Alpine renders. */
.clp-wdoc.clp-drawn x-markdown,
.clp-wdoc.clp-drawn .wb-skel { display: none; }

/* WHAT A TEMPLATE THAT DRAWS SHOWS WHILE IT WRITES, which is not its own raw text.

   THE RAW TEXT IS NEVER THE ANSWER ON THESE. The finished page is built by the server once the run
   is saved, so the words streaming in are a different-looking thing that gets replaced moments
   later. Watching a result appear and then change is worse than watching it arrive, so it is not
   shown at all: the wait reads as a wait, and the result appears once.

   IT IS TWO FACTS AND BOTH ARE REQUIRED. `clp-draws` is the template's, written by the server, and
   is absent on every ordinary writer template, where the streaming text IS the result and must keep
   showing. `clp-wait` is this run's, and lasts from the moment a run starts until its drawn body is
   in hand or is known not to exist.

   THE BLOCK IS THE APP'S OWN, the same one the brief shows while it works, so a wait reads the same
   wherever it happens. */
.clp-wb .clp-wbusy { display: none; }
.clp-wdoc.clp-draws.clp-wait .clp-wbusy { display: flex; }
.clp-wdoc.clp-draws.clp-wait x-markdown,
.clp-wdoc.clp-draws.clp-wait .wb-skel { display: none; }

/* THE RUN BEING WRITTEN, said beside the run's buttons. In the accent's text step: the run is work under
   way, not a state reached, which is what success green says. */
.clp-wb .writing {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--mono); font-size: var(--fs-xs); color: var(--accent-hi); letter-spacing: .04em;
}
.clp-wb .writing .dot {
  width: 6px; height: 6px; border-radius: var(--r-pill); background: var(--accent); flex: none;
  animation: clp-pulse 1.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .clp-wb .writing .dot { animation: none; opacity: .8 } }


/* THE DOCUMENT TITLE. It is an editable field and it should read as one: a heading that takes a
   caret when clicked, not a heading that silently happens to be a textarea. It is the page top's
   title, so the field takes its type from the box around it, which the box's growing copy reads too. */
.clp-wb .wb-title textarea {
  display: block; width: 100%; font: inherit; letter-spacing: inherit; color: inherit;
  background: none; border: 0; outline: 0; resize: none; padding: 0; overflow: hidden;
}
.clp-wb .wb-title textarea::placeholder { color: var(--text-4); }

/* WHAT YOU MADE HERE BEFORE, on the template's opening. It is the library's own list bar over
   the library's own ledger, so it declares no layout: only the space between it and the action
   above it, which is the same 30px the browse grid takes under its search. */
.clp-wb .wb-recent { margin-top: 30px; }

/* THE PROSE RESULT. The one object no finished page had, because no finished page shows text.
   Derived from the chat markdown scale rather than invented: same families, same rhythm, sized up
   because this is a document being read rather than a message being scanned.

   TWO CONTAINERS HOLD IT AND EVERY RULE BELOW NAMES BOTH. A result is drawn either by core's
   markdown element, which is every ordinary template, or by run-view.twig, which is a run whose
   niche declares a shape. They hold the same heads, the same prompt blocks and the same controls, so
   scoping to one of them dresses half the app: the prompt block loses `white-space: pre-wrap` and
   stops wrapping, and the rule that strips highlight.js's colours stops applying, which is exactly
   what happened when only two of these rules were widened. Every selector in this section carries
   `:is(x-markdown, .clp-rv)`. Add a rule here and it carries it too. */
.clp-wb :is(x-markdown, .clp-rv) { display: block; }
.clp-wb :is(x-markdown, .clp-rv) > div { max-width: none; font-size: var(--fs-md); line-height: 1.7; color: var(--body); }
.clp-wb :is(x-markdown, .clp-rv) > div :where(p) { margin: 0; font-size: inherit; line-height: inherit; }
.clp-wb :is(x-markdown, .clp-rv) > div :where(p) + :where(p, ul, ol, blockquote, table) { margin-top: 13px; }
.clp-wb :is(x-markdown, .clp-rv) > div :where(h1, h2, h3, h4) {
  font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w); letter-spacing: var(--display-ls);
  color: var(--text); margin: 26px 0 10px;
}
.clp-wb :is(x-markdown, .clp-rv) > div :where(h1) { font-size: var(--fs-xl); }
.clp-wb :is(x-markdown, .clp-rv) > div :where(h2) { font-size: var(--fs-lg); }
.clp-wb :is(x-markdown, .clp-rv) > div :where(h3, h4) { font-size: var(--fs-md); }
.clp-wb :is(x-markdown, .clp-rv) > div > :where(h1, h2, h3, h4):first-child { margin-top: 0; }
.clp-wb :is(x-markdown, .clp-rv) > div :where(ul, ol) { margin: 13px 0 0; padding-inline-start: 20px; }
.clp-wb :is(x-markdown, .clp-rv) > div :where(li) { margin: 0; padding-inline-start: 2px; }
.clp-wb :is(x-markdown, .clp-rv) > div :where(li) + :where(li) { margin-top: 6px; }
.clp-wb :is(x-markdown, .clp-rv) > div :where(strong) { font-weight: 600; color: var(--text); }
.clp-wb :is(x-markdown, .clp-rv) > div :where(a) { color: var(--accent-hi); text-underline-offset: 2px; }
.clp-wb :is(x-markdown, .clp-rv) > div :where(code) {
  font-family: var(--mono); font-size: var(--fs-sm); color: var(--text);
  background: var(--raised); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 1px 5px;
}
.clp-wb :is(x-markdown, .clp-rv) > div :where(code)::before, .clp-wb :is(x-markdown, .clp-rv) > div :where(code)::after { content: none; }

/* THE VIEWER: the picture bigger, on the page it was already on.

   THE BACKDROP IS THE COMMAND PALETTE'S, value for value: the app's one overlay, the scrim under --overlay-blur.
   It is the one recipe this app has for putting the page behind something, and a viewer is that
   and nothing more. What differs is where the thing sits: the palette hangs from the top because
   it is a panel you type into, and this centres because it is a picture you look at.

   32px OF AIR, which is core's own preview backdrop padding, so the picture never touches an edge.
   Beyond that the picture takes whatever the viewport gives it: no width cap, because core's
   720px cap exists to hold a caption column under the media and there is no caption here.

   THE ACTIONS ARE THE TILE'S OWN GLYPH, at the 16px inset core's preview puts its close at. They
   are fixed to the viewport rather than to the picture: a picture that is tall and narrow would
   otherwise carry them halfway across the screen from where the eye expects them. */
.clp-viewer {
  position: fixed; inset: 0; z-index: var(--z-overlay); display: flex; align-items: center; justify-content: center;
  padding: 32px; background: var(--scrim); backdrop-filter: blur(var(--overlay-blur)); -webkit-backdrop-filter: blur(var(--overlay-blur));
}
.clp-viewer[hidden] { display: none; }
.clp-viewer img, .clp-viewer video {
  display: block; max-width: 100%; max-height: 100%; width: auto; height: auto;
  object-fit: contain; border-radius: var(--r-lg); margin: 0;
}
.clp-viewer .v-acts { position: fixed; top: 16px; right: 16px; display: flex; gap: 4px; }

/* A CARRIED-OUT STEP FOLDS ITS PROMPT AWAY. What the step made is on the page directly below, so
   the instruction that made it is the one thing there is no reason to keep reading. The disclosure
   is drawn only once the step is done: a step still waiting shows its prompt and offers nothing to
   press, because the prompt is the whole of what there is to read. */
.clp-wb .clp-ptog { display: none; }
.clp-wb .clp-ptog.is-on { display: inline-flex; }
.clp-wb :is(x-markdown, .clp-rv) x-code.clp-p-off { display: none; }

/* THE PROMPT BOX, one declaration and two consumers: the fenced block a niche writes into a run,
   and the lightbox's own prompt. "The text a machine was given, exactly as it was given" is one
   thing wherever it appears, so it is one box: a tinted body in mono under a mono label, with the
   copy control closing the label line.

   IN THE WRITER IT COSTS NOTHING, because core already builds it. The shared markdown renderer
   replaces every fenced block with an `x-code` element carrying an `.actions` bar: a `.lang` label
   taken from the fence tag, and an `x-copy` control. Styling that bar is the whole job. A niche
   fencing its output tags each block, and the tag becomes the heading, so eight prompts arrive as
   eight blocks each with its own copy. THE TAG MUST BE ONE WORD: core captures it with
   `/language-(\w+)/`, and a hyphen is not a word character, so `image-prompt` silently labels the
   block "image". Underscores survive.

   ONE CONTAINER, ONE PADDING, NO DIVIDER. The block is a single 13px box holding a label and the
   prompt, not a panel with a titled header: it was the only container in this app carrying a rule
   under its own heading, and a divider is what a container draws when it holds two different
   things. The 13px is the padding this block already had around its text, moved out to the box so
   the label and the first line of the prompt start on the same edge instead of being aligned by
   two paddings that happen to match. */
.clp-wb :is(x-markdown, .clp-rv) pre {
  padding: 13px;
  background: var(--raised); border: 1px solid var(--border-strong); border-radius: var(--r-lg);
}
.clp-wb :is(x-markdown, .clp-rv) pre .actions {
  display: flex; align-items: center; gap: 8px; padding: 0; background: none;
}
/* the one control in the bar is a 28px square: core's copy element */
.clp-wb :is(x-markdown, .clp-rv) pre .actions x-copy {
  width: 28px; height: 28px; border-radius: var(--r-sm); display: grid; place-items: center; margin-left: auto;
  color: var(--text-3); background: none; border: none; cursor: pointer; transition: color .14s, background .14s;
}
.clp-wb :is(x-markdown, .clp-rv) pre .actions x-copy:hover { color: var(--text); background: var(--hover); }
.clp-wb :is(x-markdown, .clp-rv) pre code {
  font-family: var(--mono); font-size: var(--fs-sm); line-height: 1.7; color: var(--text-2);
  background: none; border: 0; padding: 0; white-space: pre-wrap; word-break: break-word;
}

/* WHAT EACH CONSUMER OWNS ALONE. The writer's block takes its own room in a column of prose and
   carries core's three stacked surfaces, which have to be zeroed rather than left off: core gives
   `.actions` an inline padding, a block padding and a tint, and `.code-wrapper` a padding, a tint
   and its own bottom corners, so dropping our declarations would hand the block back to them
   instead of clearing them. Its copy control is core's `<i>` webfont glyph; ours is the sprite,
   which `svg.ic-sm` already sizes. */
.clp-wb :is(x-markdown, .clp-rv) pre { margin: 16px 0 0; overflow: visible; }
.clp-wb :is(x-markdown, .clp-rv) pre .actions x-copy i { font-size: var(--icon); }
.clp-wb :is(x-markdown, .clp-rv) pre .code-wrapper { padding: 0; background: none; overflow-x: auto; }

/* A PROMPT IS NOT CODE, SO IT IS NOT COLOURED.

   The renderer runs highlight.js in AUTO-DETECT whenever the fence tag is not a language it knows,
   and `image_prompt` never will be, so every prompt is coloured according to whichever language the
   library decided the English resembled. The colours are a guess and they read as though they meant
   something.

   MONO STAYS. Mono is the signal that this text is copied exactly, which is true. Colour is the
   signal that its parts mean different things, which is not.

   Specificity beats the highlight.js theme rather than `!important`. This sheet uses that override
   once, on `.clp-off`, and documents why where it does. */
.clp-wb :is(x-markdown, .clp-rv) pre code.hljs,
.clp-wb :is(x-markdown, .clp-rv) pre code.hljs [class*="hljs-"] {
  color: var(--text-2); background: none; font-weight: inherit; font-style: inherit;
}

/* THE STEP IS ONE BOUNDED UNIT: a heading naming the beat, its settings on a quiet line beneath,
   the prompt, then the control that sends it onward.

   IT IS SELECTED OFF THE MARKDOWN ITSELF, with no class and no script. A result is rendered from
   markdown and markdown carries no classes, so the structure has to come from the shape the
   template emits: `## Step N. <name>` and the settings line under it. That shape is the step
   contract in `_NICHE-BOILERPLATE.md`, and this styling matches nothing until a template writes to
   it. A step written as bold text runs into its own settings and stays one grey paragraph.

   THE HEADING STARTS THE UNIT AND THE SETTINGS FINISH IT, so the room goes above the heading and
   the line beneath it sits close, quiet and mono: it is reference, not prose. */
.clp-wb :is(x-markdown, .clp-rv) > div :where(h2) { margin-top: 34px; }
.clp-wb :is(x-markdown, .clp-rv) > div :where(h2) + :where(p) {
  margin-top: 5px; font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.6; color: var(--text-3);
}

/* THE CONTROL UNDER A PROMPT. Written in beside each block by `prompt-blocks.twig`, from the
   block's own fence tag, so a template that fences correctly gets its handoff and cannot forget
   one. It replaced a fixed row of three links under the whole result, which pointed at pages
   rather than carrying anything to them and had to be trusted to stay in step with sixty niches.

   THE PILL IS THE TOOL PAGES' OWN `.apill`, re-scoped from `.clp-tool` to `#content` so both
   surfaces draw the same control. Only the room around it is stated here. */
.clp-wb :is(x-markdown, .clp-rv) .t-actions.t-next { margin-top: 8px; }

/* A STEP'S CONTROL IS A BUTTON, NOT A CHIP, AND THE CORNER IS WHAT SAYS SO.
   `.apill` and `.tchip` are one object in this stylesheet, listed as a pair in the control table at
   the top of this file: same border, same card fill, same colour, the pill radius on both. So the control that
   carries a step out was wearing the model picker Imagine and Video put above their composer. The
   app's own actions, `.open-act` and `.ebtn.primary`, are square-cornered on `var(--r)`; only pickers
   and filters are pills. Taking the pill off is the whole change.

   NOT FILLED WITH THE ACCENT, though that is the other half of the action family. An accent fill would
   be a second place this page says how a step stands, and the head already says it once beside the
   step's name. The stylesheet also reserves the accent for a different job in every other state it
   draws: `.tchip.active` and `.gtile.on` both put the accent on the BORDER to mean "this is the current one". A
   filled control would be saying a state in a language this app uses for actions.

   SCOPED TO THE WRITER. `.t-actions.t-next` is also the Imagine result's own row, which lives under
   `.clp-tool` and must stay pixel-identical. `.clp-wb` covers the run drawn by the server and the one
   drawn on the page, and nothing else.

   IT CARRIES `#content` BECAUSE THE RULE IT OVERRIDES DOES. `#content .t-actions .apill` sets the
   pill radius, and an id outranks any number of classes, so a selector built only from classes loses
   however specific it looks. */
#content .clp-wb .t-actions.t-next .apill { border-radius: var(--r); }

/* A DISABLED PILL IS NOT PRESSABLE AND MUST NOT LOOK IT. The blocked step's "Reference unavailable"
   and the voice step's "Finish N more scenes" both carry `disabled`, so they dim like the workbench's
   own disabled control (`.wb-gen-btn`, .45) rather than reading as a live button. */
#content .clp-wb .t-actions.t-next .apill[disabled] { opacity: .45; cursor: default; }


/* ---- the template page's own spacing, named rather than inlined.
   Four of these were `style=` attributes written straight into the markup, which is the exact
   thing this stylesheet exists to prevent: a number in a template cannot be found, cannot be
   reused, and cannot move with the system. ---- */
.clp-wb .wb-body { margin-top: 18px; }
.clp-wb .wb-skel { display: flex; flex-direction: column; gap: 10px; margin-top: 16px; }
.clp-wb .wb-foot { margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--border); }
.clp-wb .wb-acts { display: flex; align-items: center; gap: 14px; }
.clp-wb .wb-dl-wrap { position: relative; }
/* the field stack: one gap between every label and its control, and between every field, so no
   input carries its own margin */
.clp-wb .wb-fields { display: flex; flex-direction: column; gap: 18px; }
.clp-wb .wb-fields > div { display: flex; flex-direction: column; gap: 7px; }


/* ---- THE STEP'S HEAD, and why it is drawn rather than styled.

   The unit used to be selected off the markdown: an `h2` and the paragraph under it. Both real
   Historical POV runs of 2026-08-12 emit `1. **Step 1. Waking before court.** Use Imagine with…`
   instead, a numbered list with the number, the name and the settings in ONE text run, so the
   `h2` rule never matched and the page rendered as a flat list. No stylesheet can split a single
   text run, so `prompt-blocks.twig` builds the head from the pieces the markdown does separate
   and hides the line it came from.

   THE SOURCE IS HIDDEN, NOT REMOVED, and the head is a sibling rather than a wrapper.
   `x-markdown` assigns its whole contents on every tick of a streaming run, so anything moved or
   deleted returns a frame later in the renderer's own shape. Hiding is idempotent: the sweep
   re-applies it and nothing fights the renderer.

   EVERY VALUE HERE IS ALREADY ON THIS PAGE. The name is the 17px display size this page's own
   headings render at; the line under it is the mono 11.5px the `h2 + p` rule already gave a
   settings line; the label is the mono microlabel the meta row, the prompt tag and the composer
   marker all speak in; the rule between steps is the hairline the ledger draws between rows. ---- */
.clp-wb .clp-src-off { display: none; }
.clp-wb .clp-shead { display: flex; align-items: center; gap: 10px; margin-top: 32px; padding-top: 30px; border-top: 1px solid var(--border); }
/* the first step opens the run rather than following one, so it takes no rule and no room */
.clp-wb :is(x-markdown, .clp-rv) > div > div.clp-shead:first-of-type { margin-top: 6px; padding-top: 0; border-top: 0; }
.clp-wb .clp-shead .s-n,
.clp-wb .clp-shead .s-state { display: inline-flex; align-items: center; gap: 6px; flex: none; }
/* HOW THE STEP STANDS, in the one weight this app's mono carries above its own: the face is loaded
   at 400 and 500 and nothing else, so 500 is the emphasis rather than a size or a colour. The tick
   is the accent mark a picker sets against the row in use, and it appears only once the step has
   been carried out: a step still waiting says its state in words alone. */
.clp-wb .clp-shead .s-state { font-weight: 500; }
.clp-wb .clp-shead .s-state svg.ic { display: none; }
.clp-wb .clp-shead .s-state.is-done svg.ic { display: block; }
.clp-wb .clp-shead .s-name { font-family: var(--display); font-variation-settings: 'opsz' var(--display-opsz); font-weight: var(--display-w); letter-spacing: var(--display-ls); font-size: var(--fs-lg); line-height: 1.3; color: var(--text); margin: 0; }
.clp-wb .clp-shead .sp { margin-left: auto; }
.clp-wb .clp-sset { font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.6; color: var(--text-3); margin: 5px 0 0; }

/* WHAT THE CONTROL WILL DO, ON THE ROW WITH THE CONTROL. In a drawn run these are the settings the
   press applies, not a description of the step, so they sit beside the button rather than above the
   prompt. 11px is the microlabel size the step's own label and its state already use, so this is
   smaller than the line above without being a size that exists nowhere else.

   `margin-right: auto` rather than `space-between` on the row, because the row is `flex-end` and a
   step without this line must leave its control exactly where it is.

   SCOPED TO THE DRAWN RUN. `.t-actions.t-next` is also the Imagine result's own row, which must stay
   pixel-identical. */
.clp-wb .clp-rv .t-actions.t-next .clp-sset {
  margin: 0 auto 0 0; font-size: var(--fs-xs); color: var(--text-4); text-align: left;
}

/* the chip row a template's tone field renders into, and the two labels beside it. Named so
   the row owns its wrap and its gap, rather than the instance carrying them. */
.clp-wb .wb-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.clp-wb .wb-gen-lab { display: inline-flex; align-items: center; gap: 7px; }
.clp-wb .wb-dl { font-size: var(--fs-body); }
/* the help glyph is now the sprite at the app's small size, so it cannot inherit a stray size
   the way the tabler class did when its page scope was renamed */
.clp-wb form label .help { color: var(--text-4); cursor: help; }
.clp-wb form label .help:hover { color: var(--text-2); }

/* ============================================================
   THE MEDIA WINDOW, the one surface a finished run opens into, over the one overlay.
   ------------------------------------------------------------ */

.clp-lb {
  /* the prompt's floor: the least room a long prompt keeps in every layout of the window · `window.clpLbLong` */
  --prompt-floor: 110px;
  position: fixed; inset: 0; z-index: var(--z-overlay);
  display: grid; place-items: center;
  padding: 24px;
  background: var(--scrim);
  backdrop-filter: blur(var(--overlay-blur)); -webkit-backdrop-filter: blur(var(--overlay-blur));
}

.clp-lb .lb-panel {
  width: 100%; max-width: var(--column-w);
  max-height: calc(100vh - 48px);
  display: flex; flex-direction: column;
  background: var(--card);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--lift-dialog);
}

/* WINDOW D: a picture or clip floats at its own shape with the spec sheet card 12px under it; the panel draws nothing
   of its own. A voiceover keeps the panel above. */
.clp-lb .lb-panel.is-media { background: none; border: 0; border-radius: 0; box-shadow: none; overflow: visible; gap: 12px; }

/* The stage takes the room the card does not and hands it to the picture; `minmax(0, 1fr)` gives the picture a definite
   area to be bounded by, so a tall one never runs under the card. */
.clp-lb .lb-stage { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.clp-lb .lb-pic { min-height: 0; flex: 1 1 auto; display: grid; grid-template-rows: minmax(0, 1fr); place-items: center; }
.clp-lb .lb-pic img,
.clp-lb .lb-pic video { display: block; max-width: 100%; max-height: 100%; object-fit: contain; border-radius: var(--r-lg); box-shadow: var(--lift-dialog); }
.clp-lb .lb-err { flex: none; }

/* THE ARROWS AND CLOSE ON THE BACKDROP, the app's scrim control at its largest size, 24px in from the backdrop's edges:
   the arrows mid-height either side, Close in the top right corner. */
.clp-lb .lb-navs { flex: none; display: flex; align-items: center; justify-content: center; gap: 10px; }
#content .clp-lb .lb-nav,
#content .clp-lb .lb-x { position: absolute; width: 38px; height: 38px; }
#content .clp-lb .lb-nav { top: 50%; transform: translateY(-50%); }
#content .clp-lb .lb-nav svg.ic,
#content .clp-lb .lb-x svg.ic { width: var(--icon-md); height: var(--icon-md); }
#content .clp-lb .lb-nav.prev { left: 24px; }
#content .clp-lb .lb-nav.next { right: 24px; }
#content .clp-lb .lb-x { top: 24px; right: 24px; z-index: 1; }

.clp-lb .lb-bar { flex: none; padding: 13px 15px; display: flex; flex-direction: column; gap: 11px; }

/* SPEECH'S OWN ROW: its title heads the panel because a voiceover has no picture to head it. */
.clp-lb .lb-r1 { display: flex; align-items: flex-start; gap: 12px; }
.clp-lb .lb-r1 .lb-t { flex: 1; min-width: 0; }
.clp-lb .lb-acts { flex: none; display: flex; align-items: center; gap: 4px; }

.clp-lb .lb-r1 .clp-facts { margin-top: 5px; }

/* ---- THE SPEC SHEET: the prompt and the files it was made from on the left; the `--facts-w` facts column on the right with
   the item's icons, its name, its model, its facts, who sees it, and every action at the column's full width. ---- */
.clp-lb .lb-bar.lb-sheet {
  display: grid; grid-template-columns: minmax(0, 1fr) var(--facts-w); gap: 0; padding: 0;
  background: var(--card); border: 1px solid var(--border-strong); border-radius: var(--r-lg); box-shadow: var(--lift-dialog);
}
.clp-lb .lb-l { min-width: 0; min-height: 200px; padding: 16px 18px; display: flex; flex-direction: column; gap: 10px; }
.clp-lb .lb-lab { display: flex; align-items: center; gap: 8px; min-height: 20px; }
.clp-lb .lb-labacts { margin-left: auto; display: flex; align-items: center; gap: 8px; }
/* The prompt fills its column to the facts column's height and scrolls inside it, never under the floor. */
.clp-lb .lb-ptext {
  flex: 1 1 0; min-height: var(--prompt-floor); overflow-y: auto;
  font-family: var(--font); font-size: var(--fs-body); line-height: 1.6; color: var(--body); white-space: pre-wrap; word-break: break-word;
}
.clp-lb .lb-att { flex: none; padding-top: 12px; border-top: 1px solid var(--border); }
/* the files something was made from (`snippets/clp/lb-files.twig`): in the result window and a version's setup */
.lb-files .clp-label { margin-bottom: 8px; }

.clp-lb .lb-r { min-width: 0; padding: 16px 18px; border-left: 1px solid var(--border); display: flex; flex-direction: column; gap: 12px; }
.clp-lb .lb-icons { display: flex; align-items: center; justify-content: flex-end; gap: 12px; min-height: 20px; }
.clp-lb .lb-model { margin-top: -6px; font-size: var(--fs-sm); color: var(--text-3); }
.clp-lb .lb-aud { padding-top: 12px; border-top: 1px solid var(--border); }
#content .clp-lb .lb-r .t-actions { margin-top: auto; }

/* The arrows and Close in the card's prompt row exist for screens with no room beside the window. */
.clp-lb .lb-cardnav,
.clp-lb .lb-cx { display: none; align-items: center; gap: 8px; }

/* ---- BELOW 908px THE ARROWS AND CLOSE JOIN THE CARD. An arrow on the backdrop needs its 24px inset, its own 38px and
   12px clear of the 760px window, 74px a side, so 908px is the narrowest screen that holds one. In the card they are
   glyphs on a surface, so they wear `--text-3` rather than the scrim. ---- */
@media (max-width: 907px) {
  .clp-lb .lb-stage .lb-navs,
  #content .clp-lb .lb-x { display: none; }
  .clp-lb .lb-cardnav,
  .clp-lb .lb-cx { display: flex; }
  #content .clp-lb .lb-cardnav .lb-nav {
    position: static; transform: none; width: 34px; height: 34px;
    background: none; backdrop-filter: none; -webkit-backdrop-filter: none; color: var(--text-3);
  }
  #content .clp-lb .lb-cardnav .lb-nav:hover { background: var(--hover); color: var(--text); }
  #content .clp-lb .lb-cardnav .lb-nav svg.ic { fill: currentColor; }
}

/* ---- AT 1024px AND WIDER THE CARD STANDS BESIDE THE PICTURE: two floating pieces centred together inside the arrows'
   gutters (24px in, the 38px arrow and 12px clear, a side). The picture takes the full height at its own shape; the
   card is one `--sheet-w` column in reading order (icons, title, model, facts, prompt, files, who sees it, actions)
   where only the prompt gives way: a short one keeps its own height, a long one gives way down to the floor and
   scrolls with the app's one scroll fade. The card itself scrolls only when its other parts leave no room for that. ---- */
@media (min-width: 1024px) {
  .clp-lb .lb-panel.is-media {
    flex-direction: row; justify-content: center; align-items: center; gap: 24px;
    width: 100%; max-width: calc(100vw - 2 * (24px + 38px + 12px)); height: calc(100vh - 48px); max-height: none;
  }
  .clp-lb .is-media .lb-stage { flex: 0 1 auto; min-width: 0; height: 100%; }
  .clp-lb .is-media .lb-pic { height: 100%; }
  .clp-lb .lb-bar.lb-sheet { flex: 0 0 var(--sheet-w); display: flex; flex-direction: column; gap: 12px; padding: 16px 18px; max-height: 100%; overflow-y: auto; }
  .clp-lb .lb-sheet :is(.lb-l, .lb-r) { display: contents; }
  .clp-lb .lb-sheet .lb-icons { order: 1; }
  .clp-lb .lb-sheet .autogrow-textarea { order: 2; }
  .clp-lb .lb-sheet .lb-model { order: 3; }
  .clp-lb .lb-sheet .lb-facts { order: 4; }
  .clp-lb .lb-sheet .lb-lab { order: 5; padding-top: 12px; border-top: 1px solid var(--border); }
  .clp-lb .lb-sheet .lb-ptext { order: 6; flex: none; min-height: 0; }
  .clp-lb .lb-sheet .lb-ptext.is-long { flex: 0 1 auto; min-height: var(--prompt-floor); }
  .clp-lb .lb-sheet .lb-att { order: 7; }
  .clp-lb .lb-sheet .lb-aud { order: 8; }
  #content .clp-lb .lb-sheet .t-actions { order: 9; margin-top: 0; }
}

/* ---- PHONES: the window scrolls as one, the facts column drops under the prompt with the title and the item's icons
   sharing its first row, and the prompt takes its own height up to the floor, scrolling past it. ---- */
@media (max-width: 767px) {
  .clp-lb:has(.lb-panel.is-media) { overflow-y: auto; place-items: start center; }
  .clp-lb .lb-panel.is-media { max-height: none; }
  .clp-lb .lb-bar.lb-sheet { grid-template-columns: minmax(0, 1fr); }
  .clp-lb .lb-l { min-height: 0; }
  .clp-lb .lb-ptext { flex: none; min-height: 0; max-height: var(--prompt-floor); }
  .clp-lb .lb-r { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; border-left: 0; border-top: 1px solid var(--border); }
  .clp-lb .lb-r > * { grid-column: 1 / -1; }
  .clp-lb .lb-r > .autogrow-textarea { grid-column: 1; grid-row: 1; }
  .clp-lb .lb-r > .lb-icons { grid-column: 2; grid-row: 1; }
}

/* The window takes the phone's own inset once the backdrop's 24px is more than a narrow screen can spare. */
@media (max-width: 47.99rem) {
  .clp-lb { padding: 12px; }
}

/* ---- the speech body: header, player, script ----------------------------
   The only type whose lightbox leads with its header rather than its media,
   because a voiceover has no picture and its script has no length the panel
   can assume. The bar, the title field, the meta line and the icon actions
   above are the same atoms the image and video bodies use; what follows is
   the two parts they have no equivalent for. */
.clp-lb .lb-player { flex: none; padding: 0 15px 13px; }
.clp-lb .lb-script {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: 13px 15px 15px;
  border-top: 1px solid var(--border);
}
.clp-lb .lb-script .s-lbl {
  font-family: var(--mono); font-size: var(--label-size); letter-spacing: .04em;
  color: var(--text-4); margin-bottom: 7px;
}
.clp-lb .lb-script .s-text {
  font-size: var(--fs-body); line-height: 1.62; color: var(--text-2);
  white-space: pre-wrap;
}
.clp-lb .lb-script .s-copy {
  margin-top: 12px; display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--border-strong); background: var(--card); color: var(--text-3);
  border-radius: var(--r-pill); padding: 5px 11px; font-size: var(--fs-sm); cursor: pointer;
}
.clp-lb .lb-script .s-copy:hover { color: var(--text); border-color: var(--text-4); }
.clp-upload-state { margin-top: 6px; color: var(--text-4); font-size: var(--fs-sm); line-height: 1.4; }

/* ============================================================
   THE VOICE AREA: the Voices page, one card per voice a customer cloned.
   ============================================================
   EACH STATE WEARS THE PART OF THE APP THAT ALREADY MEANS THAT STATE. The design and its reasons
   are `.state/specs/2026-09-11-voice-area-design.md`, "The voice card".

   THE SURFACE IS NOT NEW. `.box`, `.pcard` and `.qk` all wear the same one: a hairline
   at --border-strong on --card at --r-lg. This is that surface at `.pcard`'s 18px inset.

   IT TAKES NO HOVER STATE, and that is a decision rather than an omission. A card lifts by filling
   to --raised, and the cards that take that fill are the ones where the WHOLE CARD IS ONE TARGET:
   `.qk` is an anchor, `.pcard` is covered by an absolutely positioned link, and `.box` takes it
   only as `a.box`, `button.box` or `.box.cursor-pointer`. A voice card holds separate targets, so
   it carries none.

   SCOPED #content, NOT .clp-page, for the reason `.led` gives where it is declared: a component
   pinned to one page family grows a duplicate the first time a second family needs it.

   CARDS STRETCH TO THE TALLEST IN THE ROW and stack what they hold, so "Use this voice" sits at the
   foot of every card that has it, level across a row, and a card without it keeps the space at its
   foot (Hatim's pick U1, 2026-09-30).
   ------------------------------------------------------------ */
#content .vgrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px;
  align-items: stretch; }

#content .vcard { border: 1px solid var(--border-strong); border-radius: var(--r-lg); background: var(--card);
  padding: 18px; display: flex; flex-direction: column; }

/* the head: the slot, the name over its sharing label, the menu control */
#content .vcard .vtop { display: flex; align-items: center; gap: 12px; }
/* THE TEXT COLUMN HAS TO BE ALLOWED TO SHRINK or the name cannot ellipse and a long one pushes the
   menu control off the card instead. `.led-row .t` carries this for the same reason and by the
   same name. */
#content .vcard .vtop .t { flex: 1; min-width: 0; }
/* THE MENU HANGS FROM HERE. `.menu` is core's and it is `position: absolute; top: 100%`, so without
   a positioned ancestor it anchors to whatever happens to be positioned further up the page. */
#content .vcard .vtop .x { position: relative; flex: none; }
#content .vcard .vtop h3 { font-size: var(--fs-md); font-weight: 500; letter-spacing: -.01em; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the sharing label under the name */
#content .vcard .vshare { margin-top: 3px; font-size: var(--fs-sm); color: var(--text-3); display: flex; align-items: center; gap: 6px; }
#content .vcard .vshare i { font-size: var(--icon-sm); }

/* THE ROW UNDER THE HEAD is 20px whatever it holds, so the waiting bar and the player that replaces
   it stand the same height. */
#content .vcard .vplay { display: flex; align-items: center; gap: 10px; margin-top: 12px; min-height: 20px; }
/* THE PLAYER, and the waiting bar before it: the meter filling the row beside the player's numerals. */
#content .vcard .vplay .clp-meter { flex: 1; min-width: 0; }
#content .vcard .vp-t { width: auto; min-width: 38px; text-align: right; }
/* WHILE THE LINE PLAYS the bar is the moving bar and the time reads played over length; paused or finished, at rest */
#content .vcard :is(.clp-seek, .vp-live),
#content .vcard:has(.clp-vr-play[state="playing"]) .vp-rest { display: none; }
#content .vcard:has(.clp-vr-play[state="playing"]) :is(.clp-seek, .vp-live) { display: block; }

/* "USE THIS VOICE": one action, full width, 14px under what the card holds, the foot this card has carried
   before, pushed to the card's bottom. */
#content .vcard .vfoot { margin-top: auto; padding-top: 14px; }
#content .vcard .vfoot .ebtn { width: 100%; }

/* IT FOLDS IN TWO STEPS, NOT ONE. The app has one example of each: the stat tiles go straight from
   three columns to one at 900px, which works because a tile holds three short lines, and the
   pricing cards go four to two at 1023px and two to one at 639px. A voice card follows the pricing
   cards until the narrow-screen look settles it: a card holds a head, its player and at most one full
   width button, and a long name is already cut at 1024px.
   THE TWO VALUES ARE REASONED, NOT MEASURED: three cards across the page's 840px reading measure
   beside the 252px sidebar. Confirm them on a narrow screen. */
@media (max-width: 860px) { #content .vgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) { #content .vgrid { grid-template-columns: minmax(0, 1fr); } }
