/* CuGate Channels: the chart-room theme.
   Ported from the locked design mockup
   (design-mockups/dashboard-chart-room-blue.html). The mockup is canonical:
   token roles, spotlight discipline and component recipes carry over 1:1.

   ============================================================
   PALETTE ROLES - cugate.com brand palette
   Source: main.css :root variables
   --color-blue-light:  #00cbd8  (bright cyan-blue)
   --color-blue-medium: #037A9A  (mid teal-blue)
   --color-blue-dark:   #07294C  (deep navy)
   --color-yellow:      #fff95b  (lemon yellow)
   --color-orange:      #ffcd00  (golden/amber) - spotlight
   --color-gray:        #9FC8E4  (powder blue)

   Role map (this file):
   --bg          Page base
   --surface     Cards / rows
   --border      Structural lines
   --primary     #00cbd8 bright cyan (interactive, nav active, revenue bar)
   --spotlight   #ffcd00 gold - #1 rank numeral, total revenue, the Open task control
                 inside the work zone, and the What's moving bar at the foot of every
                 page. Nowhere else.
   --text        Primary text
   --muted       #9FC8E4 powder blue - labels / secondary
   --danger      Down-trend red (unchanged - semantic, not brand)

   NO magenta. NO mint/green-teal.

   GOLD IS NO LONGER ONCE PER SCREEN. It has THREE JOBS, and they are named here
   because the number has grown twice and each time the comment was rewritten
   rather than quietly left behind: the money that is the point of the work, the
   control that opens a piece of that work, and the bar along the foot that says
   the work is moving. Jobs, not objects -- a day holding five tasks shows five
   gold buttons, and they are one job repeated, which is what a reader learns to
   scan for.

   THREE IS THE CEILING, not a trend. The bar is chrome and sits across a gap from
   the buttons rather than beside them, which is what makes the third job survivable;
   a fourth would be inside the content and would finish the colour off.

   It is deliberately NOT available for decoration, for a heading, for a hover
   state, or for a second call to action anywhere else in the app. A gold thing
   that is neither the money nor an Open task control means this rule has failed
   and the colour is carrying nothing.
   ============================================================ */

:root {
  --radius: 10px;
  --transition: 0.22s cubic-bezier(0.25, 1, 0.2, 1);
  /* Outside both theme blocks on purpose: see .work-zone. The one filled region on
     the page keeps one colour in both themes, so it stays the same landmark after a
     reader flips the toggle. It cannot be --primary, which is a bright cyan in the
     dark theme and a deep teal in the light one; the dark ink this block needs for
     its eyebrow and its chips is legible on the first and not on the second. */
  --work-zone: #00cbd8;
  /* Held here for the same reason as the fill it sits on. --spotlight is a darker
     gold in the light theme, chosen to carry on a pale page; on this block the page
     behind it is cyan in both themes, so the theme's answer is the wrong one and it
     renders as olive. */
  --work-cta: #ffcd00;
  --work-cta-hover: #ffd733;
  /* WHAT'S MOVING, the ticker fixed to the foot of the window. Held here rather than
     in the theme blocks for the same reason as --work-zone above: the bar is one dark
     surface in both themes, so it stays the same landmark after a reader flips the
     toggle, and every colour set ON that surface therefore has to be fixed with it.
     --text is #d8eef8 in the dark theme and #07294C in the light one, which on this
     bar would be the bar's own colour -- the sentences would vanish. The height is
     here because three rules have to agree about it: the bar is this tall, the page
     reserves exactly this much at its foot, and the sidebar stops this far short of
     the floor. See the WHAT'S MOVING block below. */
  --ticker-h: 42px;
  /* Amber, on the client's call, and it is the largest gold surface on the app. That
     makes three jobs for gold rather than two; the palette note at the top of this
     file says so rather than going on claiming two. The trade is deliberate: this bar
     is the one thing on the page that has to be noticed from the far side of a room,
     and it is chrome rather than content, so it competes with the Open task control
     across a gap rather than beside it. If those buttons stop reading as the thing to
     press once this is live, that is the cost arriving and one of the two moves. */
  --ticker-surface: #ffcd00;
  /* Dark ink, because the surface went light. Near-black rather than the app's navy:
     on amber, navy at body size reads as a colour rather than as text, and this bar
     is read in passing at 50 pixels a second. */
  --ticker-ink: #2e2200;
  --ticker-name: #1a1300;
  --ticker-quiet: rgba(46, 34, 0, 0.66);
  --ticker-edge: rgba(26, 19, 0, 0.22);
  /* The hover colour is near-black rather than the app's cyan. Cyan is this app's
     interactive colour everywhere else, but on amber it is close to illegible, and a
     hover state that is harder to read than the resting state is not a hover state.
     Depth of ink carries it here instead of hue. */
  --ticker-hover: #000000;
}

html[data-theme="dark"] {
  /* Opaque on purpose. See .work-zone: the cards inside the filled block cannot
     use the translucent --surface, because over cyan it tints them toward the
     fill. This is that surface flattened against the page background. */
  --work-card:       #0b2c47;
  --work-card-hover: #113755;
  --bg:            #041829;
  --bg-deeper:     #020e1a;
  --surface:       rgba(7, 41, 76, 0.55);
  --surface-hover: rgba(0, 203, 216, 0.07);
  --border:        rgba(0, 203, 216, 0.13);
  --border-strong: rgba(0, 203, 216, 0.24);
  --primary:       #00cbd8;
  --primary-dim:   rgba(0, 203, 216, 0.13);
  --spotlight:     #ffcd00;
  --spotlight-dim: rgba(255, 205, 0, 0.12);
  --silver:        #c4ccd6;  /* rank #2 medal */
  --bronze:        #cd7f32;  /* rank #3 medal */
  --text:          #d8eef8;
  --text-strong:   #f0f8ff;
  --muted:         #5a8aaa;
  --muted-mid:     #7aafc8;
  --danger:        #e05050;
  --up:            #00cbd8;
  --sidebar-bg:    #020e1a;
  --sidebar-border: rgba(0, 203, 216, 0.10);
  --header-bg:     rgba(2, 14, 26, 0.92);
  --rank-track:    rgba(255, 255, 255, 0.04);
  --shadow:        rgba(0, 0, 0, 0.50);
  /* Audience-detail accents: gender pie segments + device bars. Kept off the
     brand primary so the two sit apart from revenue's cyan. */
  --gender-female: #a155da;
  --gender-male:   #009ab7;
  --device-bar:    #6f9fc4;
}

html[data-theme="light"] {
  --work-card:       #ffffff;
  --work-card-hover: #f4f9fd;
  --bg:            #eef5fb;
  --bg-deeper:     #d8eaf4;
  --surface:       #ffffff;
  --surface-hover: #f4f9fd;
  --border:        rgba(3, 122, 154, 0.14);
  --border-strong: rgba(3, 122, 154, 0.26);
  --primary:       #037A9A;
  --primary-dim:   rgba(3, 122, 154, 0.10);
  --spotlight:     #c89700;
  --spotlight-dim: rgba(200, 151, 0, 0.10);
  --silver:        #8c97a5;  /* rank #2 medal (darker for light bg) */
  --bronze:        #a4691f;  /* rank #3 medal (darker for light bg) */
  --text:          #07294C;
  --text-strong:   #041829;
  --muted:         #5b8aaa;
  --muted-mid:     #3d6f90;
  --danger:        #c03030;
  --up:            #037A9A;
  --sidebar-bg:    #07294C;
  --sidebar-border: rgba(255, 255, 255, 0.10);
  --header-bg:     rgba(238, 245, 251, 0.94);
  --rank-track:    rgba(7, 41, 76, 0.07);
  --shadow:        rgba(7, 41, 76, 0.14);
  /* Darker on the light bond-paper background so both segments read. */
  --gender-female: #8629c0;
  --gender-male:   #0084a4;
  --device-bar:    #4a7ba8;
}

/* ===== RESET + BASE ===== */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  font-size: 16px;
  transition: background-color 0.3s ease, color 0.3s ease;
}

body {
  font-family: 'DM Sans', 'Noto Sans', sans-serif;
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  min-height: 100vh;
  overflow-x: hidden;
}

* {
  transition-property: background-color, border-color, color, fill, stroke, opacity;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 200ms;
}

