/* ══════════════════════════════════════════════════════════════════════════
   Course-start notice wizard - WaCourseStartWizard.vue, mounted by
   /admin/enrollments.

   Three steps inside one AdminSheet: the template and the cohorts, the roster,
   the approval. Everything here is `csn-` prefixed; the message bubble in step 3
   deliberately is NOT - it borrows `.wai-tpl__preview`, `.wai-tpl__body`,
   `.wai-tpl__footertext` and `.wai-tpl__buttons` from admin-whatsapp.css, the
   same rules the inbox thread and the bulk sheet draw a template with, so what
   is approved here and what is read on the customer's phone cannot drift apart.

   That borrowing is why the component wraps itself in `.wab-scope`: the `--wa-*`
   palette those classes resolve against is declared on the inbox page and on
   that class, and nowhere else. This file therefore links AFTER
   admin-whatsapp.css and admin-wa-bulk.css - see the cascade note in index.html.

   Direction: `.admin-main` is pinned `direction: ltr; text-align: left` for the
   whole content region, and `.wab-scope` re-establishes RTL. This file adds
   `text-align: start` on the root so the island never depends on which physical
   side that resolves to.
   ══════════════════════════════════════════════════════════════════════════ */

.csn {
  display: flex;
  flex-direction: column;
  gap: var(--space-md, 16px);
  direction: rtl;
  text-align: start;
}

/* ── The sheet this lives in is wider than the shared default ──────────────

   `.adm-sheet` is `min(460px, 100%)` on desktop, which is right for a filter
   panel and far too narrow for a roster carrying a name, a cohort and three
   money columns: at 460px the money columns collapse to nothing and the reader
   is deciding who to charge from a table they cannot read.

   Scoped with `:has()` rather than by widening `.adm-sheet` (that would resize
   every sheet in the panel) and rather than by an ancestor page class (the host
   page mounts other sheets of its own). Below 768px nothing here applies: the
   sheet is already full width and bottom-anchored. */
@media (min-width: 768px) {
  .adm-sheet:has(.csn) {
    width: min(880px, 100%);
    max-height: min(760px, 90vh);
  }
}

/* ── The step rail ────────────────────────────────────────────────────────

   Three labels, not a progress bar. The reader needs to know which of the three
   questions they are answering and that two more are coming - a percentage says
   neither. `list-style: none` with no padding, so the RTL indentation question
   never arises. */

.csn-steps {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs, 4px) var(--space-sm, 8px);
  margin: 0;
  padding: 0;
  list-style: none;
}

.csn-steps__item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border-radius: var(--radius-full, 980px);
  background: var(--wa-header);
  font-size: 0.76rem;
  color: var(--text-secondary, #5a5b6a);
}

.csn-steps__item--on {
  background: var(--wa-green-wash);
  color: var(--wa-green-ink);
  font-weight: 600;
}

/* A step already answered stays legible rather than fading out: it is the one
   the reader goes BACK to when the preview shows the wrong day. */
.csn-steps__item--done {
  color: var(--wa-ink-2);
}

.csn-steps__n {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
}

.csn-step {
  display: flex;
  flex-direction: column;
  gap: var(--space-md, 16px);
}

/* ── Blocks and their headings ────────────────────────────────────────────

   `<fieldset>` for the two that really are groups of controls (the template
   radios, the cohort checkboxes) and a plain block for the parameters, which is
   a heading over a list of labelled inputs rather than one choice. Both wear the
   same box so step 1 reads as three bands. */

.csn-fieldset {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm, 8px);
  margin: 0;
  padding: 0;
  border: none;
}

.csn-legend {
  padding: 0;
  margin: 0;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--wa-ink);
}

.csn-legend--plain {
  /* An `<h3>` rather than a `<legend>`, so it needs the line height a legend
     gets from the UA. */
  line-height: 1.4;
}

