/* 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 and total revenue. 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 HAS ONE JOB: the money that is the point of the work. That is the #1 rank
   numeral on the board and the revenue total in the header, and nothing else in
   the app.

   IT HAD A SECOND FROM 7 AUGUST TO 28 AUGUST: the Open task control on the home
   page, which sat inside a cyan filled block and needed a colour that could win
   against it. The client asked on 28 August for that block to read like the
   morning digest mail, which is a white page with the app's own teal button on
   each row, so the fill went and the control went back to being .btn-dark like
   every other primary action. The mail is painted from the tokens in this file,
   so nothing was traded away to do it.

   IT HAD A THIRD FOR A MONTH BEFORE THAT, and how that one ended is why this note
   still spells the rule out. The What's moving bar took amber across the foot of
   every page on the client's call, far and away the largest gold surface in the
   app; the bar turned out to be too loud for its own content, went through three
   rounds and was dropped. A large gold surface is the one shape this palette
   cannot absorb, whatever it is carrying.

   Gold is deliberately NOT available for decoration, for a heading, for a hover
   state, or for a call to action anywhere in the app. A gold thing that is not
   the money 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);
}

html[data-theme="dark"] {
  /* The header menu floats over the page, and this theme's --surface is translucent,
     so whatever it covers would read through it. That surface, flattened onto --bg. */
  --menu-surface:  #0b2c47;
  --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);
  /* The label on a filled button, sitting on --primary. Its own token rather than
     the --bg-deeper it used to borrow: that one is a page GROUND with four other
     consumers (the sidebar, the inline expander, the select menus, the pie centre),
     and here the two agree by luck rather than by decision. 9.75:1, and the light
     twin is where borrowing went wrong. */
  --btn-ink:       #020e1a;
  --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;
  /* A RISE, AND IT HAS TO LOOK LIKE THE OPPOSITE OF A FALL. This was #00cbd8, which
     is --primary to the digit: every rise in the app printed in the brand cyan that
     also paints the money figure and the link on the same row, so a reader who takes
     red as bad had nothing that read as good. Seven surfaces use this token and all
     seven move together, which is the point rather than a side effect.
     Chosen by measurement, not by eye: 7.26:1 on the card (#0b2c47), where the cyan
     it replaces was 7.19:1, so a rise keeps exactly the prominence it already had and
     only its hue changes. Far enough from --primary in hue (~45 degrees) to be told
     apart beside it, and nowhere near the amber that means rank. */
  --up:            #4fd07a;
  --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"] {
  /* Already what --surface is in this theme. The dark twin is where it earns its name. */
  --menu-surface:  #ffffff;
  --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);
  /* WHITE, AND MEASURED. This borrowed --bg-deeper like its dark twin, which in
     this theme is #d8eaf4, a pale blue meant to sit UNDER content: on --primary
     that is 3.99:1, and the label is 12px at weight 600, so every filled button in
     the light theme failed WCAG AA at 4.5:1. White on the same fill is 4.94:1. The
     morning digest already used white on this exact fill, so the app now agrees
     with its own mail as well. Four months live and nobody reported it, which is
     the case for measuring the ratio in a test rather than looking at the button. */
  --btn-ink:       #ffffff;
  --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;
  /* The light half of the same decision. 5.42:1 on white, against --danger's 5.67:1
     on the same ground: the rise and the fall carry equal weight, so neither
     direction shouts over the other on a board whose subject is a decline. */
  --up:            #0f7a3f;
  --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;
  /* THE FULL HEIGHT OF THE WINDOW, unqualified. It spent a month at
     calc(100vh - 42px) on pages carrying the What's moving bar, because a bar fixed
     across the foot would otherwise have covered the log-out control at the bottom of
     this column. The bar is gone and so is the subtraction: nothing in the app is
     fixed to the foot of the viewport now, and a sidebar that stops short of the
     floor for a component nobody renders is a gap somebody has to explain. */
  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;
}

/* "Nudged 24 Aug" under the responsible person on the release-pipeline table.
   Set quieter than the name it sits under: it is context for the name, not a
   second fact competing with it, and it must not read as a flag against the
   person. Own class rather than a shared muted utility, because this line is
   the visible half of the release chaser and should be findable from it. */
.row-chased {
  display: block;
  /* Named directly: there is no --mono token in this stylesheet, so var(--mono, ...)
     always fell through to the browser default and rendered as Courier or Menlo
     against a page that is otherwise DM Sans. This is the same stack the other two
     monospace blocks in this file use. */
  font-family: ui-monospace, 'SF Mono', 'DM Sans', monospace;
  font-size: 10.5px;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-top: 2px;
}

/* The action under a next-in-line name. NOT set like .row-chased above it, which is
   a monospaced date stamp; this is a sentence and takes the body face. Same ink,
   because both are a second fact about their row rather than a heading for it, and
   that is the whole of what they share. Block, so it never runs on into the name. */
.row-next {
  display: block;
  font-size: 11.5px;
  line-height: 1.35;
  color: var(--muted);
  margin-top: 2px;
}

/* The scope line under the Company Today bays. Quiet: it is a caveat on the
   figures above it, not a fifth figure. */
/* A caveat on the card above it, not the first line of the sentences below it: it had
   a top margin and no bottom one and sat straight on top of the decline lines. */
.ctoday-scope {
  font-size: 12px;
  color: var(--muted);
  margin-top: 10px;
  margin-bottom: 20px;
}

/* Not-submitted WITH a date, on the release-pipeline table only. Memo asked for
   red/bold/flagged; this is the scoped version. Deliberately NOT a rule on
   .attention-need alone, which the dashboard's personal attention strip shares.

   COMPOUND SELECTOR, AND IT HAS TO BE. Written as a bare `.need-flagged` this rule
   did nothing at all: `.attention-need` is declared 1,000 lines further down with
   the same specificity, so it won on order and the tag rendered muted-blue at
   weight 600, pixel-identical to an unflagged one. The markup test passed the
   whole time because it asserts the class ATTRIBUTE, not the rendering. Compound
   is (0,2,0) and cannot be lost to a later same-specificity rule or to anyone
   reordering this file. See NeedFlagOutranksTheBaseTagTests. */
.attention-need.need-flagged {
  color: var(--danger);
  font-weight: 700;
}

/* ===== WHO IS CARRYING WHAT: two-column roster cards =====
   Replaces the full-width one-row-per-person list. That row named EVERY open item
   in one paragraph, so somebody holding nine printed four lines of concatenated
   YouTube SEO titles ("... | Classical Music But Hard | Fireplace"), and the next
   two rows repeated most of them because several people hold the same videos.

   STILL NOT A LEAGUE TABLE, and the rules below are where that is kept: the count
   badge is the same neutral pill at 1 and at 9, the age is never the danger colour
   whatever the number, and the shared idle card is the same card as any other with
   no span and no fade. The only colour step is between a title and its reason,
   which is a reading of the WORK and not of the person holding it. */

/* Cards fill their row so a one-line card beside a two-line one does not leave a
   short box. */
.roster-grid > li { display: flex; }
.roster-grid > li > .roster-card { flex: 1 1 auto; }

.roster-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px 20px;
}

.roster-card {
  display: block;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-hover);
  padding: 14px 16px 15px;
  text-decoration: none;
  color: inherit;
  transition: background-color 0.2s ease, border-color 0.2s ease;
}

a.roster-card:hover {
  background: var(--surface);
  border-color: var(--border-strong);
}

a.roster-card:hover .roster-card-name { color: var(--primary); }

.roster-card:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* The badge and the age are UNCHANGED from the list this replaced: same neutral
   pill at 1 and at 9, and an age that is never the danger colour whatever the
   number. The layout changed; the restraint did not. */
.roster-card-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 8px;
}

.roster-card-name-wrap {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}