/* ===== THIN LABEL OVERRIDE (prevents fuzzy webkit rendering) ===== */
.label-thin {
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

/* ===== LAYOUT SHELL ===== */
.app {
  display: grid;
  grid-template-columns: 60px 1fr;
  grid-template-rows: auto 1fr auto;
  min-height: 100vh;
}

/* ===== SIDEBAR ===== */
.sidebar {
  grid-row: 1 / -1;
  background: var(--sidebar-bg);
  border-right: 1px solid var(--sidebar-border);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px 0;
  gap: 4px;
  position: sticky;
  top: 0;
  height: 100vh;
  z-index: 100;
}

.sidebar-logo {
  width: 36px;
  height: 36px;
  margin-bottom: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.sidebar-logo svg {
  width: 28px;
  height: 28px;
}

.nav-item {
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  cursor: pointer;
  color: var(--muted);
  text-decoration: none;
  position: relative;
  background: none;
  border: 0;
  font: inherit;
}

.nav-item:hover {
  background: var(--surface-hover);
  color: var(--text);
}

.nav-item.active {
  background: var(--primary-dim);
  color: var(--primary);
}

.nav-item svg {
  width: 18px;
  height: 18px;
  stroke-width: 1.5;
}

.nav-tooltip {
  position: absolute;
  left: calc(100% + 10px);
  background: var(--bg-deeper);
  color: var(--text);
  font-family: 'DM Sans', sans-serif;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 5px 10px;
  border-radius: 6px;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease;
  border: 1px solid var(--border);
  z-index: 110;
}

.nav-item:hover .nav-tooltip,
.nav-item:focus-visible .nav-tooltip {
  opacity: 1;
}

/* Unread count on the Inbox icon (real nav needs it; tokens only) */
.nav-badge {
  position: absolute;
  top: 4px;
  right: 4px;
  min-width: 14px;
  height: 14px;
  padding: 0 3px;
  border-radius: 7px;
  background: var(--primary);
  color: var(--bg-deeper);
  font-size: 9px;
  font-weight: 700;
  line-height: 14px;
  text-align: center;
}

.sidebar-spacer { flex: 1; }

.sidebar-auth { margin: 0; }

/* ===== HEADER ===== */
.header {
  grid-column: 2;
  background: var(--header-bg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
  padding: 0 32px;
  height: 64px;
  display: flex;
  align-items: center;
  gap: 24px;
  position: sticky;
  top: 0;
  z-index: 50;
}

.header-brand {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 700;
  font-size: 18px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-strong);
  text-decoration: none;
}

.header-brand span {
  color: var(--primary);
}

.header-divider {
  width: 1px;
  height: 24px;
  background: var(--border);
}

.header-label {
  font-family: 'DM Sans', sans-serif;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

.header-label.label-thin {
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

.header-spacer { flex: 1; }

/* Hamburger + drawer overlay: hidden on desktop, shown at the mobile width. */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin-right: 4px;
  border: 0;
  background: none;
  color: var(--text);
  cursor: pointer;
  border-radius: 8px;
}
.nav-toggle:hover { background: var(--surface-hover); }
.nav-toggle svg { width: 22px; height: 22px; }
.nav-overlay { display: none; }

/* Revenue total in header - gold spotlight, precious moment #1 */
.header-revenue {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}

.header-revenue-label {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

.header-revenue-value {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800;
  font-size: 22px;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--spotlight);
}

.header-revenue-note {
  font-size: 10px;
  color: var(--muted);
  letter-spacing: 0.04em;
}

/* Theme toggle */
.theme-toggle {
  width: 36px;
  height: 36px;
  border-radius: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  flex-shrink: 0;
}

.theme-toggle:hover {
  background: var(--surface-hover);
  color: var(--text);
}

.theme-toggle svg {
  width: 16px;
  height: 16px;
}

/* Icon visibility is theme-driven so the right icon shows pre-JS. */
html[data-theme="dark"] .theme-toggle .icon-sun { display: none; }
html[data-theme="light"] .theme-toggle .icon-moon { display: none; }

/* ===== MAIN CONTENT ===== */
.main {
  grid-column: 2;
  padding: 32px;
  max-width: 1200px;
  width: 100%;
  /* the mockup was designed at ~1440px where the 1200px column fills the
     track; on wider screens it pins left without this */
  margin-inline: auto;
}

/* ===== SECTION EYEBROW ===== */
.section-eyebrow {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 20px;
}

.eyebrow-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

.eyebrow-line {
  flex: 1;
  height: 1px;
  background: var(--border);
}

.eyebrow-date {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--muted);
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

a.eyebrow-date {
  text-decoration: none;
}

a.eyebrow-date:hover {
  color: var(--primary);
}

/* The Board "Load more" (Kelsey: 25 channels at once was too much). The board
   shows the top 3; each Load-more click reveals 5 more (static/js/board.js).
   Rows past the cut start hidden; with JS off they simply all show.
   Qualified with .chart-row so it outranks the later `.chart-row { display:
   grid }` (equal single-class specificity would otherwise lose on order). */
.chart-row.board-row-hidden { display: none; }
.board-more-wrap { margin-top: 14px; text-align: center; }

/* ===== CHART HEADING ===== */
.chart-heading {
  margin-bottom: 8px;
}

.chart-title {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 900;
  font-size: 48px;
  line-height: 1;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  color: var(--text-strong);
}

.chart-subtitle {
  font-size: 14px;
  color: var(--muted-mid);
  margin-top: 4px;
  letter-spacing: 0.02em;
}

/* ===== CHART COLUMNS HEADER ===== */
.chart-col-headers {
  display: grid;
  grid-template-columns: 72px 1fr 140px 120px 160px;
  gap: 0;
  padding: 8px 20px 8px 20px;
  margin-bottom: 4px;
}

.col-header {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

.col-header.align-right { text-align: right; }
.col-header.align-center { text-align: center; }

/* Revenue-hidden variant (non-staff): the per-channel money column is removed
   from the DOM, so collapse the grid to four tracks and keep it intact. The
   revenue visibility decision is centralised in accounts.access. */
.chart-col-headers.no-revenue { grid-template-columns: 72px 1fr 120px 160px; }

/* ===== CHART ROWS ===== */
.chart-rows {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.chart-row {
  display: grid;
  grid-template-columns: 72px 1fr 140px 120px 160px;
  align-items: center;
  gap: 0;
  padding: 18px 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: default;
  position: relative;
  overflow: hidden;
}

.chart-row.no-revenue { grid-template-columns: 72px 1fr 120px 160px; }

.chart-row::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--surface-hover);
  opacity: 0;
  transition: opacity 0.18s ease;
}

.chart-row:hover::before { opacity: 1; }

/* Rank numeral - the HERO of this stance */
.rank-wrap {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  position: relative;
  z-index: 1;
}

.rank-num {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 900;
  font-size: 52px;
  line-height: 1;
  letter-spacing: -0.04em;
  /* Legible secondary: ranks past the top-3 podium must still be readable now
     the board reveals all the way to 25 (was a ~4% watermark that vanished). */
  color: var(--muted);
  position: relative;
}

/* Rank #1 gets gold spotlight - the ONE precious warm moment on this page */
.chart-row.rank-1 .rank-num {
  color: var(--spotlight);
  text-shadow: 0 0 40px rgba(255, 205, 0, 0.30);
  font-size: 60px;
}

/* Podium medals: #2 silver, #3 bronze (gold #1 above). No opacity pull-back so
   the medal colours read; #1 stays primary via its glow + larger size. */
.chart-row.rank-2 .rank-num { color: var(--silver); }
.chart-row.rank-3 .rank-num { color: var(--bronze); }

/* Ranks #4+ use the legible base colour (var(--muted)) -- no extra dimming, so
   every revealed rank stays readable. The gold #1 + cyan #2-3 keep the podium. */

/* Channel info */
.channel-info {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-right: 16px;
  position: relative;
  z-index: 1;
}

.channel-name {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 700;
  font-size: 18px;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--text-strong);
  line-height: 1.1;
}

.channel-last-video {
  font-size: 12px;
  color: var(--muted-mid);
  font-style: italic;
  line-height: 1.3;
  letter-spacing: 0.01em;
}

.channel-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 2px;
}

.channel-subs {
  font-size: 11px;
  font-weight: 500;
  color: var(--muted);
  letter-spacing: 0.04em;
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

/* Revenue column */
.channel-revenue {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 3px;
  position: relative;
  z-index: 1;
}

.revenue-amount {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800;
  font-size: 26px;
  letter-spacing: -0.01em;
  line-height: 1;
  color: var(--text-strong);
}

.revenue-label {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

/* #1 row revenue gets spotlight gold - precious moment #2 (tightly related) */
.chart-row.rank-1 .revenue-amount {
  color: var(--spotlight);
}

/* Movement column */
.channel-movement {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  position: relative;
  z-index: 1;
}

.movement-arrow {
  width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.movement-up { color: var(--up); }
.movement-down { color: var(--danger); }

.movement-value {
  font-family: 'DM Sans', sans-serif;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* Each line colours itself by its own sign (movement-line-*, set per line in
   the template); the wrapper's movement-up/down colours the ARROW only. A
   rising subs figure must never read red because revenue leads the arrow. */
.movement-line-up .movement-value { color: var(--up); }
.movement-line-down .movement-value { color: var(--danger); }
.movement-line-flat .movement-value { color: var(--muted); }

.movement-sub {
  font-size: 10px;
  font-weight: 400;
  color: var(--muted);
  letter-spacing: 0.06em;
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
  margin-left: 2px;
}

/* Stacked movement lines: the revenue delta leads above the subs figure when the
   viewer can see revenue (revenue outranks subs). Kept tight so the extra line
   does not grow the row height. With no revenue line the column shows the single
   subs line exactly as before. */
.movement-figures {
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.15;
  gap: 1px;
}

.movement-rev {
  display: flex;
  align-items: baseline;
  gap: 2px;
}

.movement-rev-value {
  font-family: 'DM Sans', sans-serif;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.movement-line-up .movement-rev-value { color: var(--up); }
.movement-line-down .movement-rev-value { color: var(--danger); }
.movement-line-flat .movement-rev-value { color: var(--muted); }

.movement-rev-label {
  font-size: 9px;
  font-weight: 400;
  color: var(--muted);
  letter-spacing: 0.06em;
  margin-left: 2px;
}

/* Views column */
.channel-views {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 3px;
  position: relative;
  z-index: 1;
}

.views-value {
  font-family: 'DM Sans', sans-serif;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
}

.views-label {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

/* ===== REVENUE BAR MINI VIZ ===== */
/* Inline bar in the revenue col - width proportional to share of total */
.revenue-bar {
  height: 3px;
  background: var(--border);
  border-radius: 2px;
  margin-top: 4px;
  width: 80px;
  overflow: hidden;
}

.revenue-bar-fill {
  height: 100%;
  background: var(--primary);
  border-radius: 2px;
  transition: width 0.6s cubic-bezier(0.25, 1, 0.2, 1);
}

/* #1 bar gets gold to match its rank and revenue */
.chart-row.rank-1 .revenue-bar-fill {
  background: var(--spotlight);
}

/* ===== ATTENTION FLAG: subscriber decline ===== */
.flag-critical {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: rgba(224, 80, 80, 0.12);
  border: 1px solid rgba(224, 80, 80, 0.28);
  border-radius: 5px;
  padding: 2px 7px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--danger);
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

/* ===== REVENUE SPARKLINE: on-board monthly trend ===== */
/* Small inline trend line under the channel meta. Server-rendered SVG; no
   client JS. Default stroke is primary cyan; a channel down from its peak gets
   the danger red plus the "vs peak" caption, matching the sub-decline flag. */
.channel-trend {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
}

.sparkline {
  width: 96px;
  height: 24px;
  display: block;
  color: var(--primary);
  opacity: 0.75;
  flex-shrink: 0;
}

.sparkline-down {
  color: var(--danger);
  opacity: 0.9;
}

.trend-peak {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--danger);
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

/* ===== NEXT RELEASES SECTION ===== */
.releases-section {
  margin-top: 40px;
}

.releases-strip {
  display: flex;
  flex-direction: column;
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.release-row {
  display: grid;
  grid-template-columns: 88px 1fr 200px 130px;
  align-items: center;
  gap: 0;
  padding: 14px 20px;
  background: var(--surface);
}

.release-row:hover { background: var(--surface-hover); }

.release-date {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted-mid);
}

.release-title {
  font-size: 14px;
  font-weight: 500;
  color: var(--text-strong);
  line-height: 1.3;
}

.release-artist {
  font-size: 12px;
  color: var(--muted-mid);
  margin-top: 1px;
}

.release-channel {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--muted);
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

/* Status badge. The mockup defines submitted / not-submitted / at-risk
   recipes; the models expose Bootstrap-style contextual names, so each
   contextual name aliases onto the matching mockup recipe. */
.status-badge,
.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: 5px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

.status-badge.submitted,
.badge.text-bg-success,
.status-badge.text-bg-success,
.badge.text-bg-info,
.status-badge.text-bg-info,
.badge.text-bg-primary,
.status-badge.text-bg-primary {
  background: rgba(0, 203, 216, 0.11);
  border: 1px solid rgba(0, 203, 216, 0.24);
  color: var(--primary);
}

html[data-theme="light"] .status-badge.submitted,
html[data-theme="light"] .badge.text-bg-success,
html[data-theme="light"] .status-badge.text-bg-success,
html[data-theme="light"] .badge.text-bg-info,
html[data-theme="light"] .status-badge.text-bg-info,
html[data-theme="light"] .badge.text-bg-primary,
html[data-theme="light"] .status-badge.text-bg-primary {
  background: rgba(3, 122, 154, 0.08);
  border-color: rgba(3, 122, 154, 0.22);
  color: var(--primary);
}

.status-badge.not-submitted,
.badge.text-bg-warning,
.status-badge.text-bg-warning {
  background: rgba(255, 205, 0, 0.10);
  border: 1px solid rgba(255, 205, 0, 0.22);
  color: var(--spotlight);
}

html[data-theme="light"] .status-badge.not-submitted,
html[data-theme="light"] .badge.text-bg-warning,
html[data-theme="light"] .status-badge.text-bg-warning {
  background: rgba(200, 151, 0, 0.08);
  border-color: rgba(200, 151, 0, 0.20);
  color: var(--spotlight);
}

.status-badge.at-risk,
.badge.text-bg-danger,
.status-badge.text-bg-danger {
  background: rgba(224, 80, 80, 0.10);
  border: 1px solid rgba(224, 80, 80, 0.22);
  color: var(--danger);
}

.badge.text-bg-secondary,
.status-badge.text-bg-secondary,
.badge.text-bg-light,
.badge.text-bg-dark {
  background: var(--rank-track);
  border: 1px solid var(--border-strong);
  color: var(--muted-mid);
}

/* Released/done = green, Medium = blue. Split out of the cyan 'info' family
   (above) so 'released' no longer looks identical to a medium badge. These
   come later in the file, so they override the grouped cyan rule. */
.badge.text-bg-success,
.status-badge.text-bg-success {
  background: rgba(63, 174, 107, 0.13);
  border: 1px solid rgba(63, 174, 107, 0.30);
  color: #46c281;
}
html[data-theme="light"] .badge.text-bg-success,
html[data-theme="light"] .status-badge.text-bg-success {
  background: rgba(34, 139, 76, 0.10);
  border: 1px solid rgba(34, 139, 76, 0.26);
  color: #1f7a47;
}
.badge.text-bg-info,
.status-badge.text-bg-info {
  background: rgba(96, 150, 230, 0.14);
  border: 1px solid rgba(96, 150, 230, 0.32);
  color: #79aaf0;
}
html[data-theme="light"] .badge.text-bg-info,
html[data-theme="light"] .status-badge.text-bg-info {
  background: rgba(43, 108, 176, 0.09);
  border: 1px solid rgba(43, 108, 176, 0.24);
  color: #2b6cb0;
}

.status-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

/* ===== TOTAL REVENUE STAT STRIP ===== */
.stat-strip {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
  margin-bottom: 32px;
}

.stat-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 20px;
}

.stat-card:hover { background: var(--surface-hover); }

.stat-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 8px;
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

.stat-value {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800;
  font-size: 30px;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--text-strong);
}

/* Total Revenue card: gold spotlight */
.stat-value.accent { color: var(--spotlight); }

.stat-sub {
  font-size: 11px;
  color: var(--muted);
  margin-top: 5px;
  letter-spacing: 0.02em;
}

.stat-sub .up { color: var(--up); font-weight: 600; }
.stat-sub .down { color: var(--danger); font-weight: 600; }

/* ===== NEEDS YOUR ATTENTION (per-viewer, private) =====
   The page's action queue: dense itemised rows, each one a release link
   ([date chip][title][need tag]), tighter than the board's chart rows.
   Quiet on purpose: surface/border tokens, primary for the live dates,
   danger only for overdue. The gold --spotlight stays reserved for the
   one revenue moment per screen. */
.attention-strip {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 20px 12px;
  /* 32px, matching .stat-strip and .dash-links: every section closes with
     the same gap before the next eyebrow (Jan, 3 Jul: consistent rhythm). */
  margin-bottom: 32px;
}

/* Two personal columns (Jan, 3 Jul): the action queue and My checklist, folded
   in from its own panel. Stacks on narrow windows.
   MY CHECKLIST TAKES THE WIDE TRACK, and sits left (Kelsey, 3 Aug). It had the
   1fr third, which is about 283px of text once the switch and the remove button
   are out, and her items are free-text sentences: six of her fourteen wrapped to
   two lines and one to three, which pushed the add-item box off the bottom of
   the screen. The queue is BOUNDED and the checklist is not: at most five
   release entries (dashboard.attention.MAX_ENTRIES), three decline notices
   (MAX_DECLINE_NOTICES) and five summary rows, so thirteen at the very worst and
   usually a handful. A bounded list of short rows is what can live in a third of
   the strip; an open-ended list of sentences is not.
   PLACED WITH grid-column RATHER THAN order, and the markup is untouched: the
   stacked layout below then falls out of document order for free, which is what
   we want there (the queue is what needs doing; the checklist is private).
   THE COST IS A REAL ONE AND IT LANDS ON THE INTERACTIVE HALF. On desktop the
   tab order reaches the queue, which is plain links, before the checklist that
   renders to its left -- and the checklist is where the controls are: a toggle,
   a save and a remove per item, plus the add field, so roughly forty focusable
   controls on a full list. Someone tabbing meets the left-hand column last.
   Taken knowingly: the alternative is reordering the markup, which moves a
   fourteen-row personal list above the action queue on every phone in order to
   fix a desktop column width. Revisit it if anyone reports the tab order, and
   the fix then is a grid-template-areas layout that lets both orders be stated
   per breakpoint rather than one order serving both. */
.attention-columns {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 8px 28px;
  align-items: start;
}
/* MIN-WIDTH ZERO IS LOAD-BEARING, not tidying. An fr track keeps an automatic
   minimum of min-content, so once the queue moved into the 1fr track it refused
   to shrink to its share: its rows carry a nowrap date chip and a nowrap need
   tag, and the track took the width back out of the checklist, restoring the
   exact wrapping this change exists to remove. It only shows up when a viewer
   HAS releases due -- Kelsey has none, so it is invisible on her screen and in
   any screenshot she sends. */
.attention-main,
.attention-checklist {
  min-width: 0;
}
/* THE ROW IS AS EXPLICIT AS THE COLUMN, and leaving it out was a real bug
   caught only by rendering the page with data in it. Naming the column alone
   still leaves the ROW auto-placed: the queue comes first in the markup and
   takes row 1 column 2, the placement cursor has then passed row 1, and the
   checklist -- asking for column 1 -- goes to row 2. The strip renders with the
   queue alone on the right and a column-height hole where the checklist should
   be, which no test sees and which the design mockup did not reproduce because
   it moved the blocks with `order` instead. */
.attention-checklist {
  grid-column: 1;
  grid-row: 1;
}
.attention-main {
  grid-column: 2;
  grid-row: 1;
}
.attention-checklist-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 6px 0 2px;
}
/* Empty-state notes read at the chart-subtitle size, not body size. */
.attention-empty {
  font-size: 14px;
  letter-spacing: 0.02em;
  margin: 4px 0;
}
@media (max-width: 860px) {
  .attention-columns { grid-template-columns: 1fr; }
  /* One track, so the explicit placement above would pin both blocks into the
     same cell and stack them on top of each other. Released back to auto
     placement, which is document order: queue first, checklist under it. Both
     axes, or the shared grid-row keeps them in one row and they overlap. */
  .attention-checklist,
  .attention-main { grid-column: auto; grid-row: auto; }
}

.attention-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 8px;
}

.attention-queue {
  display: flex;
  flex-direction: column;
}

/* One queue row, wholly clickable. Hover reuses the surface-hover wash. */
.attention-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 8px;
  margin: 0 -8px;
  border-radius: 6px;
  border-top: 1px solid var(--border);
  text-decoration: none;
}

.attention-item:first-child {
  border-top: none;
}

.attention-item:hover {
  background: var(--surface-hover);
}

/* Date chip: the mono-style label treatment, tabular so dates align. */
.attention-due {
  flex: 0 0 104px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--primary);
}

.attention-due.overdue {
  color: var(--danger);
}

.attention-due.undated {
  color: var(--muted);
}

/* WRAPS, IT DOES NOT ELLIPSISE, since the queue moved into the narrow track.
   A release title is "Artist - Album", which at this width truncated to about
   twenty characters: "Tbilisi Sympho..." does not say which release it is, and
   two releases by the same orchestra were indistinguishable. Truncation is the
   right answer when the alternative is a wrecked row; here the row is allowed to
   be two lines, and the entries are capped, so nothing runs away. */
.attention-item-title {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  color: var(--text);
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  /* WITHOUT THIS THE TITLE PAINTS OUTSIDE ITS COLUMN. white-space: normal only
     breaks at soft wrap opportunities, and an unbroken token has none, so with
     min-width: 0 already on the box it shrinks to the track while the text runs
     straight out of it -- and past ~1000px wide, out of the page, giving the
     whole dashboard a horizontal scrollbar. The clipping we just removed was
     what hid that. Not hypothetical input: attention.py builds the title from
     Release.__str__, which falls back to the CATALOGUE REF when the album title
     is empty, and a catalogue ref is exactly a long unbroken alphanumeric with
     nowhere to break. `anywhere` rather than `break-word` so it also counts for
     min-content sizing, which is what the shrunk track is negotiating over. */
  overflow-wrap: anywhere;
}

/* A ROW CARRYING TWO BADGES STOPS BEING THREE COLUMNS. Chip, title and tag side
   by side hands the title the narrow middle and wraps it to five lines, one or
   two words each, which no longer reads as a title. So the title is dropped onto
   a full-width line of its own, below the badges. It is moved with order rather
   than in the markup, because the chip must still be the first thing read and
   the accessible order is the markup's.
   TWO LINES ON A WIDE WINDOW, THREE ON A NARROW ONE, and the split is not worth
   chasing. `.attention-need` does not shrink and `.badge` does not wrap, so
   below roughly 1100px the track cannot seat both badges together and the tag
   takes a line of its own: chip / tag / title. It costs nothing that matters --
   `align-items: start` on the columns means a taller queue never pushes the
   checklist's add-item box down, which is the thing this change is protecting.
   Only the itemised release entries carry both badges: the summary and notice
   rows are one badge and an arrow, already one short line, and breaking those in
   two would read worse than leaving them. */
.attention-item-detail {
  flex-wrap: wrap;
  row-gap: 6px;
}
.attention-item-detail .attention-item-title {
  order: 1;
  flex: 1 1 100%;
}

.attention-item:hover .attention-item-title {
  color: var(--text-strong);
}

/* Need tag: the quiet badge recipe (rank-track + strong border, muted). */
.attention-need {
  flex-shrink: 0;
  padding: 2px 8px;
  border-radius: 5px;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  background: var(--rank-track);
  border: 1px solid var(--border-strong);
  color: var(--muted-mid);
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

/* Summary rows (team schedule / inbox): count lines, arrow affordance. */
.attention-go {
  flex-shrink: 0;
  font-size: 12px;
  color: var(--muted);
}

.attention-item:hover .attention-go {
  color: var(--primary);
}

/* Notice rows: staff-only board facts. A leading dot stands in for the date
   chip the summary rows carry, keeping the compact two-part grammar; tone is
   colour only, from the existing semantic tokens (no gold). */
.attention-notice .attention-item-title::before {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-right: 8px;
  border-radius: 50%;
  vertical-align: middle;
  background: currentColor;
}

/* Hanging indent, because the dot is a ::before INSIDE the title rather than a
   sibling of it. Harmless while the title could not wrap; once it could, the
   second line returned to the title's own left edge and sat under the dot
   instead of under the words. 13px is the dot (5px) and its margin (8px), so a
   wrapped line lands exactly on the first line's text. */
.attention-notice .attention-item-title {
  padding-left: 13px;
  text-indent: -13px;
}

.attention-notice-danger .attention-item-title {
  color: var(--danger);
}

.attention-notice-muted .attention-item-title {
  color: var(--muted);
}

.attention-more {
  display: inline-block;
  margin-top: 6px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--muted);
  text-decoration: none;
}

.attention-more:hover {
  color: var(--primary);
}

/* ===== DATA NOTE (sample-data / fixture tag) ===== */
.data-note {
  font-size: 12px;
  color: var(--muted-mid);
  letter-spacing: 0.04em;
  font-style: italic;
  margin-bottom: 32px;
  padding: 8px 12px;
  background: var(--rank-track);
  border-radius: 6px;
  display: inline-block;
}

.data-note code {
  font-style: normal;
  color: var(--primary);
  font-size: 11px;
}

/* ============================================================
   INNER PAGES - the rest of the hub on the same surface tokens.
   These re-express the existing page components (panels, data
   tables, metric cards, forms, inbox) in chart-room roles so
   every module renders cleanly inside the new shell. Full
   per-page redesigns come in later waves.
   ============================================================ */

/* --- Page head --- */
.page-head {
  border-bottom: 1px solid var(--border);
  margin-bottom: 24px;
  padding-bottom: 14px;
}

.page-title {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 900;
  font-size: 34px;
  line-height: 1;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: var(--text-strong);
}

.page-sub {
  font-size: 14px;
  color: var(--muted-mid);
  margin-top: 6px;
  letter-spacing: 0.02em;
}

/* The open-work counts line needs air before the table panel below it. */
.open-work-summary { margin: 12px 0 16px; }

/* --- Panels --- */
.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 20px 20px;
  margin-bottom: 24px;
}

.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  border-bottom: 1px solid var(--border);
  padding-bottom: 10px;
  margin-bottom: 12px;
  gap: 12px;
}

/* A panel-head that opens its panel needs no space above it: the panel's own
   padding provides it. One that starts a LATER section inside the same panel
   does, or its rule sits flush against the end of the previous section's table
   and the two read as one block. */
.panel-head:not(:first-child) {
  margin-top: 30px;
}

.panel-title {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 700;
  font-size: 19px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-strong);
  margin: 0;
}