.csn-hint {
  margin: 0;
  font-size: 0.72rem;
  line-height: 1.5;
  color: var(--text-tertiary, #8e8f9e);
}

.csn-note {
  margin: 0;
  font-size: 0.74rem;
  line-height: 1.55;
  color: var(--wa-ink-2);
}

/* The payment parameter's own line, and the only one of these that is a RULE
   rather than a remark: there is no field for it anywhere in this wizard and
   there must never be one. Boxed and ruled so it reads as part of the parameter
   block it is explaining, not as a footnote under it. */
.csn-note--locked {
  padding: 8px 10px;
  border-radius: var(--radius-md, 12px);
  border: 1px dashed var(--wa-green-line);
  background: var(--wa-green-wash);
  color: var(--wa-green-ink);
}

/* ── Step 1: the PAIR ─────────────────────────────────────────────────────

   One offer is a PAIR of templates, not a template: the student who owes gets
   the half carrying his own balance and payment link, the student who is square
   gets the half that mentions money nowhere (D17). So a row here carries TWO
   name-and-status lines, one per half, and the pair is sendable only when both
   of them are.

   That is the whole reason both states are drawn rather than one summary word.
   A pair whose unpaid half is still in Meta's review queue must not read as
   sendable: half the cohort would receive nothing, and which half is decided by
   who owes money.

   ── AND EACH HALF SHOWS ITS REAL WORDS (D22) ──────────────────────────────

   D21 is the owner's decision that every approved pair is offered, including
   the one whose body hard-codes `שישי הקרוב (4.9)` and `10x Developer`. What
   makes that safe is not a block - the option is live and the send goes - it is
   that the operator reads the actual copy of each option here, with every
   placeholder drawn as a slot rather than as text. A date typed into a body and
   a date filled in per cohort are six identical characters on a handset; they
   must not look identical in the picker.

   THE CARD IS NO LONGER A `<label>`. It carries several hundred characters of
   body text now, and a card-wide label turns "select some words to read them"
   into "change which templates 24 people receive". The click target is the head
   line and the facts line, both real `<label for>` elements pointing at the one
   radio. */

.csn-tpl {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm, 8px);
  padding: 10px 12px;
  border: 1px solid var(--wa-line);
  border-radius: var(--radius-md, 12px);
  background: var(--wa-chip);
}

.csn-tpl--on {
  border-color: var(--wa-green-line);
  background: var(--wa-green-wash);
}

/* Not sendable is MUTED, never hidden: a template missing from the picker reads
   as "there is no such template", which is the opposite of what is true while
   Meta is reviewing one. */
.csn-tpl--off {
  cursor: default;
  background: var(--wa-header);
}

.csn-tpl--off .csn-tpl__name,
.csn-tpl--off .csn-tpl__note {
  color: var(--text-tertiary, #8e8f9e);
}

.csn-tpl__radio {
  flex: 0 0 auto;
  margin-block-start: 3px;
  width: 18px;
  height: 18px;
  accent-color: var(--wa-green-ink);
}

.csn-tpl__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs, 4px);
  min-width: 0;
  flex: 1 1 auto;
}

/* The one click target that selects the pair, so the body text below is free to
   be read and selected without changing what 24 people receive. */
.csn-tpl__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs, 4px) var(--space-sm, 8px);
  min-width: 0;
  cursor: pointer;
}

.csn-tpl--off .csn-tpl__head {
  cursor: default;
}

/* The two halves of one offer.

   SIDE BY SIDE where the sheet is wide enough, exactly as step 3 shows them, and
   for the same reason measured there: each half now carries six lines of real
   body text, so stacking them made one option cost a full screen and pushed the
   other pairs, the cohort list and every parameter box below a screen of
   scrolling. Measured at 1440x900 before and after: 596px per two-sided card
   stacked, 330px side by side.

   Under 1024px the sheet is narrower than two readable columns and they stack,
   which is the honest answer on a phone: still both halves, still both named,
   one after the other. */
.csn-tpl__halves {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-sm, 8px);
  min-width: 0;
}

@media (min-width: 1024px) {
  .csn-tpl__halves {
    grid-template-columns: 1fr 1fr;
  }

  /* The degenerate pair is ONE template on both sides, so it has one half and
     takes the whole row rather than sitting in a half-width column beside
     nothing - which would read as a missing second body. */
  .csn-tpl__halves:has(.csn-tpl__half:only-child) {
    grid-template-columns: 1fr;
  }
}

/* One half of the pair: who it goes to, its name, its own approval state, what
   its body does about money, and its body.

   A hairline between the two rather than a heading on each. They are one offer
   and read as one block, and a heading per half would make them look like two
   independent offers - which is exactly the misreading that lets somebody act on
   one green "approved" and ignore the other. */
.csn-tpl__half {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs, 4px);
  min-width: 0;
}

.csn-tpl__half + .csn-tpl__half {
  padding-block-start: var(--space-sm, 8px);
  border-block-start: 1px solid var(--wa-line);
}

/* Stacked, the divider is above; side by side it is between, so it keeps
   separating the same two things. */
