/* The dashboard's design system.
 *
 * Moved verbatim out of base.html's inline <style> (2026-08-03) so the new console and
 * trail screens build on one file instead of a second copy that would drift. NO class was
 * renamed — every existing page keeps rendering exactly as it did. The new components
 * (.lane / .chip / .eprow / .state / .rail / .empty) are appended below.
 *
 * The two QC editors are deliberately NOT on this file: they are standalone dark-themed
 * pages with their own <style>, and the prior-work panel carries its own CSS for that
 * reason (app/templates/_prior_work.html).
 */

/* ---- tokens ---- */
:root{
  --bg:#fafafa; --fg:#18181b; --muted:#71717a; --line:#e4e4e7; --card:#fff;
  --ok:#047857; --ok-bg:#ecfdf5; --warn:#c2410c; --warn-bg:#fff7ed;
  --bad:#b91c1c; --bad-bg:#fef2f2; --idle:#1d4ed8; --idle-bg:#eff6ff;
  --r:10px; --r-sm:7px; --gap:8px;
}

/* ---- base (verbatim from base.html) ---- */
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:0;background:#fafafa;color:#18181b}
a{color:#2563eb}
.wrap{max-width:1100px;margin:0 auto;padding:24px 20px}
table{width:100%;border-collapse:collapse;background:#fff;border:1px solid #e4e4e7;border-radius:10px;overflow:hidden}
th,td{text-align:left;padding:10px 12px;border-bottom:1px solid #f0f0f0}
th{background:#fafafa;font-size:13px;color:#52525b;font-weight:600}
tr:last-child td{border-bottom:0}

/* A table whose last column is prose — a note, an error, a detail — declares the widths of
   its OTHER columns and carries this class. `overflow-wrap` INHERITS, so `.card`'s
   `anywhere` (see the phone section below) reaches every cell inside a card, and
   `anywhere` makes a cell's min-content ONE CHARACTER. Auto layout then hands each column
   a share of the width proportional to that, which is how the episode trail rendered
   "stillsgen" one letter per line beside a Note column holding a 3000-character Cloud Run
   dump (2026-09-10). Fixed layout ignores min-content entirely: the declared columns get
   what they ask for, the prose column takes the rest, and the long token breaks INSIDE it.
   The timestamp gives up `.mono`'s nowrap here for the same reason — a column that cannot
   wrap cannot be held to a width. */
table.fixedcols{table-layout:fixed}
table.fixedcols td.mono{white-space:normal}
.pill{display:inline-block;font-size:12px;font-weight:700;padding:2px 8px;border-radius:6px}
.s-needs_review,.s-ready_to_match,.s-awaiting_image_qc,.s-awaiting_video_qc{background:#fff7ed;color:#c2410c}
.s-done,.s-delivered,.s-ready_to_deliver,.s-client_approved{background:#ecfdf5;color:#047857}
.s-error,.s-errored,.s-client_comments{background:#fef2f2;color:#b91c1c}
.s-queued,.s-matched,.s-approved,.s-built,.s-matching,.s-rendering,.s-delivering,.s-running,.s-checking,.s-awaiting_qc{background:#eff6ff;color:#1d4ed8}
.muted{color:#71717a}.mono{font-variant-numeric:tabular-nums;white-space:nowrap}
button,input,select{font:inherit}
.card{background:#fff;border:1px solid #e4e4e7;border-radius:10px;padding:16px;margin-bottom:16px}
.btn{display:inline-block;padding:8px 16px;border-radius:8px;border:1px solid #d4d4d8;background:#fff;text-decoration:none;color:#18181b;font-weight:600;cursor:pointer;font:inherit}
.btn:hover{background:#fafafa;border-color:#c7c7cc}
.btn.primary{background:#2563eb;border-color:#2563eb;color:#fff}
.btn.primary:hover{background:#1d4ed8}
.btn.sm{padding:4px 10px;font-size:13px;border-radius:7px}
.btn.danger{color:#b91c1c;border-color:#fecaca}
.linkbtn{border:0;background:none;color:#2563eb;cursor:pointer;font:inherit;padding:0}
.pill.s-sync-ok{background:#ecfdf5;color:#047857}
.pill.s-sync-warn{background:#fff7ed;color:#c2410c}
.pill.s-sync-bad{background:#fef2f2;color:#b91c1c}
/* A cut left waiting for QC (_qc_wait.html); its row takes .row-bad past QC_WAIT_LATE_S. */
.pill.qcwait{white-space:nowrap}
.qcwait-late{color:#b91c1c}
h1{font-size:26px;margin:0 0 14px}
/* top nav */
.nav{background:#18181b;color:#fff;padding:0 20px;display:flex;align-items:center;gap:4px}
.nav .brand{font-weight:800;letter-spacing:-.01em;color:#fff;text-decoration:none;margin-right:14px;padding:14px 0}
.nav .brand .dot{color:#60a5fa}
.nav a.sec{color:#d4d4d8;text-decoration:none;font-weight:600;font-size:14px;padding:15px 12px;border-bottom:2px solid transparent}
.nav a.sec:hover{color:#fff}
.nav a.sec.on{color:#fff;border-bottom-color:#60a5fa}
.nav .bell .badge,.nav a.sec .badge,.nav .menu .badge{background:var(--pp-accent,#c33);color:#fff;border-radius:9px;padding:0 6px;font-size:11px}
.nav .sp{flex:1}
.nav .who{color:#a1a1aa;font-size:13px;display:flex;align-items:center;gap:10px}
.nav .who b{color:#fff}
.nav .who form{margin:0}
.nav .who button{border:0;background:none;color:#60a5fa;cursor:pointer;font:inherit;padding:0;font-size:13px}

/* ---- console ---- */
.filters{display:flex;flex-wrap:wrap;gap:var(--gap);align-items:center;margin:0 0 18px}
.chips{display:flex;gap:6px;flex-wrap:wrap;margin-left:auto}
.chip{display:inline-block;font-size:13px;font-weight:600;padding:4px 10px;
  border:1px solid var(--line);border-radius:999px;background:var(--card);
  color:var(--fg);text-decoration:none}
.chip:hover{background:#f4f4f5}
.chip.on{background:var(--fg);border-color:var(--fg);color:#fff}
.lane{margin:0 0 22px}
.lane h2{font-size:13px;margin:0 0 8px;color:var(--muted);font-weight:700;
  text-transform:uppercase;letter-spacing:.05em}
.lane h2 .count{color:var(--fg);background:var(--line);border-radius:999px;
  padding:1px 8px;font-size:12px;margin-left:6px}
.lane-body{background:var(--card);border:1px solid var(--line);border-radius:var(--r);
  overflow:hidden}
.lane.l-attention .lane-body{border-color:#fecaca}
.eprow{display:flex;gap:12px;align-items:center;padding:9px 12px;
  border-bottom:1px solid #f4f4f5;font-size:13px}
.eprow:last-child{border-bottom:0}
.eprow-id{width:230px;flex:none}
.eprow-id .series{color:var(--muted);margin-right:6px}
.eprow-state{flex:1;min-width:0}
.eprow-act{display:flex;gap:6px;align-items:center;flex-wrap:wrap;justify-content:flex-end}
.eprow-act form{margin:0}
.eprow-act form.inline{display:inline}
.state{font-weight:600}
.t-ok{color:var(--ok)} .t-warn{color:var(--warn)} .t-bad{color:var(--bad)}
.t-idle{color:var(--muted)}
/* `wait` is epstate's per-ROW tone for work nobody has picked up — the lane is still
   `editing`, but the row is not healthy there. Declared here because app/templates/
   episode.html renders `t-{{ state.tone }}` off this file; the console has its own. */
.t-wait{color:var(--warn)}
.empty{padding:28px;text-align:center;color:var(--muted);background:var(--card);
  border:1px dashed var(--line);border-radius:var(--r)}

/* ---- episode trail ---- */
.ephead{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:4px}
.ephead h1{margin:0}
#ep-state .pill{margin-right:8px}
.rail{display:flex;gap:var(--gap);flex-wrap:wrap;align-items:center;margin:0}
.rail form{margin:0}

/* ---- send-back form ---- */
.sendback summary{cursor:pointer;display:inline-block;list-style:none}
.sendback summary::-webkit-details-marker{display:none}
.stack{display:flex;flex-direction:column;gap:var(--gap);max-width:520px;margin-top:10px}
.stack textarea,.stack select{font:inherit;padding:7px 9px;border:1px solid var(--line);
  border-radius:var(--r-sm)}
.stack button{align-self:flex-start}

/* narrow: the action column stacks under the state rather than squeezing it */
@media (max-width:700px){
  .eprow{flex-wrap:wrap}
  .eprow-id{width:auto}
  .eprow-act{width:100%;justify-content:flex-start}
  .chips{margin-left:0}
}

/* ---- phone (2026-08-15) ----
 * The operator pages were desktop-only in practice: on a 390px phone the page's
 * min-content width blew past the viewport (Safari then shrinks the WHOLE page to fit,
 * which is the tiny-text screenshot this section exists because of). Two culprits, both
 * fixed generically rather than page by page:
 *
 *   1. Unbreakable strings. A worker error is one giant token — an <HttpError …> with a
 *      120-char sheets.googleapis.com URL in the middle — and a flex row or table cell
 *      cannot shrink below its longest unbreakable word. `overflow-wrap:anywhere` lets
 *      those break; numbers keep `.mono`'s nowrap, which is fine because numbers are
 *      short. Applied un-media-queried: breaking a URL at the edge is correct on desktop
 *      too, it just never had the chance to matter there.
 *
 *   2. Wide-by-design blocks: the staff nav and every data table. Under 760px the nav
 *      WRAPS onto as many rows as it needs and tables scroll INSIDE their own box
 *      instead of stretching the document — the page itself must never scroll sideways.
 *      (The nav was a horizontally-scrollable row until 2026-09-19; see "the nav,
 *      narrow" at the foot of this file for why that had to go.)
 *
 * The QC editors are standalone pages with their own CSS and are deliberately not
 * touched by any of this (the motion editor stays desktop-only by design). */
/* NOT on td/th here: `anywhere` counts every point as a break opportunity when computing
   min-content, so a table with it on every cell "fits" any viewport by stacking one
   CHARACTER per line instead of overflowing into its scroll container. Cells get a
   scoped version inside the media query below, where the table is a scroller. */
.state, .eprow-state, .why, .empty, .card { overflow-wrap: anywhere; }

@media (max-width:760px){
  .wrap{padding:14px 12px}
  h1{font-size:21px}

  /* As many rows as the bar needs. `flex:0 0 auto` is the load-bearing half: without it
     each item would shrink-wrap mid-word rather than move to the next line. */
  .nav{flex-wrap:wrap;padding:0 12px;gap:0}
  .nav .brand,.nav a.sec,.nav .who,.nav .menu{flex:0 0 auto}
  .nav a.sec{padding:15px 9px}
  .nav .sp{flex:0 0 18px}

  /* Tables keep their desktop column layout and scroll within their own border-radius
     box. display:block on <table> is the classic trick: the inner table box still lays
     out as a table, the element itself becomes an overflow container. The header cells
     are nowrap so every column keeps a readable floor (without it the cells' break-
     anywhere rule below lets the whole table "fit" 390px at one character per line),
     and long cell content wraps inside a capped column instead of dragging the table
     out to its full unbroken width. */
  table{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}
  th{white-space:nowrap}
  td{overflow-wrap:anywhere;max-width:62vw;min-width:0}
  /* Auto table layout sizes a break-anywhere column by its (one-character) min-content,
     so the cell with the most to say got the least room — a 300-char worker error
     rendered ~5 characters per line. The header floor above keeps short columns honest;
     this floor keeps prose columns readable. Header cells are exempt (a checkbox column
     should stay a checkbox wide). */
  td:not(:empty){min-width:12ch}
  /* :empty misses a cell whose only content is a checkbox (an element child), and a
     select-all column does not need a prose floor. */
  td:has(> input[type="checkbox"]:only-child){min-width:0}

  /* A size= / cols= attribute is a fixed intrinsic width (size=50 ≈ 440px); the cap
     turns it into "as wide as the row allows". */
  input,select,textarea{max-width:100%;box-sizing:border-box}
  fieldset{min-width:0}                /* fieldsets refuse to shrink by default */
  /* The cap alone was not enough inside a `.toolbar`, which is a flex row: a flex item's
     `min-width` is `auto`, so the form around a `size=44` input kept its intrinsic 365px
     and `max-width:100%` then resolved against that — the input stayed wider than the
     panel holding it. The floor has to be lifted on the ITEM before the cap can bite. */
  .toolbar > form,.fieldrow > form,.act > form{min-width:0}
  .toolbar > form > input,.fieldrow > form > input{width:100%}

  /* Tap targets. A finger needs about 40px and this dashboard is built for a mouse: a
     bare `<select>` on /admin/metrics — which opts out of the theme, so nothing else was
     going to size it — rendered 18px tall. The action LINKS are sized in theme.css,
     which is where their padding is declared: a phone rule here would lose to it. */
  select,textarea,input:not([type="checkbox"]):not([type="radio"]){min-height:40px}

  /* `table-layout:fixed` exists so a prose column cannot be starved by a neighbour whose
     min-content is one character (see `table.fixedcols` above). Inside the phone's
     scroller that job is already done by the 12ch cell floor and the nowrap headers, and
     the declared PERCENTAGES then work against us: the episode trail's Note column got
     19% of a 653px table — 131px — and a worker error rendered as a 522px-tall ribbon.
     Auto layout inside a box that scrolls gives the prose column the room it asks for. */
  table.fixedcols{table-layout:auto}

  /* Console rows: id / state / actions stack into three readable lines. */
  .eprow{flex-wrap:wrap;gap:4px 12px}
  .eprow-state{flex:1 1 100%}
}


/* ======================================================================================
 * THE SHARED CHROME (2026-09-17)
 *
 * Two things live down here rather than in theme.css, and for one reason: they must reach
 * EVERY page, including one that opts out of the theme. `/admin/metrics` does — Editor pay
 * feeds invoicing and was frozen on the legacy look by name — but the header is global,
 * and the first cut of this put the nav's styling in theme.css, so that page rendered its
 * menus as bare disclosure triangles on the dark bar.
 *
 *   1. the `--pp-*` palette, which the nav rules read. Declaring it changes nothing on its
 *      own: the legacy `--bg`/`--fg` tokens above are untouched here, and theme.css is what
 *      repoints them. An opted-out page gets the new HEADER and its old BODY.
 *   2. the nav itself.
 * ==================================================================================== */

/* ---- the Pixel Press palette ------------------------------------------------------------------------ */
:root {
  --pp-ink: #2b3446;          /* dark slate — body copy, solid buttons */
  --pp-ink-soft: #626b7a;     /* muted text */
  --pp-paper: #f5f2ec;        /* warm cream — the page */
  --pp-paper-alt: #eee9df;    /* recessed surfaces: wells, tracks, disabled */
  --pp-line: #ddd7ca;         /* hairlines */
  --pp-line-soft: #ebe6dc;    /* the quieter line, between rows */
  --pp-accent: #4a90c2;       /* brand blue — links, focus */
  --pp-card: #ffffff;

  /* Status. Green/amber/red keep their conventional meaning: this is an operations
     product and a failed episode has to read as failed from across a desk. */
  --pp-ok: #2f7d68;    --pp-ok-bg: #e6f2ee;
  --pp-wait: #a9722a;  --pp-wait-bg: #f7eedd;
  --pp-bad: #b0463f;   --pp-bad-bg: #f7e7e5;
  --pp-idle: #4a6a8a;  --pp-idle-bg: #e8eef4;

  --pp-serif: "Fraunces", Georgia, serif;
  --pp-sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;

  --pp-r: 12px;
  --pp-r-sm: 9px;
  --pp-focus: 0 0 0 3px color-mix(in srgb, var(--pp-accent) 28%, transparent);

  /* The brand ramp, and the ribbon made from it. One definition: `nav.nav::after` below
     and Clip Lab's own wallet meter both draw it, and a five-stop gradient written twice
     is five chances to disagree. */
  --pp-c1: #e8739e;  --pp-c2: #4a90c2;  --pp-c3: #54b8a6;
  --pp-c4: #eab94e;  --pp-c5: #a98ac9;
  --pp-gradient: linear-gradient(90deg, var(--pp-c1) 0%, var(--pp-c2) 26%,
                 var(--pp-c3) 50%, var(--pp-c4) 74%, var(--pp-c5) 100%);

  /* ui.css's own tokens, repointed. Anything still reading `var(--fg)` or `var(--line)`
     — the episode rail, the send-back form, the status pills — comes along without
     needing to be found and rewritten. */
  --bg: #f5f2ec; --fg: #2b3446; --muted: #626b7a; --line: #ddd7ca; --card: #ffffff;
  --ok: #2f7d68; --ok-bg: #e6f2ee; --warn: #a9722a; --warn-bg: #f7eedd;
  --bad: #b0463f; --bad-bg: #f7e7e5; --idle: #4a6a8a; --idle-bg: #e8eef4;
  --r: 12px; --r-sm: 9px;
}


/* ---- the nav ------------------------------------------------------------------------
 * Twelve flat links wrapped onto two rows at 1440px. They are now five: Work, Seasons
 * and Monitor stay, and the rest group under Studio and Team — each menu one mental
 * category rather than a list of leftovers. A member's nav is at most four items and
 * stays flat; grouping is a fix for the staff bar's length, not a house style.
 */
.nav {
  background: var(--pp-ink); color: #fff; gap: 2px;
  padding: 0 clamp(12px, 2vw, 22px);
}
.nav .brand {
  font-family: var(--pp-serif); font-weight: 600; font-size: 1.05rem;
  letter-spacing: -.01em; padding: 13px 0; margin-right: 18px;
}
.nav .brand .dot { color: #8fc0e0; }

/* One rule for a top-level link and for a menu's trigger, so the two read as peers. */
.nav a.sec, .nav .menu > summary {
  color: #cbd3e0; font-size: .85rem; font-weight: 600;
  padding: 7px 11px; margin: 9px 0; border-radius: 999px;
  border-bottom: 0;              /* ui.css's underline tab; this bar uses a pill instead */
  text-decoration: none; cursor: pointer; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 5px;
}
.nav a.sec:hover, .nav .menu > summary:hover {
  color: #fff; background: rgba(255, 255, 255, .09);
}
.nav a.sec.on, .nav .menu > summary.on {
  color: var(--pp-ink); background: #fff; border-bottom: 0;
}

.nav .menu { position: relative; }
.nav .menu > summary { list-style: none; }
.nav .menu > summary::-webkit-details-marker { display: none; }
/* The caret, drawn so it can rotate and so it sits on the text's baseline weight. */
.nav .menu > summary::after {
  content: ""; width: 0; height: 0; margin-left: 1px;
  border: 3.5px solid transparent; border-top-color: currentColor;
  transform: translateY(2px); transition: transform .15s ease;
}
.nav .menu[open] > summary::after { transform: translateY(-1px) rotate(180deg); }

.nav .menu-body {
  position: absolute; top: calc(100% + 2px); left: 0; z-index: 40;
  min-width: 12.5rem; padding: 6px;
  background: var(--pp-card); border: 1px solid var(--pp-line);
  border-radius: var(--pp-r); box-shadow: 0 18px 40px -20px rgba(43, 52, 70, .55);
  display: flex; flex-direction: column; gap: 1px;
}
.nav .menu-body a {
  color: var(--pp-ink); text-decoration: none; font-size: .84rem; font-weight: 600;
  padding: 7px 10px; border-radius: var(--pp-r-sm); white-space: nowrap;
}
.nav .menu-body a:hover { background: var(--pp-paper); }
.nav .menu-body a.on { background: var(--pp-ink); color: #fff; }
/* A divider before the item that is not like the others (Logout; the marketing site's
   RSVP list). One class rather than :nth-child, which would move with the menu. */
.nav .menu-body .sep {
  height: 1px; background: var(--pp-line); margin: 4px 6px;
}
/* A group label inside a menu (QC ▾: "Internal QC" / "PocketFM QC"). Not a link, so it
   takes no hover and no `on`; the first one needs no space above it. */
.nav .menu-body .grp {
  font-size: .68rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--pp-ink-soft); padding: 8px 10px 2px;
}
.nav .menu-body .grp:first-child { padding-top: 2px; }

/* The account menu sits at the right end, so its panel hangs from the right edge or it
   would run off the viewport. */
.nav .who { color: #9aa5bb; font-size: .82rem; gap: 6px; }
.nav .who b { color: #fff; font-weight: 600; }
.nav .menu.acct { margin-left: 4px; }
.nav .menu.acct .menu-body { left: auto; right: 0; }
.nav .menu.acct > summary { color: #fff; }
.nav .menu-body button {
  border: 0; background: none; color: var(--pp-bad); cursor: pointer;
  font: inherit; font-size: .84rem; font-weight: 600; text-align: left;
  padding: 7px 10px; border-radius: var(--pp-r-sm); width: 100%;
}
.nav .menu-body button:hover { background: var(--pp-bad-bg); }
.nav .menu-body form { margin: 0; }


/* ---- the nav, narrow ----------------------------------------------------------------
 * A menu stays a MENU here. It does not, and the difference cost every phone operator
 * two thirds of the dashboard.
 *
 * The first cut (2026-09-17) made the bar one horizontally-scrollable row and tried to
 * UNFOLD each menu into it: hide the `<summary>`, lay the `.menu-body` out inline, and
 * let the links scroll past like a tab bar. A dropdown panel inside an overflow-x
 * container is clipped by it, so unfolding looked like the only way to keep the menus
 * reachable. It is not reachable at all:
 *
 *   - A CLOSED `<details>` does not render its body. Browsers hide it on the
 *     `::details-content` pseudo-element with `content-visibility: hidden`, which no
 *     author rule on `.menu-body` can reach, and which makes the whole `<details>`
 *     measure ZERO wide.
 *   - With the trigger `display:none` there was nothing left on the bar to open it with.
 *   - And because the only wide items in the row measured zero, the row had nothing to
 *     scroll: `nav.scrollWidth === nav.clientWidth === 390`. The links were laid out at
 *     x=341..639, off the side of the phone, with no gesture that could reach them.
 *
 * Measured on production at 390px: fourteen destinations gone — Studio and everything
 * under it, Team and everything under it, and the entire account menu, which is where
 * Log out lives. A member is hit too: the account menu is the only `<details>` in an
 * editor's nav, so an editor on a phone could not change their password or sign out.
 *
 * So the bar WRAPS instead. That removes the overflow container, which is the only
 * reason a dropdown was impossible here, and leaves ONE menu behaviour at every width —
 * the same `<details>`, the same `nav.js`, no second implementation of the header for
 * small screens. A phone spends two rows on the header; that is the price, and it is
 * cheaper than a menu nobody can open.
 *
 * The panel is pinned to the NAV, not to its own trigger: `.menu-body` is positioned
 * `left:0` against `.menu` on desktop, and a trigger near the right edge would open its
 * panel off the side of the screen. `.menu` goes `position:static` so the nearest
 * positioned ancestor is `.nav`, and the panel becomes a full-width sheet under the bar.
 */
@media (max-width:760px){
  .nav { position: relative; }
  .nav .menu { position: static; }
  .nav .menu-body, .nav .menu.acct .menu-body {
    top: 100%; left: 12px; right: 12px; min-width: 0;
  }
  /* Two rows, not three. The desktop pill's 9px of margin above and below was what made
     each wrapped row 52px tall, and the role in "boss (admin)" was what pushed the account
     menu onto a row of its own. The person knows their own role; the menu still says it. */
  .nav { padding: 0 8px; }
  .nav .brand { margin-right: 6px; padding: 10px 4px; font-size: .98rem; }
  .nav a.sec, .nav .menu > summary { padding: 8px 9px; margin: 3px 0; font-size: .82rem; }
  .nav .who .role { display: none; }
  .nav .menu.acct { margin-left: 0; }
  .nav .sp { flex: 0 0 0; }
}


/* ---- tables as cards, narrow (2026-09-27) --------------------------------------------
 * The scroller above kept the PAGE from moving sideways, but not the reader: a manager on
 * a phone had to swipe right on every board row to see its state and its buttons, and
 * back left to see which episode it was. Under 760px a data table is now a stack of
 * cards — the first cell (the episode) as the card's title, every other column beneath it
 * as "LABEL value", buttons full-width at the bottom, "—" cells left out.
 *
 * app/static/nav/cards.js does the annotating (`table.cards`, `td[data-card]`,
 * `td[data-label]` = the column's header), so nothing here applies to a table it has not
 * seen, and a table it has not seen still gets the scroller. Everything is scoped to the
 * media query: a desktop never sees a card.
 *
 * The `!important`s are deliberate and bounded to box geometry. Page-local rules such as
 * the board's `table.board td.act { min-width: 26rem }` (specificity 0,2,2) and theme.css's
 * `td.act { display: flex }` are desktop column sizing; in a card there are no columns,
 * and one surviving min-width puts the sideways scroll straight back. */
@media (max-width:760px){
  table.cards{display:block;overflow:visible;border:0;border-radius:0;background:none;
    max-width:100%}
  table.cards > thead{display:block}
  table.cards > thead > tr{display:flex;align-items:center;gap:8px}
  table.cards > thead > tr > th{display:none}
  /* A select-all box is the one header cell with a job left to do. */
  table.cards tr[data-card-head] > th:has(input[type="checkbox"]){
    display:flex !important;align-items:center;gap:8px;width:auto !important;
    padding:4px 12px 10px !important;background:none !important;border:0 !important}
  table.cards tr[data-card-head] > th:has(input[type="checkbox"])::after{
    content:"Select all";font-size:.78rem;font-weight:600;color:var(--muted);
    text-transform:none;letter-spacing:0}
  /* A header row written inside the body (no <thead>) goes the same way. */
  table.cards > tbody > tr[data-card-head]{display:flex !important;padding:0 !important;
    margin:0 !important;border:0 !important;background:none !important}
  table.cards > tbody > tr[data-card-head] > th{display:none}
  table.cards > tbody, table.cards > tfoot{display:block}
  table.cards > tbody > tr, table.cards > tfoot > tr{
    display:flex;flex-wrap:wrap;align-items:flex-start;gap:8px 14px;
    background:var(--card);border:1px solid var(--line);border-radius:var(--r);
    padding:11px 13px;margin:0 0 10px;box-sizing:border-box}
  table.cards > tbody > tr > td, table.cards > tfoot > tr > td{
    display:block !important;box-sizing:border-box;
    flex:1 1 calc(50% - 7px);min-width:0 !important;max-width:100% !important;
    width:auto !important;padding:0 !important;border:0 !important;
    background:none !important;white-space:normal !important;overflow-wrap:anywhere;
    text-align:left !important}
  table.cards td[data-label]::before{
    content:attr(data-label);display:block;margin:0 0 1px;
    font-size:.64rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
    color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  table.cards td[data-card~="check"]{flex:0 0 auto !important;align-self:center}
  table.cards td[data-card~="check"] input{width:22px;height:22px;margin:0}
  /* The title is the row's name: first line, next to its checkbox, no label. A cell that
     LEADS with a status pill (the board's Stage) rides on the same line — state is the
     first thing anyone looks for — and drops to its own line only when it carries an
     error message with it. */
  table.cards td[data-card~="title"]{flex:0 1 auto !important;font-size:1.02rem;
    font-weight:600;align-self:center}
  table.cards td[data-card~="title"]::before{display:none}
  table.cards td[data-card~="badge"]{flex:0 1 auto !important;align-self:center}
  table.cards td[data-card~="badge"]::before{display:none}
  table.cards td[data-card~="wide"]{flex-basis:100% !important}
  table.cards td[data-card~="full"]::before{display:none}
  /* Short values (a count, a percent, a name) read as "LABEL value" chips that pack
     several to a line instead of spending two lines each. */
  table.cards td[data-card~="short"]{display:inline-flex !important;align-items:baseline;
    gap:5px;flex:0 0 auto !important;font-size:.86rem}
  table.cards td[data-card~="short"]::before{margin:0}
  /* The buttons: a real row of thumb-sized controls, wrapping as needed. The column is
     called "Action" on every page that has one — saying so again is noise. */
  table.cards td[data-card~="actions"]{
    display:flex !important;flex-wrap:wrap;gap:6px;align-items:center}
  table.cards td[data-card~="actions"]::before{flex:0 0 100%}
  table.cards td[data-card~="actions"] > *{margin:0 !important}
  /* The row's own buttons close the card, under a rule. */
  table.cards td[data-card~="actions"]:last-child{
    padding-top:9px !important;border-top:1px solid var(--line) !important}
  /* One button shape in a card, whatever the desktop cell dressed it as. */
  table.cards td a.btn, table.cards td button{display:inline-flex !important;
    align-items:center;min-height:38px;padding:0 13px !important;box-sizing:border-box;
    margin:0 !important}
  /* Last, and doubled, so no display rule above can bring back a "—". */
  table.cards td[data-card~="empty"][data-card~="empty"]{display:none !important}
  table.cards td select{min-height:38px}
  table.cards td form{display:inline-flex;flex-wrap:wrap;gap:6px;margin:0}
}