.panel-note {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

.panel-link, .panel-link:hover {
  color: var(--primary);
  font-size: 13px;
  text-decoration: none;
}

.panel-link:hover { text-decoration: underline; }

/* --- Your objectives (the board's per-viewer target list) ---
   A ruled list rather than cards. The stat cards elsewhere are the portfolio's
   figures and these are one person's, so borrowing their treatment would read as
   more of the same board; rules and type carry the separation instead, the same
   way the roadmap list below does.

   THE DISTANCE IS A SHAPE HERE AND A SENTENCE UNDER IT. The bar is current over
   target and carries NO second marker for where anybody would need to be by now:
   that needs the objective's value on the day it was set, which nothing stores, so
   it could only come from an assumed straight line. dashboard/objectives.py refuses
   to compute it and this refuses to draw it. */
.objective-list {
  list-style: none;
  margin: 0 0 32px;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

/* A panel apiece, matching the work cards above rather than the ruled list this
   was when the objectives ran down a narrow column. Side by side, a rule between
   two blocks separates them along the wrong axis: it draws a line under each one
   instead of between them, and the pair reads as two orphans rather than as two
   of a kind. The border is the whole card, never a stripe down one edge. */
.objective-item {
  padding: 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.objective-name {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 700;
  font-size: 16px;
  letter-spacing: 0.02em;
  color: var(--text-strong);
  margin: 0 0 10px;
}

/* Track and fill. Cyan because that is what this stylesheet's primary means on a
   measured bar (the board's revenue bar is the same recipe) -- a position moving
   toward something, never a verdict on whether it is moving fast enough. */
.objective-track {
  height: 8px;
  border-radius: 4px;
  background: var(--rank-track);
  border: 1px solid var(--border);
  overflow: hidden;
  margin: 0 0 10px;
}

.objective-fill {
  height: 100%;
  background: var(--primary);
  border-radius: 4px 0 0 4px;
}

/* Where it stands and where it is going, on one line, each under its own label.
   Two labelled numbers rather than the sentence that used to carry them both:
   "430,000 now, 70,000 short of the 500,000 target set for 31 October 2026" is
   three figures a reader has to hold at once to compare any two of them. */
.objective-figures {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 14px;
  margin: 0 0 10px;
}

.objective-figure {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.objective-figure-target { text-align: right; align-items: flex-end; }

.objective-figure strong {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800;
  font-size: 21px;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
}

.objective-figure-label {
  font-size: 11px;
  line-height: 1.3;
  color: var(--muted);
}

.objective-reading {
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
  margin: 0;
}

.objective-work {
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted-mid);
  margin: 6px 0 0;
}

/* The one sentence on this page set whole in the danger colour, and the one place
   the "colour the number, never the sentence" rule is deliberately broken. There is
   no number in it: a target with no work pointed at it is a state, and it spent the
   previous build as the smallest, greyest type in the panel while being the most
   useful thing on the screen. Everything else about the line is unchanged -- no
   badge, no icon, no softening -- so what landed flatly still lands flatly, louder. */
.objective-work-loud {
  color: var(--danger);
  font-weight: 700;
}

.objective-empty {
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted-mid);
  margin: 0 0 32px;
}

/* ============================================================
   WHAT'S MOVING -- the ticker along the foot of the window, on every page.

   It was a five-row section at the top of the dashboard. The client's answer was "we
   need to show movement", and a list cannot supply it: a list is a state, and a state
   can be perfectly true of a company that has stopped. The sentences crawl right to
   left instead, continuously, and the motion carries the message before a word of it
   has been read.

   FIXED, SO IT COSTS NOTHING VERTICALLY. That is the whole reason it could leave the
   page flow and the reason its cap went from five entries to ten: as a section every
   row pushed the reader's own work further down, and here every row costs the same
   nothing. What it does cost is 42px at the foot of the viewport, and three rules
   below pay for it together -- the page's own padding, the shell's height and the
   sidebar's -- so that nothing ever ends up underneath it.

   SLOW ENOUGH TO READ. 50 pixels a second, set by static/js/moving-ticker.js from the
   measured width of one pass so that a long day and a quiet one crawl at the same
   speed. A reader can finish a sentence comfortably as it goes past, and if they
   cannot, hover or a tab stop stops it dead until they leave.

   ONE ANIMATION ON THE WHOLE APP. It is a transform, so the compositor runs it and
   nothing executes per frame. Nothing else in this stylesheet animates position or
   moves on its own, deliberately: motion is a signal, and a page with two of them has
   none.

   NO GOLD. Gold has exactly two jobs here (the revenue total and the Open task
   control) and a third gold thing costs both of them their meaning. The bar is a dark
   surface in both themes with its type set in two weights of the same near-white; the
   one colour on it is the interactive cyan, on hover and focus, where every other link
   in the app already takes it.
   ============================================================ */
.ticker {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--ticker-h);
  display: flex;
  align-items: center;
  /* The window is the frame: everything outside it is clipped, which is what makes
     an entry arrive and leave rather than pop. */
  overflow: hidden;
  background: var(--ticker-surface);
  border-top: 1px solid var(--ticker-edge);
  /* Above the sticky sidebar (100) so the bar is genuinely full width, and below the
     mobile nav drawer and its overlay (120/110) so an open drawer is never cut off
     by a decorative feed. */
  z-index: 105;
}

.ticker-track {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  /* Composited on purpose: the only thing that changes per frame is a transform, so
     the browser runs it off the main thread and no JavaScript timer is involved. */
  will-change: transform;
  animation: ticker-crawl var(--ticker-duration, 60s) linear infinite;
}

/* The shift is exactly ONE pass, so the copy behind arrives already in position and
   the wrap has no jump in it. The fallbacks are what the bar does with the script
   blocked: two passes are rendered server-side, so half the track is one pass, and it
   still crawls and still loops -- only its speed stops being a constant. */
@keyframes ticker-crawl {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(var(--ticker-shift, 50%) * -1)); }
}

/* PAUSED WHILE SOMEBODY IS READING IT, and paused while nobody can be. Hover and
   focus-within are both here because a line must not slide out from under a reader
   mid-sentence, whichever way they arrived at it; the tab-hidden class comes from the
   script's visibilitychange handler, so a backgrounded tab is not crawling all day. */
.ticker:hover .ticker-track,
.ticker:focus-within .ticker-track,
.ticker.is-tab-hidden .ticker-track {
  animation-play-state: paused;
}

.ticker-seq {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 16px;
  /* THE SEAM, AND IT IS MEANT TO SHOW. Three seconds of empty bar between the end of
     the sequence and its restart, against sixteen pixels between entries, so a reader
     can tell the feed has come round again rather than reading the same sentences as
     new work. The repetition is honest -- it is the same ten things cycling -- and a
     loop that hides its seam is the one that lies about that. */
  padding-right: 160px;
}

.ticker-entry {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: baseline;
  gap: 7px;
  /* The bar is one line tall: a sentence that wrapped would be cut in half by it. */
  white-space: nowrap;
  font-size: 14px;
  line-height: 1;
  color: var(--ticker-ink);
  text-decoration: none;
}

/* A middot between entries, never an em dash, and never a rule down an entry's edge.
   It belongs to the entry on its right and sits centred in the space between the two
   (16px of flex gap on its left, 9 plus 7 on its right), so the first entry of every
   pass opens clean and the seam stays the only break in the sequence. */
.ticker-entry:not(:first-child)::before {
  content: "\00B7";
  color: var(--ticker-quiet);
  margin-right: 9px;
}

/* The person, one step heavier than what they did. Every entry that has an actor is
   set this way and no entry is set any other way: this is a grammatical split, not an
   emphasis anybody earns, and the moment one line can be made louder than another the
   feed is a scoreboard. */
.ticker-who {
  font-weight: 600;
  color: var(--ticker-name);
}

/* When it happened, quiet, and travelling with its own entry rather than pinned
   anywhere. It says when and nothing about whether that is soon enough: there is no
   late on this feed, so this never takes a colour and a two-week-old entry and this
   morning's look exactly alike. */
.ticker-when {
  margin-left: 5px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ticker-quiet);
}

a.ticker-entry:hover .ticker-said,
a.ticker-entry:hover .ticker-who {
  color: var(--ticker-hover);
}

.ticker-entry:focus-visible {
  outline: 2px solid var(--ticker-hover);
  outline-offset: 3px;
  border-radius: 3px;
}

/* NO CRAWL AT ALL, not a slower one. Moving UI makes some people ill and they set
   this at the operating system; a ticker that merely slowed down for them would still
   be the thing that did it, on every page, with no way to turn it off. So the motion
   stops entirely and what is left is the newest entry, stated once and sitting still.
   The script reads the same query and does not run, so nothing is measured or cloned
   for a bar that is not going to move. */
@media (prefers-reduced-motion: reduce) {
  .ticker-track {
    animation: none;
    transform: none;
  }
  .ticker-seq { padding-right: 0; }
  .ticker-seq:not(:first-child),
  .ticker-entry:not(:first-child) { display: none; }
}

/* WHAT PAYS FOR THE BAR'S HEIGHT. A fixed element is out of the flow, so without
   these three the bar would sit on top of the last line of every long page and on top
   of the sidebar's own log-out control. The class is on <body> and is set on exactly
   the condition that renders the bar, so a page with no bar reserves nothing.

   The shell loses the same height it gains in padding, which is what keeps a SHORT
   page short: 100vh of shell plus 42px of padding is a scrollbar on every page in the
   app, for a bar that is already in view. */
body.has-ticker { padding-bottom: var(--ticker-h); }
body.has-ticker .app { min-height: calc(100vh - var(--ticker-h)); }
body.has-ticker .sidebar { height: calc(100vh - var(--ticker-h)); }

/* ============================================================
   THE TWO TABS -- your own half of the page, and everybody else's.

   Two words and a rule. The strip carries no fill, no pill and no box around
   itself: this page states its structure in type and lines, and a boxed control at
   the top of it would be the loudest thing on the screen while saying the least.
   The selected tab takes weight and a solid underline in the interactive colour;
   the other is muted, which is the same pair of signals the sidebar already uses
   for where you are and where you are not.

   HIDDEN UNTIL THE SCRIPT TAKES CHARGE. The panels are visible and the strip is
   not, so a browser with static/js/dashboard-tabs.js blocked gets the page it had
   before the split rather than half of one. A strip that cannot switch anything is
   worse than no strip: it says the rest of the page is elsewhere and then does not
   go there.
   ============================================================ */
.home-tabs {
  display: none;
  align-items: flex-end;
  gap: 28px;
  margin-bottom: 28px;
  border-bottom: 1px solid var(--border);
}

.home-tabs.is-ready { display: flex; }

.home-tab {
  appearance: none;
  background: none;
  border: 0;
  /* The one border on it, and it sits ON the strip's own rule rather than above
     it, so the selected tab reads as a piece of that line rather than as a second
     line under it. */
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  padding: 0 0 12px;
  font-family: inherit;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--muted);
  cursor: pointer;
}

.home-tab:hover { color: var(--text); }

.home-tab:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }

.home-tab[aria-selected="true"] {
  color: var(--text-strong);
  font-weight: 700;
  border-bottom-color: var(--primary);
}

/* How many tier-one lines are sitting on the panel behind the tab, which is the
   whole reason the company half is safe to cover. Set as a figure rather than as a
   badge with a fill: the panel it points at spends colour on the specific bad
   number and on nothing around it, and a tab that shouted louder than the section
   it stands for would be a second opinion about the same three counts. Red because
   red on this page is bad news, which is exactly what it counts. */
/* Both tabs carry a count, and the two say different things by COLOUR rather than
   by one having a figure and the other not. The number always answers "how many
   things are in here"; the colour answers "are they a problem". A red figure on one
   tab and nothing on the other read as "this tab has news and that one is empty",
   which was wrong about a day holding two pieces of work. */
.home-tab-count {
  margin-left: 8px;
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800;
  font-size: 17px;
  color: var(--muted-mid);
  font-variant-numeric: tabular-nums;
}

.home-tab-count.is-bad { color: var(--danger); }

/* ============================================================
   THE TOP FOLD -- what you have to do, beside what it is for.

   Two columns, sixty/forty. The work takes the wide one because it is the only
   part of this page anybody can act on; the objectives take the narrow one
   because a target is read rather than done. They were stacked, and the second
   of them sat below the fold on every screen the team uses, so a page whose
   argument is "here is the work, here is what it is in service of" could only
   ever make half the argument at a glance.

   Nothing below the fold inherits this. The split exists to put those two
   sections in one view and is not a layout the rest of the page adopts.
   ============================================================ */
/* The two sections stack, and each pairs its own cards two to a row.

   They were side by side, work in a 3fr track and objectives in a 2fr one, to
   put what to do and what it is for in one glance. Pairing the cards keeps that
   and costs nothing: each section is now one row tall rather than two or three,
   so both still land above the fold, and every card is wider than it was inside
   the old 3fr column. Two objectives could not have paired inside a 2fr track at
   all, which is what settled it: a figure like 1,923,589 against a target of
   4,237,288 needs the width more than the layout needed its columns. */
.call-sheet-fold {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 40px;
  align-items: start;
}

.fold-main, .fold-side { min-width: 0; }

/* YOUR WORK IS THE ONE FILLED REGION ON THE PAGE, and it is filled precisely
   because nothing else is. The rest of this stylesheet lets type and rules carry
   structure; one block that breaks that rule is the fastest thing on the screen to
   find, and a second block breaking it would cost the first its whole effect. Do
   not give this treatment to the objectives, the company sections, or the Board.

   The colour belongs to the REGION, not to the cards inside it. An earlier pass
   coloured each card and alternated them, which meant a card changed colour when a
   different card was closed: the alternation is positional, so finishing the first
   piece of work restyled the second for a reason that had nothing to do with it.
   Filling the container says the same thing ("this block is yours") and says it
   about something that does not reorder.

   Cyan in both themes rather than a per-theme hue, because the block is the app's
   one fixed landmark and a reader who switches themes should not have to find it
   again. Gold was the alternative and lost on a rule this stylesheet already
   states at the top: gold is precious and spent once per screen, on the revenue
   total. A gold block this size would have taken that. */
.work-zone {
  background: var(--work-zone);
  border-radius: 14px;
  padding: 20px 24px 26px;
}

/* The eyebrow sits ON the fill, so its colour comes off the fill rather than off
   the theme. Both themes get the same dark ink here for the same reason the fill
   is one colour in both. */
.work-zone .eyebrow-label { color: rgba(4, 24, 41, 0.78); }
.work-zone .eyebrow-line { background: rgba(4, 24, 41, 0.28); opacity: 1; }
.work-zone .section-eyebrow { margin-top: 0; }

/* Opaque, unlike the surface everywhere else. The dark theme's --surface is
   translucent so panels pick up the page behind them, which is right against the
   background and wrong here: over cyan it tints every card toward the fill and
   the text loses the contrast the card exists to give it. The border likewise:
   the usual one is cyan at low alpha and disappears completely on cyan. */
.work-zone .today-row {
  background: var(--work-card);
  border-color: rgba(4, 24, 41, 0.16);
}

.work-zone .today-row:hover { background: var(--work-card-hover); }

/* The control that opens a piece of work, in gold, on both themes. It is the one
   thing on this page a reader is meant to press, and it sits inside the one filled
   region, so it has to win against a cyan field without borrowing cyan to do it.
   Dark ink rather than white: gold is a light colour and white on it fails contrast
   at this size whatever the weight. */
.work-zone .today-row-open.btn.btn-dark {
  background: var(--work-cta);
  border-color: var(--work-cta);
  color: #241a00;
}

.work-zone .today-row-open.btn.btn-dark:hover {
  background: var(--work-cta-hover);
  border-color: var(--work-cta-hover);
  color: #241a00;
}

/* ============================================================
   YOUR WORK -- every row offered on the same terms.

   THERE IS NO HERO ROW, and its removal is the point of this pass rather than a
   side effect of it. The first row used to print its DUE DATE at 88px in gold: a
   deadline set at four times the size of the work it belonged to, spending the
   page's one precious colour on the wrong noun, with every other row below it at
   one weight so the eye had nowhere to go after the shout. One tier of emphasis
   is the same as none.

   Gold is now spent once on this page, in the header's monthly revenue, and
   nowhere in this section. Emphasis here comes from four things at once instead:
   the section's position, a filled control on every row, the largest display type
   in the body of the page on the row's headline, and the card the row sits in.

   THE KIND OF WORK IS A LABEL AND THE VIDEO IS THE HEADLINE. The kind repeats
   verbatim on every row of its kind and the video is the only part that differs,
   so the differing half takes the size. Both rows of a two-video campaign used to
   open with the same eight words and end with the part that told them apart.

   Page-scoped rather than a reuse of the strategy page's .move grammar, which
   looks similar and carries modifiers (.is-late, .is-block) that colour a whole
   row red -- one stray class from that family landing here would put a verdict on
   a queue that is only ever ordered by date.
   ============================================================ */
/* Two to a row. minmax(0, 1fr) rather than 1fr because a grid item's default
   min-width is auto, and one long unbroken video title would otherwise take more
   than its half and push its neighbour narrow. An odd last card stays at half
   width rather than stretching: a lone wide card reads as a different kind of
   row, and the only thing it would be signalling is that the count is odd. */
.today-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.today-row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 16px 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.today-row:hover {
  border-color: var(--border-strong);
  background: var(--surface-hover);
}

/* The row-wide hit area, the same stretched-overlay trick .board-row-link already
   uses on the chart rows: it keeps the row a plain flex container rather than
   forcing every part of it inside one anchor. It sits ABOVE the row's text and
   BELOW the button, so a click anywhere opens the work and a click on the button
   is still the button. */
.today-row-hit {
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: var(--radius);
  text-decoration: none;
}

.today-row-body { min-width: 0; }

.today-row-job {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted-mid);
  margin: 0 0 5px;
}

.today-row-what {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800;
  font-size: 24px;
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--text-strong);
  margin: 0 0 8px;
  overflow-wrap: anywhere;
}