@media (min-width: 1024px) {
  .csn-tpl__half + .csn-tpl__half {
    padding-block-start: 0;
    padding-inline-start: var(--space-sm, 8px);
    border-block-start: 0;
    border-inline-start: 1px solid var(--wa-line);
  }
}

/* What THIS half's body does about money - the one difference between the two
   halves of a pair, and it lives some twenty lines into the copy where a clamped
   read never reaches it. */
.csn-tpl__money {
  margin: 0;
  font-size: 0.72rem;
  line-height: 1.55;
  color: var(--wa-ink-2);
}

/* The send key, in the mono face the rest of the panel uses for a template
   name. `overflow-wrap` because it is one long Latin token inside an RTL box. */
.csn-tpl__name {
  font-family: var(--font-mono, monospace);
  font-size: 0.8rem;
  color: var(--wa-ink);
  overflow-wrap: anywhere;
}

.csn-tpl__status {
  padding: 1px 8px;
  border-radius: var(--radius-full, 980px);
  font-size: 0.7rem;
  line-height: 1.6;
}

.csn-tpl__status--ok {
  background: var(--wa-green-wash);
  color: var(--wa-green-ink);
}

/* Amber, not red: a template waiting for Meta is not an error, it is a state
   this screen exists to report. */
.csn-tpl__status--wait {
  background: var(--wa-warn-wash);
  color: var(--wa-warn);
}

.csn-tpl__note {
  margin: 0;
  font-size: 0.74rem;
  line-height: 1.55;
  color: var(--wa-ink-2);
}

/* ── D22: the template's REAL WORDS, in the picker ────────────────────────

   Drawn as a quoted block on the panel colour rather than as more UI prose, so
   it reads as the message and not as another sentence about the message. It is
   the same content the approval step renders through the inbox's bubble rules;
   here it is deliberately plainer, because step 1 is a comparison of options and
   step 3 is an approval of one composed message.

   DIRECTION IS SET TWICE ON PURPOSE. `.admin-main` pins the whole admin content
   region `direction: ltr; text-align: left`; `.csn` re-establishes RTL and
   `text-align: start` for the island. This block re-states both anyway, because
   it is the one place in the wizard carrying long unmodified Hebrew paragraphs -
   a future rule leaking LTR in here would silently reverse the punctuation of
   every line of a message somebody is about to approve. */

.csn-tplbody {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  min-width: 0;
}

.csn-tplbody__text {
  margin: 0;
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--wa-line);
  border-radius: var(--radius-sm, 8px);
  background: var(--wa-panel);
  direction: rtl;
  text-align: start;
  font-size: 0.78rem;
  line-height: 1.7;
  color: var(--wa-ink);
  /* The template's own line breaks are part of the words being approved. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Clamped by default so four or five pairs still fit on one step, and opened in
   full from the link below. Six lines is not arbitrary: on every course-start
   body offered today the first placeholder - or, on the pair that has none, the
   literal date standing where a placeholder would be - falls inside them, which
   is the single comparison this screen exists to make possible.

   `-webkit-line-clamp` for the ellipsis, `max-height` behind it so a renderer
   that ignores the clamp still cuts at the same place rather than printing the
   whole 800 characters. */
.csn-tplbody__text--clamped {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 6;
  line-clamp: 6;
  max-height: calc(6 * 1.7em + 16px);
  overflow: hidden;
}

/* A PLACEHOLDER, never the literal `{{n}}` and never a value.

   Dashed, and carrying its position and its meaning in words - a colour alone
   would not survive greyscale, a colour-blind reader or a screenshot pasted into
   a chat, and the whole question this answers is "will this template fill the
   date in, or is the date typed into it". */
.csn-tplbody__slot {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  margin-inline: 2px;
  padding: 0 6px;
  border: 1px dashed color-mix(in srgb, var(--wa-link) 45%, transparent);
  border-radius: var(--radius-full, 980px);
  background: color-mix(in srgb, var(--wa-link) 10%, transparent);
  color: var(--wa-link);
  font-size: 0.72rem;
  line-height: 1.6;
  white-space: nowrap;
}

/* The payment slot, in the same green the approval step marks it with, because
   it is the same statement: this one is the server's and no human types it. */
.csn-tplbody__slot--derived {
  border-color: var(--wa-green-line);
  background: var(--wa-green-wash);
  color: var(--wa-green-ink);
}

.csn-tplbody__slotn {
  font-family: var(--font-mono, monospace);
  font-size: 0.66rem;
  opacity: 0.85;
}

.csn-tplbody__more {
  font-size: 0.72rem;
}