.roster-card-name {
  font-size: 15px;
  font-weight: 600;
  color: var(--text-strong);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.roster-card-age {
  flex-shrink: 0;
  margin-left: auto;
  font-size: 12px;
  font-weight: 500;
  color: var(--muted-mid);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.roster-card-body { min-width: 0; }

.roster-card-line {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: 13px;
  line-height: 1.5;
  white-space: nowrap;
  overflow: hidden;
}

.roster-card-title {
  color: var(--muted-mid);
  overflow: hidden;
  text-overflow: ellipsis;
  /* A floor, so the reason can never squeeze the name out entirely. */
  min-width: 6ch;
}

.roster-card-reason {
  color: var(--text);
  font-weight: 500;
  /* Shrinks, with its own ellipsis. Pinned at flex-shrink:0 the title was the only
     thing that could give, so between the 1100px fold and about 1400px a long
     reason ate the column and the card showed everything except which work it was.
     The title keeps a floor below so neither can be squeezed to nothing. */
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.roster-card-more {
  flex-shrink: 0;
  color: var(--muted);
  font-size: 12px;
}

.roster-card-meta {
  margin-top: 6px;
  font-size: 11px;
  color: var(--muted);
}

/* The shared idle card's names, in the BODY where they can wrap. The head slot
   ellipsises, which is right for one name and hides everyone after the second when
   the row is a comma-joined list. No nowrap here, deliberately: every name printed
   is the condition the collapse had to meet. */
.roster-card-idle-names {
  font-size: 13px;
  color: var(--text);
  line-height: 1.5;
}

.roster-card-nothing {
  font-size: 13px;
  color: var(--muted-mid);
}

/* One column below 1100px, the same breakpoint .dash-top-grid already folds at
   rather than a new number: two 13px title lines side by side stop being readable
   well before the grid itself runs out of room. */
@media (max-width: 1100px) {
  .roster-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ===== SIDEBAR, EXPANDED =====
   Eleven icons and no labels, readable only by pointing at them (Kelsey, 13 Aug:
   "Can you make this side bar expandable, so we can see the different pages
   without having to hover?"). Widening the rail turns the hover tooltip into an
   inline label, which is exactly what the mobile drawer below already does -- so
   this is the same treatment at desktop width rather than a second design.

   Keyed off data-nav on <html>, written by base.html's pre-paint block before the
   first frame and toggled afterwards by nav.js. Not a class on .app, because .app
   has not been parsed yet at the point the preference has to be applied, and a
   sidebar that opens collapsed and then jumps wider is worse than one that never
   opened at all.

   COLLAPSED REMAINS THE DEFAULT: the rail is what the team has used for two
   months and it gives the content 150 more pixels.

   Confined to widths above the drawer breakpoint. Below it the sidebar is a fixed
   off-canvas panel that is already labelled, and these rules would fight its own
   -- winning, because a [data-nav] selector outranks the plain ones in that media
   block, which would leave the page laid out for a 210px column that is not
   there. */
@media (min-width: 601px) {
  html[data-nav="expanded"] .app { grid-template-columns: 210px 1fr; }
  html[data-nav="expanded"] .sidebar {
    align-items: stretch;
    padding: 20px 12px;
  }
  html[data-nav="expanded"] .sidebar-logo {
    align-self: flex-start;
    margin-left: 4px;
  }
  html[data-nav="expanded"] .nav-item {
    width: 100%;
    height: auto;
    justify-content: flex-start;
    gap: 12px;
    padding: 11px 12px;
  }
  html[data-nav="expanded"] .nav-item svg { flex: 0 0 auto; }
  /* The tooltip becomes the label, in place. Same element, so nothing is
     duplicated into the markup and a screen reader is told the page name once. */
  html[data-nav="expanded"] .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;
    white-space: normal;
  }
  /* The count stays ON THE ICON, the same place it sits collapsed, so widening
     the rail moves the labels and nothing else. Still absolute, so it is out of
     flow and cannot push the word "Inbox" out of line with the names above it.

     Anchored from the LEFT here rather than the right: collapsed, the row is a
     44px square and "right: 4px" lands on the icon; expanded, the row is the
     full width of the rail and the same rule would fling the badge to the far
     edge. The offsets track the row's own geometry directly above -- 12px of
     left padding plus the 18px icon, less an 8px overlap onto its corner, and
     11px of top padding less 5px of the same overlap. If that padding or the
     icon size changes, these change with it. */
  html[data-nav="expanded"] .nav-badge {
    top: 6px;
    right: auto;
    left: 22px;
  }
}

/* ===== SIDEBAR, GROUPS =====
   Jan, 26 Aug: "the menu seems messy... should we have submenus for some items.
   also consider the order." Thirteen rows appended in build order, with the page
   the team calls its daily checklist at position seven.

   What shipped (design/nav/nav-pick-d.html, argued in NAV-IA-SPEC.md) is grouping
   WITHOUT submenus: three headings that fold shut, every page still exactly one
   click from the rail. Most traffic here arrives by deep link from the morning
   digest rather than by browsing this column, so the people who would pay for a
   hidden submenu are everybody, and the people it would tidy things for are the
   four who scan the whole rail.

   The open/closed state is a token list on <html> (data-nav-closed), written by
   base.html's pre-paint block before the first frame -- the same store and the
   same reason as the rail's width. A group applied after the stylesheet would draw
   open and then snap shut.

   EVERY GROUP IS FOLDED UNTIL SOMEBODY OPENS IT (Jan, 26 Aug: "the menu sections
   should not be expanded by default"), except the group of the page being opened.
   localStorage holds the OPEN set and the block subtracts it, so the attribute
   read here is still the closed one and nothing below had to change. */
.nav-scroll {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 100%;
  /* THE PAGE LIST SCROLLS AND THE FOOT DOES NOT. A staff rail with every group
     open measured 665px against a 13-inch laptop's 790 when it had twelve pages
     that all opened by default; measured again on 26 August, with eleven pages
     and every group folded until somebody opens it, it is 603 fully open and 196
     as a first visit gets it. The margin is real now rather than one browser
     toolbar, but the reader who opens all three groups on a short screen is still
     who this is for: without it the control that hides the page names is the
     first thing off the bottom of the screen.

     min-height: 0 is the half that does the work -- a flex child will not shrink
     below its content without it, so the column would simply grow past the
     viewport and scroll the page instead.

     THE OVERFLOW IS NOT HERE, it is on the wide rail below. overflow-y makes
     overflow-x compute to auto, and the icon rail's page names are popouts that
     hang past its right edge, so clipping this column there erases them. What the
     icon rail does instead is in the block after the group rules. */
  flex: 1 1 auto;
  min-height: 0;
}

/* SOMETHING THAT SAYS IT SCROLLS, always drawn. macOS gives an overflowing box an
   overlay scrollbar, and an overlay scrollbar stays invisible until it is already
   moving: offsetWidth - clientWidth measured 0 on the rail that was hiding four
   pages, so a rail cut off at Campaigns looked exactly like a rail that ends at
   Campaigns. A 6px scrollbar in the rail's own muted TAKES UP ROOM, and the room
   is the affordance -- it is drawn whether or not anything is moving.

   THE PSEUDO-ELEMENTS DO THAT AND THE STANDARD PROPERTIES DO NOT, which is the
   opposite of what the modern spelling being modern suggests, and it was measured
   rather than assumed (26 Aug, headless Chrome, four boxes side by side):
   scrollbar-width: thin left an overlay reserving 0px, ::-webkit-scrollbar
   reserved 6, and the two together reserved 0 -- setting scrollbar-width to
   anything but auto makes Chrome ignore the skin entirely. So they cannot both be
   written here, and the one that works is the old one.

   Firefox has no ::-webkit-scrollbar, and its scrollbars take up room anyway, so
   it gets the standard pair and nothing else does. @supports rather than a
   comment: this is the sort of pair somebody tidies into one declaration. */
.nav-scroll::-webkit-scrollbar { width: 6px; }
.nav-scroll::-webkit-scrollbar-track { background: transparent; }
.nav-scroll::-webkit-scrollbar-thumb {
  background: var(--muted);
  border-radius: 3px;
}

@supports not selector(::-webkit-scrollbar) {
  .nav-scroll {
    scrollbar-width: thin;
    scrollbar-color: var(--muted) transparent;
  }
}

/* AND A FADE UNDER THE LAST ROW YOU CAN SEE, because a scrollbar is a thin thing
   at the edge of a 60px column and the rows themselves are what a reader is
   looking at. A mask rather than an overlay: it fades to nothing rather than to a
   colour, so the sidebar's own background shows through and neither theme needs a
   value of its own. Stamped by nav.js, which is the only thing that can ask
   whether this box overflows -- CSS cannot. */
.nav-scroll.is-scrollable {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent);
}

/* NOTHING IN THE RAIL SHRINKS. The three pages that stand on their own are direct
   flex children of this column and the ones inside a group are a level down, so a
   column short of room squeezed the first three and left the rest alone:
   Dashboard and Inbox measured 18px tall against Tasks at 44. That reads as a
   rendering fault rather than as a rail that has run out of room, and it is the
   rows the reader uses most. */
.nav-scroll .nav-item,
.nav-scroll .nav-group-items { flex-shrink: 0; }

/* The two boxes OUTSIDE the page list shrink too, and once the icon rail's list
   stopped giving (flex: 0 0 auto below) the whole deficit of a short window
   landed on them: the control at the foot measured 16px at a 577px viewport,
   its padding gone, and the logo 28. The page list is the one thing meant to
   give, and it is the one thing that says so with a scrollbar. */
.sidebar > .nav-item,
.sidebar-logo { flex-shrink: 0; }

.nav-group-items {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 100%;
}

/* THE HEADING IS NOT DRAWN IN THE ICON RAIL: it has no icon, so there is nothing
   to put in a 44px column. What survives the collapse is the CLUSTERING -- a gap
   where each heading was, set below -- so the four channel icons still read as one
   run. Nothing hides behind a hover, which is what the picked mockup shows. */
.nav-item.nav-group { display: none; }

.nav-group-title {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

.nav-group-caret {
  display: inline-block;
  margin-left: auto;
  color: var(--muted);
  font-size: 12px;
  line-height: 1;
  transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Points at the pages when they are there, and along the heading when they are
   not. Keyed off aria-expanded so the glyph cannot disagree with what a screen
   reader is told: there is one state, and it is the accessible one. */
.nav-item.nav-group[aria-expanded="false"] .nav-group-caret {
  transform: rotate(-90deg);
}

.nav-item.nav-group:hover .nav-group-title,
.nav-item.nav-group:hover .nav-group-caret {
  color: var(--text);
}

@media (min-width: 601px) {
  html[data-nav="expanded"] .nav-scroll,
  html[data-nav="expanded"] .nav-group-items { align-items: stretch; }
  html[data-nav="expanded"] .nav-scroll { overflow-y: auto; }
  /* Compound, and with the html[data-nav] prefix, because the plain .nav-item
     rules for this state carry that prefix too: a bare .nav-item.nav-group would
     lose to them on specificity and this heading would draw as a 44px page row.
     This stylesheet has been caught by exactly that four times. */
  html[data-nav="expanded"] .nav-item.nav-group {
    display: flex;
    align-items: center;
    height: auto;
    min-height: 34px;
    padding: 9px 12px;
    margin-top: 6px;
  }
  /* The indent is the whole visual difference between a page inside a group and
     one standing on its own, and the smaller icon keeps the child from competing
     with the three rows that are not in a group. */
  html[data-nav="expanded"] .nav-item.is-child { padding-left: 24px; }
  html[data-nav="expanded"] .nav-item.is-child svg { width: 16px; height: 16px; }
  /* The Inbox badge tracks its row's own geometry (see the expanded block above);
     a child row is indented, so the badge moves with the icon it sits on. Inbox
     itself is never a child, but the offset is stated here rather than left to be
     rediscovered if it ever moves into a group. */
  html[data-nav="expanded"] .nav-item.is-child .nav-badge { left: 34px; }
  /* THE ICON RAIL KEEPS THE CLUSTERS. 18px above each group puts 22px between the
     last icon of one run and the first of the next, counting the column's own 4px
     gap -- the spacing the mockup's third column shows. Only here: with the words
     on screen the heading is doing this job itself. */
  html:not([data-nav="expanded"]) .nav-group-items { margin-top: 18px; }
}

/* A group the reader folded shut, on the two rails that draw a heading to fold it
   back open with: the wide desktop rail and the phone drawer. Never in the icon
   rail -- no heading is drawn there, so a group closed on the wide rail would
   vanish from it with nothing able to bring it back.

   The keys are written out because CSS cannot compare an attribute on <html>
   against one on a descendant. cugate_channels.tests fails if NAV_GROUPS grows a
   key with no rule here. */
@media (min-width: 601px) {
  html[data-nav="expanded"][data-nav-closed~="production"] #nav-group-production,
  html[data-nav="expanded"][data-nav-closed~="channels"] #nav-group-channels,
  html[data-nav="expanded"][data-nav-closed~="protect"] #nav-group-protect {
    display: none;
  }
}

@media (max-width: 600px) {
  html[data-nav-closed~="production"] #nav-group-production,
  html[data-nav-closed~="channels"] #nav-group-channels,
  html[data-nav-closed~="protect"] #nav-group-protect {
    display: none;
  }
}

/* ===== SIDEBAR, THE ICON RAIL'S OWN HEIGHT =====
   The wide rail clips and scrolls. The icon rail cannot, and it is worth writing
   down why, because it looks like an oversight and it is the opposite.

   The 60px column's only page names are the hover popouts, and they are laid out
   2px past its right edge. overflow-y: auto makes overflow-x compute to auto as
   well -- there is no CSS that scrolls one axis and lets the other spill, and
   overflow-x: clip is downgraded to hidden in that pairing -- so scrolling this
   column erases every label it has.

   Two escapes were measured in headless Chrome on 26 Aug and both are dead. A
   position: fixed popout and an absolutely-positioned one whose containing block
   sits outside the scroller each keep their layout-time position: the row moved
   120px on a 120px scroll and the label did not move at all. Anchor positioning
   would do it and is too new to put a rail's only labels on.

   So the region takes its natural height here, and the density block below is
   what keeps that height inside a short screen. Squeezing it into what was left
   over -- which is what flex: 1 1 auto did, with no overflow to make it safe --
   spilled the rows out of the bottom of a box that does not clip: the last icons
   painted on top of the collapse control and past the sidebar. */
@media (min-width: 601px) {
  html:not([data-nav="expanded"]) .nav-scroll { flex: 0 0 auto; }
  /* AND WHEN IT STILL WILL NOT FIT, reaching the icons beats naming them. nav.js
     stamps is-scrollable only when the column has run out of room, and only then
     does this rail clip -- losing the popouts in the one state where the
     alternative is the last pages and the control that widens the rail both being
     off the bottom of the screen with no way to get to either. Widening it brings
     the names back inline, and that control is pinned outside this region. */
  html:not([data-nav="expanded"]) .nav-scroll.is-scrollable {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* SPELLED OUT, because the value it would take otherwise costs 16px of the
       height this rule is trying to find. overflow-y makes overflow-x compute to
       auto, and the popouts hang 100px past this column, so the rail grew a
       horizontal scrollbar along its foot -- measured: the region reported 509px
       of height and 493 of clientHeight, and the 16 it lost made the overflow
       worse. The popouts are already clipped in this state; hidden only stops the
       browser offering to scroll to something it will not draw. */
    overflow-x: hidden;
  }
}

/* ===== SIDEBAR, SHORT SCREENS =====
   Written when the rail had twelve pages and every group opened by default: it
   measured 665px against the 647 a 13-inch MacBook -- the machine that started
   this build -- leaves for the page list once the logo and the foot have taken
   theirs. The last row sat 18px under the fold.

   BOTH OF THOSE CHANGED ON 26 AUGUST. The twelfth page was retired and every
   group is now folded until somebody opens it, so the rail most people see is two
   pages and three headings. Re-measured in headless Chrome that day: fully open
   the staff rail is 603px at full size (900px viewport, 756 of room) and 547 with
   this block applied (790px viewport, 668 of room); folded, which is what a first
   visit gets, it is 196. It fits at 790 at either density now.

   IT STAYS ANYWAY, because what it buys is height on the screens BELOW 790, where
   somebody who has opened all three groups still runs out. Scrolling is one answer
   and it is above; this is the other. A row is 40px because 11px of padding above
   and below an 18px icon is the density the rest of the hub is drawn at, and two
   of those pixels are worth more to somebody on a 13-inch screen than they are to
   the design. Nine gives back 44px across the eleven pages, and the fade and the
   scrollbar above are what happens when even that is not enough.

   820 and not 790: a breakpoint has to sit above the height it is fixing or it
   arrives exactly when it is already too late.

   THE COLUMN ROUND THE LIST comes down here too, and that half serves both rails:
   the padding at each end and the space under the logomark are the same 40px on a
   1440 screen and on a 13-inch one. The icon rail's own squares are a separate
   step, lower down, because it needs one much later than the wide rail does.

   Every rule here carries the state prefix its full-size twin carries. Written
   bare they would lose to those on specificity and do nothing, which is how this
   stylesheet has gone wrong five times. */
@media (min-width: 601px) and (max-height: 820px) {
  html:not([data-nav="expanded"]) .sidebar,
  html[data-nav="expanded"] .sidebar {
    padding-top: 14px;
    padding-bottom: 14px;
  }
  .sidebar-logo { margin-bottom: 14px; }

  html[data-nav="expanded"] .nav-item {
    padding-top: 9px;
    padding-bottom: 9px;
  }
  /* The badge tracks its row's own geometry (see the expanded block above): 9px
     of top padding, less the same 5px of overlap onto the icon's corner. */
  html[data-nav="expanded"] .nav-badge { top: 4px; }
  html[data-nav="expanded"] .nav-item.nav-group {
    min-height: 30px;
    padding-top: 7px;
    padding-bottom: 7px;
  }
}

/* ===== SIDEBAR, THE ICON RAIL ON A SHORT SCREEN =====
   A LOWER BREAKPOINT THAN THE ONE ABOVE, and the reason is arithmetic rather than
   taste. The icon rail draws no headings, so twelve 44px squares and their gaps
   come to 570px and the column round them to 126 -- it fits any screen down to
   about 715. Tightening it at 820 as well would shrink the squares the picked
   mockup shows on a 13-inch laptop that has plenty of room for them.

   Below this it does not fit, and it is the one rail that cannot fall back on
   scrolling: clipping it takes the hover popouts with it, and they are the only
   page names a 60px column has. So the squares come down instead, and the icon
   with them so the proportion holds. 34 and 16 puts the whole staff rail at 608
   against the 625 a 1366x768 laptop leaves after the browser's own chrome.

   The heading gaps come down too. They are what stands in for the headings here,
   and 10px still reads as a break between two runs of icons. */
@media (min-width: 601px) and (max-height: 720px) {
  html:not([data-nav="expanded"]) .nav-item {
    width: 34px;
    height: 34px;
  }
  html:not([data-nav="expanded"]) .nav-item svg {
    width: 16px;
    height: 16px;
  }
  html:not([data-nav="expanded"]) .nav-group-items { margin-top: 10px; }
}

/* ===== 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-value {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800;
  font-size: 22px;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--spotlight);
}

/* WHAT THE FIGURE IS AND WHAT IT COVERS, ON ONE LINE. It was two: a small-caps
   eyebrow above the figure and a muted note below it, which gave "what this is"
   two visual weights in the same width as the two plain controls beside it. One
   muted line in sentence case, and the gold figure is left as the only thing on
   this side of the header asking to be read first. */
.header-revenue-caption {
  font-size: 10px;
  color: var(--muted);
  letter-spacing: 0.02em;
}

/* 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; }

/* ===== HEADER, ACCOUNT CHIP =====
   Settings and Log out were the bottom two rows of the rail, drawn exactly like
   the pages above them, so a rail of eleven pages read as one of thirteen. They
   are account controls, and this is where the header already kept one.

   NO BORDER AND NO FILL, unlike the theme switch it sits beside. That is a button
   you press for an effect and it says so with a box; this opens a panel that then
   explains itself, and two boxed controls side by side in a 64px header read as a
   toolbar rather than as a person's name. Pinned in cugate_channels.tests, because
   a chip drawn with a box renders, passes and looks wrong. */
.header-user {
  position: relative;
  flex-shrink: 0;
}

.header-chip {
  height: 36px;
  padding: 0 6px 0 4px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: 0;
  background: none;
  border-radius: 8px;
  color: var(--text);
  font-family: 'DM Sans', sans-serif;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  /* The chip is the <summary> of the panel below it, and a summary is drawn with
     a disclosure triangle of its own. It lands in front of the person's initial
     and reads as a bullet, beside the caret this control already carries. */
  list-style: none;
}

/* The same triangle under its older name. Not interchangeable with the line
   above: list-style drives the standard ::marker, and the WebKit still in
   circulation draws this pseudo-element instead and ignores list-style. */
.header-chip::-webkit-details-marker { display: none; }

.header-chip:hover { color: var(--text-strong); }

.header-chip:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* The one page the chip now owns the highlight for, since Settings no longer has
   a row to light up. Colour only: a fill here would be the box the chip is
   deliberately without. */
.header-chip.is-current { color: var(--primary); }

/* The only filled thing in the control, and it is a person's initial rather than
   a state, so it is the quiet tint the rail already uses for "you are here". */
.header-chip-avatar {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  border-radius: 6px;
  background: var(--primary-dim);
  color: var(--primary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
}

.header-chip-caret {
  color: var(--muted);
  font-size: 11px;
  line-height: 1;
}

/* Shown and hidden by the <details> it sits in, so this rule has no display of
   its own and no hidden attribute to honour: a shut disclosure does not render
   its contents, which is also what keeps Tab out of them. */
.header-menu {
  position: absolute;
  right: 0;
  top: 42px;
  min-width: 160px;
  padding: 6px;
  background: var(--menu-surface);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: 0 10px 30px var(--shadow);
}

.header-menu-form { margin: 0; }

.header-menu-item {
  display: block;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text);
  font-family: 'DM Sans', sans-serif;
  font-size: 13px;
  font-weight: 500;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.header-menu-item:hover { background: var(--surface-hover); }

.header-menu-item:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
}

.header-menu-item[aria-current="page"] { color: var(--primary); }

/* ===== 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;
}

/* A WIDE TABLE SCROLLS THE PAGE INSTEAD OF PUSHING IT, and this one line is what
   lets it. `.main` is a grid item of `.app`, so its automatic minimum width is
   `auto`, which resolves to its min-content size: the widest thing inside it. On
   any page carrying a wide table that is the table -- the track grows past the
   viewport, the header and the right-hand column run off the edge, and
   `.table-responsive`, whose only job is to scroll a wide table, never gets the
   chance because there is nothing left to scroll against.

   Measured at 1024 on the releases calendar without it: the document was 1172px
   against a 1024px window, `.main` was 962px in an 814px track, and the wrapper's
   scrollWidth equalled its clientWidth. With it: the document is exactly 1024,
   `.main` is 814, and the wrapper scrolls 962 inside 856.

   COMPOUNDED WITH THE GRID IT IS A CHILD OF, not written bare. A single `.main`
   rule is beaten by any later rule of the same weight with the page still
   rendering, and two of them already sit in the responsive block at the end of
   this file. It arrived scoped to `body.page-social` because the Social grid was
   the page that noticed; the fault was never that page's. */
.app .main { min-width: 0; }

/* ===== 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;
}

/* One column: the queue, with My checklist under it (Jan, 3 Jul: both are the
   viewer's personal surface, so they live in one strip).

   IT WAS TWO COLUMNS FROM 3 JULY TO 28 AUGUST, 2fr/1fr, with My checklist placed
   LEFT in the wide track (Kelsey, 3 Aug). It had the 1fr third, 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 -- so the bounded list took
   the third and the open-ended one took the rest.

   THE WHOLE STRIP IS HALF A PAGE NOW, so there is no wide track left to argue
   over. It moved into the fold on 28 August because the team uses it and it was at
   the bottom of the panel; splitting 660px again would put the checklist back in
   roughly the 283px that started the 3 August pass. Document order is the order:
   the queue is what needs doing, the private list sits under it.

   AND THE HAND PLACEMENT HAD TO GO WITH THE SPLIT, not after it. .attention-
   checklist was pinned to column 1 row 1 and .attention-main to column 2 row 1;
   left on a one-track grid those two rules put both blocks in the SAME cell and
   draw them on top of each other. The narrow breakpoint below already had to
   release them for exactly this reason and its comment says so. Held now by
   cugate_channels.tests.FoldIsTwoColumnsCssContractTests, because the failure
   renders as a column-height hole that no other test can see.

   ONE THING IS RECOVERED BY THE COLLAPSE. The two-column form put the tab order
   the wrong way round on desktop: the queue is plain links and the checklist is
   about forty focusable controls, and the checklist rendered to the LEFT of the
   column reached first. Stacked in document order, tabbing meets them in the order
   they are read. */
.attention-columns {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 8px 28px;
  align-items: start;
}
/* MIN-WIDTH ZERO IS LOAD-BEARING, not tidying. A grid item's automatic minimum is
   min-content, and the queue's rows carry a nowrap date chip and a nowrap need
   tag, so without this the track can be forced wider than the column it sits in
   and take the width out of the fold's other half. 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;
}
.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;
}

.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;
}

/* A page whose one action sits in the header row beside the title, rather than on
   a line of its own under it (Campaigns, 26 Aug). Compound selectors on purpose: a
   bare .page-head-split would be one more single-class rule for a later one in
   this file to beat silently. */
.page-head.page-head-split {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.page-head.page-head-split .page-head-text {
  min-width: 0;
}

.page-head.page-head-split .page-head-action {
  flex: none;
  margin-top: 2px;
}

.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. */
/* One to a row, same reason as .today-list and on the same date: the column is
   half the page now. This list has less margin for error than that one, because
   an objective prints two figures with a label under each on one line, and a
   target date is the first thing to wrap. */
.objective-list {
  list-style: none;
  margin: 0 0 32px;
  padding: 0;
  display: grid;
  grid-template-columns: 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;
}

/* ============================================================
   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 everyone else is waiting for.

   Two equal columns. Your work on the left because it is the viewer's own ranked
   queue; on the right, Also waiting on you above Your objectives. The right-hand
   pair is one wrapper in one track rather than two more tracks: three columns
   across this page is three narrow ones, and the two on the right are read
   together anyway (what other people need, then what it is all in service of).

   EVERY ARRANGEMENT HERE HAS BEEN FIXING THE SAME FAULT, which is that whatever
   goes last is not read. The objectives sat below the fold when the two sections
   stacked. Also waiting on you sat below the fold when the sections paired their
   cards instead -- and that one is worse, because it is the block the rest of the
   team works out of rather than the viewer's own. The client's words on 28 August:
   "other team members use that section and currently buried at the bottom."

   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.
   ============================================================ */
/* Two equal columns, and the lists inside them stop pairing their cards.

   THE THREE ARRANGEMENTS THIS HAS HAD, because the third is not a return to the
   first. Side by side at 3fr/2fr, to put what to do and what it is for in one
   glance. Then one full-width track with the two sections stacked and each list
   pairing its cards two to a row, which kept both above the fold and gave an
   objective's figures the width they need (1,923,589 against 4,237,288 could not
   pair inside a 2fr track). Now two EQUAL tracks, because a third region joined
   them on 28 August: Also waiting on you, which had been full width at the bottom
   of the panel where the team could not find it. It is the top of the right-hand
   column now, with the objectives under it.

   EQUAL RATHER THAN 60/40 because the right-hand track carries two blocks. The old
   split gave the work the wide side since it was the only actionable column; the
   right side is now the strip the rest of the team works out of plus the
   objectives, and a 40% track holding My checklist's free-text items is the exact
   wrapping the 3 August pass removed.

   AND THE PAIRING GOES WITH IT. Two cards to a row inside half a page is about
   320px a card, which is a video title down a column of single words. Both lists
   are one to a row; the two changes are one decision and
   cugate_channels.tests.FoldIsTwoColumnsCssContractTests holds them together. */
.call-sheet-fold {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 40px;
  align-items: start;
}

.fold-main, .fold-side { min-width: 0; }

/* YOUR WORK IS A BARE RULED LIST, exactly like Your objectives beside it, and the
   .work-zone class on the section draws nothing at all. The name is there for a
   reader of the markup, next to the aria-label a screen reader announces.

   IT WAS THE PAGE'S ONE FILLED BLOCK from 7 August (#105) to 28 August: a cyan
   field, held in :root rather than in a theme block so it stayed one colour through
   a theme switch; opaque cards, because the dark theme's --surface is translucent
   and picked the cyan up through them; dark eyebrow ink, because the eyebrow sat ON
   the fill; and a gold Open task control, because gold was the one colour that could
   win against a cyan field without borrowing cyan. Four rules and three tokens, each
   one there only because of the one above it.

   The client put the morning digest mail beside the block and asked for the mail's
   shape instead: a white page, a card with a one-pixel border, one filled button per
   row in #037A9A. That mail is painted from this file's own tokens, hex for hex, so
   what he asked for is the base .today-row on --surface and --border with the app's
   own .btn-dark on it. The app is matching itself, and the way to keep it matching
   is to add nothing here.

   cugate_channels.tests.WorkZonePlainCssContractTests holds that: no rule whose
   selector names .work-zone may paint a ground or reach into the control, and the
   three tokens may not come back. */

/* ============================================================
   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 three things instead: the
   section's position, a filled control on every row, and the card the row sits in.

   IT NO LONGER COMES FROM THE TYPE, and that is the 28 August change. The headline
   was the largest display face in the body of the page, Barlow Condensed 800 at
   24px, and the client read it beside the digest mail: "the your work bars in the
   website have the title way too powerful." The mail sets the same fact in DM Sans
   600 at 16px, and that mail is drawn from this file's own tokens hex for hex, so
   the rule below is the app matching itself rather than taking a new look.

   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. The
   gap between the two is 16px against 10px now rather than 24 against 10, which is
   less shout and the same ordering -- and the label is uppercase at 0.12em tracking
   while the headline is neither, so they do not read as one line at one size.

   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.
   ============================================================ */
/* One to a row. It paired them two across while this section was the full width
   of the panel; the section is half the page since 28 August, so a pair is two
   cards of about 320px, holding a video title and a filled control side by side.
   THE REASON FIRST WRITTEN HERE HAS GONE: it was that a 24px Barlow Condensed
   headline does not survive 320px, and the headline came down to 16px the same
   afternoon. The rule stays because the cards were never only their headline and
   because nobody has asked for a pair, not because that sentence still holds.
   minmax(0, 1fr) rather than 1fr is still load-bearing on one track:
   a grid item's default min-width is auto, so one long unbroken title would push
   the track wider than its share of the fold and squeeze the column beside it. */
.today-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
}

.today-row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  /* The mail's own row inset, read off .mail-item-main in
     templates/notifications/digest_email.html rather than approximated: 14px 18px.
     It was 16px 18px, set under a 24px headline that needed the room. The
     horizontal 18px was already the mail's; only the vertical moves.

     THE CARD IS STILL TALLER THAN A MAIL ROW AND ALWAYS WILL BE. A mail row is an
     eyebrow and a title; this one carries a third line under them -- the channel,
     the date chip, the hand-off sentence -- which is the reason the inset is 14px
     and not the mail's exact stack. The inset is the part that can match, and does. */
  padding: 14px 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.

   THE TRUNCATED HEADLINE'S TOOLTIP DEPENDS ON IT SITTING THERE: this is the element
   a hover lands on, so the title attribute is written here and not on the paragraph. */
.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;
}

/* The row's headline, set the way the digest mail sets its row title: DM Sans at
   600, 16px, on a 1.35 line. Those three are the mail's values, not new ones.

   NO letter-spacing DECLARATION, which is a deliberate omission rather than one.
   The rule carried -0.01em, which a condensed display face at 24px wants and DM
   Sans at 16px does not, and there is nothing to override: measured on the rendered
   page, no ancestor of this element sets letter-spacing anywhere in this file, so
   the computed value with the declaration gone is the initial "normal". Writing
   "letter-spacing: 0" would be a declaration that changes nothing while looking
   like it is holding something off.

   ONE LINE, AND THE REST CUT OFF ("can we try to truncate the main title because
   it is so long. make it only one line and cutt off the rest?", the client, 28
   August, with this branch open in front of him). The titles on these channels run
   past eighty characters and still wrapped at 16px, on a card that carries three
   more lines under the title.

   It is one behaviour in three declarations and it needs all three: nowrap stops
   the break, hidden clips what is then too wide, and the ellipsis is the only sign
   the reader gets that anything was clipped. It also depends on a fourth, two
   hundred lines up -- .today-row-body's min-width: 0 -- because a flex item refuses
   to shrink below its content by default and there would be nothing to overflow.

   overflow-wrap IS GONE, and its removal is the same decision. It was here so that
   one unbroken eighty-character string wrapped rather than widening the card, and
   nowrap suppresses every break in this element, so the property now describes
   line-breaking behaviour that cannot happen. The whole title is still on the card,
   in the title attribute the template writes, so the cut costs the reader nothing
   they cannot get back. */
.today-row-what {
  font-family: 'DM Sans', sans-serif;
  font-weight: 600;
  font-size: 16px;
  line-height: 1.35;
  color: var(--text-strong);
  margin: 0 0 6px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.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 route the work takes, drawn across the card as the people on it. It was
   a sentence naming the next step ("Finishing this hands it to Kelsey."), and the
   client asked for the shape instead: "we should have a pipeline here in text. so
   Jan - Murat - Lado and the person currently with it is in white text".

   NOT THE .baton RAIL, although it is the same fact. That is a vertical rail of
   dotted stage markers built for the task page and the board's row headers, where
   the stage NAMES are what a reader is looking for. This card has room for one line
   and the names on it are people, so the two would have had to be one shape at the
   cost of both.

   EVERY SELECTOR HERE IS COMPOUND. A bare single-class rule in a file this length
   loses silently to any later rule of the same specificity, which has caught this
   project four times; .is-empty is the one that would have gone next, since the
   sheet already carries a .rel-cover-cell.is-empty for the release table. --- */
.today-row-pipeline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--muted-mid);
  margin: 8px 0 0;
}

/* "the person currently with it is in white text", which in the light theme is what
   --btn-ink on --primary is: white on teal. The pair rather than a literal white,
   because the dark theme's answer to the same fill is near-black, and a hard-coded
   white there is a label nobody can read on the app's own cyan. It is the treatment
   every filled button in the product already wears, so
   FilledButtonInkContrastCssContractTests measures this exact pair at 4.5:1 in both
   themes and nothing here needs to measure it again.

   A chip rather than a weight or a colour on the name. At 12px, in a line of three
   names, weight alone is the ladder the baton rail was rebuilt to get away from --
   the team could not tell a bold name from a bright one (demo call, 11 August). A
   filled shape is legible at a glance and reads as one state, not as a ranking. */
.today-row-pipeline .today-row-holder {
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--primary);
  color: var(--btn-ink);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* nowrap, and it is not cosmetic: a step with nobody on it is called "nobody yet",
   which is two words, and a line that wraps between them puts "nobody" at the end
   of one line and "yet" at the start of the next with an arrow beside it. */
.today-row-pipeline .today-row-stage { white-space: nowrap; }

/* The hole in the route, and the only thing on this line that takes a colour. A
   step nobody is named for is where the work will stop, which is the one part of a
   chain worth interrupting a reader for; if the plain steps were coloured too it
   would be one of four coloured names. */
.today-row-pipeline .today-row-stage.is-empty { color: var(--danger); }

/* A rung below the names either side of it. It separates them and is not one of
   them, and the template hides it from the accessibility tree for the same reason. */
.today-row-pipeline .today-row-sep { color: var(--muted); }

/* 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;
}

/* The one line under Others, which says what the rows below it are: finished work
   this reader is on no part of, that any manager may sign off. Set exactly like
   .today-empty above it, because both are the section speaking rather than a row.

   ITS OWN NAME RATHER THAN A REUSE OF THAT ONE. A sentence introducing five rows
   and a sentence standing in place of none are not the same fact, and sharing a
   rule means the day either one wants moving the other follows it silently. The
   margin is the only value that differs and it differs for that reason: this one
   has a list under it, that one has the next section. */
.today-note {
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted-mid);
  margin: 0 0 14px;
}

/* --- 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, the first of which is drawn in two shapes.

   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. What splits tier one into two shapes is only whether the line has
   a numeral to set large, which IS something layout gets to decide.

   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. They are drawn as CELLS IN THE STRIP now, beside the
   four standing figures, because a bad count and a standing count are the same kind
   of fact and were being drawn in two different components one above the other. See
   .ctoday-cell. The tile rules that used to live here are deleted rather than left
   unreferenced: an orphaned component in a five-thousand-line stylesheet is how the
   next person rebuilds the shape this change removed. --- */

/* TIER ONE, SECOND SHAPE -- bad news with nothing to count. "No metrics stored yet"
   has no figure to set at display size, so a tile would be a box with a hole in it.
   It is a sentence, and it takes the danger colour WHOLE, which is the one place on
   this page that happens: the rule below (colour the figure, not the sentence) exists
   because a decline line has a figure to carry the colour and a reading around it,
   and this line has neither. Unmarked it rendered down in tier three set exactly like
   "2 videos are booked", while the tab badge above counted it -- a red 4 over three
   red boxes. Sits with the tiles rather than with the sentences, so the loud tier is
   one block. --- */
.company-marked { max-width: 640px; margin: 0 0 18px; }

.company-marked-line {
  font-size: 16px;
  line-height: 1.6;
  font-weight: 600;
  color: var(--danger);
  margin: 0 0 8px;
}

.company-marked-line:last-child { margin-bottom: 0; }
.company-marked-line a { color: inherit; text-decoration: none; }
.company-marked-line a:hover { text-decoration: underline; }

/* 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. --- */



/* 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. */



/* 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-empty {
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted-mid);
  margin: 0 0 32px;
}

/* The fold collapses 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; the three regions
   stack instead, in the order they are read -- work, then the shared strip, then
   the objectives, which is document order and needs nothing said here.

   THE TWO LISTS USED TO BE NAMED HERE and no longer are. They paired their cards
   two to a row at full width and this put them back to one below 1100px; they are
   one to a row at every width since 28 August, when the fold took two columns and
   each section became half the page. The rule had stopped doing anything, and a
   dead override is how the next person concludes the pairing is still live. */
@media (max-width: 1100px) {
  .call-sheet-fold { grid-template-columns: minmax(0, 1fr); gap: 36px; }
}