.today-row-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 9px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--muted-mid);
  margin: 0;
}

/* A separator between metadata items, drawn rather than typed, so the template does
   not have to know which of the three optional parts is present.

   The date chip is exempt, and the exemption is not cosmetic: a ::before renders
   INSIDE the element it is attached to, so the chip drew the separator within its
   own border and read as "· DUE 8 AUGUST". It needs no separator anyway, being the
   one part of the line that is already a bordered shape. */
.today-row-meta > span + span::before {
  content: "\00B7";
  margin-right: 9px;
  color: var(--muted);
}

.today-row-meta > .today-row-when::before { content: none; }

/* The handoff takes the whole second line and drops its separator with it. As an
   inline item it wrapped, which left the middot stranded at the START of the new
   line: a separator with nothing before it, reading as a bullet the sentence does
   not have. Giving it the full basis makes the wrap deliberate rather than
   incidental, and once it is always first on its line it separates nothing. */
.today-row-meta > .today-row-handoff {
  flex: 1 1 100%;
}

.today-row-meta > .today-row-handoff::before { content: none; }

.today-row-which { overflow-wrap: anywhere; }

/* The date, as a chip. Neutral by default: a date that has not passed is not news,
   and cyan on it would spend the interactive colour on something nobody can click.
   Only a date that has PASSED takes red, and only the chip, never the row --
   colouring the whole row would read as a rating of the work rather than a reading
   of its date. Due today is emphasised by contrast rather than by hue, which is the
   same reason: nothing has gone wrong yet. */
.today-row-when {
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  border-radius: 5px;
  border: 1px solid var(--border-strong);
  background: var(--rank-track);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--muted-mid);
}

.today-row.overdue .today-row-when {
  border-color: rgba(224, 80, 80, 0.28);
  background: rgba(224, 80, 80, 0.12);
  color: var(--danger);
}

.today-row.due-today .today-row-when { color: var(--text-strong); }

/* The control. .btn.btn-dark is the app's own filled primary button, reused rather
   than reinvented, so the one thing this page asks a reader to press looks like
   every other primary action in the app. Only its place in the row is declared
   here, plus the z-index that lifts it clear of the stretched hit area above. */
.today-row-open {
  position: relative;
  z-index: 2;
  flex-shrink: 0;
  white-space: nowrap;
}

.today-empty {
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted-mid);
  margin: 0 0 32px;
}

/* --- Section rhythm on the home page.

   Every section here is a bare ruled list rather than a boxed panel, so nothing
   supplies the gap between one section's last row and the next section's label --
   the eyebrow only carries a margin BELOW itself. Without this the objectives
   heading sits hard against the last row of Today and the page reads as one
   undifferentiated column.

   Scoped to .call-sheet rather than applied to .section-eyebrow globally: the
   eyebrow is shared by every page in the app, most of which sit inside panels that
   already space themselves, and widening this would be a silent layout change to
   pages nobody asked about. The first eyebrow is exempt because the page head above
   it already provides the gap. --- */
.call-sheet .section-eyebrow:not(:first-child) { margin-top: 40px; }

/* --- The company divider: the one gear-shift on the page, from "you" to
   "everyone". A bigger pause than the section rhythm and a slightly heavier label,
   but still label vocabulary -- never chart-title scale and never gold, so it
   cannot compete with the hero above it. Restated inside .call-sheet so it outranks
   the 40px rule above rather than losing to it on specificity. --- */
.section-eyebrow.eyebrow-shift,
.call-sheet .section-eyebrow.eyebrow-shift { margin-top: 64px; }
.section-eyebrow.eyebrow-shift .eyebrow-label {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
}

/* The company divider is now the first thing in its own panel, and its 64px run-up
   was the pause before the gear-shift from "you" to "everyone". The tab strip is
   that pause, so keeping both would spend it twice and, worse, start the two panels
   at different heights: switching tabs would move the first line of the page. The
   divider keeps its weight and loses its run-up, and only where it opens a panel. */
.call-sheet .home-panel > .section-eyebrow:first-child { margin-top: 0; }

/* --- Company today, in three tiers.

   It was one stack of sentences at one weight, in which "0 pieces of work went live
   this month" was set exactly like "2 videos are booked to go out in the next 31
   days". A reader scanning it registered neither. Which tier a line belongs in is
   decided in dashboard/company.py, because whether a zero is bad news is a
   judgement about what the number means and not something a stylesheet can read off
   its formatting.

   NO GOLD ANYWHERE IN HERE. The temptation to give the loudest count the header's
   colour is refused explicitly: gold means exactly one thing on this page, and a
   gilded count here would end that. Bad news is red and everything else is untinted.

   TIER ONE -- the bad counts, as tiles. A figure at display size in the danger
   colour over its own caption, which is the same sentence with the figure lifted
   out of it. Bordered and tinted, never striped down one edge. --- */
.company-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px;
  margin: 0 0 22px;
}

.company-tile {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 16px 18px;
}

.company-tile-value {
  display: block;
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800;
  font-size: 30px;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--danger);
  font-variant-numeric: tabular-nums;
  margin-bottom: 8px;
}

.company-tile-caption {
  display: block;
  font-size: 12px;
  line-height: 1.45;
  color: var(--muted);
}

/* TIER TWO -- a channel standing below its own peak. Only the PERCENTAGE takes the
   colour. The sentence around it is the reading and stays body-coloured, and the
   link stays plain text rather than cyan: a whole cyan sentence beside a red figure
   dilutes both, and the underline on hover is enough to say it goes somewhere. */
.company-declines { max-width: 640px; margin: 0 0 18px; }

.company-decline {
  font-size: 16px;
  line-height: 1.6;
  color: var(--text);
  margin: 0 0 8px;
}

.company-decline:last-child { margin-bottom: 0; }
.company-decline a { color: inherit; text-decoration: none; }
.company-decline a:hover { text-decoration: underline; }
.company-figure-bad { font-weight: 700; color: var(--danger); }

/* TIER THREE -- everything neutral and everything good, in plain sentences at body
   weight. Nothing here is tinted at all, and that is what makes the two tiers above
   it read as loud: a page where good news is decorated is a page where bad news
   does not stand out. The numerals get weight for scannability and nothing else. */
.company-lines { max-width: 640px; margin: 0 0 8px; }

.company-line {
  font-size: 16px;
  line-height: 1.6;
  color: var(--text);
  margin: 0 0 8px;
}

.company-line:last-child { margin-bottom: 0; }
.company-line strong { font-weight: 600; color: var(--text-strong); }

/* --- The roster rows on Who is carrying what.
   Page-scoped for the same reason .today-row is: the strategy page's lookalike
   carries a danger-red modifier, and this is the one section in the app where
   red would do real damage. --- */
.roster-list {
  list-style: none;
  margin: 0 0 8px;
  padding: 0;
}

.roster-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 0;
  border-top: 1px solid var(--border);
  text-decoration: none;
}

.roster-list li:first-child .roster-row {
  border-top: 0;
  padding-top: 0;
}

/* The open-work count, as a badge. Set IDENTICALLY whatever the number: same size,
   same weight, same neutral surface, no tint that deepens as the count climbs. A
   badge that reddened or grew with the number is the league table this section
   exists not to be, and --danger is sitting right there in this stylesheet. Centred
   rather than baseline-aligned because it is a shape beside the text, not a word in
   the same line as it. */
.roster-count {
  flex-shrink: 0;
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 28px;
  height: 28px;
  padding: 0 7px;
  border-radius: 14px;
  border: 1px solid var(--border-strong);
  background: var(--rank-track);
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 700;
  font-size: 14px;
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
}

/* Grows so the age stays pinned right whether or not a count badge precedes it. */
.roster-row-body { min-width: 0; flex: 1 1 auto; }

.roster-row-what {
  font-size: 15px;
  font-weight: 600;
  color: var(--text-strong);
  margin: 0 0 2px;
}

.roster-row-which {
  font-size: 13px;
  color: var(--muted-mid);
  margin: 0;
  overflow-wrap: anywhere;
}

/* NEVER var(--danger), whatever the number. 19 days and 90 days are set
   identically, and every row is the same weight whether the person is holding
   eleven things or none. A row that looks quieter because its count is low, or
   louder because its count is high, is a performance judgement rendered in CSS --
   which is the one thing this section exists not to be. --danger is sitting right
   there in this stylesheet and must stay out of these rules. */
.roster-row-age {
  flex-shrink: 0;
  font-size: 12px;
  font-weight: 500;
  color: var(--muted-mid);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

a.roster-row:hover .roster-row-what { color: var(--primary); }

.roster-empty {
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted-mid);
  margin: 0 0 32px;
}

/* The pairs collapse well before the app's usual breakpoints. Half of a 1024px
   viewport minus the rail is about 460px, and an objective's two figures plus
   their labels stop fitting on one line somewhere above that; one card to a row
   is better than wrapping a target date under its own number. */
@media (max-width: 1100px) {
  .call-sheet-fold { grid-template-columns: minmax(0, 1fr); gap: 36px; }
  /* One to a row below this width. A half of a narrow viewport is not a card,
     it is a column of single words, and the figures are the first thing to go. */
  .today-list, .objective-list { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 860px) {
  .today-row { flex-direction: column; align-items: stretch; gap: 14px; }
  .today-row-open { align-self: flex-start; }
}

@media (max-width: 600px) {
  .today-row-what { font-size: 21px; }
  .objective-figures { gap: 10px; }
  .objective-figure strong { font-size: 19px; }
}

/* --- Roadmap (static Live now / Coming lists) --- */
.roadmap-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.roadmap-item {
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
}

.roadmap-item:first-child { padding-top: 0; }

.roadmap-item:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

.roadmap-item-title {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 600;
  font-size: 16px;
  letter-spacing: 0.02em;
  color: var(--text-strong);
  margin: 0 0 2px;
}

.roadmap-item-detail {
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted-mid);
  margin: 0;
}

/* --- Metric cards (rights overview etc.) --- */
.metric-row {
  display: flex;
  gap: 12px;
  margin-bottom: 24px;
  flex-wrap: wrap;
}

.metric-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 20px;
  min-width: 220px;
}

.metric-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 8px;
}

.metric-value {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800;
  font-size: 30px;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--text-strong);
}

.metric-foot {
  font-size: 11px;
  color: var(--muted);
  margin-top: 5px;
  letter-spacing: 0.02em;
}

/* --- Data tables --- */
.table-responsive { overflow-x: auto; }

.table {
  width: 100%;
  border-collapse: collapse;
}

.table.align-middle td,
.table.align-middle th { vertical-align: middle; }

.data-table { margin: 0; }

.data-table thead th {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  text-align: left;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border-strong);
}

.data-table td,
.data-table tbody th {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  text-align: left;
}

.data-table tbody tr:last-child td,
.data-table tbody tr:last-child th { border-bottom: 0; }

.data-table tbody tr:hover { background: var(--surface-hover); }

.data-table tbody th {
  font-weight: 600;
  color: var(--text-strong);
}

.data-table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.data-table thead th.num { text-align: right; }

.data-table .nowrap { white-space: nowrap; }

/* Strategy actions table sits inside the narrower target panel: keep the short
   badge / type / owner / status columns to their content width so the action
   title takes the remaining space instead of wrapping to several lines. The
   sitting gate is the same shape (a title plus short channel / owner / age /
   status columns) and shares the rule rather than restating it. */
.strategy-actions td:not(:first-child),
.strategy-actions thead th:not(:first-child),
.strategy-sitting td:not(:first-child),
.strategy-sitting thead th:not(:first-child) {
  white-space: nowrap;
  width: 1%;
}
.strategy-actions tbody th,
.strategy-sitting tbody th { min-width: 220px; }

/* Measurement panel: two rows per shipped action (the action's own figure, then
   the control set's), so the action column needs room for its title plus the
   ship line under it. The figure columns stay on one line; the control note in
   place of a comparison wraps normally rather than pushing the table sideways. */
.strategy-measurement tbody th { min-width: 260px; }
.strategy-measurement .num { white-space: nowrap; }
/* The date under a figure, and the ship line under an action title: same voice
   as .stat-sub, on its own line so the number stays the thing you read first. */
.measure-sub {
  display: block;
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.03em;
  color: var(--muted);
}

/* Projects (the /overview/ page): the same table shape the strategy and task tables
   use, so the page reads as part of the app rather than as a new one. A title column
   with room for its kind line and its baton rail, and short columns held to their
   content so the title takes the remaining width instead of wrapping to three
   lines. */
.overview-upcoming tbody th,
.overview-stuck tbody th { min-width: 240px; }
.overview-measured tbody th { min-width: 260px; }
.overview-measured .num { white-space: nowrap; }
.overview-upcoming td:first-child,
.overview-upcoming thead th:first-child,
.overview-stuck td:first-child,
.overview-stuck thead th:first-child,
.overview-carrying td:not(:last-child),
.overview-carrying thead th:not(:last-child) {
  white-space: nowrap;
  width: 1%;
}

/* The roll-call of every channel. The middle column is held to its content and the
   outer two share what is left, because holding BOTH state columns tight parks
   them against the right edge and leaves half a screen of white between a channel
   and its own state -- twenty-seven rows of that is a table nobody tracks across.
   Nothing wraps: one channel per line is the whole shape of the section. */
.overview-roll-call td:first-of-type,
.overview-roll-call thead th:nth-child(2) {
  white-space: nowrap;
  width: 1%;
}

.overview-roll-call td { white-space: nowrap; }

/* A channel nobody has been set up on. Full strength against the muted count
   beside it -- the usual muting inverted, because this is the line somebody has to
   act on and the settled ones are the ones that can be skimmed. Deliberately NOT
   the danger red: twenty-five red rows would tell a reader the app is broken
   rather than that the configuration is unfinished. */
.roll-call-gap {
  color: var(--text-strong);
  font-weight: 600;
}

/* The one sentence at the top of Projects. Given the weight of a lead paragraph
   rather than of a note, because for a reader with ten seconds it is the whole
   page: how much work is moving, on how many channels, with how many people on it,
   and whether anything is waiting for an owner. */
.overview-headline {
  font-size: 17px;
  line-height: 1.55;
  color: var(--text-strong);
  max-width: 74ch;
  margin: 0 0 28px;
}

/* What a channel earned last complete month, between its eyebrow and its work panel.
   Only a viewer who may see per-channel money gets this line at all; there is no
   empty-state rule for it because there is no empty state -- for everybody else the
   element is not rendered. Prose weight rather than a tile: it is a sentence a
   non-technical reader reads once, not a figure to be scanned against other figures,
   and giving it a box would make the block look like the board. Sits above the panel
   so the money is read before the work rather than found under it. */
.overview-revenue {
  font-size: 14px;
  line-height: 1.55;
  color: var(--muted-mid);
  max-width: 74ch;
  margin: 0 0 14px;
}

/* The earnings figure itself, the one number in the line worth stopping on. Weight
   and ink only -- no colour of its own, because the coloured span in the same
   sentence is the DIRECTION, and two colours in one line would set them competing. */
.overview-revenue-figure {
  font-weight: 600;
  color: var(--text-strong);
}

/* Who is on a channel, above that channel's table: the kind of work, the person,
   and the steps they hold on it, one person per line. Three columns rather than a
   sentence -- three people with two steps each does not read as prose -- with the
   kind printed once per group so the eye runs down the names under it. */
.overview-cast { margin: 0 0 18px; }

.overview-cast-head {
  display: block;
  margin: 0 0 8px;
}

.overview-cast-line {
  display: flex;
  gap: 14px;
  margin: 0 0 4px;
  font-size: 13px;
}

.overview-cast-line:last-child { margin-bottom: 0; }

.overview-cast-kind {
  flex: 0 0 auto;
  width: 88px;
  color: var(--muted);
}

.overview-cast-name {
  flex: 0 0 auto;
  min-width: 96px;
  font-weight: 600;
  color: var(--text-strong);
}

.overview-cast-does { color: var(--muted-mid); }

/* One piece of somebody's work inside the "what they are on" cell. Stacked lines
   rather than a comma list: four titles run together are unreadable, and the kind
   and date belong under each one. */
.overview-item { margin: 0 0 6px; }
.overview-item:last-child { margin-bottom: 0; }

/* The channel name over each measurement table. A heading, not a panel of its own,
   so a channel with two shipped actions does not get the same weight as a section. */
.overview-channel {
  margin: 18px 0 8px;
  font-size: 14px;
}
.overview-channel:first-of-type { margin-top: 0; }

.muted { color: var(--muted-mid); }
.trend-up { color: var(--up); }
.trend-down { color: var(--danger); }

.cat-ref {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--primary);
}

.server-link { text-decoration: none; color: var(--primary); }

/* Screen-reader-only: visually hidden but announced (e.g. an icon column's
   header). Standard clip pattern. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0);
  white-space: nowrap; border: 0;
}

/* Release-type icon (its own column): audio (music-only) vs video, so the row
   type is scannable. Colour distinguishes them on top of the shape. */
.rel-type { display: inline-flex; vertical-align: middle; width: 22px; height: 22px; flex: none; }
.rel-type svg { width: 100%; height: 100%; }
.rel-type--audio { color: var(--muted-mid); }
.rel-type--video { color: var(--primary); }

/* Album cover (the catalogue's own artwork, served from img.cugate.com). A
   fixed square box in both places it appears, so a hundred sleeves of slightly
   different proportions still read as one column and not a ragged edge.
   object-fit: contain rather than cover: these are somebody's record sleeves and
   cropping one to fit our grid is us editing the artwork. Square art fills the
   box exactly; anything else sits inside it on the row's own surface.
   64px rather than a thumbnail: at 40 the artwork read as an icon beside a text
   row, which is a spreadsheet with a favicon column, not a catalogue. 64 is one
   of the service's stored sizes. It does make the row taller -- roughly 84px
   against the 60px a text row sat at -- and that is the trade being taken
   deliberately: fewer releases on a screen, but the page reads as a catalogue. */