.csn-tplbody__missing {
  margin: 0;
  font-size: 0.74rem;
  line-height: 1.55;
  color: var(--wa-warn);
}

/* ── The facts line, and the warning D21 needs ────────────────────────────

   What the send fills in per cohort, and whether the pair's own words are tied
   to one intake. Both derived from the composer's tables, so neither can
   describe a shape the send does not have. A `<label>` like the head line, so
   the whole readable summary of an option is also the way to choose it. */
.csn-tpl__facts {
  font-size: 0.74rem;
  line-height: 1.55;
  color: var(--wa-ink);
  cursor: pointer;
}

.csn-tpl--off .csn-tpl__facts {
  cursor: default;
}

/* The pair's copy names one intake's date and course. AMBER and never red, and
   never a block: the owner was shown this exact risk and chose to keep every
   approved pair on offer (D21). This states the consequence beside the body that
   proves it, which is the whole of what D22 adds. */
.csn-tpl__fixed {
  margin: 0;
  padding: 6px 10px;
  border: 1px solid var(--wa-warn-line);
  border-radius: var(--radius-sm, 8px);
  background: var(--wa-warn-wash);
  color: var(--wa-warn);
  font-size: 0.74rem;
  line-height: 1.55;
}

/* The "ask the provider again" row.

   The approval state on those two rows is read through a catalogue the SERVER
   caches for ten minutes, and the client's own copy is loaded once per SPA
   session, so without this there is no way at all to learn that a template was
   approved while the wizard was open. The link is a link for the same reason the
   roster's two are: it changes what is on screen and sends nothing. */
.csn-tplrefresh {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs, 4px) var(--space-sm, 8px);
}

.csn-tplrefresh .csn-hint {
  flex: 1 1 16rem;
  min-width: 0;
}

/* ── Step 1: the cohorts ──────────────────────────────────────────────────

   Every cohort is listed, not only the imminent ones, so the reader can see
   what they are NOT sending to. The pre-tick is the server's 10-day verdict and
   is named in words on the row that carries it. */

.csn-cohort {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm, 8px);
  padding: 9px 12px;
  border: 1px solid var(--wa-line);
  border-radius: var(--radius-md, 12px);
  background: var(--wa-chip);
  cursor: pointer;
}

.csn-cohort--on {
  border-color: var(--wa-green-line);
  background: var(--wa-green-wash);
}

.csn-cohort__check {
  flex: 0 0 auto;
  margin-block-start: 2px;
  width: 18px;
  height: 18px;
  accent-color: var(--wa-green-ink);
}

.csn-cohort__body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.csn-cohort__name {
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--wa-ink);
}

.csn-cohort__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs, 4px) var(--space-sm, 8px);
  font-size: 0.74rem;
  color: var(--wa-ink-2);
}

.csn-cohort__auto {
  padding: 1px 8px;
  border-radius: var(--radius-full, 980px);
  background: var(--wa-green-wash);
  color: var(--wa-green-ink);
  font-size: 0.7rem;
}

/* One cohort's three editable values, ruled off from the next cohort's. Without
   the rule, six inputs in a column give no hint which day belongs to which
   intake - and putting the developer cohort's time on the bundle cohort is a
   wrong meeting time on 20 phones. */
.csn-cparams {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm, 8px);
  padding: 10px 12px;
  border: 1px solid var(--wa-line);
  border-radius: var(--radius-md, 12px);
  background: var(--wa-chip);
}

.csn-cparams__who {
  margin: 0;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--wa-ink);
}

/* The borrowed input fills its cell; admin-whatsapp.css sizes it for the inbox
   sheet, which is narrower than this one. */
.csn-cparams .wai-tpl__input {
  width: 100%;
  box-sizing: border-box;
}

/* A value carrying a link or a payment token, marked at the box it was typed
   into.

   The three editable values are ONE array for the whole batch, so a payment link
   pasted into any of them puts one person's bearer capability on every phone in
   the cohort. The step refuses to move on while this is showing; the red is
   there so the reader knows WHICH box, rather than reading a refusal about a
   recipient two steps later. */
.csn-input--bad,
.csn-input--bad:focus {
  border-color: var(--wa-danger-ink);
  box-shadow: 0 0 0 3px var(--wa-danger-wash);
}

/* A required box that is still empty.

   Amber, not red: nothing is WRONG here, something is missing - and for a cohort
   whose first meeting could not be read, or whose first meeting is online, the
   box is empty from the moment the step opens. Red on open would cry wolf over
   the one colour this file reserves for a pasted payment link. */
.csn-input--empty {
  border-color: var(--wa-warn-line);
}