@media (max-width: 860px) {
  .today-row { flex-direction: column; align-items: stretch; gap: 14px; }
  .today-row-open { align-self: flex-start; }
}

/* THE CARD HEADLINE IS NOT NAMED HERE ANY MORE. It was 21px, a step down from a
   24px desktop headline and correct only relative to it; the desktop headline is
   16px since 28 August, so the override would have had a phone shouting a title
   five points LARGER than a laptop says it -- an inversion no test and no review
   catches, only a render. Deleted rather than lowered: the desktop size is now the
   root size this file sets on html, so there is nothing left to step down from. The
   ordering is pinned in cugate_channels.tests, so it cannot come back larger. */
@media (max-width: 600px) {
  .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 (the claims board's tiles, Where we stand, 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; }
/* "How the advice has done": each rule's record, in the same table shape as the
   measurement panel directly above so a rule's row reads as part of this page
   rather than as a new kind of thing. The rule column is held wider than its
   neighbours because a rule's name is a short sentence and, where the rule has
   been wrong more often than right, a second line sits under it. Written
   compound, never as a bare .strategy-advice: the responsive block further down
   this sheet restyles .data-table cells, and a bare class is also the shape that
   loses silently to a later single-class rule in a file this long. Held by
   cugate_channels.tests.StrategyAdviceCssContractTests. */
.strategy-advice tbody th { min-width: 300px; }
.strategy-advice .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; }

/* --- 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. */
.release-filters,
.promo-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px;
  margin-bottom: 16px;
}
.release-filter,
.promo-filter { flex: 1 1 180px; max-width: 260px; margin: 0; }
.release-filter .form-label,
.promo-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,
.promo-filter-actions { display: flex; gap: 8px; }

/* THE TASKS FILTER ROW PARTED COMPANY WITH THAT SHAPE, and this is why it has its
   own rule rather than a modifier on the shared one. Those two pages hold two or
   three same-sized boxes, which is what a wrapping flex row is for. This one holds
   a search field and two rows of pills that run the width of the page, and in one
   flex context they wrapped against each other -- a 260px box orphaned at the left
   of a line, two long rows folding under it, and a Clear link that appeared and
   disappeared with the filter state, moving whatever sat beside it. Two blocks in a
   column instead: the search cannot wrap into the pills, and nothing below can push
   the line above it.

   The label wrapper (.task-filter, .task-filter .form-label) went with the label:
   the search has a placeholder and an accessible name now, so nothing on this page
   uses that class and a rule kept for it would be a rule for nothing. */
.task-filters {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
  margin-bottom: 16px;
}

/* The search field and its submit as ONE control: a single border and radius round
   both, so it reads as a search box with a button on the end rather than as a box
   and a button that happen to be adjacent. The divider is the field's own RIGHT
   border -- never a left one, which this app does not draw anywhere. */
.task-search {
  position: relative;
  display: flex;
  align-items: stretch;
  max-width: 520px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--bg-deeper);
  overflow: hidden;
}

/* Compound with .task-search so the shared field treatment is untouched
   everywhere else: inside the unit the container draws the edge, so the input
   must not draw its own. The left padding is the glyph's room. */
.task-search .form-control {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  border-right: 1px solid var(--border);
  border-radius: 0;
  background: transparent;
  padding-left: 36px;
}

/* The focus ring belongs to the whole unit, not to the field inside it: a ring
   drawn round the input alone stops halfway across a control that reads as one
   thing. Redrawn with :focus-within on the container, so a keyboard reader still
   gets the same signal the rest of the app's fields give. */
.task-search .form-control:focus {
  box-shadow: none;
}

.task-search:focus-within {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-dim);
}

.task-search-icon {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  width: 15px;
  height: 15px;
  stroke-width: 1.5;
  color: var(--muted);
  /* The glyph sits over the field, so a click on it has to reach the field. */
  pointer-events: none;
}

/* Flush into the unit's right-hand end: the container's own border and radius are
   the button's, so it drops both rather than drawing a second edge inside one. */
.task-search .btn {
  flex: 0 0 auto;
  border: 0;
  border-radius: 0;
  background: var(--surface);
}

.task-search .btn:hover {
  background: var(--surface-hover);
}

/* The two pill rows and the thing that clears them, stacked. The rows keep the
   treatment they shipped with; only what contains them changed. */
.task-filter-pills {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}

/* The Tasks page's Person and Status rows: every option a pill, every pill
   carrying the count of tasks its click would list, so the distribution is
   readable without opening anything. VIEW filters, not actions, so the active
   state is the seg-toggle's "you are here" tint (--primary on --primary-dim),
   never a solid button fill. Compound with .task-filters throughout: a bare
   single-class rule up here is one later group away from losing on source
   order, which this file has done four times. */
.task-filters .filter-pill-row {
  /* Full width of the block that holds it. It was `flex: 1 1 100%`, which was the
     same instruction while .task-filters was a wrapping ROW; in the column stack it
     now is, a flex basis is a height, and the row would have claimed the block. */
  align-self: stretch;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
/* The row label sits on the pills' own baseline, so the stacked-label margin
   the shared rule above sets would push the pills down for nothing. */
.task-filters .filter-pill-row .form-label { margin: 0 4px 0 0; }
.task-filters .filter-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  font-family: 'DM Sans', sans-serif;
  font-size: 12px;
  font-weight: 500;
  color: var(--muted);
  text-decoration: none;
  background: transparent;
}
.task-filters .filter-pill:hover { color: var(--text); background: var(--surface-hover); }
.task-filters .filter-pill.is-active {
  border-color: var(--primary);
  background: var(--primary-dim);
  color: var(--primary);
}
/* The count reads quieter than its label in either state -- colour is
   inherited, so the active pill's figure goes teal with it -- and tabular, so
   a row of pills lines its figures up. */
.task-filters .filter-pill .pill-count {
  font-size: 11px;
  opacity: 0.75;
  font-variant-numeric: tabular-nums;
}

/* --- Social (Promotion): the weekly entry grid --- */
/* The minimum width that used to sit here has moved to `.app .main` in the MAIN
   CONTENT section: a grid item whose automatic minimum is its own content is the
   layout's behaviour, not this page's, and every wide table in the app had it. */

/* Ten columns on one screen, nine of them a number box. A field sized for a
   release title would push the last three off the page, so the cells are tightened
   here rather than by giving the grid a form layout of its own. Compound with
   .data-table on purpose: a bare .promo-grid rule would sit above the responsive
   block at the end of this file and lose to it. */
.data-table.promo-grid td { padding: 8px 6px; vertical-align: top; }
/* THE GRID IS THE ONE TABLE HERE THAT MAY NOT BE SQUEEZED. `.table` is width:100%,
   so every other table in the app narrows its columns to fit and reflows the text
   inside them. A number box does not reflow: measured at 1024, "21400" rendered as
   "214" and "340" as "34", which is a person unable to read the figure they had just
   typed. So this one keeps its natural width, never narrower than the column it sits
   in, and the wrapper scrolls instead. */
.data-table.promo-grid { width: auto; min-width: 100%; }
/* THE ACCOUNT COLUMN IS NOT THE ONE THAT GIVES WAY. It has no minimum of its own,
   so nine number boxes that do took the width and left it the remainder: measured
   at 1440 with six accounts and every cell five digits, the column came out 113px
   and "CUGATE Classics" broke onto two lines, "CUGATE company page" and "Essential
   Classics shorts" onto three each. A name split down the middle of the word is
   the one thing on this page a reader uses to tell six rows apart.

   THE NUMBER IS MEASURED, not chosen. Those names want 155px at most on one line
   at this table's 13px/600 DM Sans, and the heading carries 12px of padding each
   side, so 200px holds the longest of them with 21px to spare. It is declared once
   here and read twice: the column takes it as a floor and the name inside takes it
   less that padding as a ceiling, so the width of the column and the point a name
   is cut at cannot drift apart.

   Compound with .data-table for the reason the cell rule above is: the accordion
   block at the end of this sheet restyles .data-table th[scope="row"] wholesale,
   and a bare .promo-grid rule up here would lose to it on source order. */
.data-table.promo-grid th[scope="row"] {
  --promo-name-column: 200px;
  min-width: var(--promo-name-column);
}
/* A box of its own, because the cut has to take the name and leave the platform
   line under it alone. All four declarations are one thing: nowrap alone overflows
   the cell, hidden alone cuts mid-glyph with nothing to say so, ellipsis does
   nothing without both, and none of them apply to an inline box. The ceiling is a
   length rather than 100%, which a table ignores while it works out how wide the
   column wants to be -- with a percentage the column simply grows to the longest
   name and nothing is ever cut. The whole name stays in the title attribute. */
.promo-grid .promo-account-name {
  display: block;
  max-width: calc(var(--promo-name-column) - 24px);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* Right-aligned like every other figure in the app, so the column reads as
   numbers rather than as text that happens to be digits. The error under a cell
   is a sentence and goes back to the left. */
/* THE 74px FLOOR DID NOT MOVE FOR THE COLUMN ABOVE, AND IT CANNOT. The obvious way
   to pay for those 200px is to take a few pixels off each of the nine boxes, and
   that was tried here first: at 66px all 36 boxes cut their figures at 1024, which
   is the "21400" rendered as "214" incident again in a smaller size. Measured on a
   real box in its own cell, walked down a pixel at a time, the narrowest that shows
   a figure whole is 70px for five digits, 79 for six and 88 for seven. Most of that
   is not the text -- "21400" is 38px of it -- but the room a number input keeps for
   its spin buttons, which no measurement taken outside the cell can see. So 74 is
   the five-digit minimum plus four, and the column above is paid for out of the
   width the table had spare instead: with the name held at 200 the grid asks for
   1000px and the page gives it 1136 at 1440, so nothing scrolls and nothing is cut.
   A six-digit figure does not fit this floor and did not before; raising it changes
   how far the table scrolls and belongs to whoever asks for it. */
.promo-grid .form-control { padding: 6px 8px; font-size: 13px; text-align: right; min-width: 74px; }
.promo-grid .field-error { text-align: left; }
/* The platform beside the account name, on its own line inside the row heading, on
   both Social tables. The two-line heading is the only thing telling a reader that
   "On TikTok" and "TikTok" are an account and its platform rather than two accounts.
   The roll-up half names .data-table like every other .promo-rollup rule here: bare
   it overrode nothing today, but it is the shape the source-order trap arrives in,
   a class with one bare rule offering itself as the place the next declaration gets
   added to, above the responsive block that restyles .data-table td at the same
   specificity. */
.promo-grid th[scope="row"] .muted,
.data-table.promo-rollup th[scope="row"] .muted { display: block; font-size: 12px; font-weight: 400; }

/* The roll-up's total line. A rule above it and weight, which is how this sheet
   already separates a table's frame from its data (.data-table thead th draws the
   same border under the headings). No fill: it is the same kind of number as the
   rows above it, added up. The border is horizontal, never down a side. */
/* The alignment is IN the group rather than in a rule of its own, because a second
   rule for the same cell at the same specificity is the source-order trap: the one
   below is deliberate and is the only one there should be. The sheet's own left
   alignment is written for `tbody th`, so without this a foot row falls through to
   the UA's centred default and the total's label sits half a column in from every
   account name above it. Read off the render, not off the source. */
.data-table.promo-rollup tfoot th,
.data-table.promo-rollup tfoot td {
  padding: 10px 12px;
  font-size: 13px;
  border-top: 1px solid var(--border-strong);
  font-weight: 600;
  color: var(--text-strong);
  text-align: left;
}
.data-table.promo-rollup tfoot td { text-align: right; }

/* The two rules under each Social table, as plain muted lines. They were
   .placeholder-note, which is the fixture-data chip: a tint, an outline, an italic
   and inline-block. Inline-block is what made it wrong here rather than merely
   heavy. A short note shrinks to its content, so once the old paragraph was split
   into two sentences the pair stopped filling the row and sat side by side in two
   outlined boxes, reading as a pair of buttons somebody was meant to press.

   NO COLOUR IS INTRODUCED: --muted-mid is what .muted, .page-sub and the chip
   itself all read, so the line is quieter than the table above it and the palette
   is unchanged.

   SCOPED TO THIS SECTION by the body class base.html provides. Twelve other templates draw a .placeholder-note and the chip is right on
   every one of them; this is not a change to that class, it is a second, plainer
   note that only these two pages ask for. The compound selector is also what keeps
   it from being beaten in silence by a later single-class rule of the same
   specificity, which this sheet has been caught by four times. Asserted, both
   halves, in promotion/tests_notes.py.

   MARGINS BECAUSE THE RESET ZEROES THEM: air off the table or the Save button
   above, and less of it between two lines that belong together. */
body.page-social .promo-note {
  font-size: 12px;
  color: var(--muted-mid);
  letter-spacing: 0.02em;
  margin-top: 16px;
}

body.page-social .promo-note + .promo-note {
  margin-top: 4px;
}

/* The rule lines under each prospect table, drawn exactly as the Social ones above
   are and for the same reason: they are short statements of fact about how a figure
   was derived, not the fixture-data chip .placeholder-note paints. Inline-block is
   what makes that chip wrong here -- a short note shrinks to its content, so a pair
   of them sit side by side in two outlined boxes and read as buttons.

   NO COLOUR IS INTRODUCED: --muted-mid is what .muted, .page-sub, .promo-note and
   the chip itself all read, so the line is quieter than the table above it and the
   palette is unchanged.

   SCOPED TO THIS SECTION by the body class base.html provides, and COMPOUND rather
   than a bare .prospect-note, which is the half that actually matters: a single-class
   rule in a sheet this long is beaten in silence by any later rule of the same
   specificity, with the markup, the class and the words all still correct. That trap
   has caught this stylesheet four times. Asserted, both halves, in
   prospects/tests_page_css.py.

   ONE PAGE OF THE TWO DRAWS THESE INSIDE A .panel, so the top margin is the air off
   a table rather than off a section. */
body.page-prospects .prospect-note {
  font-size: 12px;
  color: var(--muted-mid);
  letter-spacing: 0.02em;
  margin-top: 16px;
}

body.page-prospects .prospect-note + .prospect-note {
  margin-top: 4px;
}

/* --- 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).

   THE INK IS --btn-ink AND IT USED TO BE --bg-deeper, which is the page's deepest
   background rather than anything about a button. In the dark theme that reads
   near-black on cyan and is right; in the light theme it is #d8eaf4 on #037A9A,
   3.99:1, and this label is 12px at weight 600, so AA wants 4.5:1 and did not get
   it. That is every filled button in the product, not one block's control. The
   ratio is now computed in
   cugate_channels.tests.FilledButtonInkContrastCssContractTests, in both themes,
   so a later palette change fails with a number instead of passing by eye. */
.btn-dark {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--btn-ink);
}

.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; }

/* A field whose widget is a GROUP of inputs is wrapped in a fieldset by the shared
   form template, so its legend can name boxes no single `for` could point at. Left
   bare, the UA draws a groove border and its own padding round it, which would put
   a box around one field on a form where nothing else has one.

   SCOPED TO THE CLASS, and that is the whole point of the class existing. Written
   as `.form-panel fieldset` this rule is specificity 0,1,1, and the release form's
   own `<fieldset class="tv-group">` sits inside a `.form-panel` too -- so the reset
   outranked `.tv-group` (0,1,0) from 2,000 lines above it and stripped the border,
   the padding and the bottom margin off a group that is drawn boxed on purpose. A
   later rule cannot win that argument; only a selector that does not match it can.
   Contract-tested in `cugate_channels.tests.CampaignPanelCssContractTests`, against
   the class the release template actually uses. */
.form-panel fieldset.field-group {
  min-width: 0;
  border: 0;
  margin: 0;
  padding: 0;
}
.form-panel legend.form-label { padding: 0; }

.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;
}

/* Roadmap moved here off the rail. Underlined rather than coloured: the footer is
   the quietest line on the page and a brand-coloured link in it reads as a call to
   action, which a static reference list is not. */
.app-footer a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.app-footer a:hover { color: var(--text); }

/* ===== 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; }
  /* THE CHIP KEEPS ITS PLACE AND LOSES ITS NAME. On a phone it is the only way to
     Settings or out of the app -- Log out left the rail on 26 Aug -- so it cannot
     be dropped the way the revenue block above is.

     Measured off a render rather than guessed: the hamburger is 40, the wordmark
     142, the theme switch 36 and the chip 92 with the name on it, which with this
     block's 16px padding and four 24px gaps wants 462px of header and does not get
     it. Without the name the chip is 49 and the row needs 347, which fits the
     narrowest phone anybody here carries. The gap that gets it there is set on the
     .header rule further down THIS SAME BLOCK, not on a second one here: a bare
     .header rule above it loses to that one on source order and does nothing,
     which is how this stylesheet has gone wrong four times. */
  .header-chip-name { 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); }
  /* The drawer already prints every page name, so there is nothing here for the
     expand control to do -- and pressing it would close the drawer, since every
     other .nav-item in the rail is wired to. */
  .nav-expand { display: none; }
  .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;
  }
  /* The drawer prints every page name already, so it keeps the headings and the
     accordion with them -- the same list, at the width the labels are inline. */
  .nav-scroll,
  .nav-group-items { align-items: stretch; }
  .nav-scroll { overflow-y: auto; }
  .nav-item.nav-group {
    display: flex;
    align-items: center;
    height: auto;
    min-height: 34px;
    padding: 9px 12px;
    margin-top: 6px;
  }
  .nav-item.is-child { padding-left: 24px; }
  .nav-item.is-child svg { width: 16px; height: 16px; }
  .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; }
  /* HIDDEN MEANS HIDDEN IN THIS BLOCK TOO. The rule above is an author declaration
     and [hidden] is a UA-stylesheet display: none, so from here down every
     <tr hidden> in a data table drew: the video page's work drawer, shut on arrival
     at 1400px, was open under every row at 375px with its caret still saying
     aria-expanded="false", and the caret could not shut it because video-work.js
     only writes the attribute this block had already overruled. The same trap the
     .mychecklist-row rules further down name twice, with the same answer: a rule
     that names the attribute, in the block that set the display. Written AFTER the
     card rule so the exception reads after the rule it is an exception to; the
     attribute outranks the bare tr on specificity whichever way round they sit.
     Held by RowCardHiddenRowCssContractTests. */
  .data-table tr[hidden] { display: none; }
  .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); }
  /* The Social grid's account name gives its ceiling back here. Up there it is a
     column in a ten-column table and a long name has to be cut so the nine number
     boxes keep their width; down here the row is a card with the width of the
     screen and nothing to protect, so a cut name would be a cut for no reason.
     MEASURED at 500: "Essential Classics shorts" was ellipsised inside a card with
     280px going spare. */
  .promo-grid .promo-account-name { max-width: none; }
  .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; gap: 12px; }
  /* 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; }
}

/* ===== FLASH NOTICES (django.contrib.messages) ===== */
/* THE ONE BLOCK. There were two, 100 lines apart, and the browser drew neither of
   them whole: the later one set `border: 1px solid var(--border)` plus a 3px
   `border-left-width`, which at equal specificity took the earlier one's
   `border-color` AND its background tint, so the per-level colouring here was dead
   and a 3px stripe down the left edge was the only thing telling an error from a
   success. Measured in Chrome, not inferred. That stripe is also the one decoration
   this project does not draw anywhere.

   So a level is said twice, in the full border and in a wash of the same colour
   behind the words. 14% is the tint the error rule already carried; all three levels
   share it so none shouts over the others, and it is faint enough that --text stays
   the right colour on it (a filled bar would need its own).

   ONLY THE LEVELS THE APP EMITS HAVE A RULE, and a test holds the two sides
   together: `messages.error` (5 call sites), `messages.info` (2) and
   `messages.success` (8) are the whole of it. The counts moved on 27 August, when
   every act on a task gained a sentence; the test reads them off the code, so a
   number here that has gone stale is prose rather than a broken contract. `.alert-danger` went because no Django
   tag produces "danger" -- the class is only ever built from
   `alert-{{ message.tags }}` -- and `.alert-warning` went because nothing emits a
   warning and this palette has no warning colour: `--positive` and `--warning`, which
   the two old rules named, are defined in neither theme block and were falling back
   to literals (#2faf5a, #d99b1f) that appear nowhere else in the app. Gold is not
   available for it either, see the palette note at the top of this file. The day a
   warning is emitted, the contract test says so and asks for a colour decision. */
.message-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }

.alert {
  font-size: 14px;
  padding: 10px 14px;
  border-radius: 8px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
}

.alert-info {
  border-color: var(--primary);
  background: color-mix(in srgb, var(--primary) 14%, transparent);
}

/* --up, not a green literal: it is the app's "a rise" colour, defined in both themes
   and picked by contrast measurement against --danger, so good news here carries the
   same weight as bad news does. */
.alert-success {
  border-color: var(--up);
  background: color-mix(in srgb, var(--up) 14%, transparent);
}

.alert-error {
  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; }
/* 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; }

/* RENAME IN PLACE (Kelsey, 28 Aug: fixing a typo meant deleting the item and
   typing it again). The stored title and the field that replaces it share the
   row's middle track, so the row keeps its three columns whichever one is
   showing; stacked rather than side by side because without JS both are on
   screen at once and a field beside the text it repeats reads as two items.
   min-width: 0 for the reason .attention-checklist has it -- a grid item's
   automatic minimum is min-content, and a long reminder would otherwise push
   the switch and the buttons out of the column.

   EVERY RULE BELOW NAMES THE ROW, all five of them. A bare .ct-edit in a sheet
   this long is beaten by any later rule of the same specificity and the page
   goes on rendering with the declaration doing nothing, which is how four
   earlier incidents in this file went. */
.mychecklist-row .mychecklist-title { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
/* HIDDEN MEANS HIDDEN IN THIS ROW. The rename field and the Rename button ship
   with the attribute, and it is worth nothing on its own: [hidden] is a
   UA-stylesheet display: none, .form-control declares display: block and .btn
   declares display: inline-flex, so both would go on showing and the row would
   be back to a text box under every title with the markup looking fixed. A
   class plus an attribute outranks the single class on each of them, and this
   is what the script opens and closes the field by setting. */
.mychecklist-row [hidden] { display: none; }
/* The rename control: a pen, drawn exactly like the remove x it sits beside
   (Jan, 29 Aug: "have a pen icon which then turns it into editable"). It was
   the word EDIT in small caps until then, which is why this rule still carries
   line-height but no longer any of the type properties -- there is no type in
   it. Only the hover colour differs from the x: removing an item is the
   destructive one and turns red, renaming it turns to the accent.

   NO DISPLAY DECLARATION HERE, ON PURPOSE. [hidden] is a UA-stylesheet
   display: none and ANY class rule setting display beats it -- the trap that
   put style.display on the Save button beside this one. The pen ships visible
   but is switched off in two places that both have to win: while the field is
   open, and for a reader with scripting off, for whom it is a button that can
   do nothing. The icon inside it carries the display instead. Held by
   ChecklistRenameMarkupTests. */
.mychecklist-row .ct-edit { background: none; border: 0; cursor: pointer; padding: 0 4px; line-height: 1; color: var(--muted); transition: color .15s ease; }
.mychecklist-row .ct-edit:hover { color: var(--primary); }
/* Sized on the button and not in the markup, the way the rail sizes its icons.
   display: block because an inline svg sits on a text baseline and carries the
   descender space under it, which would leave the pen riding above the x. */
.mychecklist-row .ct-edit svg { display: block; width: 14px; height: 14px; stroke-width: 1.6; }
/* A refused rename stays open with the typed text still in it, so it is fixed
   rather than retyped; the border is the only thing that says the server said
   no. Same declaration the inline release editor marks a bad field with. */
.mychecklist-row .form-control.is-invalid { border-color: var(--danger); }

/* ============================================================
   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); }
/* The same link's clothes worn by several: a trail of them (Memo, 9 September: "the back buttons going back from one cascaded issue to the former") is one nav, so the colour and the hover sit on the anchors inside it rather than on the element. */
nav.back-link a { color: inherit; text-decoration: none; }
nav.back-link a:hover { color: var(--primary); }
nav.back-link span { margin: 0 2px; }
.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; }