.rel-cover {
  width: 64px;
  height: 64px;
  border-radius: 4px;
  object-fit: contain;
  background: var(--surface-hover);
  vertical-align: middle;
}
.rel-cover--detail { width: 132px; height: 132px; border-radius: 8px; }

/* Release detail head: artwork beside the release's identity block, with the
   Edit control still pushed to the far end by .ch-head's space-between. */
.rel-head-main { display: flex; align-items: flex-end; gap: 18px; }

/* --- Finding panel (Rights) --- */
.finding {
  background: var(--primary-dim);
  border-color: var(--border-strong);
}
.finding-body { font-size: 14px; line-height: 1.5; }
.finding-body ol { margin: 8px 0; padding-left: 20px; }
.finding-body li { margin-bottom: 8px; }
.finding-foot {
  margin: 8px 0 0;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  color: var(--muted-mid);
  font-size: 13px;
}

/* --- Deliverables --- */
.deliverable-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 12px;
}

.deliverable-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 18px 18px;
  display: flex;
  flex-direction: column;
}

.deliverable-card.is-missing { opacity: 0.55; }

.deliverable-kind {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--primary);
}

.deliverable-title {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 700;
  font-size: 18px;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--text-strong);
  margin: 6px 0 6px;
  line-height: 1.15;
}

.deliverable-date { color: var(--muted); font-size: 12px; }

.deliverable-open {
  margin-top: auto;
  color: var(--primary);
  text-decoration: none;
  font-weight: 500;
  font-size: 13px;
  padding-top: 12px;
}

.deliverable-open:hover { text-decoration: underline; }

.deliverable-missing {
  margin-top: auto;
  color: var(--danger);
  font-size: 12px;
  padding-top: 12px;
}

/* --- Action bar + row edit --- */
/* align-items: stretch (default) so the filter toggle matches the button
   height for an even row; the toggle centres its own text within that height. */
.action-bar { margin-bottom: 16px; display: flex; justify-content: flex-end; gap: 8px; }
/* Releases: the type + date-window toggles sit on the left, the New/Import
   buttons on the right. The auto margin goes on the window toggle (the last
   left-aligned control) so both toggles group together against the left edge. */
.action-bar .releases-window { margin-right: auto; }

/* "Open ↗" link under a URL field on the release form (opens the saved value). */
.field-open-link { display: inline-block; margin-top: 6px; font-size: 12px; color: var(--primary); text-decoration: none; }
.field-open-link:hover { text-decoration: underline; }

/* Settings: pick which curated apps to show, each row a toggle switch. */
.app-picker { list-style: none; margin: 16px 0 4px; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.app-pick { display: flex; align-items: center; gap: 10px; }
.app-pick-label { cursor: pointer; }

/* Per-user dashboard quick links (a titled row of tiles above the board;
   the title reuses the section-eyebrow label + line). */
/* 32px bottom, matching .stat-strip, so every section closes with the same
   gap before the next eyebrow (Jan, 3 Jul: consistent rhythm). */
.dash-links { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 32px; }
.dash-link { display: inline-flex; align-items: center; gap: 6px; padding: 7px 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: var(--text); font-size: 13px; text-decoration: none; }
.dash-link:hover { background: var(--surface-hover); border-color: var(--border-strong); }
.dash-link-arrow { color: var(--muted); font-size: 11px; }
/* A horizontal group of buttons/forms (e.g. Approve + Send back in one table
   cell). Each <form> is block by default, so without this they stack. */
.row-actions { display: flex; gap: 6px; align-items: center; }

/* Desktop only: a fixed column grid so sibling tables line up (the per-month
   release tables) instead of each auto-sizing to its own content. Scoped above
   the 600px breakpoint because at or below it the .data-table collapses into
   the mobile card layout, which must keep its own widths. The wide text column
   absorbs the remaining width. (The task stage-tables that shared this
   treatment became the channel board -- see CHANNEL BOARD further down.) */
@media (min-width: 601px) {
  .releases-table { table-layout: fixed; width: 100%; }
  .releases-table tr > *:nth-child(1) { width: 88px; }    /* Cover (64px art + cell padding) */
  .releases-table tr > *:nth-child(2) { width: 40px; }    /* Format icon */
  .releases-table tr > *:nth-child(3) { width: 150px; }   /* Artist */
  .releases-table tr > *:nth-child(4) { width: 96px; }    /* Date (fits the inline date input) */
  .releases-table tr > *:nth-child(5) { width: 76px; }    /* Type */
  .releases-table tr > *:nth-child(7) { width: 118px; }   /* Status */
  .releases-table tr > *:nth-child(8) { width: 104px; }   /* Priority */
  .releases-table tr > *:nth-child(9) { width: 96px; }    /* Due (fits the inline date input) */
  .releases-table tr > *:nth-child(10) { width: 88px; }   /* Actions (quick-edit toggle + Edit) */
  .releases-table .rel-cover-cell { text-align: center; line-height: 0; }
  .releases-table .rel-type-cell { text-align: center; }
  /* Catalogue/Title (6) absorbs the remaining width; 5 is Type, sized above. */
  /* While any row is in quick-edit the fixed columns need room for their
     inputs (the selects and date pickers clip at read-mode widths); the
     Catalogue/Title remainder gives the slack back. The JS toggles the class
     (releases-inline.js setEditing). */
  .releases-table.has-editing-row tr > *:nth-child(3) { width: 170px; }  /* Artist */
  .releases-table.has-editing-row tr > *:nth-child(4) { width: 112px; }  /* Date */
  .releases-table.has-editing-row tr > *:nth-child(5) { width: 96px; }   /* Type */
  .releases-table.has-editing-row tr > *:nth-child(7) { width: 136px; }  /* Status */
  .releases-table.has-editing-row tr > *:nth-child(8) { width: 122px; }  /* Priority */
  .releases-table.has-editing-row tr > *:nth-child(9) { width: 112px; }  /* Due */
  /* The quick-edit detail row is a single colspan cell -- let it span the full
     width instead of inheriting the 88px first-column rule above. */
  .releases-table tr.inline-expander > td { width: auto; }
}

/* Segmented filter toggle (e.g. All tasks / My tasks). A VIEW filter, not an
   action -- so it reads as a tab group, not a button. The selected option uses
   the same primary-dim "you are here" tint as the sidebar nav, never a solid
   button fill. */
.seg-toggle {
  display: inline-flex;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  overflow: hidden;
}
.seg-option {
  display: inline-flex;
  align-items: center;
  padding: 5px 12px;
  font-family: 'DM Sans', sans-serif;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  background: transparent;
}
/* Divider between segments via inset shadow, not border-left (design hard-rule). */
.seg-option + .seg-option { box-shadow: inset 1px 0 0 var(--border-strong); }
.seg-option:hover { color: var(--text); background: var(--surface-hover); }
.seg-option.is-active { background: var(--primary-dim); color: var(--primary); }

/* --- List filters (Tasks: whose work + search + person + status;
   Releases: search + status) --- */
/* The All tasks / My tasks toggle is a filter, so it sits with them on the left
   rather than beside the New task button. Same auto-margin trick the releases
   action bar uses to split view filters from the controls that write. */
.action-bar .tasks-scope { margin-right: auto; }

/* One GET form under the action bar. The controls keep their labels rather than
   relying on placeholders, because the row has two or three of them and "which
   box is which" is not something to work out from grey text inside the field.
   Each is capped so the cluster stays compact on a wide screen instead of
   stretching across the page; below that they wrap a control at a time. The
   two pages share the treatment because they are the same control doing the
   same job, and one of them drifting a few pixels is how a hub stops reading
   as one product. */
.task-filters,
.release-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px;
  margin-bottom: 16px;
}
.task-filter,
.release-filter { flex: 1 1 180px; max-width: 260px; margin: 0; }
.task-filter .form-label,
.release-filter .form-label { margin-bottom: 4px; }
/* Filter + Clear sit on the baseline of the fields, not stretched to their
   height, so the row reads as controls-then-action. */
.task-filter-actions,
.release-filter-actions { display: flex; gap: 8px; }

/* --- Pagination (Releases list) --- */
/* A centred bar of Prev / elided page numbers / Next with the total count
   trailing. Reuses the seg-toggle tokens (border-strong outline, the primary-dim
   "you are here" tint for the current page) so it reads as the same control
   family. Each number is its own outlined chip -- no border-left dividers
   (design hard-rule). */
.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: 24px;
}
.page-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  height: 32px;
  padding: 0 10px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  font-family: 'DM Sans', sans-serif;
  font-size: 12px;
  font-weight: 500;
  color: var(--muted-mid);
  text-decoration: none;
  background: transparent;
}
.page-link:hover { color: var(--text); background: var(--surface-hover); }
.page-link.is-active {
  background: var(--primary-dim);
  color: var(--primary);
  border-color: var(--primary);
}
/* Disabled Prev/Next at the ends: dimmed and unclickable, but still in the row
   so the controls do not shift as you page. */
.page-link.is-disabled { opacity: 0.4; pointer-events: none; }
.page-ellipsis { padding: 0 4px; color: var(--muted); font-size: 12px; }
.page-count { margin-left: 8px; font-size: 12px; color: var(--muted); white-space: nowrap; }

@media (max-width: 600px) {
  /* Bigger tap targets, and the count drops onto its own centred line below the
     controls (which stay centred and wrap as needed). */
  .page-link { min-width: 40px; height: 40px; }
  .page-count { flex-basis: 100%; margin-left: 0; margin-top: 6px; text-align: center; }
}

.row-edit { color: var(--primary); text-decoration: none; font-size: 12px; }
.row-edit:hover { text-decoration: underline; }

/* --- Inline quick-edit (Releases list; static/js/releases-inline.js) --- */
/* Kelsey edits a row without opening its detail page. The pencil toggle swaps the
   row's read cells for inputs and opens the detail row below for the off-row
   fields; one Save button posts every changed field at once. Read and edit views
   both render server-side and the row's .is-editing class flips which one shows. */
/* The toggle and the Edit link sit inline in the actions cell -- NOT via
   display:flex on the td, which would take the cell out of the fixed table
   layout on desktop. */
.inline-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  margin-right: 6px;
  vertical-align: middle;
  border: 1px solid var(--border-strong);
  border-radius: 7px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.inline-toggle svg { width: 14px; height: 14px; }
.inline-toggle:hover { color: var(--text); background: var(--surface-hover); }
tr.is-editing .inline-toggle {
  color: var(--primary);
  border-color: var(--primary);
  background: var(--primary-dim);
}

/* The inputs are hidden until the row enters edit mode, then they replace the
   read cells in place. */
.inline-edit { display: none; }
tr.is-editing .inline-read { display: none; }
tr.is-editing .inline-edit { display: block; }
tr.is-editing .inline-edit--stack { display: flex; flex-direction: column; gap: 4px; }

/* Compact controls so they fit the fixed calendar columns without reflowing the
   table (the Date/Due columns are widened a little for this -- see the desktop
   grid below). */
.inline-edit .form-control,
.inline-edit .form-select,
.inline-field .form-control,
.inline-field .form-select {
  font-size: 12px;
  padding: 5px 7px;
}
.inline-edit .field-error { white-space: normal; }
.inline-edit .form-control.is-invalid,
.inline-edit .form-select.is-invalid,
.inline-field .form-control.is-invalid,
.inline-field .form-select.is-invalid { border-color: var(--danger); }

/* The full-width detail row for the off-row fields (notes, distributor, video,
   links). Hidden until its row is editing. */
.inline-expander { display: none; }
tr.inline-expander.is-editing { display: table-row; }
.inline-expander > td { background: var(--bg-deeper); padding: 14px 16px; }
/* The off-row fields stack in the 1/3 LEFT column; notes takes the wide 2/3
   RIGHT with the Save button under it (Jan, 3 Jul review). */
.inline-expander-grid {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: 16px;
  align-items: stretch;
}
.inline-field { display: flex; flex-direction: column; gap: 4px; }
.inline-expander-main { display: flex; flex-direction: column; gap: 12px; }
/* Let the notes box fill its column height so the Save row sits at the foot,
   level with the left stack, and stay vertically resizable for a long note. */
.inline-field--notes { flex: 1; }
.inline-field--notes .form-control {
  flex: 1;
  min-height: 96px;
  resize: vertical;
}
.inline-expander-side { display: flex; flex-direction: column; gap: 12px; }
/* Save is primary-filled to read as the commit action; its non-field error
   sits to its left. */
.inline-save-row {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}
.inline-save-row .field-error { margin-top: 0; }
.inline-field-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
/* Narrow desktop windows: collapse the two columns into one (below the 600px
   accordion breakpoint the whole expander is hidden, so this only bites between
   the two). Notes drops to its own height and Save unpins. */
@media (max-width: 860px) {
  .inline-expander-grid { grid-template-columns: 1fr; }
  .inline-save-row { margin-top: 0; }
}

/* --- Buttons --- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: 'DM Sans', sans-serif;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 8px 14px;
  border-radius: 8px;
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
}

/* Solid fill so a primary action reads unmistakably as a button, never as a
   status chip (which are dim accent-tinted pills). */
.btn-dark {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--bg-deeper);
}

.btn-dark:hover {
  filter: brightness(1.08);
}

.btn-outline-dark {
  background: transparent;
  border-color: var(--border-strong);
  color: var(--muted-mid);
}

.btn-outline-dark:hover {
  border-color: var(--primary);
  color: var(--primary);
}

.btn-sm { padding: 5px 10px; font-size: 11px; }

.btn-link {
  background: none;
  border: 0;
  color: var(--muted-mid);
  text-transform: none;
  letter-spacing: 0.02em;
}

.btn-link:hover { color: var(--primary); }

/* --- Forms --- */
.form-panel { max-width: 640px; }
.login-panel { max-width: 420px; }

/* Import page: the upload form sits left with the example-download panel to its
   right; both are .panel cards, so only the row and the aside's width need
   declaring. They stack on narrow screens (flex-wrap). */
.import-layout {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 24px;
}
.import-layout .form-panel { flex: 1 1 380px; margin-bottom: 24px; }
.import-aside { flex: 1 1 240px; max-width: 320px; }
.import-aside-note { font-size: 13px; color: var(--muted); margin: 0 0 16px; }

.mb-3 { margin-bottom: 16px; }

.form-label {
  display: block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 6px;
}

.form-control,
.form-select {
  /* Tell the UA these controls are dark so native chrome (notably the
     <input type="date"> calendar-picker indicator) renders light-on-dark
     instead of the default black glyph that vanished against the dark field. */
  color-scheme: dark;
  display: block;
  width: 100%;
  font-family: 'DM Sans', sans-serif;
  font-size: 14px;
  color: var(--text);
  background: var(--bg-deeper);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  padding: 9px 12px;
}

/* Textareas (task notes, release notes) grow taller but never wider, so a long
   note can't blow out the form column. */
textarea.form-control { resize: vertical; }

html[data-theme="light"] .form-control,
html[data-theme="light"] .form-select {
  color-scheme: light;
  background: var(--surface);
}

.form-control:focus,
.form-select:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-dim);
}

.form-select option { background: var(--bg-deeper); }

html[data-theme="light"] .form-select option { background: var(--surface); }

.form-text { font-size: 12px; color: var(--muted); margin-top: 4px; }

.form-actions { display: flex; gap: 8px; align-items: center; margin-top: 8px; }

.field-error { color: var(--danger); font-size: 12px; margin-top: 4px; }

/* Duplicate heads-up on the release form (static/js/release-form.js): a
   non-blocking note that the typed catalogue ref / EAN already sits on another
   release. The base slot reuses .form-text (muted) for the EAN-format hint;
   .is-duplicate lifts it to the same spotlight amber as the "Pending" status. */
.field-warning.is-duplicate { color: var(--spotlight); font-weight: 600; }

/* --- File upload drop zone (static/js/upload.js) --- */
/* The real <input type="file"> is visually hidden but kept in the DOM and
   focusable (clip technique, not display:none), so the form submits through it
   and works with JS off. The labelled .upload-zone is the click target and the
   drop target; .is-dragover lifts it to --primary while a file is hovering. */
.upload-zone {
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg-deeper);
  transition: border-color var(--transition), background-color var(--transition);
}

html[data-theme="light"] .upload-zone { background: var(--surface); }

.upload-zone-input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.upload-zone-target {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 18px 16px;
  cursor: pointer;
}

.upload-zone-hint { font-size: 13px; color: var(--muted); }

.upload-zone-filename {
  font-size: 13px;
  color: var(--muted-mid);
  margin-left: auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

.upload-zone-filename.has-file { color: var(--text); font-weight: 600; }

/* Keyboard focus on the hidden input rings the visible trigger. */
.upload-zone-input:focus-visible + .upload-zone-hint ~ .upload-zone-trigger,
.upload-zone-input:focus + .upload-zone-hint ~ .upload-zone-trigger {
  box-shadow: 0 0 0 3px var(--primary-dim);
}

.upload-zone.is-dragover {
  border-color: var(--primary);
  border-style: solid;
  background: var(--primary-dim);
}

/* --- Choices.js select skin (vendor: static/vendor/choices/) --- */
/* Token-only re-skin of the vendored widget, driven through its --choices-*
   hooks plus a few cascade overrides (chart-room.css loads after the vendor
   sheet). The closed field mirrors .form-select exactly, including its 8px
   radius alongside the other inputs; the floating dropdown uses var(--radius)
   and paints --surface over a --bg-deeper base so the translucent dark-theme
   surface never lets page content bleed through. No spotlight gold here:
   selects are interactive chrome, so they stay on --primary roles. */
.choices {
  --choices-guttering: 0;
  --choices-font-size-lg: 14px;
  --choices-font-size-md: 14px;
  --choices-font-size-sm: 12px;
  --choices-bg-color: var(--bg-deeper);
  --choices-bg-color-dropdown: var(--bg-deeper);
  --choices-bg-color-disabled: var(--rank-track);
  --choices-disabled-color: var(--muted);
  --choices-item-disabled-color: var(--muted);
  --choices-keyline-color: var(--border-strong);
  --choices-border-radius: 8px; /* matches .form-control / .form-select */
  --choices-text-color: var(--muted-mid); /* the dropdown arrow */
  --choices-primary-color: var(--primary-dim);
  --choices-item-color: var(--text);
  --choices-highlighted-color: var(--surface-hover);
  --choices-input-height: 38px;
  --choices-inner-padding: 5px 12px;
  --choices-inner-one-padding: 5px;
  --choices-list-single-padding: 4px 16px 4px 0;
  --choices-dropdown-item-padding: 9px 12px;
  --choices-z-index: 20;
  color: var(--text);
}

html[data-theme="light"] .choices {
  --choices-bg-color: var(--surface);
  --choices-bg-color-dropdown: var(--surface);
}

.choices.is-focused .choices__inner,
.choices.is-open .choices__inner {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-dim);
}