/* The same fact in words, under the box rather than at the end of the step.
   A coloured border says "look here" and nothing more. */
.csn-required {
  font-size: 0.72rem;
  line-height: 1.5;
  color: var(--wa-warn);
}

/* ── Why a box came up empty ──────────────────────────────────────────────

   Two cases, both of them the SERVER deliberately declining to invent a
   customer-facing sentence: no first meeting could be read at all, and a first
   meeting that is `online`, which has no room to arrive early at.

   Boxed, because it answers the question the operator is actually asking. The
   empty field beside it and the dead button below it have no other explanation
   anywhere in the UI: until this note existed the only statement of the cause
   was a `console.warn` on the SERVER, which no operator will ever read, and the
   likeliest repair from an unexplained empty box was to type the in-person
   arrival sentence back in - the one wrong answer. */
.csn-why {
  margin: 0;
  padding: 8px 10px;
  border-radius: var(--radius-md, 12px);
  border: 1px solid var(--wa-warn-line);
  background: var(--wa-warn-wash);
  color: var(--wa-warn);
  font-size: 0.72rem;
  line-height: 1.6;
}

/* ── Step 2: the roster ───────────────────────────────────────────────────

   A grid, not a `<table>`. A table with declared column widths past the
   viewport gives the remaining columns width 0 and stacks them unreadably on a
   phone; the same grid reflows into a card below 768px with no second markup.

   Column order, inline-start to inline-end: checkbox, person, cohort, which half
   of the template pair that person receives, then the three money figures. */

.csn-rosterbar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-xs, 4px) var(--space-sm, 8px);
}

.csn-rosterbar__count {
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--wa-ink);
}

/* The paid/unpaid split of the current selection, live, beside the count. The
   pair is the thing an operator most needs to believe before pressing send, and
   the confirm dialog is too late to meet it for the first time. */
.csn-rosterbar__split {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
}

.csn-rosterbar__links {
  display: inline-flex;
  gap: var(--space-sm, 8px);
}

/* A link, drawn as a link: these two change a selection and nothing else, and a
   pair of solid buttons beside a send flow reads as two more things that might
   send something. 44px of touch target through the padding, not through a
   height, so the pair still sits on the count's baseline. */
.csn-link {
  padding: 6px 2px;
  border: none;
  background: none;
  font-family: var(--font-body);
  font-size: 0.78rem;
  color: var(--wa-link);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.csn-link:hover {
  color: var(--wa-green-ink);
}

.csn-link:focus-visible {
  outline: 2px solid var(--wa-green-ink);
  outline-offset: 2px;
  border-radius: 4px;
}

.csn-roster {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--wa-line);
  border-radius: var(--radius-md, 12px);
  overflow: hidden;
}

.csn-rhead,
.csn-row {
  display: grid;
  grid-template-columns:
    auto minmax(0, 2.2fr) minmax(0, 1.2fr) minmax(0, 1fr)
    repeat(3, minmax(0, 0.9fr));
  align-items: center;
  gap: var(--space-sm, 8px);
  padding: 8px 12px;
}