/* The per-video list carries a fourth cell, the languages the video already
   ships in, so its rows get one more column -- scoped to .bar-list.pervideo so the
   traffic-source list and the age ladder keep the three they have. Every rule here
   is compound for that reason: a bare .bar-row.wide added later in this file would
   take the whole app's bar rows with it. The head row holds the column label alone
   (a bar list has no thead), placed in the fourth column rather than padded out
   with three empty cells, which would draw an empty track.

   THE FOURTH COLUMN IS A FIXED WIDTH, not auto, and that is the whole of why it
   is written this way. Each row is its own grid, so the columns line up across
   rows only where their edges are fixed: with an auto last column the revenue
   figures sat at a different x in every row, because each one was pushed by the
   width of the codes beside it. A fixed last column puts the money's right edge
   back on one line and lets a long list of codes wrap instead. */
.bar-list.pervideo .bar-row.wide { grid-template-columns: 220px 1fr auto 132px; }
.bar-list.pervideo .bar-lang { font-size: 12px; color: var(--muted-mid); text-align: right; }
.bar-list.pervideo .bar-head .bar-lang {
  grid-column: 4;
  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;
}

/* Same codes in the description manager's table. Muted, because the title is
   what a reviewer scans and the codes sit beside it. */
.data-table td.video-languages { color: var(--muted-mid); }

/* 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; }
  /* Compound, and after the base rule in source order: the narrow template has
     to out-rank .bar-list.pervideo .bar-row.wide above, which a bare
     .bar-row.wide cannot do. */
  .bar-list.pervideo .bar-row.wide { grid-template-columns: 120px 1fr auto 96px; }
}
@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. --- */
/* THE TWO CARDED PANELS DRAW NO FRAME OF THEIR OWN, and that is a decision taken
   at the render rather than in the abstract. .panel is a filled, bordered box; the
   queue used to add a teal ring to it and the results an amber one, which was the
   right answer while their contents were a hairline list. With the rows carded it
   became a ring round a box round a stack of ringed cards -- three frames deep --
   and worse, the cards take var(--surface), which is the panel's own fill, so the
   thing that was supposed to stand out was the same colour as what it stood on.

   Taking the panel's fill and border away puts the cards on the page ground, which
   is exactly where the dashboard's .today-row sits and the reason that treatment
   reads there. The queue carries no heading (deliberately: it opens the page), so
   what marks it out now is the cards themselves.

   THE RESULTS PANEL KEEPS ITS COLOUR, moved onto the cards. "Marked out from Your
   move by colour rather than size" is still the rule -- the loop is not finished
   when something ships, it is finished when somebody has read what it did -- and
   the accent says it on the row instead of round the group. */
.panel.yours,
.panel.results {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  padding: 0;
}

.movelist {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Two surfaces draw a .move and they are not the same kind of thing, so only the
   layout is written bare here. The Tasks page's own work -- the reader's queue and
   the results waiting to be read -- is a stack of CARDS (below); the task-event
   history on the task page and the edit form is a compact log and stays a hairline
   list (.movelist-log). The inset and the border used to sit in this rule, with
   :first-child and :last-child correcting its ends, and all three of those are gone
   rather than overridden: a bare single-class rule plus a same-weight compound one
   is decided by source order, and this file has lost four rules that way. */
.move {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
}

/* THE HISTORY LOG. A dozen "field: old -> new" lines is a record to scan, not a
   dozen things to act on; carding them would be bloat, and the client asked for
   cards on the queue, not on an audit trail. */
.movelist-log .move {
  padding: 16px 0;
  border-bottom: 1px solid var(--border);
}

.movelist-log .move:last-child {
  border-bottom: none;
}

.movelist-log .move:first-child {
  padding-top: 4px;
}

/* THE TASKS PAGE'S OWN WORK, AS CARDS. Jan, looking at the live page: "here it just
   has tasks bleeding into each other so suggest cards for each task." Three queue
   items separated by a hairline read as one wall of text, and a queue item is not a
   line in a list -- it carries a form, a hand-in block and its own buttons.

   PORTED FROM THE DASHBOARD'S OWN "Your work" CARD (.today-list / .today-row), not
   designed again here: same inset, same gap, same tokens. The two surfaces show one
   person the same work, and two treatments of it is two products.

   FULL WIDTH, ONE PER ROW, and that is not the Work Pack's masonry. These rows hold
   a five-row textarea, hint paragraphs and a stacked button; packed into half-width
   columns the textarea lands in a gutter. The tiles above pack because they are
   uniform and compact, which these are not.

   The results panel is carded too although nobody asked: it sits directly under the
   queue on the same page, and a hairline list immediately below a stack of cards
   reads as something that was missed rather than as a decision. */
.panel.yours .movelist,
.panel.results .movelist {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
}

.panel.yours .move,
.panel.results .move {
  padding: 14px 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.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; }
/* The results card's own button and, beside it, the way into the task. */
.panel.results .move-do { display: flex; gap: 10px; }

/* 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.

     COLOR CARRIES ONE FACT: who holds this now. It used to carry two, and the
     team could not read it (demo call, 11 Aug: "two names are white and others
     muted, not clear who is responsible now"). The cause was this ladder: a done
     step took --text and the current one --text-strong, which are #d8eef8 and
     #f0f8ff on dark. At 11px that is the same color twice, so a finished step and
     the live one both read as a white name, and the assignee beside them took
     --text as well. Done now sits with pending at --muted-mid, the live step is
     bold but not brighter, and the assignee is the only thing at full strength.
     Reached-vs-not is not lost: the marker below carries it, filled against
     hollow, and a shape reads at 11px where a 4% lightness step does not. */
  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. The marker is the ONLY thing separating done from pending now, which is
   why it is not decorative. */
.baton-step::before {
  content: "\25CB";
  margin-right: 4px;
  font-size: 9px;
  vertical-align: 1px;
}

.baton-step.is-done {
  /* Deliberately the same color as a pending step. See the ladder note above:
     the filled marker says it is behind us, and color is reserved for now. */
  color: var(--muted-mid);
}

.baton-step.is-done::before {
  content: "\25CF";
  color: var(--muted-mid);
}

/* The live stage, in --text rather than --text-strong. It is a STAGE marker, and on
   the Tasks board it is not the answer to "who has this": the assignee sits two
   elements along the same line at --text-strong, and it is the only thing on that row
   at full strength.

   THE OTHER FOUR SURFACES DO NOT READ THAT WAY, which is what keeps this rule at
   --text rather than letting it drop further. The baton is drawn in six places --
   tasks/list.html, dashboard/overview.html three times, dashboard/strategy.html, and
   tasks/detail.html as a vertical rail -- and the board is the only one that puts a
   person beside it ON THE SAME LINE. The rail stacks them instead, which is why it
   may take weight where this rule cannot: nothing competes with it across a row, and
   its current step now names the task's actual holder rather than the configured
   one, so the two places a person appears on that page agree. The tables put the baton
   inside the row header and the holder in a plain cell of its own, and two of them,
   "Nobody has this work" and the waiting table under it, carry no owner column at all,
   deliberately and for reasons stated in that template. On those rows the only person
   named anywhere is the one written into these steps.

   So the live step has to be findable on its own, and at 11px colour cannot do that
   alone. Weight 700 and the cyan filled marker are what carry "here" on all five: one
   rung above the --muted-mid either side of it, one below a name the board states
   outright, and never a second bright name beside the first. */
.baton-step.is-current {
  color: var(--text);
  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; }
/* The title is the row's permanent link (Ilya, 12 Aug) and is drawn exactly as it
   was drawn as text: it is the most-read thing on the board, and colouring 60 rows
   of it as links would turn the page into a wall of accent. The affordance is on
   hover and on focus instead, where a keyboard reader also needs the ring. */
.c-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text-strong);
  line-height: 1.4;
  text-decoration: none;
}
.c-title:hover { color: var(--primary); text-decoration: underline; }
/* The label's own number, ahead of the title on the same line. Quiet rather than
   coloured, exactly as .move-ref is on the queue card: the number is the channel's
   word for the row and the title is ours, and a number drawn at the title's weight
   would compete with the thing it names. The line's own column-gap spaces it, so
   it carries no margin. */
.c-ref {
  font-size: 13px;
  color: var(--muted);
}
.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; }
/* WHO HAS THIS NOW. The one thing on a board row at full strength, because that is
   the question the row exists to answer and the team could not read it on 11 Aug.
   Everything else on line two, the stage baton included, sits a rung below. */
.c-person { font-weight: 700; color: var(--text-strong); }
/* 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);
}

/* WHY THIS ROW EXISTS, one click down (Kelsey, 13 Aug: "can we make the tasks
   expandable so we can get preview of the task before deciding to edit, claim or
   cancel"). A native <details>, so it needs no script and cannot break without
   one, and closed by default so the board stays scannable -- the client's own
   ruling on 7 Jul, when notes went onto the listing itself.

   The marker is a chevron of our own drawing rather than the browser's triangle,
   which differs per platform and is the wrong weight beside 9px tags. */
.c-why { margin-top: 6px; }
.c-why-open {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  list-style: none;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.c-why-open::-webkit-details-marker { display: none; }
.c-why-open::before {
  content: "";
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--transition);
}
.c-why[open] .c-why-open::before { transform: rotate(45deg); }
.c-why-open:hover { color: var(--text-strong); }
/* pre-wrap because notes are free-form and a hand-over appends dated lines to
   them; anywhere-wrapping because somebody pastes a URL into them.

   BOUNDED, and that is the difference between a preview and the page. A real
   note on Fireplace Classics runs to a corrected title, a publishing
   instruction, a full YouTube description and a twenty-nine-track listing --
   opened inline it pushed every other row off the screen, which is the same
   complaint (a bloated row) that kept notes off this list in the first place.
   Kelsey asked for a preview, so it is one. Nothing is cut off: the block
   scrolls, and the whole thing is one click away on the task's own page, which is
   where the note is read rather than glanced at. The difference between the two is
   not the height -- the page clamps at 15rem, barely more than this -- it is that
   the page carries a control that opens the note in full and a row cannot. */
.c-why-text {
  margin: 8px 0 0;
  max-width: 68ch;
  max-height: 12.5rem;
  overflow-y: auto;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-strong);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.c-why-more {
  display: inline-block;
  margin-top: 6px;
  font-size: 12px;
  color: var(--primary);
  text-decoration: none;
}
.c-why-more:hover { text-decoration: underline; }

/* 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;
  }
}

/* ===== THE WORK PACK =====
   Memo's dated sheet of numbered cards, rendered as a wall of small expandable
   tiles above the channel board (design-research/task-cards/option-c.html, the
   client's call on 31 August). Ported from that mockup with its own :root left
   behind: every colour, radius and easing below is one of this app's tokens, so
   the tiles follow the theme switch like everything else on the page.

   TWO COLUMNS, AND CSS MULTI-COLUMN RATHER THAN A GRID. A two-track grid shares a
   row height between the pair sitting in it, so opening one <details> stretches
   the row and leaves a hole beside the tile that did not open -- on a wall of
   eighteen, that is a page of holes. Multi-column packs instead: each tile is as
   tall as its own content and the next one starts underneath it. `grid-template-
   rows: masonry` would say this more directly and is not shipped widely enough to
   depend on, and doing it with script would cost the page the one property that
   makes these tiles work at all, which is that they need no script.

   TWO THINGS THAT FOLLOW FROM COLUMNS, both intended. Reading order is column-
   major -- C1 down the left, then the rest down the right -- which is how a printed
   sheet of cards reads anyway. And opening a tile reflows the columns, so tiles can
   move from one to the other; that is what packing means, and suppressing it would
   mean giving up the packing.

   `break-inside: avoid` on the tile is load-bearing, not tidiness: without it the
   browser is free to cut a tile across the column boundary and print its top half
   at the foot of the left column and its criterion at the head of the right one.
   The contract test in cugate_channels.tests pins it. */
.wp-section {
  margin-bottom: 8px;
}

.wp-grid {
  columns: 2;
  column-gap: 12px;
}

.wp-tile {
  break-inside: avoid;
  width: 100%;
  /* The column gap's vertical twin: `row-gap` does not apply to a multi-column
     flow, so the space between stacked tiles is the tile's own bottom margin. */
  margin-bottom: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* An open tile is holding the reader's attention, so its edge comes up one step.
   A full border, never a left stripe -- the house rule, and the board's own tags
   directly above obey it too. */
.wp-tile[open] {
  border-color: var(--border-strong);
}

.wp-tile-summary {
  display: block;
  cursor: pointer;
  /* Both spellings, because the two engines hide the disclosure triangle
     differently and the chevron below is our own -- the same pair .c-why-open
     already sets on the board row. */
  list-style: none;
  padding: 14px 16px;
}

.wp-tile-summary::-webkit-details-marker {
  display: none;
}

/* The id, then the state chip, then the chevron pushed to the far end. It was
   justify-content: space-between with two children; a third would have spread all
   three across the tile and left the chip floating in the middle of nothing. */
.wp-tile-top {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 8px;
}

.wp-tile-top .wp-tile-chevron {
  margin-left: auto;
}

/* The card's printed id, as a chip. Condensed and small: it is what somebody
   matches against Memo's sheet by eye, not something to read through. */
.wp-tile-id {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--primary);
  background: var(--primary-dim);
  border: 1px solid var(--border-strong);
  border-radius: 5px;
  padding: 2px 7px;
}

/* The board row's chevron at the board row's angle, drawn the same way for the
   same reason: the browser's own triangle differs per platform and is the wrong
   weight beside an 11px chip. */
.wp-tile-chevron {
  width: 7px;
  height: 7px;
  border-right: 1.5px solid var(--muted);
  border-bottom: 1.5px solid var(--muted);
  transform: rotate(-45deg);
  transition: transform var(--transition);
}

.wp-tile[open] .wp-tile-chevron {
  transform: rotate(45deg);
}

/* The short handle, which is the one thing on a closed tile at full strength --
   the same discipline the board row keeps, where only .c-person is. The floor
   keeps a two-word handle and a two-line one the same height, so the owner and
   the date sit on one line across the wall instead of stepping. */
.wp-tile-handle {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-strong);
  line-height: 1.35;
  margin-bottom: 10px;
  min-height: 38px;
}

.wp-tile-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

/* WHO HAS THIS NOW, the one thing on a closed tile at full strength -- the same
   discipline .c-person keeps on a board row. */
.wp-tile-owner {
  font-size: 12px;
  font-weight: 700;
  color: var(--text-strong);
}

/* Except when nobody has it. Written compound so it cannot lose to the rule above
   on source order, and worded off .c-person.muted, which says why: "Unassigned"
   must not read like a named holder. */
.wp-tile-owner.muted {
  font-weight: 400;
  color: var(--muted-mid);
}

/* A closed tile dims its metadata, not its handle: the work still names itself.
   Line for line what .c-row.is-done does to a closed board row, and needed for the
   same reason -- an approved card stays on the page, and without this the only
   difference between it and the day it was imported is a chip. The children are
   named rather than the wrapper because both of them set their own colour, which
   an ancestor rule cannot reach. */
.wp-tile.is-done .wp-tile-owner,
.wp-tile.is-done .wp-tile-meta .attention-due {
  color: var(--muted-mid);
}

/* The rule between the summary and the body is the tile's own top border for the
   opened half, so the two states share one edge rather than drawing two. */
.wp-tile-body {
  padding: 14px 16px 16px;
  border-top: 1px solid var(--border);
  margin-top: 2px;
}

/* Memo's instruction, unabridged. Full strength: it is the sentence the work is
   judged against, and the reader opened the tile to read it. */
.wp-tile-doline {
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-strong);
  margin-bottom: 12px;
}

.wp-tile-finished {
  border: 1px solid var(--border-strong);
  background: var(--bg-deeper);
  border-radius: 8px;
  padding: 10px 12px;
  margin-bottom: 12px;
}

/* Named rather than the mockup's `.lbl`. A two-letter class is not this
   stylesheet's vocabulary, and a bare generic name in a file this long is the
   exact shape that gets silently redefined 3,000 lines later. */
.wp-tile-finished .wp-tile-finished-label {
  display: block;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 4px;
}

.wp-tile-finished p {
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text);
}

.wp-tile-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
}

.wp-tile-open {
  font-size: 12px;
  color: var(--primary);
  text-decoration: none;
}

.wp-tile-open:hover {
  text-decoration: underline;
}

/* Narrow screens: one column, at the breakpoint the board above already stacks
   its second line at. Two columns of tiles inside a phone's width would put the
   handle on four lines and the owner under the date. */
@media (max-width: 640px) {
  .wp-grid {
    columns: 1;
  }
}

/* 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. On the CARD since 31 August,
   for the reason written over .panel.yours: the ring round the group became a third
   frame the moment the rows inside it grew their own. */
.panel.results .move {
  border-color: var(--accent, #d9a441);
}

/* 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;
}
/* BODY LEFT, BUTTONS RIGHT on every card (Jan, 3 September: "not underneath
   the rest, to the right"): a two-column row, the words on the left, Open task
   and the card's own button in a column on the right, both starting at the
   top. What a chained card carries beyond that, the hand-in and the form to
   move it on, folds under the row across both columns, closed by default. */
.move.is-block {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 20px;
  align-items: start;
}
.move.is-block .move-do {
  margin-top: 0;
  display: flex;
  gap: 10px;
}
.move.is-block .move-more { grid-column: 1 / -1; }
/* The fold's one line, in the label vocabulary, with the browser's own marker
   off so the card does not grow a triangle the rest of the app never draws. */
.move-more > summary {
  cursor: pointer;
  list-style: none;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--primary);
}
.move-more > summary::-webkit-details-marker { display: none; }
.move-more > summary::before { content: "+ "; }
.move-more[open] > summary::before { content: "\2013  "; }
.move-more[open] > summary { margin-bottom: 12px; }

.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: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 11px;
  color: var(--muted);
  margin: 0 0 2px;
}

/* THE SAME TWO CLASSES ON THE TASK PAGE, WHERE THE PANEL IS THE CARD. The queue
   card sets this list at 13.5px because a card is dense and a reader is scanning
   a column of them. The task page prints the same list inside a .panel, one task
   at a time, and at 13.5 the proposed TITLE came out smaller than the description
   beneath it, which is backwards and is visible only on a render.

   A DIRECT CHILD, and not `.panel .handed-in-item`: the queue card's own copy of
   this list also sits inside a .panel (templates/tasks/list.html, its section is
   .panel.yours), nested in .move > .handed-in, so the descendant form would resize
   the card as well. On the task page the items are the panel's own children.

   15px IS .task-why's SIZE AND HAS TO STAY EQUAL TO IT. The long description in
   this list carries .task-why for the clamp, so a difference here is two sizes for
   two peer values in one panel, and it also parts the clamp from the line estimate
   that decides whether the control is drawn (tasks.detail.WHY_MEASURE_CH, which is
   a character count at that size). Pinned equal by
   cugate_channels.tests.TaskDetailNoteCapCssContractTests. */
.panel > .handed-in-item { font-size: 15px; }


/* 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 label's own number for the video, ahead of the kind on the same line. Quiet
   rather than coloured: the kind is our word for the work and the number is theirs,
   and a number shouted in the accent colour reads as a category. Nothing else about
   the line changes, so a row with no number is drawn exactly as it was. */
.move-ref {
  color: var(--muted);
  margin-right: 8px;
}


/* 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,
/* Shared with the task page below, which asks the same thing of a label/value
   grid and would otherwise carry a forty-line copy of it that drifts. The two
   pages are the hub's only read-a-single-record pages and they are deliberately
   built the same way; the .rel- name stays because this is where it was written
   and renaming it would touch the release page for nothing. */
.task-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,
.task-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,
.task-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,
.task-facts dd.is-missing { color: var(--muted); }

/* THE TWO ROWS AT THE TOP OF THE TASK PAGE, directly under the head: the facts
   beside the stage rail, then the reason beside the expectation. One grid for
   both rows, three fifths to two, because the left-hand panels carry the wide
   things (a label track of up to 190px and then the video URL on one line;
   a pasted reason) and the right-hand ones are a rail and three short lines.
   GENERAL SIBLING of the head rather than a bare .task-row, for the
   source-order reason every rule in this section carries: a bare single class
   at 0,1,0 loses in silence to any later rule of the same specificity, and this
   sheet has been caught by that more than once. align-items: start keeps the
   shorter panel of a row from stretching to meet the taller one; the panels
   keep their own bottom margin, which is what spaces the rows. */
.ch-head ~ .task-row {
  display: grid;
  grid-template-columns: 3fr 2fr;
  gap: 24px;
  align-items: start;
}
/* The rail's column: min-width 0 is the one thing a grid child does not get
   for free, and without it a long unbroken token widens the column past its
   track and pushes the facts panel off the grid. */
.ch-head ~ .task-row .task-row-side { min-width: 0; }
/* At two fifths a panel head's title and its note no longer fit on one line
   side by side; the note drops under the title rather than the title breaking
   mid-phrase beside it. Applied to every panel in the rows, so the two rows
   read the same. */
.ch-head ~ .task-row .panel-head { flex-wrap: wrap; }

/* The Copy on a handed-in value: the app's own small outline button, at the
   right end of the label's line, so the value follows its label directly and
   the control never sits between them. A button and not clickable text (Jan,
   3 September, on the rendered page); on its own line it had read as a third
   thing between label and value, so the line is where it lives. */
.handed-in-copy { flex: 0 0 auto; }

/* 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; }

/* THE ROWS FOLD AT 1,100px AND NOT ON A PHONE, which is a wider fold than the
   facts grid below and is deliberate. Two of the four panels in these rows are
   PROSE -- the reason and, beside it, the expectation -- and the narrow track is
   two fifths of what is left of .main after a 24px gap, so it shrinks with the
   window and with the sidebar: measured in the browser with the sidebar expanded,
   which is the default, the reason column is 403px at a 1,440px window, 319px at
   1,200px, 248px at 1,024px and 168px at 800px. Somewhere around 280px it stops
   being a column of text and becomes a ribbon, and the row is costing the reader
   rather than saving them a scroll.

   1,100px AND NOT A NEW NUMBER: it is the fold .roster-grid and .dash-top-grid
   already use, for the same reason written there -- "two title lines side by side
   stop being readable well before the grid itself runs out of room".

   IT IS ALSO WHAT MAKES tasks.detail.WHY_BRIEF_MEASURE_CH HONEST, and that is not
   a side effect, it is half of why this moved. The server estimates how many lines
   the reason note will take so it can decide whether to draw the control that
   opens it, and a character-per-line count is a single number against a column
   that has no single width. Under-counting is not cosmetic: the note is judged
   short, .task-why.is-short lifts the cap off it, and the box has no bound at all,
   which is precisely the complaint this whole change answers. Folding here bounds
   the range the estimate has to be right across at 281px, and the constant is
   derived from that width. Move this number down and the estimate is wrong again;
   tasks.tests_detail.LongNoteEstimateTests fails if it moves without the constant.

   Left panel first in each row once folded: the order the wide layout implies
   left to right. */