.choices__list--dropdown,
.choices__list[aria-expanded] {
  /* --surface composed over --bg-deeper: opaque in both themes. */
  background: linear-gradient(var(--surface), var(--surface)) var(--bg-deeper);
  border-color: var(--border-strong);
  border-bottom-left-radius: var(--radius);
  border-bottom-right-radius: var(--radius);
  box-shadow: 0 12px 28px var(--shadow);
}

.choices[data-type*=select-one] .choices__input {
  background-color: transparent; /* sit on the dropdown's own paint */
  border-bottom: 1px solid var(--border);
  color: var(--text);
}

.choices__list--dropdown .choices__item--selectable.is-selected,
.choices__list[aria-expanded] .choices__item--selectable.is-selected {
  background-color: var(--primary-dim);
  color: var(--text-strong);
}

.choices__list--dropdown .choices__item--selectable.is-highlighted,
.choices__list[aria-expanded] .choices__item--selectable.is-highlighted {
  background-color: var(--surface-hover);
  color: var(--text-strong);
}

/* Dropdown grows to fit the longest option (up to a cap) so names show in full;
   each option stays on a single line and ellipsis-truncates beyond the cap, so it
   never wraps regardless of channel-name length. */
.choices__list--dropdown,
.choices__list[aria-expanded] {
  width: max-content;
  min-width: 100%;
  max-width: 340px;
}

.choices__list--dropdown .choices__item,
.choices__list[aria-expanded] .choices__item {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- Inbox --- */
.btn-link-plain { background: none; border: 0; padding: 0; cursor: pointer; }

.inbox-list { list-style: none; margin: 0; padding: 0; }
.inbox-list li { border-bottom: 1px solid var(--border); }
.inbox-list li:last-child { border-bottom: 0; }

.inbox-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  background: none;
  border: 0;
  text-align: left;
  padding: 10px 0;
  cursor: pointer;
  font-family: 'DM Sans', sans-serif;
  font-size: 13px;
  color: var(--text);
}

.inbox-item:hover { color: var(--text-strong); }

.inbox-list li.unread .inbox-item { font-weight: 600; color: var(--text-strong); }

.inbox-item .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--primary);
  flex: none;
}

.inbox-msg { flex: 1; }

.inbox-time {
  color: var(--muted);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

/* --- Placeholder note (fixture-data tag) --- */
.placeholder-note {
  font-size: 12px;
  color: var(--muted-mid);
  letter-spacing: 0.04em;
  font-style: italic;
  /* Top margin only when something precedes it. The box is inline-block, so its
     margins never collapse with the paragraph above and it otherwise sits hard
     against that line. Where the note opens its container it keeps its old
     flush-top position. */
  margin-bottom: 24px;
  padding: 8px 12px;
  background: var(--rank-track);
  border: 1px solid var(--border);
  border-radius: 6px;
  display: inline-block;
}

.placeholder-note:not(:first-child) {
  margin-top: 14px;
}

.placeholder-note code {
  font-style: normal;
  color: var(--primary);
  font-size: 11px;
}

/* --- Footer --- */
.app-footer {
  grid-column: 2;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: 11px;
  letter-spacing: 0.04em;
  padding: 16px 32px;
}

/* ===== RESPONSIVE ===== */
@media (max-width: 900px) {
  .app { grid-template-columns: 52px 1fr; }
  .main { padding: 20px; }
  .chart-row { grid-template-columns: 56px 1fr 110px; }
  .chart-col-headers { grid-template-columns: 56px 1fr 110px; }
  .channel-movement,
  .channel-views,
  .col-header:nth-child(4),
  .col-header:nth-child(5) { display: none; }
  /* Revenue-hidden variant: revenue is already out of the DOM. Keep movement
     visible in its place (headers shift to rank/channel/movement/views) and
     only drop the views column at this width. */
  .chart-row.no-revenue { grid-template-columns: 56px 1fr 110px; }
  .chart-col-headers.no-revenue { grid-template-columns: 56px 1fr 110px; }
  .chart-row.no-revenue .channel-movement { display: flex; }
  .chart-col-headers.no-revenue .col-header:nth-child(3) { display: block; }
  .chart-row.no-revenue .channel-views,
  .chart-col-headers.no-revenue .col-header:nth-child(4) { display: none; }
  .stat-strip { grid-template-columns: repeat(2, 1fr); }
  .release-row { grid-template-columns: 72px 1fr 120px; }
  .release-channel { display: none; }
}

@media (max-width: 600px) {
  .app { grid-template-columns: 1fr; }
  .header { grid-column: 1; }
  .main { grid-column: 1; }
  .app-footer { grid-column: 1; }

  /* Mobile nav: hamburger toggles the sidebar as a left drawer (a labelled
     list, no squashing). Header declutters to toggle + brand + theme. */
  .nav-toggle { display: inline-flex; }
  .header-divider,
  .header-label,
  .header-revenue { display: none; }

  .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    width: 250px;
    max-width: 84vw;
    height: 100vh;
    align-items: stretch;
    padding: 16px 12px;
    gap: 4px;
    transform: translateX(-100%);
    transition: transform var(--transition);
    z-index: 120;
  }
  .app.nav-open .sidebar { transform: translateX(0); }
  .nav-item {
    width: 100%;
    height: auto;
    justify-content: flex-start;
    gap: 12px;
    padding: 11px 12px;
  }
  .nav-item svg { flex: 0 0 auto; }
  /* The desktop hover-tooltip becomes the inline label inside the drawer. */
  .nav-tooltip {
    position: static;
    opacity: 1;
    pointer-events: auto;
    background: none;
    border: 0;
    padding: 0;
    color: inherit;
    text-transform: none;
    letter-spacing: 0.02em;
    font-size: 13px;
  }
  .nav-overlay {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--transition);
    z-index: 110;
  }
  .app.nav-open .nav-overlay { opacity: 1; pointer-events: auto; }

  .chart-title { font-size: 34px; }

  /* On mobile each table row is a collapsed one-line card (the row-header,
     tappable, with a chevron); tapping expands the rest as "label: value"
     lines. No sideways scrolling. Toggle class added by table-cards.js. */
  .data-table thead { display: none; }
  .data-table tr { display: block; border: 1px solid var(--border); border-radius: 10px; margin-bottom: 10px; }
  .data-table th[scope="row"] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 14px;
    font-size: 15px;
    border: 0;
    cursor: pointer;
  }
  /* Disclosure chevron (border-drawn, not a glyph): points down, flips up open. */
  .data-table th[scope="row"]::after {
    content: "";
    width: 7px;
    height: 7px;
    border-right: 2px solid var(--muted);
    border-bottom: 2px solid var(--muted);
    transform: rotate(45deg);
    transition: transform var(--transition);
    flex: 0 0 auto;
  }
  .data-table tr.is-expanded th[scope="row"]::after { transform: rotate(225deg); }
  .data-table td {
    display: none;
    justify-content: space-between;
    gap: 16px;
    padding: 6px 14px;
    border: 0;
    text-align: right;
    white-space: normal;
  }
  .data-table tr.is-expanded td { display: flex; }
  .data-table tr.is-expanded td:last-child { padding-bottom: 12px; }
  .data-table td::before { content: attr(data-label); color: var(--muted); font-weight: 500; text-align: left; }

  /* Inline quick-edit is disabled below the accordion breakpoint: here the row
     header IS the tap-to-expand target, so an in-cell input would fight the
     collapse and the interaction is unusable. Hide the toggle and the detail
     row; the per-row Open link (release detail, then Edit on it) is the mobile
     edit path. The read cells still show in the expanded card as normal. */
  .releases-table .inline-toggle { display: none; }
  .releases-table tr.inline-expander { display: none; }

  /* The artwork opens the card, centred and bigger, rather than arriving as a
     "Cover: [thumbnail]" line among the label/value rows -- it is the one cell
     with nothing to label. A release with no artwork drops the cell entirely
     (is-empty, set in the template) so the card cannot open on a blank line. */
  .releases-table .rel-cover-cell::before { content: none; }
  .releases-table tr.is-expanded .rel-cover-cell { justify-content: center; padding-top: 12px; }
  .releases-table tr.is-expanded .rel-cover-cell.is-empty { display: none; }
  .releases-table .rel-cover-cell .rel-cover { width: 96px; height: 96px; }

  .stat-strip { grid-template-columns: repeat(2, 1fr); gap: 8px; }
  .chart-row { padding: 14px 12px; }
  .release-row { grid-template-columns: 72px 1fr 110px; padding: 12px; }
  .header { padding: 0 16px; }
  /* Attention queue: let the row wrap (date chip + title stay together,
     the need tag flows onto the next line when it does not fit). This still
     earns its place for the summary and notice rows, which are not
     .attention-item-detail and so do not wrap at any other width. The title's
     white-space is no longer set here: it wraps everywhere now that the queue
     lives in the narrow column, and repeating it made this look like the rule
     that governed it.
     row-gap here ALSO restyles the detail rows, which is worth knowing rather
     than discovering: it and .attention-item-detail's 6px are both one class, so
     this one wins on source order and a detail row gaps at 3px below 600px.
     Deliberate -- the tighter gap suits a phone -- but it is not "summary and
     notice rows only". */
  .attention-item { flex-wrap: wrap; row-gap: 3px; }
  .attention-due { flex-basis: auto; }
  .attention-item-title { flex: 1 1 60%; }
}

/* ===== DESCRIPTION MANAGER REVIEW ===== */
/* Current vs proposed, side by side, with an editable proposed textarea. The
   monospace blocks keep the timestamps and PLAYLIST columns aligned the way
   they read on YouTube, so the reviewer judges the real shape. */
.review-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  margin-bottom: 16px;
}

.description-current,
.description-proposed {
  width: 100%;
  font-family: ui-monospace, 'SF Mono', 'DM Sans', monospace;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
  background: var(--bg-deeper);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  padding: 12px;
  white-space: pre-wrap;
  word-break: break-word;
}

html[data-theme="light"] .description-current,
html[data-theme="light"] .description-proposed {
  background: var(--surface);
}

.description-current {
  margin: 0;
  color: var(--muted);
  max-height: 520px;
  overflow: auto;
}

.description-proposed {
  display: block;
  resize: vertical;
  min-height: 360px;
}

.description-proposed:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-dim);
}

@media (max-width: 720px) {
  .review-grid { grid-template-columns: 1fr; }
}

/* ===== MESSAGES (django.contrib.messages) ===== */
.message-list { margin-bottom: 16px; display: flex; flex-direction: column; gap: 8px; }

.alert {
  font-size: 14px;
  padding: 10px 14px;
  border-radius: 8px;
  border: 1px solid var(--border-strong);
  background: var(--primary-dim);
  color: var(--text);
}

.alert-error,
.alert-danger {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 14%, transparent);
}

/* ===== MANUAL PULL CONTROL (descriptions list) ===== */
/* A compact single-row toolbar: [channel] [how many] [custom] [Pull].
   The inputs size to their content (not width:100%); flex-wrap drops them onto
   the next line on narrow viewports rather than overflowing. */
.pull-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px;
}

/* Each control is a labelled column; the columns sit inline so the row holds
   one line while every field keeps its label. */
.pull-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-start;
}

/* The preset select and its custom-number sibling sit side by side under the
   one "How many" label. */