.csn-rhead {
  background: var(--wa-header);
  font-size: 0.72rem;
  color: var(--text-secondary, #5a5b6a);
}

.csn-row {
  border-block-start: 1px solid var(--wa-line);
  font-size: 0.8rem;
  color: var(--wa-ink);
  cursor: pointer;
}

.csn-row:hover {
  background: var(--wa-header);
}

/* ── AMBER means this person owes money ───────────────────────────────────

   The whole reason the roster shows money at all: the message these rows get
   carries a balance and a payment link, and the rows beside them get a hyphen.
   A wash plus an inline-start rule, so the distinction survives a greyscale
   print and does not depend on the reader telling two similar greens apart. */
.csn-row--due {
  background: var(--wa-warn-wash);
  box-shadow: inset 3px 0 0 0 var(--wa-warn-line);
}

.csn-row--due:hover {
  background: rgba(255, 171, 0, 0.18);
}

.csn-row--due .csn-row__due {
  color: var(--wa-warn);
  font-weight: 600;
}

/* A cancelled sale still carries its full expected amount, so its apparent
   balance is the largest on the screen and means nothing. Named in red on the
   row, and unticked by the server. */
.csn-row--cancelled .csn-row__name {
  color: var(--wa-danger-ink);
}

/* Nothing can be sent to this row at all. Muted and not struck through: the
   reason is printed beside the name, and a struck-out person reads as deleted. */
.csn-row--off {
  cursor: default;
  color: var(--text-tertiary, #8e8f9e);
}

.csn-row--off .csn-row__name {
  color: var(--text-tertiary, #8e8f9e);
}

.csn-row__check {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  accent-color: var(--wa-green-ink);
}

.csn-row__check:disabled {
  cursor: default;
}

.csn-row__who {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs, 4px) var(--space-sm, 8px);
  min-width: 0;
}

.csn-row__name {
  min-width: 0;
  overflow-wrap: anywhere;
  font-weight: 600;
}

.csn-row__phone {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--text-tertiary, #8e8f9e);
}

.csn-row__badges {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
}

.csn-row__cohort {
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: 0.76rem;
  color: var(--wa-ink-2);
}

.csn-row__variant {
  min-width: 0;
}

/* ── Which half of the template pair a person receives ────────────────────

   Two states, and the colours are not decoration: AMBER is already this
   screen's word for "this person owes money" (D16 washes the whole row in it),
   and the amber pill sits on exactly those rows, so the two readings reinforce
   each other instead of competing. The other pill is deliberately quiet - a
   student who is square with us is the unremarkable case and does not need a
   second colour shouting on every row.

   Both carry a word, never a colour alone: greyscale, a colour-blind reader and
   a screenshot pasted into a chat all have to survive this, and the whole point
   of the column is that somebody can tell at a glance which of two messages a
   named person is about to receive. */

.csn-vpill {
  display: inline-block;
  max-width: 100%;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid var(--wa-line);
  background: var(--wa-header);
  color: var(--wa-ink-2);
  font-size: 0.68rem;
  line-height: 1.6;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.csn-vpill--unpaid {
  border-color: var(--wa-warn-line);
  background: var(--wa-warn-wash);
  color: var(--wa-warn);
  font-weight: 600;
}

.csn-vpill--paid {
  border-color: var(--wa-line);
  background: var(--wa-header);
  color: var(--wa-ink-2);
}

/* Figures are tabular and read left to right inside the RTL island - the `<bdi
   dir="ltr">` in the markup isolates the run, this aligns the column. */
.csn-num {
  text-align: end;
  font-variant-numeric: tabular-nums;
}

.csn-row__money {
  white-space: nowrap;
}

/* The column caption carried inside the cell.

   VISUALLY hidden at desktop width rather than `display: none`, and that is the
   whole point: the header row above is `aria-hidden` (it heads a grid, not a
   table, so it names nothing a screen reader can associate), and `display: none`
   would take these captions out of the accessibility tree too - leaving a reader
   who cannot see the header with three bare numbers per person on a screen about
   who to ask for money. Revealed as ordinary text on a phone, where each row is
   a card and the header is gone for everybody. */
.csn-cell__k {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  color: var(--text-tertiary, #8e8f9e);
  font-size: 0.7rem;
}

/* ── Badges ───────────────────────────────────────────────────────────────

   Three of them un-tick a row by the server's rule (cancelled, staff, already
   sent) and one reports a refusal. All four say WHY in words: a row that is
   simply not ticked, with nothing beside it, is a row somebody re-ticks. */

.csn-badge {
  padding: 1px 8px;
  border-radius: var(--radius-full, 980px);
  background: var(--wa-header);
  color: var(--wa-ink-2);
  font-size: 0.68rem;
  line-height: 1.7;
  white-space: nowrap;
}

.csn-badge--cancelled {
  background: var(--wa-danger-wash);
  color: var(--wa-danger-ink);
}

.csn-badge--warn {
  background: var(--wa-warn-wash);
  color: var(--wa-warn);
  white-space: normal;
}

.csn-badge--derived {
  background: var(--wa-green-wash);
  color: var(--wa-green-ink);
}

/* ── Step 3: BOTH HALVES OF THE PAIR, side by side ────────────────────────

   One press sends two different messages, so the step approves two. Side by side
   rather than one above the other, and that is a decision about the FOLD, not
   about taste: round 1 measured this step opening on a decorative header image
   with the body of the message eight pixels below the bottom edge, and a stacked
   pair would put the second message a full screen further down. Two columns cost
   no vertical space at all - both bubbles start on the same line - so the
   measurement round 1 fixed still holds with twice the content.

   Two columns only where there is room for two. `.adm-sheet:has(.csn)` is
   `min(880px, 100%)`, so at a viewport of 1024px or more the sheet is 880px and
   each column is roughly 420px, which the 380px bubble cap below sits inside
   comfortably. Under that the sheet is narrower than two bubbles and the panes
   stack, which is the honest answer on a phone: still both messages, still both
   named, one after the other. */

.csn-pair {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md, 16px);
  align-items: start;
}

@media (min-width: 1024px) {
  .csn-pair:has(.csn-pane + .csn-pane) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* One pane: which half, who it is being read for, the bubble, the parameters.

   Bordered, because the single most important thing about this screen is that
   these are TWO messages. Without an edge between them the two bubbles read as
   one long preview that happens to repeat itself, which is precisely the
   misreading that would let an operator approve one shape and believe they had
   seen the send. */
.csn-pane {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm, 8px);
  min-width: 0;
  padding: 10px 12px;
  border: 1px solid var(--wa-line);
  border-radius: var(--radius-md, 12px);
}

.csn-pane__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs, 4px) var(--space-sm, 8px);
  margin: 0;
  min-width: 0;
}

.csn-pane__count {
  font-size: 0.76rem;
  font-weight: 600;
  color: var(--wa-ink);
}

/* The name this pane's message is ACTUALLY being sent under, as the server
   resolved it - not as the picker guessed. Mono, like every other template name
   in the panel, and `overflow-wrap` because it is one long Latin token inside an
   RTL box. */
.csn-pane__tpl {
  min-width: 0;
  font-family: var(--font-mono, monospace);
  font-size: 0.7rem;
  color: var(--text-tertiary, #8e8f9e);
  overflow-wrap: anywhere;
}

/* ── Step 3: who the message is being read for ────────────────────────────

   A named person, always. The 2026-08-31 mis-send was one person's balance and
   one person's payment link on somebody else's message, and a preview that names
   nobody could not have shown it. */

/* NO `flex` shorthand here, and that is load-bearing rather than tidying.
   The field used to sit in a flex ROW and carried `flex: 1 1 14rem`, where the
   basis was a WIDTH. Its parent is now `.csn-pane`, a flex COLUMN, and the same
   declaration made 14rem a HEIGHT: measured at 1440x900 the label-plus-select
   block stood 224px tall and pushed the message body down with it, which is the
   fold that round 1's F9 fixed. It is a plain block child now and sizes to its
   contents. */
.csn-pickwho__field {
  min-width: 0;
}

.csn-pickwho__select {
  width: 100%;
  box-sizing: border-box;
  cursor: pointer;
  font-size: 0.88rem;
  line-height: 1.4;
}

.csn-who {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs, 4px) var(--space-sm, 8px);
  padding: 8px 12px;
  border-radius: var(--radius-md, 12px);
  background: var(--wa-header);
  font-size: 0.8rem;
}

.csn-who__name {
  font-weight: 600;
  color: var(--wa-ink);
}

.csn-who__cohort {
  color: var(--wa-ink-2);
}

/* The roster's own badges, carried onto the approval step - same classes, so the
   two surfaces cannot drift into two visual languages for one fact. Cancelled,
   staff and already-sent are the three categories the server unticks and D1, D10
   and D11 invite the operator to tick back on; the step that confirms the send
   is the last place they may go missing. */
.csn-who__badges {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px;
  min-width: 0;
}

.csn-who__due {
  margin-inline-start: auto;
  color: var(--wa-ink-2);
  font-variant-numeric: tabular-nums;
}

/* Amber here for the same reason it is amber in the roster: this is the reader's
   one chance to notice that the figure beside the name is not the figure inside
   the message. */
.csn-who__due--owed {
  color: var(--wa-warn);
  font-weight: 600;
}

/* ── Step 3: the message itself, capped to a phone's width ────────────────

   THE measurement this rule exists for, taken at 1440x900 before it: the sheet's
   scrolling body is 633px tall, the preview column was the sheet's full ~823px,
   and this template's header image is 2400x1256. `.wai-tpl__hero` is
   `width: 100%` with no height of its own, so the artwork rendered 823x431 and
   ate 68 per cent of the viewport - the body of the message began EIGHT PIXELS
   below the bottom edge. The approval step opened on a decorative picture, an
   enabled send button, and not one word of the message about to reach 25 phones,
   which is the exact invariant the step exists to hold.

   Capped by constraining the COLUMN rather than the image, so the bubble reads
   at roughly the width of the handset it lands on - the shape the reader is
   actually being asked to approve. The bare `.wai-tpl__hero` rule in
   admin-whatsapp.css is deliberately untouched: the inbox thread and the 1-on-1
   picker share it and are already narrow.

   The hero's `max-height` is the second guard, for a header image that is not
   this one. A portrait asset at this width would be 800px tall and put the body
   straight back under the fold. `object-fit: contain` letterboxes rather than
   crops, so nothing about the artwork is hidden from the person approving it. */

.csn .wai-tpl__preview {
  width: 100%;
  max-width: 380px;
  margin-inline: auto;
}

.csn .wai-tpl__hero {
  max-height: 220px;
  object-fit: contain;
  object-position: center;
}

/* The way back from a refused send or a preview that would not render: re-read
   the roster. Several refusals mean the same thing - the list on screen is older
   than the truth - and none of them is answered by pressing send again. */
.csn-recover {
  margin: 0;
}

/* ── Step 3: the positional parameters, as they will be sent ──────────────

   Under the bubble rather than instead of it. The bubble is what the customer
   reads; this is which value landed in which slot, which is the level the
   mis-send happened at. The payment row is marked as the server's. */

.csn-params {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px 10px;
  border: 1px solid var(--wa-line);
  border-radius: var(--radius-md, 12px);
}

.csn-params__title {
  margin: 0;
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--wa-ink-2);
}