@media (max-width: 1100px) {
  .ch-head ~ .task-row { grid-template-columns: 1fr; }
}

@media (max-width: 640px) {
  .rel-facts,
  .task-facts { grid-template-columns: 1fr; gap: 2px 0; }
  .rel-facts dd,
  .task-facts dd { margin-bottom: 12px; }
  .rel-facts dd:last-child,
  .task-facts dd:last-child { margin-bottom: 0; }
}

/* ===== TASK DETAIL: the read page behind a board row =====
   Built from the same parts as the release page above (.panel, .ch-head, .badge,
   .flag-critical, .movelist for the history, and the shared .task-facts grid), so
   it adds no new visual vocabulary beyond the two things that had no equivalent:
   the reason paragraph and the vertical stage rail. */

/* THE REASON. Set larger than the facts under it and given a measure, because it
   is prose rather than a value and it is the single thing four people opened a
   task to find. pre-wrap: a hand-over appends its own dated lines to the notes,
   and collapsing those into one paragraph would lose the record of who added
   what and when.

   CAPPED, reversing the original "the page sets no cap because that is where the
   note lives". That reasoning is sound and it did not survive the real data:
   note length here is bimodal, 29 of 38 under 500 characters and 8 over 8,000,
   with almost nothing in between, because a channel-ops note is either one line
   or a whole YouTube description plus a tracklist. At 13,126 characters the
   reason panel buried the stage rail, the facts, the holder and the history --
   every other answer the page exists to give -- under a screen and a half of
   prose. Nothing is cut off.

   THE 29 SHORT NOTES ARE NO LONGER ALL UNTOUCHED, and this line used to say they
   were. In the narrow column the server's estimate calls a note long past about
   270 characters, so one between 270 and 500 draws the control below even though
   at a wide window it was already printing whole -- click it and nothing moves.
   Untidy and deliberate: the estimate has to sit under what the column really
   holds at its narrowest, because the other way round is a note clamped with
   nothing offering to open it. tasks.detail has the arithmetic and the reason.

   TIGHTENED FROM 30rem FOR THE REASON PANEL ONLY, which is the bit that has to be
   read off the SELECTOR and not off this number. 30rem was set while this panel
   had the full width of the page to itself and was the only thing in its row; it
   has sat in the narrow track of .ch-head ~ .task-row since 3 September, beside a
   "What is expected of it" that is three lines. Kelsey, the same day: "It is
   strange to have such a long box on the right." Half a screen of prose in a 281
   to 403px column next to a three-line neighbour is what she was reading. Ten
   lines is about the height of that neighbour, which is the answer to her.

   THE OTHER PANEL WEARING THIS CLASS DID NOT MOVE AND MUST NOT BE TIGHTENED WITH
   IT. "What was handed in" is a section of its own at the full width of the page,
   and its long value is somebody's proposed YouTube DESCRIPTION -- 1,500 to 4,000
   characters with a tracklist in it, which is the ordinary length and not an edge
   one. Measured in the browser in that panel's own 1,094px: a 4,000-character
   description draws 480px of its 624 at 30rem, and 240 of them at 15rem -- 77% of
   it against 38%. A 2,200-character one is not judged long at the full-width line
   estimate at all, so it carries no cap and no control and prints whole; under the
   tighter pair it lost five lines with nothing under it offering to open them. So
   the cap below is the narrow panel's, and .handed-in-item takes its own back. Two numbers, one per width,
   the way tasks.detail now holds two line estimates -- and the pair of them is
   pinned selector by selector in
   cugate_channels.tests.TaskDetailNoteCapCssContractTests.

   SPECIFICITY, NOT SOURCE ORDER, decides all five of the rules that touch this
   height, and it was written that way on purpose in a file this long: the cap
   here is 0,1,0, the handed-in override and the Work Pack steps override are both
   0,2,0, and .panel > .task-why.is-short and the :checked release are both 0,3,0.
   Every one of them beats what it has to beat no matter where in the file it lands.

   .is-short WAS THE EXCEPTION UNTIL THE THIRD CAP ARRIVED, and this paragraph said
   otherwise for three weeks. Written bare it is 0,2,0, which ties with both scoped
   caps and won on source order alone -- so moving one line changed what a short block
   rendered as, with every class and every word on the page still right. Its own rule
   below carries the reasoning; what matters here is that the claim this paragraph
   makes is now true of every rule it names. The obvious alternative -- leaving 30rem
   here and scoping the tighter cap to .ch-head ~ .task-row -- reads better and is
   wrong: at 0,3,0 it ties with the :checked release, and on a tie the later rule
   wins, so appending anything below could silently stop the control opening the
   note. Both states are read off the rendered page rather than argued here.

   CLAMPED, NOT SCROLLED. A scrolling box was the first attempt and it was worse
   than the bloat: macOS overlay scrollbars stay invisible until something is
   already scrolling, so a capped note looked exactly like a note that ended, in
   the middle of a sentence, with nothing on screen offering a way on. overflow
   is hidden and an explicit control does the opening instead. */
.task-why {
  margin: 0;
  /* NO MEASURE, unlike the 68-74ch the rest of the hub sets on prose. Deliberate,
     and a judgement call that went against the typographic rule on purpose.

     That rule assumes flowing prose read start to finish. A task note is not: it
     is a working document of label-and-value pairs, timestamps, an operational
     hold, and a tracklist that runs one entry per line, and it is SCANNED far
     more often than it is read through. Against that, a measure cost more than it
     bought -- at 72ch a third of the panel sat empty, and even at 90ch the gap
     read as a rendering fault to every person who looked at it, which is a real
     cost on a page the team opens to find out where work stands.

     The tradeoff being accepted, stated plainly: the long paragraphs now run the
     panel's full width, and a reader's eye has further to travel back at the end
     of each line. Restoring the measure is one declaration, and the line estimate
     in ``tasks.detail`` moves with it -- a test fails if they part company.

     THE ESTIMATE STILL HAS TO KNOW HOW WIDE THE PANEL IS, measure or no measure,
     and two panels carry this class at two widths: the reason panel in the narrow
     track of the row under the head, and the full-width handed-in list. That is
     why ``tasks.detail`` holds two character counts and one clamp. */
  max-height: 15rem;
  overflow: hidden;
  font-size: 15px;
  line-height: 1.6;
  color: var(--text-strong);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/* THE FULL-WIDTH PANEL'S OWN CAP, and the only rule on this page that still wants
   the original 30rem. Twenty lines of the same 15px text, against a 1,094px line
   rather than the reason panel's 281 to 403 -- so what it holds is the whole of a
   2,200-character description and three quarters of a 4,000-character one, which
   is what the panel was added for: a reader sees the new title and description and
   copies them without opening anything. It is scoped to .handed-in-item rather
   than the cap above being scoped to the row, so that this sits at 0,2,0 and the
   :checked release above it at 0,3,0 still opens it. tasks.detail.WHY_CLAMP_LINES
   is the line count that goes with this number and moves with it.

   .handed-in-item AND NOT .panel > .handed-in-item, unlike the size rule further
   up. The queue card draws the same list (templates/tasks/list.html) and the
   descendant form would reach it -- but nothing there carries .task-why, because
   the card renders no clamp and no control at all, so there is nothing for this to
   find. Written as the shorter selector because what it is scoping to is the
   ITEM's width, which is the same wherever the item is drawn. */
.handed-in-item .task-why { max-height: 30rem; }
/* THE WORK PACK CARD'S STEPS, AT THE SAME WIDTH AND SO AT THE SAME CAP. A section of
   its own across the page like the handed-in list above, and holding the same shape of
   text: the longest How section in Memo's pack is 3,189 characters of numbered list,
   which at the reason panel's 15rem would show a third of itself. Twenty lines of the
   same 15px text is what a reader gets before the control appears, and
   tasks.detail.card_steps takes the full-width line estimate that goes with it.

   WRITTEN AS A DESCENDANT PAIR, not as a bare .task-steps, for the specificity reason
   the rule above and the comment before it both give: this is 0,2,0, so the :checked
   release at 0,3,0 still opens it wherever either lands in six thousand lines, and it
   is beaten by the release of the cap further down, which is written one class heavier
   for that purpose. Scoped to the section rather than to the paragraph because what is
   being scoped to is the panel's WIDTH.

   NO pre-wrap, WHICH IS THE ONE THING IT TAKES BACK FROM .task-why. The steps used to
   be one string in one <p>, and preserved whitespace was what gave the numbered list
   its line breaks. They are markup now -- a list, a section head per figure and a
   label/value grid per set of rows -- so the breaks come from the elements, and
   pre-wrap here would render the template's own indentation between those elements as
   visible blank lines inside the clamp. The same failure the "one line per value"
   warnings on .task-facts and .handed-in-value describe, arriving from the other side.
   The include is wrapped in {% spaceless %} as well; this is the half that holds if
   somebody unwraps it. */
.task-steps .task-why { max-height: 30rem; white-space: normal; }

/* THE NUMBERED HALF. A hanging indent, which is the whole of what this rule is for:
   without one, the wrapped tail of step 47 sits flush under "47." and reads as a new
   item, and the pack's longer steps run to three and four lines each.

   3ch IS THE WIDTH OF THE NUMBER, ITS FULL STOP AND ITS SPACE at two digits, which is
   ninety of the pack's hundred and fifteen steps; the sixteen three-digit ones wrap
   half a character short of their own text and the nine single-digit ones a character
   past it. Both are still unmistakably a continuation, which is what the indent is
   asked to say, and neither can be fixed by a constant that suits the other.

   THE MARKER IS OFF AND THE NUMBER IS IN THE TEXT. Memo numbers 1 to 115 across the
   whole pack rather than restarting per card, so a browser-drawn marker would need
   <ol start> -- right today, and silently wrong the first time a pack numbers a card
   any other way. His digits are printed because they are his text. */
.task-steps-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.task-steps-list li {
  padding-left: 3ch;
  text-indent: -3ch;
  margin-bottom: 8px;
}
.task-steps-list li:last-child { margin-bottom: 0; }

/* THE NAME OF ONE FIGURE, over the three rows that define it. The panel title's own
   face, weight and case one step down, because it heads a subsection of a section that
   already has a head: above it is a .panel-head carrying Memo's own line, below it is
   a .task-facts grid whose labels are 11px. Left at the body size and colour -- which
   is what the panel did until now -- "Views" stood alone between two blank lines and
   read as a truncated sentence rather than as the thing the rows under it are about.
   No new colour and no box: the hierarchy is size, face and case, as it is everywhere
   else on this page. */
.task-steps-term {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 700;
  font-size: 15px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-strong);
  margin: 22px 0 8px;
}

/* A run of the section that matched none of the shapes above, printed line by line
   exactly as it is stored. This is the flat rendering the whole panel used to have,
   kept as the floor under an unforeseen revision: it can look plain, and it can never
   drop a character. */
.task-steps-line {
  margin: 0 0 8px;
}
.task-steps-line:last-child { margin-bottom: 0; }

/* The grid inside this panel keeps its labels in sentence case. Everywhere else on
   the page a .task-facts label is one or two of the app's own words ("Due", "Song or
   video") and small caps make a scannable rail of them; here the label is a sentence
   Memo wrote ("What it is for, and who reads it"), and a sentence in caps is shouted
   rather than labelled. Same size, same weight, same colour, same column. */
.task-steps .task-facts dt { text-transform: none; letter-spacing: 0.01em; }
/* The rows of one figure are one grid, and the next figure's name follows it. Without
   this the name butts straight onto the last value above it. */
.task-steps .task-facts { margin-bottom: 4px; }

/* THE CONTROL THAT OPENS IT. A checkbox and its label, so the note opens with no
   JavaScript on a page that carries none -- the same discipline as the board row's
   <details>. The input is moved off-screen rather than display:none, which would
   take it out of the tab order and leave a keyboard reader with a note they cannot
   open.

   WHETHER it appears is decided in Python, not here. CSS cannot ask whether an
   element overflows, so the template renders the control only for a note long
   enough to be clamped (``answer.why_is_long``). The estimate lives next to the
   cap it is estimating, and is deliberately generous: a control over a note that
   did not need one is untidy, a note clamped with no way to open it is the bug
   this whole control exists to fix. */
.task-why-toggle { position: absolute; width: 1px; height: 1px; opacity: 0; }
.task-why-toggle:checked ~ .task-why { max-height: none; }
/* A note the server judged short of the clamp carries no control, so it must not
   be clamped either -- otherwise an estimate that came in low cuts a note off
   with nothing offering to open it, which is the one failure mode this design
   must not have.

   A CHILD OF THE PANEL, AND THAT IS THE WHOLE POINT OF THE SELECTOR. Written bare
   this is (0,2,0) and TIES with both scoped caps above -- .handed-in-item .task-why
   and .task-steps .task-why -- and a tie is settled by whichever rule is later in a
   file of eight thousand lines. It won only because it happened to sit twenty lines
   below the second of them: move that one line to the end of the file and every block
   the server judged short renders capped with no control under it, which is exactly
   the failure the paragraph above says must not be reachable. The comment further up
   claims all four rules touching this height are decided by specificity "no matter
   where in the file it lands", and until this line was written that claim was false
   for this one. At (0,3,0) it now beats every cap on its own weight.

   It TIES with the :checked release at (0,3,0), and that tie is provably harmless
   rather than tolerated: the two never meet, because .is-short is rendered only in the
   branch that draws no checkbox at all, and both declare the same max-height anyway.

   EVERY .task-why.is-short ON THE PAGE IS A DIRECT CHILD OF A .panel -- the reason,
   the card's instruction, its steps and its criterion, four sections of
   templates/tasks/detail.html -- and the child combinator is the form .panel >
   .handed-in-item and .panel > .first-days already use on this same page.
   cugate_channels.tests.TaskDetailNoteCapCssContractTests pins the weight rather than
   the spelling, so a different (0,3,0) selector is free to replace this one. */
.panel > .task-why.is-short { max-height: none; }
.task-why-more {
  display: inline-block;
  margin-top: 10px;
  font-size: 13px;
  font-weight: 600;
  color: var(--primary);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.task-why-more::before { content: "Show the whole note"; }
.task-why-toggle:checked ~ .task-why-more::before { content: "Show less"; }
.task-why-toggle:focus-visible ~ .task-why-more {
  outline: 2px solid var(--primary);
  outline-offset: 3px;
}

/* THE SAME CONTROL UNDER A HANDED-IN DESCRIPTION, saying what it opens. The
   reveal above names the field it belongs to, and "What was handed in" puts the
   identical control over somebody's proposed DESCRIPTION -- which is what makes
   that value long enough to need clamping in the first place. A reader offered
   "show the whole note" under a heading of its own would reasonably take it for
   the note in the panel above, which is a different field on the same page.

   ONE RULE, AND THE OPEN STATE IS DELIBERATELY NOT RESTATED. This is (0,2,1) and
   the checked rule above is (0,3,1), so that one still wins while the box is
   ticked and the control reads "Show the whole description" closed and "Show
   less" open with no second copy of the second string. Specificity rather than
   source order, which in a file this long is the difference between a rule that
   holds and one that holds until something is appended. Both states are read off
   the rendered page rather than argued here, and both selectors are pinned by
   cugate_channels.tests.TaskDetailNoteCapCssContractTests. */
.handed-in-item .task-why-more::before { content: "Show the whole description"; }
/* AND UNDER THE STEPS. The two rules above name their field because the headings they
   sit under do not, and three panels on this page carry the same control: "show the
   whole note" under a heading of its own would reasonably be taken for the note in the
   reason panel, which is a different field. This one cannot name a field, because the
   panel it belongs to is two shapes and no name is true of both -- eleven cards are a
   numbered list, and seven are that list plus a table of which figure to look up and
   where it lives, which on C9 is 82% of the card. "Show all the steps" was true of the
   eleven and wrong about the seven. The heading above it already says which shape this
   card is, so the reveal says only how much of it is hidden.

   One rule, at (0,2,1), so the (0,3,1) checked rule above still says "Show less" while
   the box is ticked without a second copy of that string. */
.task-steps .task-why-more::before { content: "Show the rest"; }
/* An absence is printed in words and only dimmed -- never blanked, and never
   italicised into looking like a system message rather than a fact about the
   task. Same rule the release page's missing values follow. */
.task-why.is-missing { color: var(--muted); font-size: 14px; }

/* The stage rail, drawn down the page rather than across it. The board's
   horizontal .baton has to fit a row; here there is room for the full label and
   the person, which is what makes the rail answer "who does what next" instead of
   just "how far along". */
.task-rail {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 2px;
}
.task-rail-step {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 7px 0 7px 20px;
  position: relative;
  font-size: 13px;
  color: var(--muted-mid);
}
/* The state dot. A full ring, filled once the step is behind us and drawn in the
   accent on the step the work is actually sitting on -- never a coloured bar down
   the side of the row. */
.task-rail-step::before {
  content: "";
  position: absolute;
  left: 0;
  top: 11px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1.5px solid var(--border-strong);
}
.task-rail-step.is-done::before { background: var(--border-strong); }
.task-rail-step.is-current { color: var(--text-strong); font-weight: 600; }
.task-rail-step.is-current::before {
  border-color: var(--primary);
  background: var(--primary);
}
.task-rail-who { color: var(--muted); font-size: 12px; }
.task-rail-step.is-current .task-rail-who { color: var(--muted-mid); }

/* The controls, shared verbatim with the board row, so they arrive already sized
   for a row. On a page they sit in the head beside the title rather than at the
   end of a line, and they wrap instead of squeezing the title. */
.task-actions { flex-wrap: wrap; justify-content: flex-end; gap: 6px; }

/* ===== 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.

   The Social account form's In use switch is the same control doing the same job,
   so it joins the group rather than getting a second copy of it a thousand lines
   further down. That is the treatment .task-filters and .release-filters already
   share, and for the same reason: one of them drifting a few pixels is how a hub
   stops reading as one product. */
.tv-switch-row,
.promo-switch-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.tv-switch-row .form-check-input,
.promo-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,
.promo-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;
}

/* --- Where the YouTube money comes from -------------------------------------
   The Dashboard's revenue-by-source panel: three headline figures for the last
   complete month, twelve months of stacked bars, the reconciliation state, and
   the caveats.

   NO GOLD, for the reason the company ledger above states: gold is the header's
   own-channel figure and nothing else. Red is reserved for the reconciliation
   failing, which is the one piece of bad news this panel can carry -- a figure
   here being LOW is not bad news, it is the finding.

   The two bar colours are the ones already in the palette for a two-part bar
   (the device family's blue and the fill cyan), so the panel introduces no new
   colour meaning to a page that spends colour carefully. --- */
.source-empty {
  font-size: 13px;
  color: var(--muted-mid);
  margin: 0 0 22px;
}

.source-empty code {
  font-family: ui-monospace, 'SF Mono', 'DM Sans', monospace;
  font-size: 12px;
}

.source-heads {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
  margin: 0 0 14px;
}

.source-head {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 16px 18px;
}

/* The total reads as the sum of the two beside it, so it is the same tile with a
   heavier rule -- never a different colour, which would make an addition look
   like a verdict. */
.source-head-total { border-color: var(--muted); }

.source-head-value {
  display: block;
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800;
  font-size: 30px;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
  margin-bottom: 8px;
}

.source-head-label { display: block; color: var(--muted); margin-bottom: 6px; }

.source-head-split {
  display: block;
  font-size: 12px;
  line-height: 1.45;
  color: var(--muted-mid);
}

.source-missing {
  font-size: 12.5px;
  color: var(--muted-mid);
  margin: 0 0 14px;
}

.source-legend {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  color: var(--muted);
  margin: 0 0 10px;
}

.source-key {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  margin-left: 8px;
}

/* ONE HUE AT TWO LIGHTNESSES, not two hues. The first draft used the device blue
   beside the fill cyan, which is the pairing the ads-vs-Premium bar already uses,
   and in light mode the two came out as one blue band with a seam in it -- which
   fails the single thing this bar exists to do. Same colour at 40% is guaranteed
   to separate in both themes, and it says the right thing besides: the two parts
   are halves of one total, and the solid half is the one the panel is about. */
.source-key-own { background: var(--primary); opacity: 0.4; margin-left: 0; }
.source-key-third { background: var(--primary); }

.source-bars { margin: 0 0 20px; }

.source-bar-row {
  display: grid;
  grid-template-columns: 56px 1fr 84px;
  align-items: center;
  gap: 12px;
  padding: 3px 0;
}

.source-bar-month { color: var(--muted); }

.source-bar-track {
  display: flex;
  height: 10px;
  background: var(--rank-track);
  border-radius: 5px;
  overflow: hidden;
}

.source-bar-seg { display: block; height: 100%; }
/* Paired with the legend keys above; see the note there on why this is one hue at
   two lightnesses rather than two colours. */
.source-bar-own { background: var(--primary); opacity: 0.4; }
.source-bar-third { background: var(--primary); }

.source-bar-total {
  text-align: right;
  font-size: 12.5px;
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
}

/* A month with no reading gets words, not a rail: an empty track reads as a
   measured zero, which is the one thing this panel must never say by accident. */
.source-bar-none {
  font-size: 12px;
  color: var(--muted-mid);
  font-style: italic;
}

.source-check {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 14px 18px;
  margin: 0 0 16px;
}

.source-check-line {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted-mid);
}

.source-check-bad { color: var(--danger); }

.source-caveats { margin: 0 0 26px; }

.source-caveat {
  margin: 0 0 10px;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--muted-mid);
  max-width: 78ch;
}

.source-caveat:last-child { margin-bottom: 0; }

@media (max-width: 640px) {
  /* The month column and the figure column both shrink before the rail does:
     the rail is the comparison, and a rail too short to compare is the one part
     worth keeping full width. */
  .source-bar-row { grid-template-columns: 44px 1fr 68px; gap: 8px; }
}

/* ============================================================
   THE DASHBOARD'S COMPANY VIEW ("Where we stand" tab, called "The team"
   until 7 September 2026), ported from the locked
   mockup design-mockups/dashboard-A-density-2026-08-18.html. That file was
   chosen over two competing directions and then revised by the client eight
   times on specifics, so the rule here is FIDELITY: every value below is the
   one that file settled on, and where the shipped page differs from it the
   difference is data, never taste.

   NOTHING HERE IS A NEW VISUAL LANGUAGE. The mockup was written against this
   stylesheet rather than against a copy of its tokens, and it composes with
   .panel / .panel-head / .panel-title / .panel-foot / .data-table /
   .rev-line / .rev-area / .rev-dot / .rev-peak-guide / .rev-peak-dot /
   .c-baseline / .chart-row -- all already shipped, most of them never used
   on this page before. What is added below is the arrangement (two columns
   that each stack two cards), four compact components that had no class yet
   (the Turnover card's chart furniture, the mini board, the workload chips,
   the Company Today strip), and one colour.
   ============================================================ */