.pull-amount-controls {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* The controls sit inline rather than stacking full-width like a normal form. */
.pull-controls .form-label { margin-bottom: 0; }

/* Size to content: the channel name is wide, the amount count is narrow. */
.pull-controls .form-select { width: auto; min-width: 0; }

.pull-controls #pull-channel { min-width: 180px; }

.pull-controls #pull-amount { min-width: 96px; }

.pull-controls .pull-custom { width: 110px; }

.pull-channel-name { font-weight: 600; color: var(--text); }

/* ===== REDUCED MOTION ===== */
@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

/* --- Release production checklist (Christoph/Kelsey, 23 Jun) --- */
/* Release edit: the form and the production checklist sit side by side (only
   on edit, where the checklist exists); stacks on narrower screens. */
.release-layout.is-split { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: start; }
@media (max-width: 900px) { .release-layout.is-split { grid-template-columns: 1fr; } }

/* Production checklist: a titled, equal-column table. Each row stays its own
   form (per-row save, no-JS friendly); JS auto-saves on change and hides the
   per-row Save button. */
.checklist-table { display: flex; flex-direction: column; }
.checklist-body { display: flex; flex-direction: column; }
.checklist-head,
.checklist-row { display: grid; grid-template-columns: auto 1fr 1fr auto auto; gap: 16px; align-items: center; }
/* Drag handle (reorder). touch-action:none lets Sortable own the gesture on
   touch devices instead of the page scrolling. */
.ct-drag { cursor: grab; color: var(--muted); user-select: none; line-height: 1; font-size: 16px; touch-action: none; }
.ct-drag:active { cursor: grabbing; }
.checklist-row.is-dragging { opacity: .4; }
.checklist-head { padding-bottom: 8px; border-bottom: 1px solid var(--border); }
.checklist-row { padding: 10px 0; border-bottom: 1px solid var(--border); }
.checklist-row:last-child { border-bottom: 0; }
.ct-col { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.ct-cell { display: flex; align-items: center; gap: 10px; min-width: 0; }
.ct-cell .choices,
.ct-cell .form-select { width: 100%; margin: 0; }
/* A done step's owner is read-only: show the settled name, hide the dropdown
   (both the no-JS native select and the Choices wrapper). One markup, toggled
   by is-owner-locked from the template on load and release-form.js in place. */
.ct-owner-done { display: none; font-size: 14px; color: var(--muted); }
.is-owner-locked .ct-owner-done { display: inline; }
.is-owner-locked select,
.is-owner-locked .choices { display: none; }
.checklist-step { font-weight: 600; min-width: 0; }
.checklist-step.is-done { color: var(--muted); text-decoration: line-through; }
/* Step name column may carry an optional muted subtitle (step help); stack it
   below the name. .form-text supplies the muted size/colour, display:block puts
   it on its own line and makes its margin-top take effect. */
.checklist-step-cell { min-width: 0; }
.checklist-step-help { display: block; }
/* Flash messages (Django messages framework). Neutral card with a left accent
   per level; shared by any page that renders the messages block. */
.message-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
.alert { padding: 10px 14px; border: 1px solid var(--border); border-left-width: 3px; border-radius: 6px; background: var(--panel, transparent); font-size: 14px; }
.alert-success { border-left-color: var(--positive, #2faf5a); }
.alert-info { border-left-color: var(--primary); }
.alert-error { border-left-color: var(--danger); }
.alert-warning { border-left-color: var(--warning, #d99b1f); }

/* Per-row remove (x): muted, turns red on hover. Submits the row's separate
   delete form via the button's form= attribute. */
.ct-remove { background: none; border: 0; cursor: pointer; padding: 0 4px; font-size: 20px; line-height: 1; color: var(--muted); transition: color .15s ease; }
.ct-remove:hover { color: var(--danger, #d23); }
/* Add a custom step: input + button under the table. */
.checklist-add { display: flex; gap: 8px; margin-top: 14px; }
.checklist-add .form-control { flex: 1; margin: 0; }

/* Generic inline "add a row" form (the control set on the strategy page). Same
   shape as .checklist-add above, but it carries several fields, so it wraps
   rather than crushing them at narrow widths. */
.inline-add { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.inline-add .form-control { flex: 1 1 160px; margin: 0; }

/* Toggle switch (the "done" control). Muted + non-interactive until the step
   has an owner. */
.switch { position: relative; display: inline-flex; align-items: center; cursor: pointer; }
.switch input { position: absolute; width: 0; height: 0; opacity: 0; margin: 0; }
.switch-track { display: inline-block; width: 38px; height: 22px; border-radius: 999px; background: var(--border); transition: background .2s ease; position: relative; }
.switch-thumb { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .35); transition: transform .2s ease; }
.switch input:checked + .switch-track { background: var(--primary); }
.switch input:checked + .switch-track .switch-thumb { transform: translateX(16px); }
.switch.is-muted { cursor: not-allowed; }
.switch.is-muted .switch-track { opacity: .35; }

/* Step-save status. Success is silent (the toggle + the count confirm it);
   only a failure shows, in red. */
.checklist-status { display: none; color: #d9534f; font-size: 12px; font-weight: 600; }
.checklist-status.is-visible { display: inline; }

/* The "N of M complete" note sits on the panel heading line, pushed right. */
.panel-head-note { font-size: 13px; color: var(--muted); white-space: nowrap; }

@media (max-width: 640px) {
  .checklist-head { display: none; }
  .checklist-row { grid-template-columns: 1fr; gap: 8px; }
}

/* Personal checklist (dashboard): the private, owner-only list. Same panel +
   switch idiom as the production checklist, trimmed to toggle | item | remove
   -- no owner and no drag, since it is nobody else's to assign or reorder. */
.mychecklist { display: flex; flex-direction: column; margin-bottom: 4px; }
.mychecklist-row { display: grid; grid-template-columns: auto 1fr auto; gap: 14px; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--border); }
.mychecklist-row:last-child { border-bottom: 0; }

/* ============================================================
   CHANNEL ANALYTICS DETAIL (Wave B). Ported from the locked mockup
   channel-analytics-detail.html -- same tokens and bar language as the
   board, namespaced so it adds no new visual vocabulary. Only the panels
   wired to real data ship; fabricated mockup panels are omitted or reduced
   to the honest placeholder in the template.
   ============================================================ */

/* Page identity head */
.back-link { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; letter-spacing: 0.04em; color: var(--muted); text-decoration: none; margin-bottom: 14px; }
.back-link:hover { color: var(--primary); }
.ch-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin-bottom: 20px; }
.ch-name-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.ch-identity { display: flex; align-items: center; gap: 8px 14px; flex-wrap: wrap; margin-top: 8px; font-size: 13px; color: var(--muted-mid); }
.ch-identity .dot { color: var(--muted); }
.ch-identity strong { color: var(--text-strong); font-weight: 600; }

/* Tier-1 hero strip. Base sets rhythm; the layout modifier picks the columns:
   .four = the revenue vitals row (subs | change | MoM | rank), .two-up = the
   money-free scorecard for viewers who can't see per-channel revenue. */
.hero-strip { display: grid; grid-template-columns: 1.7fr 1fr 1fr; gap: 12px; margin-bottom: 32px; }
.hero-strip.two-up { grid-template-columns: 1fr 1fr; }
.hero-strip.four { grid-template-columns: repeat(4, 1fr); }
.hero-tile { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px 20px; display: flex; flex-direction: column; }
.hero-tile:hover { background: var(--surface-hover); }
.hero-label { font-size: 10px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); margin-bottom: 10px; -webkit-font-smoothing: subpixel-antialiased; -moz-osx-font-smoothing: auto; }
.hero-value { font-family: 'Barlow Condensed', sans-serif; font-weight: 800; font-size: 44px; line-height: 0.95; letter-spacing: -0.02em; color: var(--text-strong); }
.hero-value.accent { color: var(--spotlight); }
.hero-value.danger { color: var(--danger); }
.hero-value.sm { font-size: 32px; }
.hero-delta { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; font-weight: 600; margin-top: 8px; }
.hero-delta.down { color: var(--danger); }
.hero-delta.up { color: var(--up); }
.hero-delta.flat, .hero-delta.neutral { color: var(--muted-mid); }
.hero-delta svg { width: 14px; height: 14px; }
.hero-sub { font-size: 12px; color: var(--muted); margin-top: 6px; letter-spacing: 0.02em; }

/* Panels that carry the audience/revenue two-ups: a touch more padding to signal
   these are the payload, and a taller revenue-snapshot that centres its stat pair
   so its box matches the chart box beside it. */
.panel-audience { padding: 22px 24px 24px; }
.rev-snapshot { display: flex; flex-direction: column; }
.snap-cols { flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-content: center; margin-top: 4px; }
.snap-cols-3 { grid-template-columns: 1fr 1fr 1fr; gap: 18px; }
/* Reserve two label lines across all three tiles so a wrapping label (e.g. "Last
   month - May 2026") does not push its value down out of line with the others. */
.snap-cols-3 .hero-label { min-height: 2.4em; }
.snap-col { display: flex; flex-direction: column; }
/* "estimate" pill on the this-month projection, so a run-rate figure is never
   mistaken for a settled number. */
.est-tag { display: inline-block; margin-left: 4px; padding: 1px 6px; border: 1px solid var(--border-strong);
  border-radius: 999px; font-size: 10px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--muted-mid); vertical-align: middle; }

/* Gender pie: the mockup's conic-gradient ring + donut hole + majority readout.
   The gradient stops come pre-built from gender_pie_stops(); only the ring's box
   and the centre chip live here. */
.pie-wrap { position: relative; display: flex; justify-content: center; align-items: center; padding: 10px 0 4px; }
.pie { width: 168px; height: 168px; border-radius: 50%; box-shadow: inset 0 0 0 1px var(--border); }
.pie-center { position: absolute; width: 100px; height: 100px; border-radius: 50%; background: var(--bg-deeper); display: flex; flex-direction: column; align-items: center; justify-content: center; box-shadow: inset 0 0 0 1px var(--border); }
.pie-center-value { font-family: 'Barlow Condensed', sans-serif; font-weight: 800; font-size: 30px; line-height: 1; letter-spacing: -0.02em; color: var(--text-strong); }
.pie-center-label { font-size: 10px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); margin-top: 4px; -webkit-font-smoothing: subpixel-antialiased; -moz-osx-font-smoothing: auto; }

/* Legend shared by the gender pie; .pie-legend centres it under the ring with
   round swatches and Barlow figures. */
.split-legend { display: flex; gap: 16px; margin-top: 12px; font-size: 11.5px; color: var(--muted-mid); flex-wrap: wrap; }
.split-legend .k { display: inline-flex; align-items: center; gap: 6px; }
.split-legend.pie-legend { gap: 6px 20px; margin-top: 12px; justify-content: center; }
/* On the Audience cards, stack Female / Male one per line under the donut (not
   wrapped onto one line), tightly spaced. */
.aud-card .split-legend.pie-legend { flex-direction: column; align-items: center; gap: 5px; }
.split-legend.pie-legend .k { align-items: baseline; gap: 7px; font-size: 12.5px; }
.split-legend.pie-legend .k strong { font-family: 'Barlow Condensed', sans-serif; font-size: 16px; font-weight: 700; color: var(--text-strong); }
.swatch { width: 9px; height: 9px; border-radius: 2px; flex: none; }
.pie-legend .swatch { border-radius: 50%; align-self: center; }

/* Revenue-trend chart + its month axis (ticks sit under the SVG, spaced apart). */
.chart-svg { width: 100%; height: auto; display: block; }
.trend-ticks { display: flex; justify-content: space-between; margin-top: 8px; padding: 0 2px; }
.trend-tick { font-size: 10px; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); -webkit-font-smoothing: subpixel-antialiased; -moz-osx-font-smoothing: auto; }

/* Horizontal bar list (age mix) -- reuses the app's bar language */
.bar-list { display: flex; flex-direction: column; gap: 11px; margin-top: 2px; }
.bar-row { display: grid; grid-template-columns: 96px 1fr auto; align-items: center; gap: 12px; }
.bar-name { font-size: 13px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar-track { height: 8px; background: var(--rank-track); border-radius: 4px; overflow: hidden; }
.bar-fill { display: block; height: 100%; background: var(--primary); border-radius: 4px; }
.bar-fill.lo { background: var(--muted); }
.bar-fill.dev { background: var(--device-bar); }
.bar-val { font-size: 12px; color: var(--muted-mid); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.bar-val strong { color: var(--text-strong); font-weight: 600; }

/* Two-ups + honest empty state. Revenue and Audience share .audience-grid
   (1.2fr : 1fr -- ~611 : 509 at page width) so chart-left/stat-right lines up
   across both. Devices is an even split (views | watch time). */
.audience-grid { display: grid; grid-template-columns: 1.2fr 1fr; gap: 16px; margin-bottom: 12px; }
.devices-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 12px; }
.audience-empty { font-size: 13px; line-height: 1.5; padding: 4px 0; }

/* Diagnostic footnote */
.diag-foot { font-size: 12px; color: var(--muted); margin-top: 14px; line-height: 1.45; }

/* ---- Portfolio audience grid ----
   The Audience page lays every reportable channel out as a card: a head (name +
   subs + a right-aligned insight tag) over the shared age / gender / devices
   panels. Only the outer grid, the card head and the insight tag are new here --
   the panels, bars and gender donut reuse the audience vocabulary above, so a
   card reads identically to the channel's own detail page. Full borders and
   spacing carry the structure; no left-accent stripe. */
/* align-items:start so each card is its own content height -- without it the grid
   stretches a shorter card to its row-neighbour, leaving dead space that floated the
   "View channel" button far below the content. */
.aud-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 8px; align-items: start; }
.aud-card { display: flex; flex-direction: column; }
.aud-card-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  margin-bottom: 14px; padding-bottom: 8px; border-bottom: 2px solid var(--border-strong);
}
.aud-card-name {
  font-family: 'Barlow Condensed', sans-serif; font-weight: 800; font-size: 22px;
  letter-spacing: -0.01em; text-transform: uppercase; color: var(--text-strong);
}
.aud-card-subs {
  font-family: 'Barlow Condensed', sans-serif; font-weight: 600; font-size: 14px;
  letter-spacing: 0.02em; color: var(--muted); margin-left: 6px; white-space: nowrap;
}
.aud-card-insight {
  font-family: 'Barlow Condensed', sans-serif; font-weight: 700; font-size: 13px;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--primary);
  text-align: right; white-space: nowrap; flex: none;
}
/* Carried-forward card: this channel's pull failed, so its numbers are the last
   good ones rather than this run's. The date sits under the rule in the muted
   label voice, and only such a card carries it -- the head tightens to keep the
   line with the name it belongs to. */
.aud-card-head.is-carried { margin-bottom: 7px; }
/* The date itself reuses .eyebrow-date, which already does this job on the channel
   detail page; only the spacing below it is particular to the card. */
.aud-card-asof { margin-bottom: 13px; }
/* "View channel" button footer: under the age / gender two-up, right-aligned. */
/* Age box is a flex column so the "View channel" button pins to its bottom,
   filling the space the age ladder leaves under a taller gender donut -- no
   separate card footer floating in dead space. */
.aud-age { display: flex; flex-direction: column; }
/* A minimum gap above the button so a full age ladder (all 7 buckets) doesn't jam
   it against the last row; margin-top:auto still pins it to the bottom, expanding
   the gap when the age box is shorter than the gender donut beside it. */
.aud-age .bar-list, .aud-age .audience-empty { margin-bottom: 16px; }
.aud-view-btn { margin-top: auto; align-self: flex-start; }
/* A reported-but-zero age bucket: dim the label + value so the empty rungs of the
   age ladder read as "no viewers here", not as missing data. */
.bar-row.is-zero .bar-name, .bar-row.is-zero .bar-val { color: var(--muted); }

.section-gap { margin-top: 40px; }

/* ---- Deep-metric empty-state panels ----
   The four deep-metric panels (monetization, retention, traffic & geography,
   per-video revenue) hold their final layout while their pulls are unbuilt, so
   they render as honest empty states. These are the few genuinely new shapes the
   mockup adds beyond the board's vocabulary: an equal-weight three-tile row, a
   chart+rail "trend-composed" band, an empty chart placeholder, the Ads-vs-
   Premium mix track, and the retention-bleed callout. Full borders + background
   tint only -- never a left-accent stripe. */

/* Equal three-up tile row (RPM | CPM | Monetization rate). */
.hero-strip.even { grid-template-columns: repeat(3, 1fr); }

/* Chart + companion rail: the chart column stays contained (never full-bleed)
   and the rail carries the headline stat, so the panel reads as one composed
   unit. The column gap alone separates them -- no divider border. */
.trend-composed { display: grid; grid-template-columns: 1fr 240px; gap: 28px; align-items: center; }
.trend-chart-col { min-width: 0; }
.trend-rail-value { font-family: 'Barlow Condensed', sans-serif; font-weight: 800; font-size: 36px; line-height: 0.95; letter-spacing: -0.02em; color: var(--text-strong); margin-top: 2px; }
.trend-rail-divider { height: 1px; background: var(--border); margin: 14px 0; }
.trend-rail-note { font-size: 12.5px; color: var(--muted-mid); line-height: 1.55; margin-top: 8px; }
.trend-rail-note strong { color: var(--text-strong); font-weight: 600; }

/* Reach and yield: the two trends side by side under one panel head, with the
   reading in words beneath them. An even split, because neither series is the
   subordinate one -- the panel exists to hold them against each other. */
.ry-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; }
.ry-col { min-width: 0; }
.ry-col .hero-label { margin-bottom: 8px; }
.ry-reading { font-size: 14px; line-height: 1.6; color: var(--text); margin: 18px 0 0; }
.ry-reading strong { color: var(--text-strong); font-weight: 600; }

/* Empty chart placeholder: a contained, dashed frame with a centred muted note
   where a curve will draw. Reads as "awaiting data", not a broken chart. */
.chart-empty {
  display: flex; align-items: center; justify-content: center; text-align: center;
  min-height: 96px; padding: 12px 16px;
  border: 1px dashed var(--border); border-radius: 8px;
  background: var(--rank-track); color: var(--muted-mid); font-size: 13px;
}

/* Ads-vs-Premium mix: an empty full-width track + its legend, sharing the bar
   track's rail language. Segments only appear once the pull fills it. */
.mix-track { display: flex; height: 8px; background: var(--rank-track); border-radius: 4px; margin-top: 4px; overflow: hidden; }
.mix-seg { display: block; height: 100%; }
.mix-seg.ads { background: var(--primary); }
.mix-seg.premium { background: var(--muted); }
.mix-legend { display: flex; gap: 16px; margin-top: 10px; flex-wrap: wrap; }
.mix-legend .k { font-size: 12px; color: var(--muted-mid); display: inline-flex; align-items: center; gap: 6px; }
.mix-legend .k strong { color: var(--text-strong); font-weight: 600; }

/* Wider name columns for the traffic-source and per-video lists (longer labels
   than the age ladder), so the skeleton holds the shape the data will take. */
.bar-list.sources .bar-row { grid-template-columns: 132px 1fr auto; }
.bar-row.wide { grid-template-columns: 220px 1fr auto; }

/* Retention-bleed finder callout: full border + tinted background + radius,
   never a left accent. */
.bleed-finder { border: 1px solid var(--border-strong); background: var(--primary-dim); border-radius: var(--radius); padding: 16px 18px; margin-top: 16px; }
.bleed-finder-label { font-size: 10px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--primary); margin-bottom: 8px; -webkit-font-smoothing: subpixel-antialiased; -moz-osx-font-smoothing: auto; }
.bleed-finder-text { font-size: 14px; line-height: 1.6; color: var(--text); }
.bleed-finder-text strong { color: var(--text-strong); font-weight: 600; }

/* Whole-card drill-down link on the board (stretched overlay, keeps the grid) */
.board-row-link { position: absolute; inset: 0; z-index: 2; border-radius: var(--radius); text-decoration: none; }
.board-row-link:focus-visible { outline: 2px solid var(--primary); outline-offset: -3px; }