.csn-params__row {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm, 8px);
  padding: 4px 0;
  border-block-start: 1px solid var(--wa-line);
}

.csn-params__row:first-of-type {
  border-block-start: none;
}

.csn-params__row--derived {
  background: var(--wa-green-wash);
  border-radius: var(--radius-sm, 8px);
  padding-inline: 6px;
}

.csn-params__n {
  flex: 0 0 auto;
  min-width: 1.2em;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-tertiary, #8e8f9e);
}

.csn-params__v {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: 0.76rem;
  line-height: 1.5;
  color: var(--wa-ink);
  overflow-wrap: anywhere;
}

/* ══════════════════════════════════════════════════════════════════════════
   The footer's blocking reason
   ══════════════════════════════════════════════════════════════════════════

   `.adm-sheet__footer` is a plain flex ROW of two buttons, each `flex: 1`. Step
   1 scrolls - a template block, a cohort list and a parameter card per selected
   cohort - so every reason the step cannot be left sat at the far end of that
   scroll while the button that will not move stayed pinned in the footer. The
   measured case: an online cohort's arrival box is empty by design, and its
   explanation was 216px below the fold.

   The row is allowed to wrap ONLY while this line is present (`:has`, scoped to
   a class this component alone emits), so it takes the first line on its own and
   the two buttons keep the widths and the 46px height the shared rule gives
   them. Every other sheet in the panel is untouched.

   The element also carries `.wab-scope`: the footer is a SIBLING of the sheet's
   body, so the `--wa-*` palette declared on the body's own `.wab-scope` does not
   reach it and every `var()` below would resolve to nothing. This file is linked
   after admin-wa-bulk.css, so the two `.wab-scope` properties that would fight
   this rule (`text-align`, `color`) lose on order, as intended. */