/* --- The one new colour, and it is not new to the palette ---
   Three lines cross each other on the Turnover chart, and the app had two
   blues to hand: --primary, spent on the total, and the muted blue-grey
   carrying Content ID. Before this, own-channels and Content ID were one hue
   at two lightnesses, which is the rule this file writes for adjacent bar
   SEGMENTS and not for three overlapping lines. --line-own reuses the exact
   violet already spent on the audience gender split rather than inventing a
   hex: the same "off-primary categorical accent" precedent, a second chart
   rather than a new colour. It is named for THIS chart's role and not
   aliased to the gender token, so nothing reads as if revenue had anything
   to do with audience gender. */
html[data-theme="dark"]  { --line-own: #a155da; }
html[data-theme="light"] { --line-own: #8629c0; }

/* ============================================================
   THE TWO COLUMNS.

   Left, 2/3: Turnover, then Company Today under it. Right, 1/3: the Board,
   then Who's Carrying What under it.

   DELIBERATELY NOT A 2x2 GRID WITH SHARED ROW TRACKS. Row-locking Turnover's
   height to the Board's would force whichever card is shorter to carry dead
   space purely to match its row partner, which is the exact "large empty
   area" the client asked to be rid of. Each column is its own flex stack, so
   the left column's height is Turnover + gap + Company Today, the right
   column's is Board + gap + Workload, and the two are free to end at
   different totals.
   ============================================================ */
.dash-top-grid {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 16px;
  margin-bottom: 16px;
  align-items: start;
}
.dash-col { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
/* .panel carries its own stacking margin; inside a flex column the gap does that job. */
.dash-top-grid .panel { margin-bottom: 0; }

@media (max-width: 1100px) { .dash-top-grid { grid-template-columns: 1fr; } }

/* ============================================================
   TURNOVER: the chart, its range control, one legend line, one note.

   The ads/Premium breakdown, the reconciliation line and the caveats are not
   below this card, they are off the page: the client removed them outright
   on the understanding that they belong on a Turnover page that does not
   exist yet.
   ============================================================ */
.tv-head { align-items: flex-start; gap: 20px; }
.tv-head-id { display: flex; flex-direction: column; gap: 2px; }
.tv-head-desc { margin: 0; font-size: 12px; color: var(--muted); letter-spacing: 0.01em; }

/* A pill-segmented switcher, not the page-level underline tabs (.home-tab):
   those are sized for a whole page's nav and this is one small control inside
   one card. The pill is the project's named exception to one radius
   everywhere -- 999px is already spent on .tv-chip and .est-tag, so this is a
   new use of a shape already in the system rather than a new shape. */
/* Hidden until turnover-range.js is listening, the same contract .home-tabs keeps
   one level up: a control that does nothing should not be on screen offering to. */
.tv-range { display: none; }
.tv-range.is-ready {
  display: inline-flex; align-items: center; gap: 2px; margin: 0 0 0 auto; flex: none;
  padding: 3px; background: var(--surface-hover); border: 1px solid var(--border); border-radius: 999px;
}
.tv-range-tab {
  appearance: none; border: 0; background: none; cursor: pointer; white-space: nowrap;
  font-family: 'DM Sans', sans-serif; font-size: 12.5px; font-weight: 600;
  padding: 6px 13px; border-radius: 999px; color: var(--muted-mid);
  transition: background-color var(--transition), color var(--transition);
}
.tv-range-tab:hover { color: var(--text); }
.tv-range-tab:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.tv-range-tab[aria-selected="true"] { background: var(--primary); color: var(--bg); font-weight: 700; }
.tv-range-note {
  margin: 18px 0 14px; font-size: 11.5px; line-height: 1.5; color: var(--muted-mid); max-width: none;
  -webkit-font-smoothing: subpixel-antialiased; -moz-osx-font-smoothing: auto;
}
/* At phone width the control gets a line of its own under the title. Beside the
   title the five buttons needed about 380px and a 390px screen leaves the card about
   310px, so with the head a no-wrap row and the control flex: none they ran off the
   right edge. The head wraps, the control takes the whole row, and the buttons share
   it with a narrower side padding, which fits all five on one line at 390px. Below
   that they wrap rather than overflow. Compound selectors, scoped to this card's
   head, so a later single-class rule elsewhere in this file cannot outrank them. */
@media (max-width: 640px) {
  .panel-head.tv-head { flex-wrap: wrap; }
  .panel-head.tv-head .tv-range.is-ready {
    flex: 1 1 100%; flex-wrap: wrap; margin: 0; border-radius: var(--radius);
  }
  .panel-head.tv-head .tv-range.is-ready .tv-range-tab { flex: 1 1 auto; padding: 6px 4px; }
}

.tv-stacked { display: flex; flex-direction: column; }
.tv-chart-full { width: 100%; }
.tv-chart-full svg { display: block; width: 100%; height: auto; overflow: visible; }
.tv-axis-label { font-family: 'DM Sans', sans-serif; font-size: 8.5px; fill: var(--muted); letter-spacing: 0.02em; }

/* One line under the chart carrying each series' own current figure. It
   replaces the readout column that used to sit beside a short chart, which
   is what left the empty area below it. */
/* COLUMN GAP IS 16px AND THE DESIGN DREW 20px, which is the one measurement on this
   card that is not the mockup's. The house vocabulary lint
   (cugate_channels.tests.ReadSurfaceVocabularyTests) will not allow the bare noun on a
   read surface, so the third key reads "our own YouTube channels" where the drawing
   read "our own channels" -- 55px longer. Measured at 1440 in the 2/3 column: the three
   keys come to 665.19px against 705px of row, so at the drawn 20px they overflow by
   0.19px and the legend wraps to two lines. 16px restores the single line the client
   asked for, with 7.8px to spare. Everything else here is the mockup's. */
.tv-legend {
  display: flex; flex-wrap: wrap; gap: 6px 16px; align-items: center;
  margin: 12px 0 0; padding-top: 12px; border-top: 1px solid var(--border);
}
.tv-key {
  display: inline-flex; align-items: baseline; gap: 8px;
  font-size: 12px; color: var(--muted-mid); white-space: nowrap;
}
.tv-key-val { font-weight: 700; color: var(--text-strong); font-size: 13px; }
.tv-key-swatch { width: 22px; height: 8px; flex: none; overflow: visible; }

/* THREE LINES, THREE SIGNATURES: hue, weight AND dash together, never
   lightness alone. Total keeps --primary, heaviest, solid (.rev-line, the
   shared shipped class, unchanged). Own channels: --line-own, mid weight,
   solid. Content ID: --muted, thinnest, dashed. Any one axis on its own would
   separate them with the labels covered; together they do not depend on each
   other. */
.tv-line-own { fill: none; stroke: var(--line-own); stroke-width: 2.25; stroke-linecap: round; stroke-linejoin: round; }
.tv-line-cid { fill: none; stroke: var(--muted); stroke-width: 1.75; stroke-dasharray: 4 3; stroke-linecap: round; stroke-linejoin: round; }
.tv-dot-own { fill: var(--line-own); }
.tv-dot-cid { fill: var(--muted); }

/* NOT MEASURED, drawn rather than hidden. A flat --muted tint at low opacity,
   never a hatch and never a colour: an absent reading should read as neutral
   and not as an event. The boundary is the same dashed-muted stroke this
   system already uses for "not measured", turned ninety degrees to mark an
   edge instead of a value. No line is bent to zero and none is stretched to
   reach the end of the axis; either would be a claim about something nobody
   read. */
.tv-gap-zone { fill: var(--muted); fill-opacity: 0.08; }
.tv-gap-boundary { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 2; }
.tv-value-sub { font-family: 'DM Sans', sans-serif; font-size: 9.5px; font-weight: 500; letter-spacing: 0.05em; fill: var(--muted); }

/* ============================================================
   THE BOARD, mini: rank, name, figure, movement, one line each. Silver and
   bronze come from the real .rank-num podium convention, so the teaser and
   the full board below agree about what ranks 2 and 3 look like.
   ============================================================ */
.board-mini-list { display: flex; flex-direction: column; gap: 7px; }
.board-mini-row {
  display: flex; align-items: center; gap: 10px; text-decoration: none;
  padding: 3px 4px; margin: -3px -4px; border-radius: 6px; transition: background-color var(--transition);
}
.board-mini-row:hover { background: var(--surface-hover); }
.board-mini-rank {
  font-family: 'Barlow Condensed', sans-serif; font-weight: 800; font-size: 19px; line-height: 1;
  color: var(--muted); width: 16px; flex-shrink: 0;
}
.board-mini-row.is-lead .board-mini-rank { color: var(--spotlight); }
.board-mini-row.is-silver .board-mini-rank { color: var(--silver); }
.board-mini-row.is-bronze .board-mini-rank { color: var(--bronze); }
.board-mini-name {
  flex: 1 1 auto; min-width: 0; font-family: 'Barlow Condensed', sans-serif; font-weight: 700; font-size: 13px;
  letter-spacing: 0.02em; text-transform: uppercase; color: var(--text-strong);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.board-mini-value {
  font-family: 'Barlow Condensed', sans-serif; font-weight: 800; font-size: 16px; color: var(--text-strong);
  flex-shrink: 0; font-variant-numeric: tabular-nums;
}
.board-mini-row.is-lead .board-mini-value { color: var(--spotlight); }
/* Fixed width whether or not there is a movement to print, so the figures
   above it stay in one column down the list. A row with no measured movement
   renders the slot empty rather than a dash, which would look like a flat
   trend and is also a claim. */
.board-mini-delta {
  font-size: 11px; font-weight: 600; color: var(--danger); flex-shrink: 0; width: 60px;
  display: inline-flex; align-items: center; justify-content: flex-end; gap: 3px;
  font-variant-numeric: tabular-nums;
}
.board-mini-delta svg { width: 10px; height: 10px; }
/* A RISE IS NOT BAD NEWS. .board-mini-delta carries --danger because a fall is the
   common value in this column, and the template has always emitted .is-up for the other
   direction -- but no rule ever matched it, so every channel that GREW printed in the
   danger colour, arrow up, on a dashboard whose subject is a decline. The template
   comment above the markup states the rule this restores: a rise must never read red for
   sitting in a column whose commonest value is a fall. Both arrows use currentColor, so
   the glyph follows the text. */
.board-mini-delta.is-up { color: var(--up); }

/* ============================================================
   WHO'S CARRYING WHAT, as chips: the compact teaser for the roster that
   still runs in full below. Three columns, each chip a name, a count and the
   age of the oldest thing in that queue on its own line.

   THE RESTRAINT RULES CARRY OVER UNCHANGED from .roster-card: no chip is
   faded, shrunk, ranked or coloured by size, the age is never in the danger
   colour, and the people holding nothing are still named rather than
   dropped -- as a sentence under the grid, because a chip is a container for
   a count and they have none.
   ============================================================ */
.workload-chips { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.wl-chip {
  display: flex; flex-direction: column; gap: 3px; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-hover); padding: 8px 12px; min-width: 0;
}
.wl-chip-name {
  font-size: 10.5px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-strong);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  -webkit-font-smoothing: subpixel-antialiased; -moz-osx-font-smoothing: auto;
}
a.wl-chip { text-decoration: none; }
a.wl-chip:hover { border-color: var(--border-strong); }
.wl-chip-stat { font-size: 11px; color: var(--muted-mid); }
.wl-chip-stat strong { font-family: 'Barlow Condensed', sans-serif; font-weight: 800; font-size: 15px; color: var(--text-strong); margin-right: 2px; }
.wl-chip-age { font-size: 11px; color: var(--muted); }
.workload-zero { margin: 10px 0 0; font-size: 12px; color: var(--muted-mid); }

@media (max-width: 640px) { .workload-chips { grid-template-columns: repeat(2, 1fr); } }

/* ============================================================
   COMPANY TODAY. The four-bay strip from the approved, locked
   company-today-mockup.html, by way of the composition file: four bays, one
   glyph family, one weight, one ink, and the red danger figure the only
   colour in the component. The figure sits to the RIGHT of its glyph and the
   caption runs under both.
   ============================================================ */
/* TWO TO A ROW, ICON THEN NUMERAL THEN WORDS, one line each. Was four across with
   the caption wrapped under the figure, which left the long captions three lines deep
   and had no room for the alerts, so those were drawn as a separate full-width box in
   a second type size underneath. Six figures are three rows of two now and there is
   one component instead of three.

   THE MIDDLE COLUMN IS FIXED AND THE NUMERAL IS LEFT-ALIGNED IN IT. Right-aligned in a
   fixed box, a single digit sat a whole numeral-width from its own icon while a
   two-digit one sat against it, and the moment one row carried a unit the numerals'
   left edges went ragged. Left-aligned: same gap from the icon on every row, every
   numeral on the same x, and the caption column still starts at a constant offset so
   the words line up too. 78px holds four digits, or three plus a unit; a wider value
   eats into the 14px gap rather than moving the caption, because the track is fixed. */
.ctoday-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0; align-items: stretch; }
.ctoday-cell {
  display: grid;
  grid-template-columns: 34px 78px 1fr;
  grid-template-areas: "icon fig cap";
  align-items: center;
  column-gap: 14px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: 18px 26px;
  transition: background-color var(--transition), border-color var(--transition);
}
.ctoday-cell:hover { background: var(--surface-hover); }
.ctoday-icon { grid-area: icon; display: block; width: 34px; height: 34px; margin: 0; }
.ctoday-icon svg {
  width: 34px; height: 34px;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
  color: var(--muted-mid);
}
.ctoday-fig {
  grid-area: fig;
  justify-self: start;
  text-align: left;
  white-space: nowrap;
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800; font-size: 34px; line-height: 1;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  color: var(--text-strong);
  margin: 0;
}
.ctoday-line { display: block; text-align: left; }
.ctoday-num { display: inline-block; }
.ctoday-unit { margin-left: 5px; font-size: 15px; font-weight: 600; letter-spacing: 0.01em; color: var(--muted-mid); }
.ctoday-cap { grid-area: cap; font-size: 13px; line-height: 1.4; color: var(--muted); margin: 0; max-width: none; }
.ctoday-cap-link { color: inherit; text-decoration: none; }
.ctoday-cap-link:hover { text-decoration: underline; }
/* The only colour in the strip, and it is the figure alone: the caption
   beside a bad count is the same caption it would carry on a good day. */
.ctoday-bad .ctoday-fig { color: var(--danger); }
.ctoday-strip { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); overflow: hidden; }
.ctoday-strip .ctoday-row { gap: 0; }
/* SQUARE CORNERS INSIDE THE CARD, and this is not cosmetic. An inset box-shadow is
   clipped to the padding box, so with the cell's own 10px radius still in force every
   divider bent away 10px before each corner: at the centre crossroads the two rules
   curved apart into four hooked stubs around a diamond hole, and the horizontal rules
   stopped short of both card walls. Invisible before this change because there was one
   row and each vertical rule's curved ends coincided with the strip's own rounded
   corners. The strip keeps its radius; the cells inside it must not. */
.ctoday-strip .ctoday-cell { border: 0; border-radius: 0; padding: 18px 26px; position: relative; }
/* An empty half at the end of an odd row. Renders nothing and carries no content, but
   it holds the grid square so the last row's rule spans the card instead of stopping
   in mid-air halfway across it. Five cells -- four figures and one alert, board data
   fresh -- is the ordinary state, not an edge case. */
.ctoday-cell-filler { padding: 0; }
/* An inset shadow rather than a border, so the divider takes no width from
   the cell and the four bays stay exactly equal. */
/* RULES BETWEEN, NEVER AROUND, so the card reads as one object rather than six boxes:
   a column line down the middle and a rule above every row after the first. Three rules
   that draw, in ascending specificity, because a cell in the right-hand column below
   the first row carries both at once and a single shadow property cannot be inherited
   from two of them.

   EVERY ONE IS A COMPOUND SELECTOR AND THERE IS A CONTRACT TEST ON THEM, sliced to
   this section of the file rather than searched across it. This stylesheet has now
   bitten this project five times with a bare single-class rule losing on source order
   to a later one at the same specificity, and each time the test passed because it
   asserted the class attribute rather than the rule. */
.ctoday-strip .ctoday-cell:nth-child(2n) { box-shadow: inset 1px 0 0 var(--border); }
.ctoday-strip .ctoday-cell:nth-child(n+3) { box-shadow: inset 0 1px 0 var(--border); }
.ctoday-strip .ctoday-cell:nth-child(2n):nth-child(n+3) {
  box-shadow: inset 1px 0 0 var(--border), inset 0 1px 0 var(--border);
}
.ctoday-strip .ctoday-cell:hover { background: var(--surface-hover); }
.dash-company-today { margin-bottom: 0; }

/* The strip sits in a two-thirds column of roughly 746px here, so the cells tighten
   before the viewport reaches a breakpoint. Padding and words only: the numeral and
   the glyph hold their size, because the middle column is a fixed width and moving
   the glyph would move every numeral off the x the row above it uses.

   The caption's 10px top margin is gone from here, not overridden: it belonged to the
   old shape where the caption sat on a line UNDER the figure, and beside it that margin
   pushed the words off centre against their own numeral. The base rule sets margin: 0,
   so restating it here would have been a no-op dressed as a fix. */
@media (min-width: 641px) {
  .dash-col .ctoday-strip .ctoday-cell { padding: 16px 18px; column-gap: 12px; }
  .dash-col .ctoday-cap { font-size: 12px; }
}
/* NO max-width:1100px BLOCK. There was one, and it was correct against the base it
   shipped with -- it made the four-across grid two-across and moved the dividers to
   suit. The base grid is two-across now, so that block had nothing left to do; the
   padding it was rewritten to carry is already set by the .dash-col rule above between
   641 and 1100px, and overridden below 641 by the block underneath. Deleted rather
   than left tightening nothing, and measured at 1100, 1000, 800, 700 and 641px to
   confirm the computed padding does not move. */
/* ONE COLUMN, so the column line has to go -- and clearing it needs selectors at least
   as specific as the base ones. The old rule here was :nth-child(even) at (0,3,0),
   which beat the base it shipped with, also (0,3,0), on source order. It was correct.
   The base is (0,4,0) now, so the override had to be raised to match; the rewrite is
   forced by this change and is not a repair of anything. */
@media (max-width: 640px) {
  .ctoday-row { grid-template-columns: 1fr; }
  .ctoday-strip .ctoday-cell:nth-child(2n),
  .ctoday-strip .ctoday-cell:nth-child(n+3),
  .ctoday-strip .ctoday-cell:nth-child(2n):nth-child(n+3) { box-shadow: none; }
  .ctoday-strip .ctoday-row > .ctoday-cell + .ctoday-cell { border-top: 1px solid var(--border); }
  .ctoday-strip .ctoday-cell { padding: 18px 20px; }
}

/* ============================================================
   GOLD, SPENT ONCE PER SCREEN.

   Two shipped surfaces each carry --spotlight by default and each is
   defensible alone: the header's own-channels figure, and the Board's rank 1
   ("ONE precious warm moment", see the comment above .chart-row.rank-1
   .rank-num). This composition is the first page to put both on one screen,
   and it puts the Board on it twice. So on THIS page the header figure is
   recoloured to --primary and the full board below has its rank-1 gold
   turned off, leaving the mini Board's rank 1 as the single gold mark. The
   same channel in gold twice is still gold twice, even when it is the same
   fact both times.

   Scoped by a body class rather than a descendant selector because the
   header sits outside {% block content %} and nothing inside the page can
   reach it. Every other page keeps the header gold it already had.
   ============================================================ */
body.page-dashboard .header-revenue-value { color: var(--primary); }
.board-detail-wrap .chart-row.rank-1 .rank-num { color: var(--primary); text-shadow: none; font-size: 52px; }
.board-detail-wrap .chart-row.rank-1 .revenue-amount { color: var(--primary); }
.board-detail-wrap .chart-row.rank-1 .revenue-bar-fill { background: var(--primary); }

/* Below-fold section spacing, varied on purpose rather than one repeated
   rhythm. The numbering has a hole in it because the section that used the
   missing one (Turnover, in detail) was deleted rather than renumbered, so
   the two survivors keep the names the design history refers to them by. */
.dash-below-1 { margin-top: 44px; }
.dash-below-2 { margin-top: 52px; }
.dash-check { font-size: 13px; line-height: 1.6; color: var(--muted-mid); margin: 14px 0 0; max-width: 74ch; }

/* ============================================================
   CONTENT ID, the parts of a line the dashboard only totals. Deliberately thin: the
   table itself is the shared .data-table, so this adds only what a ranking of money
   needs and no new component. Any rule here that could have been .data-table's belongs
   there instead.
   ============================================================ */

/* Right-aligned and tabular, so a column of money reads as a column. Figures of
   different widths left-aligned are the reason a ranking looks ragged and a reader
   compares the wrong digits. */
.cid-num { text-align: right; font-variant-numeric: tabular-nums; }
.cid-rank { color: var(--muted); width: 2.5rem; }
.cid-money { font-weight: 600; }
/* The two parts are set below the total they add to, because they are its components
   and not three figures of equal standing. */
.cid-part { color: var(--muted); }
.cid-title a { color: inherit; }
.cid-uploader { color: var(--muted); }
/* The coverage sentence and the cap note are the two things on this page that stop a
   200-row slice being read as the whole catalogue, so they are set as prose at reading
   size rather than as fine print. */
.cid-coverage { margin: 10px 0 18px; max-width: 70ch; }
.cid-note { margin-top: 18px; max-width: 70ch; color: var(--muted); font-size: 13px; }

/* ============================================================
   CAMPAIGNS - the same material as the strategy page read the other way up: by the
   attempt rather than by the channel. Two prose lines above the tables, which is the
   only thing the panel needs that the shared furniture does not already provide.
   Compound selectors per the source-order rule.
   ============================================================ */
.panel.panel-campaign .campaign-strategy {
  font-size: 14px;
  line-height: 1.55;
  color: var(--text);
  margin: 0 0 6px;
  max-width: 80ch;
}

/* The source-order trap: bare .muted sits 3,000 lines above and loses to the compound rule directly above this one, so without this an unwritten strategy printed "No strategy written yet." at full reading strength. */
.panel.panel-campaign .campaign-strategy.muted { color: var(--muted-mid); }

/* The campaign name is the panel; "Targets" and "Work" are sections inside it. At
   one weight the render read "EC PACKAGING RECOVERY (SEP)" and "TARGETS" as
   siblings, so the eye had nothing to anchor a panel on. One step up the page's own
   scale, between .panel-title (19px/700) and .page-title (34px/900). Scoped to the
   h2, which is the difference the markup already makes: the section heads are h3s
   and keep the base size. */
.panel.panel-campaign > .panel-head h2.panel-title {
  font-size: 24px;
  font-weight: 800;
}

/* The goal reads as the caption to the strategy above it rather than as a second
   statement of equal weight: the argument is the campaign, the goal is what it is
   pointed at. */
.panel.panel-campaign .campaign-goal {
  font-size: 13px;
  color: var(--muted-mid);
  margin: 0 0 18px;
}

.panel.panel-campaign .campaign-goal-scope { color: var(--muted); }

/* The "Where it is" line: when the push started, when it is answerable by, and which
   day of that it is on. It borrows .panel-note's small caps rather than setting a
   size of its own, because a third type size between the campaign name and the
   section notes would give the panel three levels of heading for two levels of
   information. Compound, per the source-order rule at the top of this block. */
.panel.panel-campaign .campaign-where {
  display: block;
  margin: 0 0 7px;
}

/* TIME GONE, AND NOTHING ELSE. Read the colour before changing it: --primary is what
   a MEASURED bar means everywhere in this sheet (the objectives track, the revenue
   bar), and this rule is the calendar, so drawing it in the same cyan would teach
   that time passing is progress. It is the quiet --muted on the same track token the
   objectives bar uses, 3px rather than 8px, so it reads as a rule under a line of
   text and not as a widget of its own.

   THE WIDTH IS A STEP, NOT A NUMBER IN THE MARKUP. Every other bar in this app sets
   its width with a style attribute; this page may not, so the fill carries a
   data-elapsed step and takes its width from the block below. The pair is held
   together by cugate_channels.tests.CampaignPanelCssContractTests, which fails if
   the steps here and dashboard.campaigns.ELAPSED_STEPS ever disagree -- a step with
   no rule draws at no width at all, which renders as a campaign that has not
   started and raises nothing. */
.panel.panel-campaign .campaign-elapsed {
  height: 3px;
  background: var(--rank-track);
  border-radius: 2px;
  overflow: hidden;
  margin: 0 0 16px;
  max-width: 80ch;
}

.panel.panel-campaign .campaign-elapsed-fill {
  height: 100%;
  background: var(--muted);
}

/* One rule per step in dashboard.campaigns.ELAPSED_STEPS. Class plus attribute, so
   nothing bare can beat them, and the base rule above sets no width of its own --
   these are the only widths this element can take. */
.campaign-elapsed-fill[data-elapsed="0"] { width: 0%; }
.campaign-elapsed-fill[data-elapsed="5"] { width: 5%; }
.campaign-elapsed-fill[data-elapsed="10"] { width: 10%; }
.campaign-elapsed-fill[data-elapsed="15"] { width: 15%; }
.campaign-elapsed-fill[data-elapsed="20"] { width: 20%; }
.campaign-elapsed-fill[data-elapsed="25"] { width: 25%; }
.campaign-elapsed-fill[data-elapsed="30"] { width: 30%; }
.campaign-elapsed-fill[data-elapsed="35"] { width: 35%; }
.campaign-elapsed-fill[data-elapsed="40"] { width: 40%; }
.campaign-elapsed-fill[data-elapsed="45"] { width: 45%; }
.campaign-elapsed-fill[data-elapsed="50"] { width: 50%; }
.campaign-elapsed-fill[data-elapsed="55"] { width: 55%; }
.campaign-elapsed-fill[data-elapsed="60"] { width: 60%; }
.campaign-elapsed-fill[data-elapsed="65"] { width: 65%; }
.campaign-elapsed-fill[data-elapsed="70"] { width: 70%; }
.campaign-elapsed-fill[data-elapsed="75"] { width: 75%; }
.campaign-elapsed-fill[data-elapsed="80"] { width: 80%; }
.campaign-elapsed-fill[data-elapsed="85"] { width: 85%; }
.campaign-elapsed-fill[data-elapsed="90"] { width: 90%; }
.campaign-elapsed-fill[data-elapsed="95"] { width: 95%; }
.campaign-elapsed-fill[data-elapsed="100"] { width: 100%; }

/* ===== LINKS THIS SHEET HAD NEVER COLOURED =====
   There is no bare `a { color }` rule anywhere in this file, which is a deliberate
   choice everywhere a link carries its own class (.panel-link, .server-link,
   .row-edit, .c-title) and an accident in the places below, where the anchor is
   written inline with no class on it. Those rendered in the browser's own default
   link blue on the app's #041829 panels, and the worst of them is the one a reviewer
   has to click: the .mp4 handed in at the edit stage is a bare URL in that blue,
   which on that background is close to unreadable.

   THE SOCIAL ACCOUNTS TABLE JOINED THEM ON 31 AUGUST, found the same way: rendered,
   looked at, and the profile column was that same unreadable blue. It is the same
   fact as the other three -- a pasted URL written bare -- so it joins the rule
   rather than getting one.

   THE TWO PROSPECT PAGES JOINED ON 1 SEPTEMBER, found the same way again: rendered
   at 1440 in both themes, looked at, and every YouTube channel name, every video
   title and the handle under the heading were that default blue. Same fact, so the
   same rule. THESE TWO ARE SCOPED TO THE PAGE and the other four are not, which is
   the note above about specificity arriving as a problem: `.data-table th a` written
   bare is a class plus two types (0,1,2) and would beat `.row-edit` (0,1,0) on the
   task board, recolouring a link that already has its own colour on a page this
   change has no business touching.

   ONE GROUPED RULE AT THE END OF THE SHEET, and both halves of that are on purpose.
   Grouped, because they are one fact -- an unclassed anchor in this app's prose --
   and four rules three thousand lines apart is four places to keep in step. At
   the end, because of the source-order trap this file has now been caught by four
   times: same specificity later in the file wins, and a rule placed beside its own
   block can be beaten by anything written after it.

   AND IT IS MORE SPECIFIC THAN A BARE CLASS ON THE ANCHOR, which is worth saying
   because the obvious assumption is the other way round. `.handed-in-value a` is a
   class plus a type (0,1,1) and `.task-facts dd a` is a class plus two (0,1,2),
   while `.panel-link` and the rest are 0,1,0 -- so putting one of those classes on
   an anchor INSIDE one of these containers would not override this rule, and the
   link would keep --primary. None of them holds a classed anchor today (all are
   written bare, in templates/tasks/list.html, templates/tasks/detail.html,
   templates/dashboard/index.html and templates/promotion/account_list.html), which
   is why this is a note rather than a problem: the day one needs its own colour, it
   needs a compound selector.

   var(--primary) rather than a colour typed here: it is the token every other link
   in the sheet already uses, and it is themed, so this reads correctly in light as
   well as dark.

   THE BROWSER'S OWN UNDERLINE IS LEFT ALONE, which is a reversal of what the
   classed links do. Those sit in tables and cards where position already says they
   are controls; these sit inside sentences and inside a pasted URL, where the
   underline is the only thing saying so before the pointer arrives. The app's own
   complaint about the Inbox row is that it has "no underline, no arrow, no button
   chrome", so removing one here would be arguing both sides. */
.handed-in-value a,
.task-facts dd a,
.objective-empty a,
.promo-accounts td a,
.page-prospects .ch-identity a,
.page-prospects .data-table th a { color: var(--primary); }

/* ===== THE REQUESTS PAGE =====
   What the team has asked for, as a record rather than a board. Ruled rows under
   the four eyebrow headings, no panel and no card: the page is read downwards, and
   a box around every ask would make forty of them look like forty things to do.

   NOTHING NEW IS INVENTED HERE. The chip is .c-tag, which the board already draws;
   the headings are .section-eyebrow; the separator dot is the board's .dot. What is
   below is the row's own geometry and the two states a row can be drawn in.

   AT THE END OF THE FILE deliberately, and the compound selectors below are the
   other half of that: this sheet has been caught four times by a bare single-class
   rule losing to a later one, so every state here is written as .request-row plus
   its modifier, and cugate_channels.tests asserts the pairs at a rule boundary. */
.request-row {
  padding: 14px 10px;
  margin: 0 -8px;
  border-bottom: 1px solid var(--border);
}
.request-row:last-child { border-bottom: 0; }
/* NO ZEBRA, WHICH THE BOARD HAS AND THIS DELIBERATELY DOES NOT. The board's rows
   are wrapped one group to a container, so nth-child counts within a channel; here
   the rows and the four headings are siblings of each other, so the same rule
   counts across the whole page and tints whichever rows happen to fall even. The
   first render of this page had exactly one striped row in six, which reads as a
   rendering fault rather than as banding. The rules between rows are what separate
   them, which is this page's whole treatment. */

.request-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 12px;
  row-gap: 4px;
}