/* SVG chart element styling (theme-aware via CSS vars) */
.rev-grad-top { stop-color: var(--primary); stop-opacity: 0.26; }
.rev-grad-bot { stop-color: var(--primary); stop-opacity: 0; }
.rev-area { fill: url(#revGrad); }
/* Retention curve reuses the revenue line + gradient colours, but its own gradient id so
   it fills on pages where the revenue chart is absent (a team member sees retention but not
   revenue) without a duplicate #revGrad on the page. Same stops, no new colours. */
.ret-area { fill: url(#retGrad); }
/* Reach and yield: two charts of the same series read two ways, so they share one
   treatment and differ only by their column label. Each owns its gradient id for
   the retGrad reason above -- both are on the page at once, and a member without
   revenue access sees this panel while the revenue chart is absent. The line
   carries .ry-line rather than reusing .rev-line so the stylesheet names what it
   draws and this panel can be restyled without dragging the charts above it along.
   It is NOT a money marker, and .rev-line is not one either: the retention curve
   carries .rev-line too and retention is deliberately open to every member, so
   "does this page carry the revenue chart" cannot be answered by grepping the HTML
   for a class at all. The money-leak tests ask the context for revenue_trend
   instead, which is the layer the gate actually lives at. */
.reach-area { fill: url(#reachGrad); }
.yield-area { fill: url(#yieldGrad); }
.rev-line, .ry-line { fill: none; stroke: var(--primary); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.rev-dot-ring { fill: var(--bg); stroke: var(--primary); stroke-width: 2.5; }
.rev-dot { fill: var(--primary); }
.c-baseline { stroke: var(--border); stroke-width: 1; }
.rev-peak-guide { stroke: var(--border-strong); stroke-width: 1; stroke-dasharray: 3 3; }
.rev-peak-dot { fill: var(--bg); stroke: var(--muted-mid); stroke-width: 2; }
.rev-peak-label { font-family: 'DM Sans', sans-serif; font-size: 10px; font-weight: 600; letter-spacing: 0.06em; fill: var(--muted-mid); }

@media (max-width: 1000px) {
  .hero-strip, .hero-strip.two-up, .hero-strip.four, .hero-strip.even { grid-template-columns: 1fr 1fr; }
  .audience-grid, .devices-grid { grid-template-columns: 1fr; }
  .aud-grid { grid-template-columns: 1fr; }
}
/* Chart+rail bands stack before the tiles do, so a contained chart never gets
   squeezed against its rail on a tablet-width column. */
@media (max-width: 900px) {
  .trend-composed { grid-template-columns: 1fr; gap: 18px; }
  .ry-cols { grid-template-columns: 1fr; gap: 22px; }
}
@media (max-width: 640px) {
  .snap-cols { grid-template-columns: 1fr; }
  .bar-row.wide { grid-template-columns: 120px 1fr auto; }
}
@media (max-width: 600px) {
  .hero-strip, .hero-strip.two-up, .hero-strip.four, .hero-strip.even { grid-template-columns: 1fr; }
  .ch-head { align-items: flex-start; }
}

/* --- Your move: the one panel on the strategy page that directs rather than
   reports. Deliberately louder than everything around it -- if a reader takes one
   thing from the page it should be the row that is theirs, and a panel that looks
   like every other panel is one a busy person scrolls past. --- */
.panel.yours {
  border-color: var(--primary);
  box-shadow: 0 0 0 1px var(--primary) inset;
}

.movelist {
  list-style: none;
  margin: 0;
  padding: 0;
}

.move {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 16px 0;
  border-bottom: 1px solid var(--border);
}

.move:last-child { border-bottom: none; }
.move:first-child { padding-top: 4px; }

.move-body { min-width: 0; }

/* The instruction, not the task title. "Write the new title and description" is
   what a person can act on; the task's own name is reference. */
.move-what {
  font-size: 17px;
  font-weight: 600;
  color: var(--text-strong);
  margin: 0 0 3px;
  line-height: 1.3;
  /* The task history renders "field: old -> new" through this class, and
     video_link is an editable field, so an unbroken URL would push the panel
     sideways. Its siblings .move-which and .move-subject already carry this;
     .move-what only escaped because its other user holds formatted numbers. */
  overflow-wrap: anywhere;
}

.move-which {
  font-size: 13px;
  color: var(--muted);
  margin: 0 0 3px;
  overflow-wrap: anywhere;
}

.move-when {
  font-size: 12px;
  color: var(--muted-mid);
  margin: 0;
  font-variant-numeric: tabular-nums;
}

.late-flag {
  color: var(--danger);
  font-weight: 600;
}

.move.is-late .move-what { color: var(--danger); }

.move-do { flex: 0 0 auto; }

/* Wide enough that the label reads as a sentence rather than a control. The
   button says where the work goes next, because "Done" alone tells the person
   nothing about whether they have just finished or just handed on. */
.btn-move {
  white-space: nowrap;
  font-size: 13px;
  padding: 10px 18px;
}

.allclear {
  font-size: 15px;
  color: var(--muted);
  margin: 4px 0 0;
}

/* --- The baton: who has had it, who has it, who gets it next. Rendered on the
   sitting rows so the board answers "where is this" without a click. --- */
.baton {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin: 4px 0 0;
}

.baton-step {
  font-size: 11px;
  letter-spacing: 0.02em;
  /* Solid theme colors, NO opacity. Opacity multiplies against whatever the
     theme supplies, so a value tuned on dark washed out on light (client call,
     8 Aug, twice). --muted-mid is the readable secondary in BOTH themes by
     construction: brighter than --muted on dark, darker on light. The ladder
     reads on color and weight alone: pending muted-mid, done text, current
     text-strong and bold. */
  color: var(--muted-mid);
  white-space: nowrap;
}

/* Three states a glance has to separate without reading the words: behind us,
   here, still ahead. Filled marker for anything reached, hollow for what has not
   been, and only the current one at full strength. */
.baton-step::before {
  content: "\25CB";
  margin-right: 4px;
  font-size: 9px;
  vertical-align: 1px;
}

.baton-step.is-done {
  color: var(--text);
}

.baton-step.is-done::before {
  content: "\25CF";
  color: var(--muted-mid);
}

.baton-step.is-current {
  color: var(--text-strong);
  font-weight: 700;
  opacity: 1;
}

.baton-step.is-current::before {
  content: "\25CF";
  color: var(--primary);
}

.baton-sep {
  color: var(--border);
  font-size: 11px;
}

/* ===== CHANNEL BOARD (Tasks list) ===== */
/* The locked variant C from task-table-layouts.html: rows grouped under a
   channel heading said once, two lines per row. Recipes lifted from the mockup
   verbatim where the app allows, with the few reconciliations the shared
   stylesheet forces noted inline. No panel box around the groups -- the
   heading's rule and the row dividers carry the structure, which is the
   mockup's whole arrangement. */
.channel-group + .channel-group { margin-top: 30px; }
/* 32px below the board, matching .stat-strip, so the section closes with the
   same gap before the release-pipeline panel as every other section. */
.task-board { margin-bottom: 32px; }

.channel-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border-strong);
  margin-bottom: 2px;
}
/* Scoped under .channel-head: the bare class already belongs to the home page's
   channel leaderboard (.channel-info .channel-name, much earlier in this file),
   and an unscoped rule here silently restyled it. It renders as an h3 for
   heading navigation, so margins are zeroed against the flex head. */
.channel-head .channel-name {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800;
  font-size: 17px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-strong);
  margin: 0;
}
.channel-count {
  font-size: 11px;
  color: var(--muted);
  letter-spacing: 0.04em;
}

.c-row { padding: 14px 10px; margin: 0 -8px; border-bottom: 1px solid var(--border); }
/* Zebra on alternate rows, in the house faint tint (nothing on the board uses
   hover, so the token is free here). The group's first child is the head, so
   even children are the 1st, 3rd, 5th rows. The negative margin above lets the
   stripe breathe past the text edge while the text keeps its alignment with
   the group heading. */
.c-row:nth-child(even) { background: var(--surface-hover); }
.c-row:last-child { border-bottom: 0; }
.c-line1 {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 12px;
  row-gap: 4px;
}
/* The date chip's 104px flex-basis belongs to the attention queue's aligned
   column; on the board the chip sits inline at its own width. */
.c-line1 .attention-due { flex: 0 0 auto; }
.c-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text-strong);
  line-height: 1.4;
}
.c-line2 {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* 3px, not the mockup's 7: both lines carry their own line-height slack (the
     title's 1.4 below its text, this line's inherited body height above), so a
     7px margin rendered as roughly double the intended visual gap. The margin
     plus the slack is the design's 7px, seen (client call, 8 Aug). */
  margin-top: 3px;
}
.c-line2-left {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  color: var(--muted-mid);
  font-size: 12px;
  line-height: 1.3;
}
.c-line2-left .dot { color: var(--border-strong); }
/* The shared .baton is a block with a top margin (it hangs under a table cell's
   title elsewhere); inside the row's second line it sits inline with the person
   beside it, as the mockup draws it. */
.c-line2-left .baton { display: inline-flex; margin: 0; }
.c-person { font-weight: 600; color: var(--text); }
/* What a row is part of, when it is part of a release. It sits in line two's
   metadata, so it takes that line's colour rather than the link accent -- a
   coloured link here would outrank the title above it. The underline is a
   bottom border on the token itself, so it reads as a link without the browser
   default's full-height underline cutting through the descenders. */
.c-release {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--border-strong);
}
.c-release:hover { color: var(--text-strong); border-bottom-color: var(--text-strong); }
/* Later in the file than .muted, so without this the person slot could never
   dim: "Unassigned" must not read like a named holder. */
.c-person.muted { font-weight: 400; color: var(--muted-mid); }
/* A done row dims its metadata, not its title: the work still names itself. */
.c-row.is-done .c-line1 .attention-due,
.c-row.is-done .c-line2-left { color: var(--muted-mid); }

/* Status tags: the old four sections as quiet per-row facts. Needs-an-owner is
   the loud one (danger tint) because nothing else on the page will move that
   work; sign-off and done recede. Full borders, never a left stripe. */
.c-tag {
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  border-radius: 5px;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
}
.c-tag-owner {
  background: rgba(224, 80, 80, 0.12);
  border: 1px solid rgba(224, 80, 80, 0.28);
  color: var(--danger);
}
.c-tag-signoff {
  background: var(--rank-track);
  border: 1px solid var(--border-strong);
  color: var(--muted-mid);
}
.c-tag-done {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--muted);
}

/* The same rows on the release detail page (its "Tasks linked to this release"
   panel). A wrapper, not a bare run of .c-row inside the panel, because the
   zebra above is :nth-child(even) and it has to count ROWS -- with the panel
   head and the action bar as siblings the stripe would land on whichever rows
   happened to be even after them. The small top margin replaces the board's
   group-heading rule, which this panel's own head already provides.

   Scoped under .rel-work, the way .channel-head .channel-name is: a bare
   row-wrapper class in a 4,700-line stylesheet is exactly the shape of name that
   silently restyles something else later. */
.rel-work .rel-work-rows { margin-top: 4px; }

/* Narrow screens: line two stacks -- the baton and person first, the action
   buttons on their own line -- per the mockup's note. The channel headings are
   not part of any row grid, so they need nothing here. */
@media (max-width: 640px) {
  .c-line2 {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }
}

/* Results carry the same weight as work still to do: the loop is not finished when
   something ships, it is finished when somebody has read what it did. Marked out
   from "Your move" by colour rather than size, so the page has two loud things and
   a reader can tell them apart without reading either. */
.panel.results {
  border-color: var(--accent, #d9a441);
  box-shadow: 0 0 0 1px var(--accent, #d9a441) inset;
}

/* The control line under a result. Deliberately plain and small: it is the thing
   that stops a number being read as an achievement, so it has to be present and
   must not compete with the figure it qualifies. */
.move-controls {
  font-size: 12px;
  color: var(--muted-mid);
  margin: 6px 0 0;
  font-variant-numeric: tabular-nums;
}

/* A row that carries input is a small form, not a line with a button, so it
   stacks instead of sitting side by side. */
.move.is-block {
  display: block;
}

.move.is-block .move-head {
  display: flex;
  justify-content: space-between;
  gap: 20px;
}

.move.is-block .move-do {
  margin-top: 14px;
}

.move-field {
  display: block;
  margin: 0 0 12px;
  max-width: 720px;
}

.move-field-label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 5px;
}

.move-input {
  display: block;
  width: 100%;
  background: var(--panel-2, rgba(0, 0, 0, 0.14));
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text-strong);
  font-family: inherit;
  font-size: 14px;
  padding: 9px 11px;
  line-height: 1.5;
}

.move-input:focus {
  outline: none;
  border-color: var(--primary);
}

.move-field-hint {
  display: block;
  font-size: 11.5px;
  color: var(--muted-mid);
  margin: 4px 0 0;
}

/* What the people before you handed in. Reading it is the job at the review and
   publish stages, so it sits above the button rather than behind a link. */
.handed-in {
  margin: 12px 0 0;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel-2, rgba(0, 0, 0, 0.10));
}

.handed-in-head {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted-mid);
  margin: 0 0 8px;
}

.handed-in-item {
  font-size: 13.5px;
  color: var(--text-strong);
  margin: 0 0 8px;
}

/* pre-wrap on the VALUE only. On the paragraph it also preserved the template's
   own indentation, which pushed every handed-in line in by a random amount. */
.handed-in-value {
  display: block;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.handed-in-item:last-child { margin-bottom: 0; }

.handed-in-label {
  display: block;
  font-size: 11px;
  color: var(--muted);
  margin: 0 0 2px;
}


/* The subject of the work -- which video, which audience -- reads first. The
   stage instruction is the same words on every row of its kind, so it sits with
   the date as supporting detail rather than as the headline it used to be. */
.move-subject {
  font-size: 18px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--text-strong);
  margin: 0 0 6px;
  overflow-wrap: anywhere;
}

.move-stage {
  color: var(--muted);
}

.move-stage::after {
  content: "\00b7";
  margin: 0 6px;
  color: var(--border-strong);
}

/* The kind of job, above the thing it is about. Small and coloured rather than
   large, because a reader scanning their queue needs to tell rows apart by SUBJECT
   first and by kind second -- four rows all headed "New thumbnail" are four rows
   you have to read the small print of. */
.move-kind {
  font-family: 'Barlow Condensed', sans-serif;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--primary);
  margin: 0 0 2px;
}


/* The queue has no heading of its own: it opens the Tasks page, so a title saying
   these are tasks is a line that tells the reader nothing they did not get from
   the page they clicked. The panel keeps its outline, which is what separates it
   from the team-wide tables below, and each row is headed by the kind of job. */
.panel.yours .move:first-child {
  padding-top: 0;
}

/* Three counts, equal weight. No sparkline, no arrow, no colour-coding against a
   target: each of those would imply a direction or a judgement the figures do not
   carry, on the one panel most likely to be read as a verdict. */
.scoreboard {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 20px;
}

.score-value {
  font-family: 'Barlow Condensed', sans-serif;
  font-size: 40px;
  font-weight: 700;
  line-height: 1;
  color: var(--text-strong);
  margin: 0 0 6px;
  font-variant-numeric: tabular-nums;
}

.score-of {
  font-size: 22px;
  color: var(--muted);
}

.score-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-strong);
  margin: 0 0 3px;
}

.score-note {
  font-size: 12px;
  color: var(--muted-mid);
  margin: 0;
  line-height: 1.45;
}

/* Late scheduled work, in the tile. Coloured because a date that has passed is a
   fact worth interrupting for, unlike the count beside it. */
.stat-late {
  color: var(--danger);
  font-weight: 600;
}

/* ===== RELEASE DETAIL: the read page behind a release row =====
   Four panels, one per question Memo asked of a planned release. Namespaced
   .rel-* and built entirely from the existing tokens and components (.panel,
   .ch-head, .badge, .flag-critical, .data-table, .placeholder-note), so the page
   adds no new visual vocabulary -- only the label/value grid and the two step
   states below, which had no equivalent. */

/* Label/value grid. Two columns on desktop so the labels form a scannable rail;
   one column on narrow screens, where a 140px label column would wrap every
   value to three lines. pre-wrap on the value because notes are free-form and
   multi-line, and anywhere-wrapping because a catalogue code or a channel name
   can be one long unbroken token. */
.rel-facts {
  display: grid;
  grid-template-columns: minmax(130px, 190px) 1fr;
  gap: 10px 24px;
  margin: 0;
  /* The label is 11px and its value 14px, so aligning the grid rows on their box
     edges left every label sitting a couple of pixels low against the value it
     names. Baselines are what the eye reads them against. */
  align-items: baseline;
}

/* The timeline panel puts its sentences under the two dates. Without this the
   first sentence butts straight against the last value and reads as a third,
   unlabelled row of the grid. */
.rel-facts + .rel-line { margin-top: 16px; }

.rel-facts dt {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  -webkit-font-smoothing: subpixel-antialiased;
  -moz-osx-font-smoothing: auto;
}

.rel-facts dd {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-strong);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* An absence is still an answer, so it is printed in words like everything else
   and only dimmed -- never left blank, and never italicised into looking like a
   system message rather than a fact about the release. */
.rel-facts dd.is-missing { color: var(--muted); }

/* Who has it right now. The loudest line on the page after the title, because it
   is the single thing a reader opens a release to find out. */
.rel-holding {
  font-size: 17px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--text-strong);
  margin: 0 0 12px;
}

.rel-line {
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-strong);
  margin: 0 0 8px;
}

.rel-line:last-child { margin-bottom: 0; }

/* Late is a fact worth interrupting for. Same colour the board and the task
   queue already use for a date that has passed. */
.rel-late { color: var(--danger); font-weight: 600; }

/* The step the work is actually sitting on. Marked by weight and a rule rather
   than a fill, so the table still reads as a table and the row does not compete
   with the sentence above it that names the same step. */
.rel-step-current th[scope="row"] { color: var(--text-strong); font-weight: 700; }
.rel-now { color: var(--primary); font-weight: 600; }

/* Holds the step name and its optional help as one unit, so the card-mode row
   header (a flex row: this, then the chevron) stacks the help under the name. */
.rel-step-name { display: block; min-width: 0; }

@media (max-width: 640px) {
  .rel-facts { grid-template-columns: 1fr; gap: 2px 0; }
  .rel-facts dd { margin-bottom: 12px; }
  .rel-facts dd:last-child { margin-bottom: 0; }
}

/* ===== CUGATE TV UPLOAD (Eva) =====
   Eva's upload group, on the release form and the release page. Built from the
   existing tokens only, so it inherits both themes without a single hard-coded
   colour, and it adds no new visual vocabulary beyond the chip and the path
   line -- neither of which had an equivalent anywhere in the app. */

/* The destination tick. Control first, label as its caption: a checkbox whose
   label sits above it reads as a heading with an orphaned box under it. */
.tv-switch-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.tv-switch-row .form-check-input {
  width: 16px;
  height: 16px;
  margin: 0;
  flex: 0 0 auto;
  accent-color: var(--primary);
}

/* The label is doing caption duty here, so it drops the form-label stack margin
   and stops being a block. */
.tv-switch-label {
  margin: 0;
  cursor: pointer;
}

/* The group itself. A full border and a tint, never a left stripe: the fields
   inside are a set, and a set is bounded on all four sides. */
.tv-group {
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--rank-track);
  padding: 18px 18px 4px;
  margin: 0 0 20px;
}

.tv-group-legend {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 0 8px;
  margin-left: -8px;
  width: auto;
}

/* Why three of the fields are not on the form. Sits under the inputs and ruled
   off them, because it explains an absence rather than annotating the field
   above it. */
.tv-group-note {
  margin-top: 4px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

/* --- Tags, as chips ---
   A display of the comma-separated string and nothing more: there is no tag
   table behind these. See Release.tv_tags for why that trade-off was made. */
.tv-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}

/* Inside the release page's fact grid the chips ARE the value, so the top margin
   the form version needs would drop them off their own baseline. */
.rel-facts .tv-chips { margin-top: 0; }

.tv-chip {
  display: inline-block;
  font-size: 12px;
  line-height: 1.4;
  padding: 3px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--rank-track);
  color: var(--text-strong);
  /* A tag can be one long unbroken token; it wraps inside the chip rather than
     stretching the row past the panel. */
  overflow-wrap: anywhere;
}

/* --- The derived file paths ---
   Their own block rather than two more rows of the fact grid: they are long and
   monospaced, and against a 190px label column every value wrapped to four
   lines. */
.tv-paths {
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.tv-paths-title {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 12px;
}

/* Monospaced because it is a path and it is going to be copied; wrapping rather
   than scrolling because a path that is cut off is a path nobody can use. */
.tv-path {
  font-family: ui-monospace, 'SF Mono', 'DM Sans', monospace;
  font-size: 12.5px;
  color: var(--text-strong);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}