.adm-sheet__footer:has(.csn-block) {
  flex-wrap: wrap;
}

.csn-block {
  flex: 1 1 100%;
  margin: 0;
  padding: 6px 10px;
  border-radius: var(--radius-md, 12px);
  border: 1px solid var(--wa-warn-line);
  background: var(--wa-warn-wash);
  color: var(--wa-warn);
  font-size: 0.72rem;
  line-height: 1.5;
  direction: rtl;
  text-align: start;
  overflow-wrap: anywhere;
}

/* ══════════════════════════════════════════════════════════════════════════
   Phone (<= 767px) - the roster becomes cards
   ══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 767px) {
  /* The header row has nothing to head any more. */
  .csn-rhead {
    display: none;
  }

  /* Checkbox in its own column, everything else stacked beside it. Money reads
     as three labelled lines rather than three anonymous numbers, which is what
     `.csn-cell__k` is for. */
  .csn-row {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    row-gap: 2px;
    padding: 10px 12px;
  }

  .csn-row__who,
  .csn-row__cohort,
  .csn-row__variant,
  .csn-row__money {
    grid-column: 2;
  }

  /* The variant reads as a labelled line like the money does, so the pill is
     never a floating word with nothing saying what it is a version OF. */
  .csn-row__variant {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-sm, 8px);
  }

  .csn-row__money {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-sm, 8px);
    text-align: start;
  }

  .csn-cell__k {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
  }

  .csn-who__due {
    margin-inline-start: 0;
  }

  /* The two panes are already stacked at this width; drop their frames' padding
     so a 380px bubble is not squeezed inside a border inside a full-width
     sheet. */
  .csn-pane {
    padding: 10px 8px;
  }
}