/* The row's permanent link, drawn as text and not as a link: this page is a wall of
   titles, and colouring every one of them would turn the record into a wall of
   accent. The affordance is on hover and on focus, where a keyboard reader needs
   the ring anyway. The board's own .c-title makes the same trade. */
.request-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text-strong);
  line-height: 1.4;
  text-decoration: none;
}
.request-title:hover { color: var(--primary); text-decoration: underline; }

.request-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 3px;
  color: var(--muted-mid);
  font-size: 12px;
  line-height: 1.3;
}
.request-meta .dot { color: var(--border-strong); }
/* Who asked, at full strength. It is the one thing on the line the page exists to
   carry: a request is somebody's, and the record is worth nothing if it does not
   say whose. Same weight the board gives the person holding a task. */
.request-who { font-weight: 700; color: var(--text-strong); }

/* The builder's answer, set as the prose it is rather than as another chip. */
.request-note {
  margin: 6px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted-mid);
  max-width: 78ch;
}

/* A group with nothing in it says so in one quiet line rather than leaving a gap
   under its heading, which reads as a rendering fault. */
.request-none {
  margin: 10px 0 0;
  font-size: 13px;
  color: var(--muted);
}

/* The state word, in the quietest chip the board has: the group heading above the
   row already said it, and this is the confirmation rather than the news. */
.c-tag.c-tag-state {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--muted);
}

/* FALLING BEHIND: old, and nothing written on it lately. The app's own --danger,
   the same treatment .c-tag-owner carries for the other thing on a page that
   nothing will move on its own, and mixed against the token rather than typed as
   an rgba so it reads in both themes. A full border, never a left stripe. */
.c-tag.c-tag-late {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--danger) 32%, transparent);
  color: var(--danger);
}
/* The title and the age go with it. The chip alone is 9px of type at the end of a
   line, and the row it is on is the one row on the page somebody has to notice; the
   age is the number the mark is a reading OF, so it is the second thing to read. */
.request-row.is-behind .request-title { color: var(--danger); }
.request-row.is-behind .request-age { color: var(--danger); font-weight: 600; }

/* THE PROMISE, ONCE THE DAY HAS GONE. A date the builder gave and did not keep is
   the one thing on this line somebody committed to, so it is read at the same
   strength as the mark on the row rather than in the meta line's grey. Written as a
   pair for the reason every state on this page is: a bare .is-passed at 0,1,0 is a
   name a later rule anywhere in six thousand lines could take. A promise still ahead
   declares nothing of its own and reads as the rest of the line does. */
.request-promise.is-passed { color: var(--danger); font-weight: 600; }

/* ===== FIRST TEN DAYS ===== */
/* How one upload opened: ten measured days, with the YouTube channel's own median for
   each of those days under it. Two rows of figures and nothing else. No bar, no curve,
   no colour against the median and no target, because what an opening MEANS depends on
   the release, the season and the playlist it landed in, and any of those treatments
   would be the app scoring a thing it cannot score. The reader compares the two rows.

   Every selector here is scoped, never a bare class. A 0,1,0 rule this far down a
   six-thousand-line sheet is one that any later rule of the same weight takes on source
   order alone, and the page goes on rendering while the declaration quietly does
   nothing -- which has happened in this file four times that are written down. */
.panel > .first-days {
  display: grid;
  grid-template-columns: repeat(10, minmax(0, 1fr));
  gap: 10px 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.first-days .first-days-cell {
  display: flex;
  flex-direction: column;
  gap: 5px;
  /* Without this a long figure widens its own column and the ten stop being a row. */
  min-width: 0;
}

/* The day number is a LABEL, not a figure, so it takes the sheet's caption recipe
   (10px / 600 / 0.12em) rather than the numeral face the two figures under it use.
   Getting that the wrong way round makes the row read as thirty numbers. */
.first-days .first-days-day {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  color: var(--muted);
}

/* The measured figure: the numeral face every compact readout in this sheet uses, with
   tabular figures so ten of them line up as one row instead of ten widths. */
.first-days .first-days-views {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 800;
  font-size: 16px;
  line-height: 1;
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
}

/* The median, one step quieter in the SAME face, because it is the same kind of number
   as the one above it and a different face would read as a different kind of thing. */
.first-days .first-days-median {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 700;
  font-size: 13px;
  line-height: 1;
  color: var(--muted-mid);
  font-variant-numeric: tabular-nums;
}

/* The one sentence under the row, and the only copy in the panel: it is what tells the
   reader which of the two lines is which. Sized as the page's other panel prose
   (.rel-line, .today-row-meta) so it does not read as a caption belonging to the grid. */
.panel > .first-days-line {
  margin: 14px 0 0;
  font-size: 13px;
  line-height: 1.4;
  color: var(--muted-mid);
}

/* Ten columns on a phone is two characters each. Five and five, which keeps day one
   under day six and so keeps the row readable as a week and a bit. */
@media (max-width: 640px) {
  .panel > .first-days { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
/* ===== PORTFOLIO (the roster ranked by a year of money) ===== */
/* THE CATALOGUE-WIDE CONTENT ID SHARE, above the roster and never in it. The API
   cannot attribute this split to a channel, so the figure is stated once at the top
   of the page instead of sitting in a column where it would be read as one row's.

   TYPE AND NOTHING ELSE. No colour, no fill, no rule line: a share of revenue is
   exactly the figure that attracts a treatment, and this page is read to decide what
   to close, where a colour answers the question instead of the number. The percentage
   is carried by <b> and the width is what makes the sentence readable. The two spans
   inside it are already drawn -- .portfolio-coverage below, .panel-link five thousand
   lines up -- so this block adds one rule rather than four.

   SCOPED BY THE BODY CLASS, like every other rule in this section: a bare
   .portfolio-split at 0,1,0 in a seven-thousand-line sheet is beaten in silence by
   any later rule of the same specificity, and this sheet has been caught by that
   more than once. The note under it reuses .portfolio-note further down rather than
   adding a near-copy of it. */
body.page-portfolio .portfolio-split {
  margin: 0 0 6px;
  max-width: 70ch;
  line-height: 1.55;
}

/* AIR BETWEEN THE TWO SECTIONS, found by rendering it (3 September, dark at 1440).
   .section-eyebrow carries a bottom margin and no top one, because it is nearly
   always the first thing in its block; where it is not, the call sheet spaces it
   with .call-sheet .section-eyebrow:not(:first-child) further up. Here it follows a
   paragraph. Without this the caveat's last line sits against the roster's eyebrow
   and reads as its caption rather than as a note on the figure above it.

   Written as the sibling of the note rather than as a modifier class on the eyebrow:
   the shift belongs to the boundary between the two sections, not to the eyebrow,
   and a class would have to be remembered by whoever adds the third section. The
   existing .eyebrow-shift is not it -- that one also resizes the label. */
body.page-portfolio .portfolio-note + .section-eyebrow { margin-top: 26px; }

/* THE FLAG IS WORDS AND THIS RULE IS WHAT KEEPS IT THAT WAY. The page is read to
   decide which YouTube channels to close, and a red cell answers that question with
   a treatment rather than with a figure -- so nothing here sets a colour, a fill or
   a border, and the contract test next to it fails if one is added. Block, because
   two flags on one row are two statements and not a phrase.

   SCOPED BY THE BODY CLASS base.html provides, like the Social notes. A bare
   .portfolio-flag at 0,1,0 in a seven-thousand-line sheet is beaten in silence by
   any later rule of the same specificity, with the markup, the class and the words
   all still correct; that trap has caught this stylesheet four times. */
body.page-portfolio .portfolio-flag {
  display: block;
  font-size: 12px;
  letter-spacing: 0.02em;
}

/* How much of the twelve months is actually stored, shown only where it is fewer
   than all of them. Quieter than the flag above it because it is the reason there
   is no flag rather than a second one. --muted is what .page-sub and every other
   secondary line in this sheet reads; no colour is introduced. */
body.page-portfolio .portfolio-coverage {
  display: block;
  font-size: 12px;
  color: var(--muted);
  letter-spacing: 0.02em;
}

/* Whose the decision is, at full strength, because it is the one sentence on the
   page that is not a number. The line under it is the smaller print it sounds
   like. Margins because the reset zeroes them: air off the table, and less of it
   between two lines that belong together. */
body.page-portfolio .portfolio-decision {
  margin: 18px 0 0;
  max-width: 70ch;
}

body.page-portfolio .portfolio-note {
  margin: 6px 0 0;
  max-width: 70ch;
  color: var(--muted);
  font-size: 13px;
}

/* THE YOUTUBE CHANNEL NAME IS THE ROW HEADER AND THE LINK INTO THAT CHANNEL'S OWN
   ANALYTICS PAGE, and without this rule it is drawn by the browser. There is no
   bare `a { color }` in this sheet on purpose -- links that carry a class are
   coloured by the class -- so an unclassed anchor falls through to the user agent,
   which is the fault UnclassedLinkColourCssContractTests exists for. Measured on
   the rendered page (30 August, dark at 1440): the most important column on the
   table was the faintest thing on it, in the browser's default blue on #041829.

   INHERIT RATHER THAN THE LINK TOKEN, unlike the four anchors that test covers.
   Those are single links inside a line of text; this is every row of the first
   column, and colouring all eight teal would make the table read as a list of
   links rather than as a table. The row header keeps its own weight and colour and
   the underline is what says it opens, which is how the Content ID table's video
   column already works. */
body.page-portfolio .data-table th[scope="row"] a { color: inherit; }

/* THE TOTAL LINE UNDER THE ROSTER, AND IT IS THE SOCIAL ROLL-UP'S LINE RATHER THAN
   A SECOND ONE. Every declaration `.data-table.promo-rollup tfoot` above sets, this
   sets to the same value (a rule above the row and weight, no fill, the border
   horizontal and never down a side), because two tables that both total themselves
   should not total themselves two ways. dashboard/tests_portfolio.py holds that,
   declaration by declaration, so retuning one moves both or the divergence gets
   written down. The one addition is named at the bottom of this comment.

   COMPOUND WITH .data-table, like every .promo-rollup rule and for the reason this
   sheet keeps learning: a bare `.portfolio-table tfoot td` is 0,1,0 plus two
   elements, and the responsive block at the 600px breakpoint restyles
   `.data-table td` wholesale a thousand lines further down at the same weight. The
   markup, the class and the figures would all still be right and the row would be
   drawn by somebody else's rule. Four incidents, and every one of them arrived as
   the SECOND rule added to a class rather than the first -- so the test scans every
   occurrence of the name in this file, not only the ones written here.

   NO `display` IN EITHER RULE, deliberately. These selectors outrank the responsive
   block, so a display here would pin the foot row open below 600px while every row
   above it folded into its card.

   THE ALIGNMENT IS IN THE GROUP rather than in a rule of its own, exactly as the
   roll-up has it: a second rule for the same cell at the same specificity is the
   source-order trap again. The sheet's own left alignment is written for `tbody th`,
   so without this the foot row's label falls through to the UA's centred default and
   sits half a column in from every YouTube channel name above it.

   ONE DECLARATION MORE THAN THE ROLL-UP HAS, AND IT WAS FOUND BY LOOKING. `.data-table
   td` gives every cell a bottom border and `.data-table tbody tr:last-child td` takes it
   off the last row, which is this sheet saying the bottom of a table is not ruled. That
   exemption is written for `tbody`, so a foot row brings the border back -- and only on
   its `td`s, because the matching `th` half of it is written for `tbody th`. What
   renders is a rule that starts a column and a half in and hangs off the right of the
   table. Measured on the render at 1440, both themes. The roll-up has the same
   artefact for the same reason and is left alone here: it is another section's page,
   and the general fix belongs in `.data-table` above rather than in either page's
   block. */
.data-table.portfolio-table tfoot th,
.data-table.portfolio-table tfoot td {
  padding: 10px 12px;
  font-size: 13px;
  border-top: 1px solid var(--border-strong);
  border-bottom: 0;
  font-weight: 600;
  color: var(--text-strong);
  text-align: left;
}
.data-table.portfolio-table tfoot td { text-align: right; }

/* What the two figures on this row are NOT: the subscriber count is a sum of
   counts rather than an audience, the twelve-month sum can hold a short window, and
   the views total can cover fewer rows than sit above it. Set under the label and
   under the views figure they qualify, quieter and lighter than the total itself,
   like the roll-up's account detail line -- they are a caveat on a number, not a
   second number. Block, so each is its own statement rather than a run-on.

   NO COLOUR IS INTRODUCED: .muted already carries --muted-mid, three thousand lines
   up, and this only turns off the weight the rule above just set. */
.data-table.portfolio-table tfoot .muted {
  display: block;
  font-size: 12px;
  font-weight: 400;
}

/* THE FOOT ROW IS NOT AN ACCORDION AND MUST NOT BE DRAWN AS ONE. Below 600px this
   sheet turns every row into a collapsed card whose header opens it, and
   table-cards.js binds only `tbody tr > th[scope="row"]` -- so nothing on the page
   can ever open this one. Left to the responsive block it renders as a card with a
   disclosure chevron that does nothing, its two qualifiers laid out across a flex
   line rather than stacked under the label, and every one of its figures still at
   `display: none`: a total row showing no totals. Read at 420 before this block
   existed, which is the only way it was ever going to be found.

   So the foot row is shown open instead, its label goes back to a block, and its
   cells fold into the same "label: value" lines an expanded card already uses. The
   padding and the borders go back to the card's too: the desktop rules above
   outrank the responsive ones, so without this the rule line meant to sit above the
   total draws through the middle of the card, once per cell. */
@media (max-width: 600px) {
  .data-table.portfolio-table tfoot th,
  .data-table.portfolio-table tfoot td {
    padding: 6px 14px;
    border: 0;
  }
  .data-table.portfolio-table tfoot th {
    display: block;
    padding-top: 14px;
    font-size: 15px;
    cursor: default;
  }
  .data-table.portfolio-table tfoot th::after { content: none; }
  .data-table.portfolio-table tfoot td { display: flex; flex-wrap: wrap; }
  /* A card cell is a flex line of label and value, so the views qualifier would be
     a THIRD item on it and `space-between` would park it at the far end, reading as
     a second figure rather than as a note on the first. Given the whole width it
     drops under the figure it belongs to, which is where it sits on the desktop. */
  .data-table.portfolio-table tfoot td .muted { width: 100%; }
  /* The flags column has no total, so its cell is empty. A card line for it would be
     a label with nothing after it, which reads as a figure that failed to load. */
  .data-table.portfolio-table tfoot td:empty { display: none; }
}
/* ===== THE YOUTUBE CHANNEL IDEAS PAGE =====
   It borrows the Requests page's row wholesale -- the ruled line, the title, the meta
   line, the quiet chip -- because the two pages are the same kind of thing: a record
   read downwards rather than a set of things to do. So the only thing here is the
   control under an idea nobody has answered yet, and it paints nothing: the box is
   .form-control, the buttons are .btn-dark and .btn-outline-dark, all already drawn.

   SCOPED TO THE ROW, WHICH IS THE POINT. These two rules are the last in a file of
   six thousand lines, which is where anything appended lands and where a bare 0,1,0
   name loses to whatever comes next. Written as descendants of .request-row they are
   0,2,0 and cannot be taken by a later single-class rule of the same name;
   ideas.tests_css asserts the pair at a rule boundary. */
.request-row .idea-decide {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
}
/* The box gives up the full width .form-control takes on a form panel, which is
   right there and wrong in a flex row: at 100% it pushes both buttons off the end.
   Scoped to this control rather than widened on .form-control, which every form in
   the app draws. It still grows into whatever the row has spare, down to a width a
   short reason is readable in and up to a line nobody has to scan across. */
.request-row .idea-decide .form-control {
  width: auto;
  flex: 1 1 240px;
  max-width: 420px;
}

/* ===== VIEWING THE APP AS SOMEBODY ELSE =====
   The bar base.html draws at the top of the main column while a superuser is looking
   at the app as another person. It is the only thing on the page saying the screen is
   not theirs, so it is drawn to be unmissable and nothing else here is.

   THE APP'S OWN NOTICE RECIPE AND NO NEW TOKEN: a full border and a wash of the same
   colour behind the words, which is what .bleed-finder and the flash notices do. No
   border down one edge, the one decoration this project does not draw anywhere. No
   red, amber or green either -- this is a state a reader has chosen, not a verdict on
   anything, and the palette has no warning colour to lend it (see the note at the top
   of this file, and FlashNoticeCssContractTests, which had to retire two rules that
   reached for one).

   THE GROUND IS MIXED, NOT TINTED. --primary-dim would have been the obvious fill and
   is translucent in both themes, which is fine for a block that sits still and wrong
   for one that sticks: the page would scroll through it. color-mix against --bg is
   opaque, uses the two tokens already there, and lands at the same weight.

   IT STICKS UNDER THE HEADER (64px, .header's own height) because the risk this
   feature carries is a reader who has stopped remembering, and a notice that leaves
   the screen after one scroll is a notice for the first paragraph only. Below the
   header's z-index, above the page.

   THESE ARE THE LAST RULES IN A FILE OF SEVEN THOUSAND LINES, which is where anything
   appended lands. Every name here is unique to this block and written once, so there
   is nothing later for a bare single-class rule to lose to -- the trap the ideas
   block above documents. */
.impersonation-bar {
  position: sticky;
  top: 64px;
  z-index: 40;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 14px;
  margin-bottom: 24px;
  padding: 12px 18px;
  border: 1px solid var(--primary);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--primary) 16%, var(--bg));
}

.impersonation-bar-label {
  font-family: 'DM Sans', sans-serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--primary);
}

/* The one thing on the bar at full strength: whose screen this is. */
.impersonation-bar-name {
  font-family: 'Barlow Condensed', sans-serif;
  font-size: 20px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.04em;
  color: var(--text-strong);
}

.impersonation-bar-note {
  font-size: 13px;
  color: var(--muted-mid);
}

/* The way out sits at the far end of the row on a wide screen and drops under the
   name on a narrow one, which the wrap above already does; this only pushes it. */
.impersonation-bar-out {
  margin: 0;
  margin-left: auto;
}

/* ============================================================
   WHERE WE STAND. The front door: three figures about the company, six movement
   cards, and a sentence under every one of them.

   FOUR RULES AND NOTHING ELSE, because the page is drawn almost entirely from
   components this file already has. The label is .stat-label, the numeral is
   .stat-value, the unit beside it is .ctoday-unit, the sentence is .company-line
   and the section rules are .section-eyebrow. What was genuinely missing is a
   three-across track and a card heading, which is all that is below.

   NO BOX AND NO TINT. Every other figure component here sits in a bordered
   surface (.stat-card, .metric-card, .ctoday-strip); these do not, and it is the
   whole visual argument of the page. Memo's complaint was that the app reads as
   numbers in boxes with no words; type, weight and spacing carry the structure so
   the sentence under a figure reads as part of it rather than as a caption on a
   tile. Nothing is coloured by whether it is good news or bad either: a fall is
   set exactly like a rise. --danger is spent on the dashboard's own alerts, and a
   page that paints its bad news red teaches the reader to skip the rest of it.

   NO border-left ANYWHERE, which is worth saying because the mockup this page
   came from had a coloured stripe down every card. It is banned in this project
   and the treatment it was doing the work of is spacing.

   SCOPED AS DESCENDANTS WHERE THEY OVERRIDE. These are the last rules in a file
   of six thousand lines, which is where anything appended lands and where a bare
   0,1,0 name loses to whatever is appended next. The .wws- names are unique to
   this page, but the one rule that reaches an existing class (.company-line)
   is written as a descendant so it is 0,2,0 and cannot be taken back by a later
   single-class rule of the same name. --- */
.wws-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 28px 32px;
  margin: 0 0 36px;
}

/* THE MONTH'S ROW HOLDS TWO FIGURES AND NOT THREE, since Content ID left it for a
   rule of its own. On three tracks the pair would sit at a third of the width each
   with an empty column beside them, and the sentence under a figure is the whole
   point of this page: it needs the measure. The movement rows keep three tracks,
   because they hold up to three cards and a short list under three of them reads
   as a list rather than as a gap. Written as a compound selector so it cannot be
   undercut by a later bare .wws-grid rule in a stylesheet this long. */
.wws-grid.wws-pair { grid-template-columns: repeat(2, 1fr); }

/* Grid items refuse to shrink below their content by default, so a long YouTube
   channel name or an eight-figure numeral would push a column past its track and
   the three would stop being equal. */
.wws-figure { min-width: 0; }

/* The sentence needs air under the numeral it belongs to. .company-line sets its
   own bottom margin and no top one, being written for a stack of sentences. */
.wws-figure .company-line { margin-top: 10px; }

/* THE ONE FIGURE THAT IS NOT IN THE ROW. Content ID is twelve months of money and
   the row above it is one month, so it stands under a section rule of its own that
   names its own period; in the row it was the middle of three numerals under a
   caption reading "August 2026". Two rules do the work of the track it left: the
   text column width the app uses for a stack of sentences, so a full-width
   paragraph does not run to a line nobody can follow back, and the row's own
   bottom margin, so the section under it starts exactly where it did before. */
.wws-solo { max-width: 640px; margin: 0 0 36px; }

/* The YouTube channel a movement card is about. Set like .roadmap-item-title,
   which is the app's existing treatment for a name at the head of a small block:
   the same size, weight and tracking, so a card here and a row there read as one
   family. It is an h3 for the outline, which is why the margin is stated. */
.wws-name {
  font-size: 16px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--text-strong);
  margin: 0 0 8px;
}

/* ONE COLUMN BEFORE THE FIGURES START WRAPPING INSIDE THEMSELVES. Three tracks in
   the main column stop holding a money figure and a four-line sentence somewhere
   under 900px, well above the app's 640px mobile breakpoint, so this page needs
   its own. Two across was tried and is worse: it leaves a lone third card on its
   own row with the same weight as a pair, which reads as an afterthought rather
   than as the third of three. */
@media (max-width: 900px) {
  /* The pair is named again rather than left to the line above it: two classes
     beat one wherever they meet, so a bare .wws-grid here would lose to the
     .wws-grid.wws-pair rule further up and the month's row alone would stay two
     across on a phone. */
  .wws-grid,
  .wws-grid.wws-pair { grid-template-columns: 1fr; gap: 24px; }
}

/* ---------------------------------------------------------------------------
   THE VIDEO PAGE. One video, its campaign, what was expected of it and every
   round of work on it. Scoped on the body class throughout, because three of the
   rules below reach into markup the rest of the app shares -- a data table's row
   heading, and the .ledger grid a second page could reasonably want later -- and
   an unscoped rule in a file this long is a rule that turns up on a page nobody
   was thinking about. Every colour is a token: nothing here paints a state.
   --------------------------------------------------------------------------- */

/* The video's own name in the controls table, and the work row's task link. A
   row heading that is a link should read as the heading it is, not as a run of
   underlined blue -- the portfolio table settled this the same way. */
/* The campaigns work table gained row links to the video page and drew them in
   the browser's default blue and visited purple. Same treatment as the video page
   below and the portfolio table above: the row header keeps its own colour and
   the underline on hover is what says it opens. */
body.page-campaigns .data-table th[scope="row"] a { color: inherit; text-decoration: none; }
body.page-campaigns .data-table th[scope="row"] a:hover { text-decoration: underline; }
body.page-video .data-table th[scope="row"] a { color: inherit; text-decoration: none; }
body.page-video .data-table th[scope="row"] a:hover { text-decoration: underline; }

/* The campaign's name is a heading first and a link second: a panel title set in
   link blue and underlined reads as a control, and the panel it heads is a summary
   of a page this reader may not have asked to leave for. Same treatment the row
   headings above take, and the campaigns page it opens sets its own titles plain. */
body.page-video .panel-campaign .panel-title a { color: inherit; text-decoration: none; }
body.page-video .panel-campaign .panel-title a:hover { text-decoration: underline; }

/* What the video IS -- the picture and the strings it goes by -- beside the campaign
   panel. A fixed picture column, with the title lines stacked under the frame.

   SHRINKING THE PICTURE WAS TRIED AND REVERTED. A 132px frame beside the titles won
   back about 190px of width and none of it went to content: the hero's left side got
   shorter than the campaign panel beside it and the space became a void. And the
   frame is a 16:9 crop of a 4:3 hqdefault at every width, so shrinking it does not
   shrink the crop, it shrinks what survives one: 132px leaves 74px of height, and a
   sleeve carrying its own title is unreadable at that size. A page about numbers
   still has to let a reader recognise which video the numbers are about. */
body.page-video .video-hero {
  display: grid;
  /* A FIXED PICTURE COLUMN, not a fraction. 1.2fr against 1fr was tried on 15
     September and it made the frame's size a function of the viewport and of what
     the other column happened to be carrying, which is not a size anybody chose.
     320px is the width the sidebar leaves, it is enough to read a sleeve by, and it
     is the same on every screen wide enough to have two columns at all. */
  grid-template-columns: 320px minmax(0, 1fr);
  gap: 28px;
  align-items: start;
  margin-bottom: 8px;
}

/* The picture, then the titles UNDER it, one column. Beside it they had to share a
   320px track, which is what wrapped "Live now" and a YouTube title mid-phrase; under
   it each title gets the whole column and breaks where the sentence does. The gap is
   the whole of the space between the frame and the first title, so the list below can
   carry no margin of its own and the two cannot come to disagree. */
body.page-video .video-ident {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 10px;
  align-items: start;
}

body.page-video .video-thumb {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--radius);
  display: block;
}

/* The title lines under it: one row per stored string, ruled rather than boxed.
   The label column is fixed so the titles start on one edge however short the
   label is, and the rule is the app's own border token. No margin: the space over
   the first row is the ident's gap, and a margin here would be added to it. */
body.page-video .video-titles { margin: 0; padding: 0; list-style: none; }

body.page-video .video-titles li {
  display: flex;
  gap: 12px;
  align-items: baseline;
  padding: 6px 0;
  border-top: 1px solid var(--border);
}

body.page-video .video-titles li:first-child { border-top: 0; }
body.page-video .video-titles .when { min-width: 96px; }

/* The four-cell ledger: what was expected, and what has been read back. Four
   equal tracks rather than auto ones, so the figures line up down the page when
   one of them is an em dash and its neighbour is six digits. */
body.page-video .ledger { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }

body.page-video .ledger .stat-value {
  font-family: 'Barlow Condensed', sans-serif;
  font-weight: 700;
  font-size: 34px;
  line-height: 1;
  margin: 4px 0 2px;
}

/* WHERE THE TARGET CAME FROM, AS ARITHMETIC RATHER THAN AS A PARAGRAPH. This was
   three sentences of prose run the full width of the panel, carrying four figures of
   six and seven digits at about 110 characters to the line, directly under the small
   print of the cell it was explaining. It exists so a reader can redo the division by
   hand, and nobody does that across a wrapped sentence.

   So: one row per step, the label in the body face on the left and the figure on the
   right in the monospaced stack this sheet already uses for a figure that is going to
   be read rather than skimmed. Tabular figures, so the digits line up down the column
   and the arithmetic can be followed with an eye rather than a finger.

   RULED, NOT BOXED, and the rule is the app's own border token on the TOP of each row
   -- the same treatment .video-titles above takes. No panel of its own, no fill, and
   no stripe down a side.

   CAPPED, because the labels are sentences and a sentence set to the full width of a
   panel is the thing this block was written to stop. */
body.page-video .derivation {
  width: 100%;
  max-width: 620px;
  border-collapse: collapse;
  margin: 18px 0 0;
}

body.page-video .derivation caption {
  caption-side: top;
  text-align: left;
  font-size: 12px;
  color: var(--muted);
  padding: 0 0 6px;
}

body.page-video .derivation th {
  text-align: left;
  font-weight: 400;
  font-size: 13px;
  line-height: 1.4;
  color: var(--text);
  padding: 7px 20px 7px 0;
  border-top: 1px solid var(--border);
}

body.page-video .derivation td {
  text-align: right;
  vertical-align: baseline;
  font-family: ui-monospace, 'SF Mono', 'DM Sans', monospace;
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  color: var(--text-strong);
  white-space: nowrap;
  padding: 7px 0;
  border-top: 1px solid var(--border);
}

/* The first step needs no rule over it: the caption is already above it. */
body.page-video .derivation tr:first-child th,
body.page-video .derivation tr:first-child td { border-top: 0; }

/* The disclosure control sits IN the row heading rather than in a column of its
   own: a column of carets is a column of empty space on every wide screen. The
   heading takes the padding the button is absolutely placed into, so the name
   starts on the same edge whether the drawer is open or shut. */
body.page-video .work-table th[scope="row"] { position: relative; padding-left: 26px; }

body.page-video .work-toggle {
  position: absolute;
  left: 4px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: 0;
  padding: 4px;
  cursor: pointer;
  color: var(--muted);
  line-height: 1;
}

body.page-video .work-toggle .work-caret {
  display: inline-block;
  transition: transform 0.15s;
  font-size: 14px;
}

/* Turned by the same attribute the script writes, so the caret cannot come to
   disagree with what the assistive tree is told. */
body.page-video .work-toggle[aria-expanded="true"] .work-caret { transform: rotate(90deg); }

/* The drawer itself: the rail on the left, what was handed in and why on the
   right. Indented to the row heading's own text edge so it reads as belonging to
   the row above it rather than as a second table. */
body.page-video .work-detail > td { padding: 14px 12px 16px 26px; }

body.page-video .work-open {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: 32px;
  align-items: start;
}

body.page-video .work-side { padding-top: 2px; }
body.page-video .work-side p { margin: 6px 0 14px; }
body.page-video .work-side .eyebrow-label + p { margin-top: 6px; }

/* ONE COLUMN BEFORE THE FIGURES START WRAPPING INSIDE THEMSELVES, the same call
   the Where we stand grid makes above and at the same width: four ledger cells
   hold a six-digit figure and a line of caption down to about 900px, which is
   well above the app's 640px mobile breakpoint, so this page needs its own. Two
   across for the ledger rather than one: the cells are a figure and a short
   caption, and a single column of four leaves the panel a ladder. */
@media (max-width: 900px) {
  /* The picture column stops being fixed once there is only one column to give it,
     or a 320px track would hold its width while everything else narrowed under it. */
  body.page-video .video-hero { grid-template-columns: 1fr; }
  body.page-video .ledger { grid-template-columns: 1fr 1fr; }
  body.page-video .work-open { grid-template-columns: 1fr; gap: 20px; }
}


/* ---------------------------------------------------------------------------
   RIGHTS AND ENFORCEMENT: THE CLAIMS BOARD

   The section that until 10 September held two fixture tables and a research essay.
   Everything here is compound with body.page-rights, which is the rule this file has
   been caught by four times: a bare single-class rule is one later group away from
   losing on source order.

   THE TILES ARE THE FILTERS, so each one is an <a> wearing .metric-card and has to
   give back the colour and the underline an anchor takes by default. The one that is
   on takes the same "you are here" tint the task filter pills take (--primary on
   --primary-dim) and never a solid fill: it is a view filter, not an action. No
   status colour anywhere on this page -- a claim's urgency is a word in its own row.
   --------------------------------------------------------------------------- */
body.page-rights .claim-tile {
  display: block;
  /* Five tiles on one row at desktop width; the fifth alone on a second row read as
     an afterthought (Jan, 11 Sep). Below about 1100px they wrap two and three. */
  flex: 1 1 180px;
  min-width: 0;
  max-width: none;
  color: inherit;
  text-decoration: none;
}
body.page-rights .claim-tile:hover { background: var(--surface-hover); }
body.page-rights .claim-tile.is-active {
  border-color: var(--primary);
  background: var(--primary-dim);
}
body.page-rights .claim-tile.is-active .metric-label,
body.page-rights .claim-tile.is-active .metric-value { color: var(--primary); }

/* The disclosure control sits IN the deadline heading rather than in a column of its
   own, the call the video page's work table already makes: a column of carets is a
   column of empty space on every wide screen. The heading takes the padding the
   button is absolutely placed into, so the date starts on the same edge whether the
   drawer is open or shut. */
body.page-rights .claims-table th[scope="row"] { position: relative; padding-left: 26px; }
body.page-rights .work-toggle {
  position: absolute;
  left: 4px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: 0;
  padding: 4px;
  cursor: pointer;
  color: var(--muted);
  line-height: 1;
}
body.page-rights .work-toggle .work-caret {
  display: inline-block;
  transition: transform 0.15s;
  font-size: 14px;
}
/* Turned by the same attribute the script writes, so the caret cannot come to
   disagree with what the assistive tree is told. */
body.page-rights .work-toggle[aria-expanded="true"] .work-caret { transform: rotate(90deg); }

/* The drawer: the instruction in full on the left, what Studio said and the places
   this claim can be opened on the right. Indented to the row heading's own text edge
   so it reads as belonging to the row above it rather than as a second table. */
body.page-rights .claim-detail > td { padding: 14px 12px 16px 26px; }
body.page-rights .claim-open {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: 32px;
  align-items: start;
}
body.page-rights .claim-open p { margin: 6px 0 14px; }
body.page-rights .claim-open .eyebrow-label + p { margin-top: 6px; }
body.page-rights .claim-links { margin: 6px 0 0; padding-left: 18px; }
body.page-rights .claim-links li { margin-bottom: 4px; }

/* EVERY ANCHOR IN THIS TABLE IS WRITTEN WITH NO CLASS -- the video title in a row
   heading, and the four links in a drawer -- and there is no bare `a { color }` rule
   in this sheet, so without this the browser draws them in its own blue on #041829.
   The same rule the prospect pages carry, added for the same reason and pinned in
   UnclassedLinkColourCssContractTests so it cannot quietly go again. */
body.page-rights .data-table a { color: var(--primary); }

/* FIVE COLUMNS, WIDTHS FIXED, so a long video title widens nothing: the claim
   column takes the room, the deadline and the escalation stay narrow, and the
   stage keeps its first sentence on two lines at most. The eight-column first cut
   wrapped every cell into a ladder (Jan, 11 Sep); ISRC and the rights position
   moved into the drawer under "The recording". */
body.page-rights .claims-table { table-layout: fixed; }
body.page-rights .claims-table .col-deadline { width: 11%; }
body.page-rights .claims-table .col-claim { width: 37%; }
body.page-rights .claims-table .col-claimant { width: 16%; }
body.page-rights .claims-table .col-stage { width: 24%; }
body.page-rights .claims-table .col-escalate { width: 12%; }
body.page-rights .claims-table td { overflow-wrap: anywhere; }

/* THE claims@ BOARD WORDS ITS CLOCKS, and at the Watchdesk's 11% the first render of
   it wrapped the longest of them to three lines. Its status is one or two
   words and its claimant carries the claimed recording under the name, so the room
   moves from the video title and the status to those two. Compound on the table's
   own second class, so the Watchdesk table under it keeps the widths above. */
body.page-rights .claims-table.claims-mail .col-deadline { width: 17%; }
body.page-rights .claims-table.claims-mail .col-claim { width: 30%; }
body.page-rights .claims-table.claims-mail .col-claimant { width: 23%; }
body.page-rights .claims-table.claims-mail .col-stage { width: 16%; }
body.page-rights .claims-table.claims-mail .col-escalate { width: 14%; }

/* ONE COLUMN BEFORE THE DRAWER'S TWO START SQUEEZING A PARAGRAPH, the same width the
   video page's drawer makes the same call at. */
@media (max-width: 900px) {
  body.page-rights .claim-open { grid-template-columns: 1fr; gap: 20px; }
}

/* --- The track list on a video's task (templates/tasks/_track_list.html) ---
   A .data-table for reading, then the block that gets pasted, set like the
   description boxes (.description-current) because it is description copy: the
   same monospace stack, ink, deep fill and full border. Every rule is compound on
   .panel.track-list so a later single-class rule in this file cannot undo it. */
.panel.track-list .track-list-table td { vertical-align: top; }
.panel.track-list .track-list-composer { font-weight: 600; color: var(--text-strong); }
.panel.track-list .track-list-gap { margin: 12px 0 0; font-size: 13px; }
.panel.track-list .track-list-copy {
  display: flex;
  justify-content: flex-end;
  margin: 16px 0 8px;
}
.panel.track-list .track-list-block {
  margin: 0;
  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;
  max-height: 520px;
  overflow: auto;
}
html[data-theme="light"] .panel.track-list .track-list-block { background: var(--surface); }

/* --- Suggested ad breaks (templates/tasks/_ad_breaks.html) ---
   Each mid-roll slot is a .btn-outline-dark that reads its time and copies it, in
   one wrapping grid, so fifteen slots take a few lines instead of a table row each.
   The columns are equal and at least as wide as an HH:MM:SS time, a video past ten
   hours included, so the rows line up at any width and "Copied" fits where the time
   was. A time has no space in it and never wraps; the rare "Select and copy" may,
   for the second and a half it shows. The list carries the button's font as well,
   because the ch in its column width is measured in the list's own font.

   MENLO AND CONSOLAS ARE NAMED HERE AND NOT IN THE OTHER MONOSPACE BLOCKS. Chrome
   does not know ui-monospace and SF Mono is not a system font on a stock Mac, so
   the house stack lands on DM Sans, and the DM Sans this app loads has no tabular
   figures: measured in Chrome, "1:11:11" set 27px wide against 50px for "0:00:00".
   That is fine for a pasted block and wrong for a row of times meant to line up, so
   this stack reaches a real monospace face on macOS and Windows before DM Sans.
   Compound on .panel.track-list like the rules above. */
.panel.track-list .ad-break-slots {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(8ch + 22px), 1fr));
  gap: 8px;
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, 'DM Sans', monospace;
  font-size: 12.5px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.panel.track-list .ad-break-slots .ad-break-slot {
  justify-content: center;
  width: 100%;
  padding: 5px 10px;
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, 'DM Sans', monospace;
  font-size: 12.5px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
}
.panel.track-list .ad-break-slots .ad-break-slot:hover { color: var(--primary); }
