/* Unified Person page (admin) - /admin/leads/:id. Hebrew RTL island.
   One person, three tabs (Lead / Open Day / Enrollment), one docked WhatsApp
   transcript and one comments footer that never narrows.

   ══════════════════════════════════════════════════════════════════════════
   SIX CONTRACTS THIS FILE LIVES UNDER
   ══════════════════════════════════════════════════════════════════════════

   1. BORROWED KITS ARE NEVER REDEFINED BARE.
      This is the LAST stylesheet in index.html's admin block, so a bare
      `.ld-x { }` here wins on /admin/enrollments/:id too, and a bare
      `.reg-x { }` wins on five list screens at once (admin.css carries 226
      distinct `.reg-*` classes across 360 rule blocks, read by six other
      stylesheets). The page renders, as MARKUP and nothing more:
        `.ld-*`   the card kit, the transcript, the marketing/channel/journey
                  family, the seats, the ledger, the contracts (admin-lead-detail.css)
        `.edt-*`  `.edt-progress--negative` + `.edt-progress-negative`, the
                  hatched negative-ledger track (admin-enrollment-detail.css)
        `.reg-*`  the chips, the attendance control, the answer tables (admin.css)
        `.llc-*`  the lifecycle map, rendered indirectly by LeadFlowChart
        `.lal-*`  LeadActivityLog, inside the comments footer
        `.wa-*`   WaActionGroup's segmented control (admin-wa-actions.css)
        `.btn`, `.btn-sm`, `.btn-primary`, `.btn-icon`, `.form-*`, `.modal*`,
        `.empty-state`, `.toast`, `.filter-label` (admin.css)
      Every divergence below is scoped `.per-page X { }` or lives on a `.per-*`
      class applied ALONGSIDE the borrowed one. Same rule
      `admin-enrollment-detail.css` states for itself, one file further down the
      chain. `tests/adminPersonRtlMirrors.spec.ts` fails the build if it slips.

      `.lal-*` and `.lct-*` are the exception INSIDE the exception: do not mirror
      them at all. LeadActivityLog is RTL BY DEFAULT (its rail sits on the right,
      its timestamp is pushed with `margin-right: auto`), so a mirror here would
      double-flip a component that is already correct - and it has three other
      hosts.

   2. RTL: this root declares its own direction and joins NEITHER shared group
      at the end of admin.css. It follows `.edt-page` / `.enr-page`, not
      `.ld-page`. Two consequences, both deliberate:

        - No root-level horizontal scroller. Quoting the sale page's own header,
          which learned it first: "a root-level scroller on a page made of
          collapsible panels grows and vanishes as panels toggle". This page is
          nothing BUT collapsible panels, across three tabs, so every wide thing
          scrolls inside its own box - the ledger in `.ld-pay-scroll`, the
          evidence lists via `overflow-wrap: anywhere`.
        - It is also what keeps the sticky header working. An ancestor with a
          non-visible overflow becomes the scrollport for every `position:
          sticky` inside it, and `.per-page` never scrolls vertically - so
          joining that group would silently stop BOTH sticky rows from sticking,
          at every viewport. `admin-leads.css` opens its file with that scar
          (`.leads-page { overflow: visible }`).

      The `.ld-page`-scoped mirrors at the end of admin-lead-detail.css do not
      reach `.per-page`, so every physically positioned bit of the borrowed kits
      is restated below under our own root.

   3. LOGICAL PROPERTIES THROUGHOUT. `inset-inline-*`, `margin-inline-*`,
      `padding-block-*`, `border-inline-*`, `text-align: start | end`. That is
      what drops the mirror bill for anything this page OWNS to zero; only the
      borrowed kits cost anything, and that cost is finite and paid below.

      The one property with no logical form is `transform: translateX()` - CSS
      has no `translate-inline` - so its sign is reasoned out in a comment at
      every use, exactly as admin-whatsapp.css does for `.wai-lead`.

   4. EXACTLY FOUR MEDIA QUERIES, and no others:
        @media (max-width: 1024px)                          tablet and below
        @media (max-width: 767px)                           phone
        @media (min-width: 768px) and (max-width: 1024px)   the tablet band
        @media (max-height: 480px)                          landscape phone
      The clone test pins the width set AND asserts exactly one `max-height`
      query exists, so the landscape guard cannot be silently dropped. A fifth
      breakpoint is a decision, not a tweak.

      One threshold that looks like it wants a query and deliberately does NOT
      get one here:
        - 1025px, the desktop sidebar rail. It lives in `admin.css`'s own
          `@media (min-width: 1025px)` block, because it styles the SHELL
          (`.admin-sidebar`, `.admin-nav-link`, `.admin-rail-toggle`) and the
          shell is not this file's to restyle. `admin.css` keeps its own widths;
          they are pre-existing and are not ours to change either.

      A SECOND one used to be listed here and is gone as of 2026-08-09: 1280px,
      the WhatsApp dock's auto-collapse. It was stylesheet-invisible (it decided
      whether the panel STARTS open, not how it looks), and the owner removed it
      outright - decision D8. Whether the dock starts open is now the stored
      per-admin preference at every docked width, answered by
      `PersonWaPanel.vue` off `useAdminBreakpoints()`, and there is no 1280
      anywhere in this page any more. Do not reinstate it as a query.

   5. NOTHING HERE MAY BECOME A CONTAINING BLOCK FOR A FIXED DESCENDANT.
      No `transform`, `filter`, `backdrop-filter`, `perspective`, `contain` or
      `container-type` on `.per-page`, on `.per-header__row--b`, on `.per-panes`
      or on `.per-panel`. Any of them makes that element the containing block for
      `position: fixed` descendants, and the action bar opens `AdminSheet`, which
      is fixed: it would drop into the middle of the page instead of covering the
      viewport. The repo has been bitten and worked around this twice already -
      `admin.css`'s `.reg-table-scroll` pushes its `container-type` DOWN off the
      page root ("a `container-type` there would trap them in the page box
      instead of the viewport"), and `.adm-sheet` refuses a blur outright ("a
      filter makes this box the containing block for `position:fixed`
      DESCENDANTS"). The sticky rows here use a flat opaque fill for the same
      reason, which does the same visual job with none of the trap.

      It was `rgba(255,255,255,0.97)` until 2026-08-10, and 3% was not a rounding
      error: screenshotted at 1440 and at 390, the page's own dark text read
      straight through all three stuck bars - an ad pill through the identity
      row, a section heading through the tab strip. A bar you can read the page
      through is not a bar. Do not reintroduce the alpha; if the stuck header
      ever needs to feel lighter, change the COLOUR, not the opacity.

   6. THE GRADIENT NEVER TOUCHES WEIGHT 500 OR ABOVE, numerals included. Money
      on this page is solid ink at 600. `--adm-grad-v` is used exactly once in
      the whole shell (the active nav accent bar) and this file does not touch it.

   ══════════════════════════════════════════════════════════════════════════
   `.wai-page` MUST NEVER APPEAR IN THIS PAGE'S DOM
   ══════════════════════════════════════════════════════════════════════════
   admin-whatsapp.css carries `.admin-main:has(.wai-page) { padding: 0;
   overflow: hidden }`, and `.admin-main` is the panel's GLOBAL scroller. One
   stray `.wai-page` anywhere below `.per-page` - on the dock, on a wrapper, on a
   modal - stops the whole page scrolling at every viewport and drops its
   padding. Nothing errors; the page just clips at the fold. The docked panel's
   root class is `.per-wa`, which is listed on admin-whatsapp.css's own palette
   selectors so it gets the tokens without the behaviour.

   Shell tokens (`--adm-*`) are defined on `.admin-layout`, an ancestor of every
   selector here - never on `:root`. #8E8F9E (tertiary ink) has no token and is
   written out, as it is in the two neighbouring files. */

/* ══════════════════════════════════════════════════════════════════════════
   1. THE ISLAND
   ══════════════════════════════════════════════════════════════════════════ */

.per-page {
  direction: rtl;
  /* `start`, not `right`. The property name is what the mirror spec looks for;
     the logical value is what stops a side being baked in. */
  text-align: start;

  /* The sticky budget, published once and consumed in two places. Row B's
     height and the tab strip's offset MUST be the same number: let row B grow
     with its content and the strip lands on top of it. 56 + 48 = the 104px of
     chrome a phone reader pays. */
  --per-rowb: 56px;
  --per-tabs: 48px;

  /* THE DETAIL BAND'S HEIGHT, AND THE ONE TOKEN ON THIS PAGE THAT IS WRITTEN
     FROM JAVASCRIPT.

     Row B and the tab strip are constants because their contents are pinned to
     one line each. The band between them is not: it holds seven compact cells
     that sit on one line at 1440px, two lines at 1024px and two on a phone, and
     it animates continuously between that and zero every time the reader scrolls
     past it. There is no breakpoint expression that is true for all of those, so
     `usePersonDetailBand` measures the element and publishes the number here.

     `0px` is the value while the band is folded AND the value before the
     composable's first measurement, which is the safe direction to be wrong in:
     everything downstream is `<constant> + var(--per-band)`, so an unset token
     degrades to exactly the geometry this page had before the band existed.

     WHAT MAY NEVER BE DERIVED FROM IT: `--per-rowb`. `.per-yield` and
     `.per-id .ld-nav` both use `--per-rowb` as a `row-gap` chosen to be LARGER
     than their own clip, which is what pushes a wrapped line out of sight
     entirely instead of drawing half of it. Tie that number to a thing that
     shrinks and half-drawn badges and half-drawn money come back (invariant C).
     Row B's 56px is not part of this arithmetic and must stay a constant. */
  --per-band: 0px;

  /* ══════════════════════════════════════════════════════════════════════
     ROW B'S YIELD LADDER - the row's INLINE budget, and who pays it
     ══════════════════════════════════════════════════════════════════════

     Row B is a hard 56px holding, in one nowrap flex line: the worklist
     arrows, the person's name, the lead-status control, an intent glyph, two
     provenance pills, the enrollment line and the action cluster. THE SUM OF
     WHAT MUST BE THERE EXCEEDS THE ROW AT NARROW DESKTOP WIDTHS - measured,
     not feared: at 1025px with the sidebar expanded the row has 697px of
     content box and the action cluster alone measures 546.3px for a
     full-permission viewer. Something has to give at that width, and the only
     question a stylesheet can answer is WHICH THING.

     Four rebalances of this row (2026-08-08 and -09) each answered that
     question by accident: whoever had just been reported invisible was given a
     floor, and the deficit moved onto whoever had no floor yet. The money cell
     paid for the pills, the arrows paid for the money cell, and the STATUS
     CONTROL - which never appeared in anybody's list - paid for the arrows by
     being clipped to zero width while staying in the tab order. So the order
     is written down here, once, and every rule in section 6, section 17 and
     section 19 that touches this row cites the rung it implements.

       RUNG 1  the two PROVENANCE PILLS        vanish, whole, one at a time
       RUNG 2  the INTENT GLYPH                vanishes, whole
       RUNG 3  the ENROLLMENT (money) CELL     vanishes, WHOLE - never cut
       RUNG 4  the ACTION CLUSTER'S LABELS     ellipsise; the buttons stay
       RUNG 5  NOTHING ELSE YIELDS.
               The status control, the person's name and BOTH worklist arrows
               hold the floors below at every width and in both sidebar states.

     WHY THAT ORDER. Rungs 1 and 3 have a fuller second home that is on screen
     anyway - the pills repeat verbatim in the general detail panel under this
     header (on all three tabs), and the money has a whole band on the
     Enrollment tab. Rung 2 has NO second home, which is why it outranks the
     pills rather than trailing them, and it is two DOM positions earlier than
     it used to be for exactly that reason (see `.per-id__intent`). Rung 4 is
     the cluster's LABELS and never its buttons: every one of them keeps a 44px
     hit target and its `aria-label`, so what is lost is a word beside a glyph
     that already carries the meaning. Rung 5 is owner items 4, 5 and 13 - the
     page's only identity, its only worklist control, and the only status
     control on two of the three tabs.

     THREE INVARIANTS THE LADDER EXISTS TO KEEP, all measured in Chromium by
     `tests/personRowBGeometry.spec.ts` over 22 widths x 2 sidebar states x
     five viewer/person compositions:

       A. NOTHING INTERACTIVE IS EVER CLIPPED TO INVISIBILITY WHILE STAYING
          FOCUSABLE. Clipping is an honest failure for TEXT. For a CONTROL it
          is not: a keyboard user tabbed onto the invisible status `<select>`
          could arrow a live person into another status with nothing on screen
          saying so. Every control in this row is proven visible by
          `document.elementFromPoint` at its own centre, never by geometry.
       B. EVERY SHRINKABLE BOX LOSES WIDTH INSIDE ITS OWN BORDER, WITH A MARK.
          No child paints outside its parent, and no text is cut in a way that
          reads as complete - captions included, which is what
          `.per-hl__label` is about.
       C. A NUMBER IS NEVER TRUNCATED. It is drawn whole or not drawn.
          Added 2026-08-09, and it is the only invariant here that came from a
          screenshot rather than from a hit test. The row is RTL and every
          figure in it is an LTR `<bdi>`, so a clip eats the figure's LEADING
          digits and leaves a shorter, complete-looking number behind: a
          customer who had paid ₪12,000 of ₪18,000 rendered at 1440px, default
          rail, as `שולם ₪12,000 מתוך 8,000` - a grammatical Hebrew sentence
          saying the customer has OVERPAID by ₪4,000, with no ellipsis and
          nothing else on screen saying it was cut. The same clip turned the
          worklist counter `7 / 43` into `7 / 4`. Money and counts are the two
          things on this page a salesperson ACTS on, so both are now
          all-or-nothing. Text (names, labels, partner codes) keeps invariant B
          instead: it may be cut where the cut carries a mark.

     THE MECHANISM, AND WHY THE PREVIOUS FOUR ROUNDS OSCILLATED. The ladder
     used to be written as flex SHRINK FACTORS - 40, 12, 2, 1 - and shrink
     factors do not sequence. Flexbox distributes a deficit across every
     shrinkable item on the line SIMULTANEOUSLY and in proportion; it never
     spends one item before starting on the next. So rung 4 and rung 5 began
     paying from the first pixel of shortfall while 128-238px of rung-1
     provenance pills were still fully drawn, and rung 3 spent its whole range
     as a partial draw - which is exactly the state invariant C forbids.
     Measured before the rewrite, 1366px default rail: the money read
     `שולם ₪12,000 מתוך 000` while `.per-marks` held 128.9px.

     WHAT REPLACES IT: THE ROW GROWS INTO ITS SLACK INSTEAD OF SHRINKING OUT OF
     A DEFICIT. Rungs 1, 2 and 3 live inside ONE wrap container, `.per-yield`,
     whose flex basis is ZERO - so they cost the row nothing and are paid for
     only out of space the row actually has spare. Inside it, `flex-wrap` is
     what sequences: flex assigns items to LINES using their hypothetical size
     before it resolves any flexible length, so an item that no longer fits
     beside its neighbours moves to line 2 WHOLE - and line 2 starts a full
     `--per-rowb` further down and is cut off by `max-block-size`, so it is not
     drawn at all. DOM order is therefore survival order, and it reads
     backwards: the last item written is the first to go.

       .per-yield::before   a zero-width spacer, so the money is not item 1
       .per-highlights      RUNG 3, the money        drawn last-but-one to go
       .per-marks           RUNG 2 then RUNG 1       intent, channel, partner

     (The spacer earns its place: a flex line always keeps at least one item,
     so whatever is written first can never wrap off - it can only be clipped,
     which for the money cell is invariant C's own failure.)

     ONLY TWO ITEMS ARE LEFT IN THE SHRINK POOL, and both carry a real floor:
     the action cluster (rung 4, floor = its buttons at 44px) and `.per-id`
     (rung 5, floor = the reservation below). They are separated by weight
     rather than sequenced, and that is honest because the leak is measurable:
     at the row's tightest composition (1025px, sidebar expanded, 276.3px
     short) `.per-id` gives up 1.5px before the cluster reaches its floor and
     freezes. A pixel and a half is not a rung.

     The four numbers below are the reservation, written once and consumed by
     six rules. `ch` resolves against the box the value lands on; every consumer
     of these runs at 1rem, which is why the nav floor can be written in `ch` at
     all (see `.per-id .ld-nav`). The status floor is deliberately PX and not
     `ch`: it is a budget shared between `.per-id` (1rem) and the `<select>`
     (0.7rem here, 16px under the touch block), and a `ch` would mean three
     different numbers for one reservation.

     ALL FOUR ARE RESTATED PER BAND, in the ≤1024px and ≤767px blocks, and that
     is the second half of the fix. Every term in the desktop sum is a different
     size lower down - the arrows take the 44px touch floor on a phone, the
     worklist counter is hidden, the name's floor drops to 7ch and the gaps
     narrow - so the desktop number is not merely too big there, it is about a
     control that has different dimensions. The old code expressed that by
     handing the floor back to ZERO below 1025px, which is why the forward arrow
     was cut off by the nav's own clip at every phone width. A floor that is
     re-derived per band is a floor that can stay switched on. */
  --per-nav-floor: calc(2 * 34px + 3 * 8px + 12.6ch);
  /* The name's own floor, and since 2026-08-09 also its flex BASIS - the two
     have to be the same number and the reason is the worklist counter. Line
     assignment inside `.ld-nav` happens before any flexible length is resolved
     and reads each item's hypothetical size, so with a basis of `auto` the
     untruncated name (measured 168.3px) claimed the whole line and pushed the
     "next lead" arrow off it. Pinning the basis to the floor makes the
     hypothetical line exactly what `--per-nav-floor` reserves - two arrows,
     three gaps and a 12ch name - and the name then GROWS into whatever the line
     has left. It is written on the name's own box, which runs at 1.05rem, which
     is why the nav floor above says 12.6ch for the same 12 characters. */
  --per-name-floor: 12ch;
  --per-status-floor: 72px;
  --per-id-gap: 0.45rem;
  /* UNCHANGED by the 2026-08-10 band, and that is the point of where its fold
     control ended up. A 40px chevron in this row plus its gap took 47px out of
     the 74px of slack the row had at 1280px railed, and `.per-yield` - which is
     `flex: 1 0 0`, so it lives entirely on slack and never shrinks - went to
     zero: the money line and all three badges wrapped onto a line the row clips
     away, each still holding a real box and zero hit-testable pixels. The
     chevron is at the end of the TAB STRIP instead, which is sticky, is 48px
     tall, and costs this row nothing. */
  --per-id-floor: calc(var(--per-nav-floor) + var(--per-id-gap) + var(--per-status-floor));

  /* Row B's own gap, published because `.per-yield` has to cancel it exactly.
     A zero-basis item that draws nothing still costs the row one gap, and this
     row has no 12px to spare at 320px - so `.per-yield` carries a negative
     start margin of exactly this value and re-spends it as its own internal
     column gap. An empty yield zone then costs the row nothing at all, and a
     full one puts the money exactly one row gap from the name, which is where
     the badges used to sit. Written once, consumed by two rules. */
  --per-rowb-gap: 0.75rem;

  /* The full-bleed reach of the two sticky rows and the tab strip. It is
     EXACTLY `.admin-main`'s inline padding at each width - one pixel more and
     the LTR scroller grows a horizontal scrollbar an RTL island can never
     reach, because RTL overflow runs leftward. Re-stated at each breakpoint
     below, and nowhere else. */
  --per-bleed: 2rem;
}

/* ══════════════════════════════════════════════════════════════════════════
   2. RTL MIRRORS FOR THE BORROWED KITS
   ══════════════════════════════════════════════════════════════════════════

   Every rule in this section cancels a PHYSICAL declaration from a kit that was
   authored left-to-right, then re-issues it logically. The `margin-left: 0` and
   `left: auto` halves look redundant next to the logical half. They are not: a
   logical property does not OVERRIDE a physical one, it coexists with it. In
   RTL, `margin-inline-start` resolves to `margin-right`, which never touches the
   base `margin-left: auto` at all - so the block ends up auto-margined on both
   sides and centres itself. Both halves, every time. */

/* The grey hint that trails a field label inside the modals. admin.css spaces
   it with `margin-left: 0.35rem`, which under RTL lays the gap down on the far
   side of the hint and butts the two strings together. Scoped, never smuggled
   into admin.css's shared group: that group is read by five other screens. */
.per-page .form-label-hint {
  margin-left: 0;
  margin-inline-start: 0.35rem;
}

/* Auto-margin rails. The kit pushes each of these to the physical LEFT, which
   is the START edge under RTL - the opposite of what they are for. */
.per-page .ld-wa-inbound {
  margin-left: 0;
  margin-inline-start: auto;
}

.per-page .ld-journey-ts {
  margin-left: 0;
  margin-inline-start: auto;
}

.per-page .ld-enroll-date {
  margin-left: 0;
  margin-inline-start: auto;
}

.per-page .ld-progress-over {
  margin-left: 0;
  margin-inline-start: auto;
}

/* Partner-journey rail: the vertical line, the dots and the step indent all sit
   on the physical left in the kit, which is where the list STARTS in LTR. Under
   RTL the start edge is the right one, so the logical half of each pair is
   `inset-inline-START` - not `-end`, which would park the rail on the left with
   the indent still on the right and the dots floating clear of both. */
.per-page .ld-journey::before {
  left: auto;
  inset-inline-start: 7px;
}

.per-page .ld-journey-step {
  padding: 0.35rem 0;
  padding-inline-start: 1.6rem;
}

.per-page .ld-journey-step::before {
  left: auto;
  inset-inline-start: 3px;
}

/* Payments ledger: the kit left-aligns its headers and right-aligns the row
   actions. Both are backwards here. */
.per-page .ld-pay-table th {
  text-align: start;
}

.per-page .ld-pay-actions {
  text-align: end;
}

/* NOT a mirror, and it will not be caught by the RTL spec (nothing physical in
   it). The base `.reg-enroll-status--*` set in admin.css ships `active` and
   `dropped` only; the `cancelled` variant exists nowhere but under `.ld-page`,
   and BOTH the sale picker and the sale head render it. Without this the pill
   on a cancelled sale renders unstyled - white text on white. */
.per-page .reg-enroll-status--cancelled {
  color: #6b7280;
  background: rgba(107, 114, 128, 0.1);
  border-color: rgba(107, 114, 128, 0.3);
}

/* Also not a mirror: the current node's ring, its drop shadow and the
   recommended dashed outline all paint OUTSIDE the pill, and `.llc-canvas`
   clips vertically. Copied verbatim from the lead card's own copy. */
.per-page .llc-canvas {
  padding-top: 8px;
}

/* The evidence and caveat lists carry landing URLs and click ids at dir="ltr".
   `.ld-ch-list`'s own `text-align: left` is DELIBERATE and is not mirrored (the
   items are English machine strings) - but with no page-level scroller to catch
   it, one unbreakable token would push the whole body sideways. */
.per-page .ld-ch-list {
  overflow-wrap: anywhere;
}

/* `.btn` (admin.css) never clears the UA underline, because every other consumer
   of it is a <button>. This page has anchors dressed as buttons: open the sale,
   open the full conversation, back to the registrations screen. */
.per-page a.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}

/* ══════════════════════════════════════════════════════════════════════════
   3. PAGE SKELETON
   ══════════════════════════════════════════════════════════════════════════ */

/* THE PAGE IS FLUSH WITH THE TOP OF THE SCROLLER (owner, 2026-08-11).

   `.admin-main`'s own block-start padding sits ABOVE a stuck child's resting
   place - measured: with the shell's 2rem in force, row B stuck at 32px from
   the viewport top, and the page's content scrolled visibly through the band
   above it. So the sticky header read as a strip FLOATING over the record
   instead of the page's own top edge. The padding is removed here, exactly the
   way admin-whatsapp.css already does for its own island, and nothing re-adds
   it inside the page: row B (or the return-chip row, when there is one) starts
   at the very top, and the stuck header is attached to the viewport.

   Longhand on purpose: the shell restates `padding` (all four sides) per
   breakpoint, and only the block-start half of it is wrong for this page.
   `--per-bleed` keeps mirroring the INLINE padding, which is untouched. */
.admin-main:has(.per-page) {
  padding-block-start: 0;
}

/* The two header rows and the tab strip go full-bleed to `.admin-main`'s
   padding edge: `inset-block-start: 0` sticks to the top of that padding BOX,
   so without this the stuck bar would float under 2rem of scroller padding with
   the page's content visible sliding past beside it. Negative inline margin
   plus a matching inline padding, both from `--per-bleed`, which is the only
   place either number is written. */
.per-header__row,
.per-gen,
.per-tabs {
  margin-inline: calc(-1 * var(--per-bleed));
  padding-inline: var(--per-bleed);
}

/* Row A is NOT sticky and simply scrolls away (decision S11). A header that
   shrinks on scroll needs an IntersectionObserver on a sentinel, and this page
   has three nested scroll containers, so a scroll listener would have to guess
   which one moved. Free, and it cannot jank.

   Since 2026-08-11 the row holds ONLY the `?from=` return chip and renders at
   all only when there is one - the trail link and the arrival pill are gone
   (deleted and moved to the detail band, respectively). The block padding is
   the row's whole layout now, and its start half doubles as the page's own top
   clearance, since the scroller's padding is gone above. */
.per-header__row--a {
  display: flex;
  padding-block: 0.6rem 0.65rem;
}

/* Row B: a HARD 56px, both bounds, because `--per-tabs`' offset below is that
   same number and a content-derived height would let a long Hebrew name push
   the strip on top of itself.

   NO transform, filter, backdrop-filter, container-type or contain here - see
   contract 5. The flat opaque fill is what a `backdrop-filter` would have been
   for, and it is the `.adm-sheet` answer to the same problem. */
.per-header__row--b {
  position: sticky;
  inset-block-start: 0;
  z-index: 12;
  display: flex;
  align-items: center;
  gap: var(--per-rowb-gap);
  min-block-size: var(--per-rowb);
  max-block-size: var(--per-rowb);
  background: #fff;
  border-block-end: 1px solid var(--adm-line);
}

/* One rung under row B, so the strip slides UNDER it on the way to its own
   stuck position rather than over it. 12 and 11 mirror `.leads-toolbar`'s 12,
   which is the panel's proven number for this job. Sticky, never fixed: it
   consumes no rung of the global z-ladder (mobile bar 1010 < tab bars 1015 <
   modal 1018 < nav scrim 1020 < drawer 1030 < sheet 1050 < toast 2000).

   No `overflow` other than the initial `visible`. A clipping value on a sticky
   element's own box is legal, but it is one refactor away from being inherited
   by a wrapper, and a clipping ancestor kills stickiness outright. Three tabs
   fit 390px by shrinking their labels instead. */
.per-tabs {
  position: sticky;
  /* Row B's height PLUS the band between them. The band is the page's third
     sticky box (2026-08-10) and the only one that changes height, which is why
     this is arithmetic on a published property instead of the constant it was
     when there were two. `--per-band` is `0px` while the band is folded, so the
     stuck state a reader spends most of their time in is byte-identical to the
     one this page had before. */
  inset-block-start: calc(var(--per-rowb) + var(--per-band));
  z-index: 11;
  display: flex;
  align-items: stretch;
  gap: 0.25rem;
  min-block-size: var(--per-tabs);
  max-block-size: var(--per-tabs);
  background: #fff;
  border-block-end: 1px solid var(--adm-line);
}

/* The pane wrapper. `position: relative` is the anchor Phase 4 needs: below
   1024px the dock becomes `position: absolute` inside THIS box (the `.wai-lead`
   recipe, local z 30 with a scrim at z 20) rather than taking a global ladder
   slot, which keeps it immune to the containing-block trap and unable to
   collide with the phone action bar.

   ── TRACK ORDER, AND WHICH SIDE EACH ONE IS (three tracks since 2026-08-10) ──
   `auto minmax(0, 1fr) auto`: the DOCK takes track 1, the tab body track 2 and
   the COMMENTS PANE track 3. Grid tracks are numbered from the inline-START
   edge, and this island is permanently RTL, so track 1 is the PHYSICALLY RIGHT
   column - the side the admin sidebar is already on - and track 3 is the
   PHYSICALLY LEFT one. Read right to left the screen is
   [sidebar] [WhatsApp dock] [tab content] [team comments].

   State BOTH halves of that in any comment about either flank: inline-START is
   physically RIGHT here and inline-END is physically LEFT, and a reader who
   knows only one of the two vocabularies gets the wrong edge. It has happened.

   The history in two lines. Until 2026-08-09 this was `minmax(0, 1fr) auto` and
   the dock was track 2, physically LEFT; decision D7 swapped it ("between the
   lead page content and the side menu"). On 2026-08-10 the comments stopped
   being a fourth TAB and became the third track, so the middle column is the
   only thing a tab click changes.

   The DOM order is `.per-main`, then the dock, then the comments pane, and none
   of the three is in the track its DOM position would give it. Both FLANKS are
   therefore placed explicitly - `grid-row: 1; grid-column: 1` on `.per-wa`,
   `grid-row: 1; grid-column: 3` on `.per-cm` (both in section 9) - and
   `.per-main` is left to auto-place into what is left, which is track 2.

   `.per-main` MUST STAY UNPLACED. Not `order`, which would also reverse the
   STACKED bands and put an open panel above the tab on a landscape phone; and
   not `grid-column: 2`, which would mint an implicit second track inside the two
   single-column blocks below (sections 17 and 20). Explicit placement on the two
   flanks alone changes exactly one band and needs no reset anywhere.

   BOTH AXES ON BOTH FLANKS, ALWAYS. Grid lays definitely-placed items out
   FIRST: with a `grid-column` but no `grid-row`, a flank would be positioned
   after `.per-main` had already auto-taken cell 1/1 and would drop into row 2,
   stacked under the tab at full desktop width - and nothing in this stylesheet
   would look wrong.

   `align-items` is deliberately left at its `stretch` initial value: that is
   what gives each flank's grid AREA the full height of the row, which is the
   travel room `position: sticky` needs on the flanks themselves. Setting `start`
   here would pin the area to the flank's own height and the sticky would never
   move. */
.per-panes {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 1rem;
  margin-block-start: 1.1rem;
}

/* `min-inline-size: 0` on the chain, everywhere. Anything that escapes
   `.admin-main` lands in `#app`, which is `overflow-x: clip` - invisible AND
   unscrollable - so a single long unbroken token (a landing URL, a click id)
   eats content while passing a "no horizontal scroll" check. */
.per-main {
  min-inline-size: 0;
}

/* The one real tabpanel. It carries `tabindex="0"` so a keyboard user can reach
   the panel body from the strip, which means it needs a visible ring: an
   invisible focus target is worse than none. No `overflow` and no `transform` -
   a tab-switch slide would trap the action sheet for exactly the duration the
   user is looking at it. */
.per-panel {
  min-inline-size: 0;
  outline: none;
}

.per-panel:focus-visible {
  outline: 2px solid var(--adm-accent);
  outline-offset: 4px;
  border-radius: 12px;
}

/* The loading and not-found frames render the worklist arrows on their own, so
   the arrows stay put under the cursor while a person loads. They open the
   frame now (the fallback breadcrumb is gone) and the scroller's top padding
   is gone with it, so the start margin is the frame's own clearance. */
.per-shell-nav {
  display: flex;
  align-items: center;
  margin-block: 0.9rem;
}

/* The frames that open with plain flow content - the failed read's and the
   404's `.empty-state`, the `course-cohorts`-only viewer's notice. Same reason
   as `.per-shell-nav` above: nothing above them supplies clearance any more. */
.per-page > .empty-state:first-child,
.per-page > .per-notice:first-child {
  margin-block-start: 1rem;
}

/* Sits in a centred `.empty-state` run of inline text between "the read failed"
   and the way back to the list, so it carries its own breathing room on all
   four sides. Same shape as the sale page's `.edt-retry`. */
.per-retry {
  margin-inline: 0.5rem;
  margin-block: 0.2rem;
  vertical-align: middle;
}

.per-retry:hover:not(:disabled) {
  border-color: var(--adm-accent);
  color: var(--adm-accent-ink);
}

/* ══════════════════════════════════════════════════════════════════════════
   4. HEADER - ROW A
   ══════════════════════════════════════════════════════════════════════════

   The `לידים` trail and the arrival pill left this row on 2026-08-11 (the
   trail deleted, the pill into the detail band - section 13), so the crumb
   line's one remaining occupant is the `?from=` return chip. The trail-text
   rules (`.per-crumb a` ink, the `__current` tail) went with them; what the
   chip used to inherit from them it now declares itself. */

.per-crumb {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-inline-size: 0;
  font-size: 0.85rem;
}

.per-crumb i {
  font-size: 0.65rem;
  opacity: 0.7;
}

/* The `?from=wa:` return chip. At the crumb line's START now (the reading edge
   in this RTL island): the auto end-margin that used to push it clear of the
   trail served a trail that no longer exists, and a lone way-back affordance
   belongs where the eye starts. */
.per-crumb__back-wa {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  flex: 0 0 auto;
  min-block-size: 36px;
  padding-inline: 0.75rem;
  border: 1px solid rgba(0, 113, 227, 0.25);
  border-radius: 999px;
  background: rgba(0, 113, 227, 0.06);
  color: var(--adm-accent);
  text-decoration: none;
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
}

.per-crumb__back-wa:hover {
  text-decoration: none;
  background: rgba(0, 113, 227, 0.12);
}

/* THE PLACEHOLDER HEADING, AND ONLY THAT (changed 2026-08-09).
   `.per-name` used to be row A's `<h1>`, the second of three renders of the
   person's name. That heading moved into sticky row B with its worklist arrows
   (owner items 4 and 12) and row A's copy was deleted, so the only markup left
   wearing this class is the loading / not-found / failed frames' placeholder
   heading in the shell - which is a heading in exactly the same sense and wants
   exactly these rules. `.per-name-row`, the flex wrapper that held the old
   `<h1>` beside the arrival pill, went with it. */
.per-name {
  margin: 0;
  font-size: 1.45rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--adm-ink);
  overflow-wrap: anywhere;
}

.per-name--placeholder {
  color: #8e8f9e;
  font-weight: 600;
}

/* ══════════════════════════════════════════════════════════════════════════
   5. OPT-OUT STRIP
   ══════════════════════════════════════════════════════════════════════════

   Between the two rows, and never sticky: it is three lines tall on a person
   who has one, and row B is a fixed 56px. It BLOCKS nothing - the owner
   declined a hard gate and a guard test asserts the send is still called - but
   it sits above the contact actions because it is the one thing to read before
   phoning or mailing this person. */

.per-optout {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  margin-block-start: 1rem;
  padding: 0.8rem 0.95rem;
  background: #fef2f2;
  border: 1px solid #fecaca;
  border-radius: 12px;
  color: #991b1b;
}

.per-optout > i {
  flex: 0 0 auto;
  margin-block-start: 0.15rem;
  font-size: 0.9rem;
}

.per-optout__lines {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-inline-size: 0;
}

.per-optout__title {
  font-size: 0.9rem;
  font-weight: 700;
}

/* Quieter than the title and the quoted words: it is an instruction to the
   reader, not a fact about the customer. */
.per-optout__hint {
  font-size: 0.78rem;
  line-height: 1.6;
  color: #7f1d1d;
  opacity: 0.9;
}

/* Modal-only: which person the warning is about, when the composer could be
   addressing somebody other than the page's subject. */
.per-optout__name {
  margin-block-start: 0.2rem;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--adm-ink);
}

/* The customer's own words, verbatim. `overflow-wrap` because one unbroken
   message must not push the strip wider than its column. */
.per-optout__quote {
  font-size: 0.83rem;
  line-height: 1.5;
  color: #334155;
  overflow-wrap: anywhere;
}

.per-optout__meta {
  font-size: 0.74rem;
  color: #7f1d1d;
  opacity: 0.85;
}

/* Inside the email modal the strip IS the body, so it loses the outer margin. */
.per-optout--modal {
  margin-block-start: 0;
}

/* ══════════════════════════════════════════════════════════════════════════
   5b. AWAITING-AN-ANSWER STRIP
   ══════════════════════════════════════════════════════════════════════════

   The customer tapped one of our WhatsApp buttons and nobody has answered
   since. Structurally a twin of the opt-out strip above, and in the same place
   for the same mechanical reason: it is three lines tall on anybody who has one
   and row B is capped at 56px, so it cannot live in the badge rail.

   AMBER, NOT RED, and the distinction is the whole reason it does not simply
   reuse `.per-optout`. Red on this page means "there is a reason not to contact
   this person" - the opt-out strip and nothing else. This is the opposite
   instruction, and it is a QUEUE rather than a warning: somebody is waiting.
   The tint is lifted from `.per-followup-flag` in section 13, which is already
   this page's word for "you owe this person something", so the two read as one
   family at a glance.

   The two can be on screen together - the opt-out matcher is a text heuristic
   and a person who asked us to stop can still tap a button afterwards - and the
   markup emits the opt-out FIRST so the caveat is read before the instruction. */

.per-await {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  margin-block-start: 1rem;
  padding: 0.8rem 0.95rem;
  background: #fffbeb;
  border: 1px solid #fde68a;
  border-radius: 12px;
  color: #92400e;
}

.per-await > i {
  flex: 0 0 auto;
  margin-block-start: 0.15rem;
  font-size: 0.9rem;
}

.per-await__lines {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-inline-size: 0;
}

.per-await__title {
  font-size: 0.9rem;
  font-weight: 700;
}

/* Quieter than the title: it is an instruction to the reader about how the
   marker behaves, not a fact about the customer. Same relationship, same two
   steps down, as `.per-optout__hint`. */
.per-await__hint {
  font-size: 0.78rem;
  line-height: 1.6;
  color: #78350f;
  opacity: 0.9;
}

/* HOW LONG they have been waiting is the actionable half of this strip, so it
   is not dimmed as far as the opt-out's timestamp is. */
.per-await__meta {
  font-size: 0.74rem;
  color: #78350f;
  opacity: 0.9;
}

/* ══════════════════════════════════════════════════════════════════════════
   6. HEADER - ROW B: IDENTITY, HIGHLIGHTS, ACTIONS
   ══════════════════════════════════════════════════════════════════════════ */

/* The group that SHRINKS. `min-inline-size: 0` is what lets the name clamp
   rather than forcing the row past 56px.

   THE CLIP IS AT EVERY WIDTH, not only below 1025px (moved up from the ≤1024px
   block on 2026-08-09). It used to be a touch-width guard, on the reasoning
   that a desktop row has slack. Two of the owner's own items removed that
   slack: D5 gave this page's reader a button that expands the sidebar back to
   264px, and D10 deleted row A's name and the breadcrumb tail so this box holds
   the only render of the person's name there is.

   Measured in Chromium with the sidebar EXPANDED, on a person carrying both
   provenance pills: `.per-id` was handed 46.7px at 1025px, 81.7px at 1060px and
   121.7px at 1100px against a 195px content, and with no clip here the deficit
   came out as OVERFLOW - the pills painted over the enrollment line, and the
   name and both worklist arrows computed to ZERO width. A person with no name
   on the one page that is about that person.

   Clipping is this row's documented honest failure (the same sentence is in the
   ≤1024px block, where this rule used to live); painting one box over another
   is not. The clip cuts the physical LEFT because the row is RTL, so what it
   takes is the END of the row - the pills, which repeat verbatim in the general
   detail panel below - and what it keeps is the START: the arrows and the name.

   ── AND THE CLIP MAY ONLY EVER REACH RUNGS 1 AND 2 ──────────────────────────
   `min-inline-size: 0` USED TO STAND WHERE THE RESERVATION IS NOW, and that
   zero is the whole of the bug this section was rewritten for. It let flex hand
   this box less than the controls inside it need, and the clip then ate them in
   DOM order without a mark: measured with the sidebar expanded, this box was
   handed 46.7px at 1025px, 81.7px at 1060px, 121.7px at 1100px and 221.7px at
   1200px against a 399px content, so everything after the worklist nav - the
   status `<select>`, both pills and the intent glyph - was laid out past the
   clip edge and drawn NOWHERE. `elementFromPoint` at the dropdown's own centre
   returned the row behind it at all four widths, and the control was still in
   the tab order the whole time (invariant A, above).

   The reservation is rung 5 of the ladder at the top of this file, expressed as
   a width: the worklist nav's own floor, the 0.45rem between it and the status
   control, and the status control's floor. Below that this box does not shrink,
   so the deficit walks on to the action cluster's labels (rung 4) instead of
   being taken out of a control.

   RUNGS 1, 2 AND 3 ARE NO LONGER IN HERE OR BESIDE IT IN THE SHRINK POOL. They
   are `.per-yield`'s, at flex-basis ZERO, so they take only space this box has
   already been given in full. What the clip is still for is a name longer than
   the box that survives at the narrowest widths. */
.per-id {
  display: flex;
  align-items: center;
  gap: var(--per-id-gap);
  /* `0 1 auto`, NOT `1 1 auto` (2026-08-09). The grow was what made this box
     swallow the row's slack - measured 776.6px at 1920px against a 403px
     content - and the slack is exactly what rungs 1 to 3 are now paid out of.
     With the grow gone the free space reaches `.per-yield` first, then the
     action cluster's auto start margin, and the identity group stays the size
     of what is in it.

     Shrink weight 1, the lowest in the row, and one of only two items left in
     the pool: the cluster beside it carries 100 (see `.per-header__actions`),
     so the name gives up 1.5px before the cluster freezes at its own floor. */
  flex: 0 1 auto;
  min-inline-size: var(--per-id-floor);
  overflow: hidden;
}

/* ── AND THE RESERVATION DROPS THE NAV TERM WHEN THERE IS NO NAV ──────────
   `LeadListNav` is rendered only for a person opened FROM a filtered worklist -
   `PersonHeader` gates it on `hasListContext`, which is `?list=` in the URL and
   nothing else. Every other door onto this page arrives without it: the
   WhatsApp inbox's "open the person" link, an open-day seat row, a lead
   mentioned in an email, a pasted URL, the Enrollments table. For those people
   `.ld-nav` is not in the DOM at all, and until 2026-08-09 `--per-id-floor`
   still reserved its whole width - two 44px arrows and their gaps - for a
   control that was never going to be drawn.

   What that cost, measured in Chromium at 390x844 on a person with no worklist
   context, no badges and no intent: `.per-id` was pinned at 215.9px around
   about 105px of content, so 110.8px of the box was blank space inside the
   `<h1>`, and `.per-yield` - which is paid for out of exactly this slack - had
   nothing left to grow into, so rung 3 was spent and the enrollment line was
   not drawn. The header said nothing about whether the person has bought, at a
   width with room to say it. The same person's money line appears at 430px.

   The nav term is therefore conditional rather than unconditional, and it is
   the only term that is: the name and the status control are in this box at
   every width and through every door.

   THE NAME TERM CARRIES THE SAME 1.05 CONVERSION `--per-nav-floor` DOES, and
   until 2026-08-09 it did not - the paragraph that stood here argued for the
   raw token off a font size that is only ever true on a phone, and the number
   it stated was backwards. `ch` resolves against the box the VALUE LANDS ON,
   and the two boxes do not share a font size:

     - `.per-id` inherits the shell. That is 16px at 768px and up, and 17px at
       767px and down - not this file's doing, `global.css` sets
       `body { font-size: 17px }` inside `@media (max-width: 767px)`. Measured
       in Chromium: one `ch` is 8.896px above the phone band and 9.452px inside
       it.
     - `.per-id__name` runs at `1.05rem` = 16.8px in EVERY band, where one `ch`
       is 9.328px. `rem` is the root's 16px and does not follow the shell.

   So above 767px the parent's character is the SMALLER one, and the raw token
   reserved less than the child it reserves for. Measured, with no worklist
   context (so this rule is the one in force), `.per-id`'s floor against the sum
   of what has to fit inside it:

     1025-1920px   12ch    185.95px reserved   191.22px needed   5.3px SHORT
     768-1024px     7ch    140.47px reserved   143.55px needed   3.1px SHORT
     320-767px      7ch    114.96px reserved   114.15px needed   0.8px over

   The phone was the only band where the old expression was on the safe side,
   and it is the band the old paragraph measured. Multiplying by 1.05 - the
   name's own `1.05rem` written as a ratio, exactly what `--per-nav-floor`
   spells out as 12.6ch and 7.35ch - makes the term the name's floor wherever
   the two boxes agree on a font size (112.09 against 112.02 at desktop, 65.39
   against 65.35 on tablet) and 4.1px over on the phone, which is the direction
   that reserves slightly too much rather than too little.

   NOTHING ON SCREEN MOVES, and that is the honest description of this edit: the
   floor has never been the binding constraint on a real composition. `.per-id`
   measures 287.6px at 1366px and 216px at 320px against floors of 191.2px and
   118.2px. This is a stated invariant made true so the next person can build on
   it, not a clip that anybody was seeing.

   `:has()` is already this file's idiom for "is there an nth button in the
   cluster" (`.per-header__actions`), and the specificity - (0,2,0) against
   `.per-id`'s (0,1,0) - is what makes the override work wherever it lands,
   including inside the two lower bands where all three tokens are restated. */
.per-id:not(:has(.ld-nav)) {
  min-inline-size: calc(
    1.05 * var(--per-name-floor) + var(--per-id-gap) + var(--per-status-floor)
  );
}

/* ══ THE YIELD ZONE: RUNGS 1, 2 AND 3, SEQUENCED BY WRAP ══════════════════
   Everything this row is allowed to drop, in one box, in survival order. The
   full argument is in the ladder block at the top of this file; what follows
   is only what each declaration is for.

   `flex: 1 0 0` - A ZERO BASIS IS THE WHOLE IDEA. This box contributes nothing
   to the row's intrinsic width, so the row's basis is rung 4 plus rung 5 plus
   the gaps and nothing else. Where the row is over-subscribed this box is
   simply 0 and the badges and the money are absent; where the row has slack it
   GROWS into it, up to its own content, and the deficit the previous four
   rebalances kept re-aiming never exists in the first place.

   `max-inline-size: max-content` is the freeze that stops the grow overshooting
   its contents and leaving a stretched box with a gap inside it. It is also
   what hands the remainder on: flex freezes an item that hits its maximum and
   redistributes what is left, which here means the action cluster's auto start
   margin, which is where the row's leftover belongs.

   `flex-wrap` + `row-gap: var(--per-rowb)` + `max-block-size` + `overflow` are
   the four halves of the discrete step, and they are the same four `.per-marks`
   has carried since it was written. Flex assigns items to lines using their
   hypothetical size BEFORE resolving any flexible length, so an item that no
   longer fits moves to line 2 whole; line 2 starts a full row-height down and
   is cut off unread. Nothing in this box ever draws part of itself.

   `align-content: flex-start` pins line 1 to the top of that cap rather than
   centring two lines across it, which would clip the top of line 1 instead of
   the bottom of line 2.

   1.9rem, against a measured 27.6px money cell (a 0.62rem caption over a
   0.8rem value at line-height 1.25) and an 18.8px badge. Line 1 always fits;
   line 2 starts 56px lower and never can.

   THE NEGATIVE START MARGIN IS NOT A NUDGE. Row B's gap applies to this item
   whether or not it draws anything, and a 12px gap around an empty box is 12px
   taken from the person's name at 320px. The margin cancels that gap exactly
   and the box re-spends it as its own leading column gap, so an undrawn yield
   zone costs the row zero and a drawn one puts the money one row gap from the
   name - the distance the badges used to sit at. Both halves read the same
   token, so they cannot drift apart. */
.per-yield {
  display: flex;
  align-items: center;
  align-content: flex-start;
  flex-wrap: wrap;
  flex: 1 0 0;
  max-inline-size: max-content;
  min-inline-size: 0;
  column-gap: var(--per-rowb-gap);
  row-gap: var(--per-rowb);
  max-block-size: 1.9rem;
  margin-inline-start: calc(-1 * var(--per-rowb-gap));
  overflow: hidden;
}

/* A flex line always keeps at least one item, so whatever is written first can
   never wrap off - it can only be clipped, and clipping the money cell is
   invariant C's own failure (`שולם ₪12,000 מתוך 8,000` on a customer who has
   paid 12,000 of 18,000). A zero-width first item makes the money cell the
   SECOND item on the line, which is the difference between "drawn whole or not
   drawn" and "drawn through the middle of a numeral".

   A pseudo-element and not a `<span>`: this is a layout artefact of the
   stylesheet, it carries no content, and a real element would be one more thing
   for a screen reader to walk past. */
.per-yield::before {
  content: '';
  flex: 0 0 0;
  min-inline-size: 0;
}

/* ── RUNGS 1 AND 2, AND A BOX THAT IS NOT A BOX ───────────────────────────
   The three badges. `PersonHeader` groups them so the template reads as one
   idea, and this rule takes that grouping straight back out of the layout:
   with `display: contents` the element generates no box at all and its three
   children become items of `.per-yield` directly.

   TWO THINGS THAT ONLY WORK BECAUSE OF IT.

   THE BADGES DROP ONE AT A TIME. Wrap decides per ITEM, so a group that is one
   item drops all three of its badges together the moment the third no longer
   fits. Measured at 1366px default rail with the group still a box: 264.8px of
   badges, of which the row could afford 212, so all three went and the reader
   was told nothing about where the person came from - at a width with room for
   two of them. As three items they wrap off in DOM order from the end: the
   partner pill, then the channel pill, then the intent glyph, which is rung 1
   before rung 2 and is why the glyph is written FIRST in the template. It is
   the only one of the three with no second render anywhere on this page.

   AND AN EMPTY GROUP COSTS NOTHING. Below 1025px the two provenance pills are
   `display: none` and only the intent glyph is re-shown, so for anybody with no
   recorded WhatsApp intent - most leads, since intent is only ever set by
   answering the auto-greet menu - this element is one CSS guarantees can never
   draw anything. As a flex item it still cost a full column gap: measured at
   375px, the name rendered 73.5px instead of 85.5px. `display: contents` makes
   the box, and therefore the gap, conditional on there being a child left to
   draw - by construction, at every width, with no `:empty` rule to keep in
   step with the markup.

   The wrap machinery this rule used to carry (`flex-wrap`, `row-gap`,
   `max-block-size`, `overflow`, a shrink weight of 40) is not gone, it moved
   one level up to `.per-yield`, which now runs it for rung 3 as well. */
.per-marks {
  display: contents;
}


/* THE `<h1>`, and since 2026-08-09 the page's ONLY render of the name.
   Row A's copy and the breadcrumb tail are both gone (owner items 4 and 12), so
   the clamp below is now the only thing between a long Hebrew name and a row B
   taller than 56px - and the floor at ≤1024px in section 17 is the only thing
   between a crowded row and a name of zero width. Both are load-bearing.

   `margin: 0` because this is an `<h1>` now and a user-agent margin inside a
   fixed 56px flex row would push the whole strip. `1.05rem` rather than row A's
   old 1.45rem: it has to read as the page's title AND fit a 56px box beside a
   dropdown and an action cluster. */
.per-id__name {
  margin: 0;
  /* A FLOOR AT EVERY WIDTH, not only below 1025px (added 2026-08-09 from a
     Chromium measurement of the merged strip). The touch floor in section 17
     exists because the row is over-subscribed there; 1280px turns out to be
     over-subscribed too, by about 24px once both provenance pills are shown at
     their full width. Measured at 1280x900 on a production person carrying both
     pills: the name computed to 75.0px against 96.0px at 1366 - five or six
     Hebrew characters of the only identity on the page.

     ── AND IT IS THE BASIS AS WELL AS THE FLOOR ──────────────────────────
     `flex: 1 1 var(--per-name-floor)`, where this used to be `flex-basis: auto`
     through the shorthand's absence. The basis is what `.ld-nav`'s line
     assignment reads, and `auto` resolves it to the whole untruncated name -
     168.3px against a 204.1px nav floor - so the moment the nav gained
     `flex-wrap` (for the counter, invariant C) the name claimed the entire line
     and the "next lead" arrow wrapped off it. With the basis pinned to the
     floor, the hypothetical line is exactly what `--per-nav-floor` reserves and
     both arrows are on line 1 by construction.

     `flex-grow: 1` is the half that gives the name back its full width: the
     name is then the item the nav's spare space flows into, and a flex
     container's max-content contribution accounts for a growable item's own
     max-content - so `.per-id` still sizes to the whole name wherever the row
     can pay for it. Measured on the geometry fixture: 168.3px at 1280px and up
     in the default rail, the 12ch floor at the widths where the row is short.

     The floor is restated per band (7ch below 1025px), which is why both halves
     read the token rather than the number. */
  flex: 1 1 var(--per-name-floor);
  min-inline-size: var(--per-name-floor);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--adm-ink);
}

/* The worklist arrows now wrap that `<h1>` in this row (`LeadListNav`, whose
   own `.ld-nav` rules live in admin-lead-detail.css). Two declarations are the
   whole integration, and both are about the SHRINK CHAIN: `.per-id` gives its
   deficit to whichever child can take it, and without these the nav is
   `flex: 0 1 auto` with an `auto` basis, so the name inside it never gets the
   growth the ≤1024px floor rule grants it. */
.per-id .ld-nav {
  flex: 1 1 auto;
  /* THE FLOOR THAT KEEPS THE CONTROL WHOLE (2026-08-09), and it replaces a
     `min-inline-size: 0` that stood here. The zero has to be REPLACED rather
     than deleted: `.ld-nav` in admin-lead-detail.css carries its own
     `min-width: 0`, so an absent declaration here is not the initial value, it
     is that zero at (0,1,0).

     `auto` would be the elegant spelling and it does not work. A flex item's
     automatic minimum size is defined as zero for a box whose `overflow` is
     anything but `visible`, and the clip below is exactly that - so `auto` here
     computes to 0 and changes nothing. `min-content` does not work either, for
     the opposite reason: `.per-id__name` is `white-space: nowrap`, so its
     min-content IS its full text, and the floor became the whole untruncated
     name (measured 284.4px on this fixture, which then made `.per-id` clip the
     arrow off at every width below 1280px).

     So the number is spelled out, and every term in it is a value declared in
     this file or in `.ld-nav-btn`:

         2 * 34px   the two arrows (`.ld-nav-btn { min-width: 34px }`)
         3 * 8px    the nav's three gaps (`.ld-nav { gap: 0.5rem }`)
         12.6ch     `.per-id__name`'s own 12ch floor. It is 12.6 and not 12
                    because `ch` resolves against the box it is written on, and
                    the name runs at `font-size: 1.05rem` while this box runs at
                    1rem: 12 x 1.05. Measured, the calc computes to 204.1px and
                    the name to 112.0px inside it, which is what the two
                    declarations independently ask for.

     The counter is not in the sum, and that is deliberate: it carries
     `min-inline-size: 0` two rules down precisely so it is the thing that fades
     out of the end of this box first.

     WHAT THE ZERO COST. It let flex shrink this box below the control inside
     it, and the clip below then cut the control rather than the counter.
     Measured in Chromium in the DEFAULT rail state on a person carrying both
     provenance pills: against a 204px content the box computed to 25.1px at
     1025px, 42.1px at 1060px, 61.5px at 1100px, 100.3px at 1200px, 131.3px at
     1280px, 139.1px at 1300px and 173.5px at 1366px. Two things followed at
     every one of those widths, and both are failures this page's comments spend
     paragraphs forbidding:

       - the "next lead" arrow was drawn ENTIRELY outside the clip.
         `elementFromPoint` at its own centre returned the row behind it at all
         seven widths, so a worklist pass could only walk backwards and nothing
         on screen said a forward arrow existed.
       - the name was cut AFTER its own ellipsis. `.per-id__name` had reached
         its 12ch floor and was drawing its own truncation mark at 112px; the
         clip then took the last 87px of that box away, mark included. A
         truncated name that does not look truncated is read as the person's
         actual name, and staff address people by it.

     With the floor, the same sweep measures the box at exactly 204.1px from
     1025px to 1366px with `scrollWidth === clientWidth`, the name at 112.0px
     with its own ellipsis inside the box, and `elementFromPoint` returning the
     forward arrow at every width.

     HANDED BACK BELOW 1025px - see the `.per-id .ld-nav` rule inside
     `@media (max-width: 1024px)`, which says why.

     THE NUMBER MOVED TO `--per-nav-floor` (2026-08-09) and the arithmetic above
     is unchanged. It moved because `.per-id`'s own reservation is built out of
     it: rung 5 of the ladder at the top of this file is "the nav floor plus the
     gap plus the status floor", and a reservation that repeats a sum instead of
     citing it is a reservation that drifts the first time an arrow changes
     size. Every consumer runs at 1rem, so the `ch` term means the same thing in
     all of them. */
  min-inline-size: var(--per-nav-floor);
  /* THE CLIP THAT STOPS A CHILD PAINTING ON THE STATUS CONTROL.
     A floor is not a guarantee: below 1025px the zero is handed back, and above
     it the box can still be handed less than `.per-id` itself has. When flex
     shrinks this box below its own content the content does not clip, it
     SPILLS, and the thing it spills onto is the status dropdown 7px away.
     Measured in Chromium on 2026-08-09: 22px of spill at 390px and 43px at
     1280px, with the worklist counter drawn on top of the dropdown at both.
     `.per-id { overflow: hidden }` does not catch it - the spill is inside the
     nav, and `.per-id`'s own scrollWidth never moves. Clipping is this row's
     documented honest failure; painting one control over another is not. */
  overflow: hidden;
  /* ── AND THE COUNTER LEAVES WHOLE, BECAUSE IT IS A NUMBER (invariant C) ──
     The same four declarations `.per-yield` runs the ladder with, for the same
     reason and at a much smaller scale. The counter is `7 / 43` and it used to
     be the item that shrank inside this box: measured across the desktop sweep
     in the default rail it read `2 /` at 1025, 1060, 1075, 1100, 1200, 1280,
     1300 and 1366px, and `2` alone at 1060px expanded. `7 / 4` is not a
     truncated counter, it is a counter that says position 7 of 4.

     With wrap it is on line 1 or on a line 56px below that `max-block-size`
     cuts off, and there is no state in between. */
  flex-wrap: wrap;
  align-content: flex-start;
  row-gap: var(--per-rowb);
  max-block-size: 2.25rem;
}

/* ... and the item that leaves first. The counter is the only part of the
   control that is decoration rather than function, so it is what goes: the two
   arrows and the name keep their size and the "7 / 43" is on the line or it is
   not. `0 0 auto` and not `0 1 auto` - a counter that may shrink is a counter
   that can be drawn as `7 / 4`, which is invariant C's failure with a smaller
   number in it. */
.per-id .ld-nav-pos {
  flex: 0 0 auto;
}

/* Shape only. The colours arrive as a computed `:style` from the status row's
   own hex in the database, which is the one thing CSS cannot know. Weight stays
   under 500 so the pill never invites the brand gradient.

   TWO ELEMENTS WEAR THIS CLASS since 2026-08-09 (owner item 5): the read-only
   pill a viewer without `leads:full` gets, and the `<select>` everybody else
   gets. Sharing the class is deliberate - the control has to sit in the row at
   exactly the size the pill did, or the strip's width budget changes with the
   viewer's permissions and the 56px promise stops being testable. */
.per-id__status {
  /* RUNG 5: THIS CONTROL NEVER YIELDS. `flex: 0 0 auto` was already here and was
     never the protection it looked like - an item that refuses to shrink is
     still an item that can be laid out past its parent's clip edge, which is
     exactly how this ended up drawing 0px at 1025, 1060 and 1100 and 12px at
     1200 with the sidebar expanded. What protects it is `.per-id`'s reservation
     above, which is built out of the floor below.

     `0 1 auto` with a floor rather than `0 0 auto`: if a future row ever does
     over-subscribe past the reservation, a control that CAN shrink stops at its
     floor inside its own border, while one that cannot spills onto the pill
     beside it. The floor is the same token the reservation is built from, so
     the two can never disagree. */
  flex: 0 1 auto;
  min-inline-size: var(--per-status-floor);
  /* ── THE CEILING, THE CLIP AND THE MARK BELONG TO BOTH HALVES ───────────
     All three used to live on `.per-id__status-select` alone, so the ONE
     viewer who cannot fix a wrong status - a `leads:read` reader, a DB-outage
     cache-mode reader, anybody looking at a soft-deleted person - was the one
     whose label was cut with no ceiling, no clip and no mark. Measured at
     1025px expanded against a staff-authored label: a 211.7px border box
     around 335px of content, with 124.1px of that content drawn OUTSIDE its own
     border and clipped by `.per-id` instead. The pill looked complete, ended in
     a full letter, and the sentence went on without it -
     `ממתין לאישור תקציב מהמחלקה הפיננסית` read as `ממתין לאישור תקציב מהמחל`.

     Status labels are staff-editable through a shipped CRUD endpoint, so long
     ones are not a hypothetical; and `.per-id`'s clip is the row's LAST resort,
     not a text treatment - it cuts at a box boundary and cannot draw a mark.

     20ch, measured. The taxonomy's Hebrew labels run from `חדש` to
     `השתתף ביום פתוח וטרם נרשם`, and a `<select>` cannot ellipsise its own
     selected text in every engine - it just cuts. 16ch showed `נרשם ליום פ` on
     a real person at 1366px, which is a status nobody can read. 18ch shows all
     but the two longest labels, and those two carry the full text in `title` -
     on BOTH elements since 2026-08-09 (`PersonStatusSelect.vue`). It is a
     CEILING and not a width: a `חדש` pill is still `חדש` wide. */
  max-inline-size: 18ch;
  padding: 0.12rem 0.55rem;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 400;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The dropdown half of the pair, and this rule is only what a `<select>` needs
   on top of the `<span>` shape it shares.

   IT DELIBERATELY DOES NOT BORROW `.reg-status-select`, which is the obvious
   candidate and is what the seat card's approval dropdown wears. Two reasons,
   and the first is not aesthetic: that class already means something else on
   THIS page. `.reg-status-select` is the open-day APPROVAL control, and a page
   carrying two selects under one class - one about the lead's funnel status,
   one about whether somebody is coming on Tuesday - is a page where
   `find('select.reg-status-select')` picks whichever comes first in the DOM.
   Six specs do exactly that. The second reason is that the borrowed class
   carries `filter: brightness(.97)` on hover, and `filter` is one of the seven
   properties this page reasons about at every use (contract 5).

   Native `appearance` is kept rather than replaced with a background-image
   caret: the UA draws its arrow on the correct side of an RTL control for free,
   and a hand-drawn one would need a PHYSICAL `background-position`, which is the
   one property family this file spends comments avoiding.

   `font-family: inherit` because a `<select>` does not inherit it. The ceiling
   stops a long staff-authored status label claiming the row before the name
   gets its share; the name's own floor at ≤1024px is the other half of that
   bargain. */
.per-id__status-select {
  /* THE CEILING, THE CLIP AND THE ELLIPSIS USED TO STAND HERE and moved up to
     `.per-id__status` on 2026-08-09, because the read-only `<span>` that shares
     that class had none of the three. What is left here is only what a `<select>`
     needs that a `<span>` does not.

     `min-inline-size: 0` also used to stand here and is deliberately gone. This
     rule is later in the file than `.per-id__status` and carries the same
     (0,1,0), so the zero silently cancelled the rung-5 floor on the ONE element
     that has to keep it - the pill half of the pair kept its floor and the
     `<select>` half, the only interactive one of the two, did not. */
  font-family: inherit;
  cursor: pointer;
}

/* In flight, not refused - `progress`, never `not-allowed`. `admin.css` has no
   shared `.btn:disabled` and this file does not add one (see `.per-action`). */
.per-id__status-select:disabled,
.per-id__status-btn:disabled {
  opacity: 0.6;
  cursor: progress;
}

/* ── THE TOUCH PILL: THE SAME CONTROL WITHOUT THE BROWSER'S CHROME ────────
   Below 1025px `PersonStatusSelect` renders a `<button>` here instead of a
   `<select>`, and it opens the catalogue in `AdminSheet`. The argument is in
   that component's block comment; what this rule is for is the arithmetic
   behind it.

   A `<select>` on this page is forced to 16px by the iOS zoom floor in section
   17, and Chromium draws it with about 20px of its own caret. Inside the 71.2px
   row B can spare at 390px that leaves roughly 33px of text - measured on the
   real strip as `ה…`, one Hebrew letter, in the one control that says whether
   this person is a new lead or a paying student, on the two tabs where nothing
   else on the page says it at all. There is no hover on a phone, so the `title`
   that carries the rest is unreachable.

   A button is not a text control, so the 16px floor does not apply to it: the
   same 71.2px box at this row's own 0.7rem holds about eighteen characters,
   which is every label in the shipped taxonomy except the two longest. It also
   draws no caret of its own, so the 14px chevron below is the whole of what the
   text gives up - against the UA's 20px.

   It wears `.per-id__status` for shape and floor, exactly as the `<select>` and
   the read-only `<span>` do, so all three sit in the row at the same size and
   the strip's width budget does not change with the viewer's permissions. */
.per-id__status-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  border-color: currentcolor;
  font: inherit;
  font-size: 0.7rem;
  font-weight: 400;
  line-height: 1.5;
  text-align: start;
  cursor: pointer;
}

/* The mark has to be on the TEXT RUN and not on the pill, for the same reason
   `.per-id__badge > span` carries it: `text-overflow` marks the overflow of a
   BLOCK container's inline content and does nothing to a flex container's
   items, so the ellipsis declared on `.per-id__status` is inert the moment that
   box becomes a flex row. `min-inline-size: 0` is what makes the shrink
   reachable at all - a flex item's automatic minimum is its content. */
.per-id__status-text {
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Never yields, and never becomes the widest thing in the pill: the chevron is
   the whole of what says this pill opens something, and a pill that has spent
   its width on a glyph has spent it on the wrong half. */
.per-id__status-caret {
  flex: 0 0 auto;
  font-size: 0.6rem;
  opacity: 0.75;
}

/* ── THE STATUS SHEET ─────────────────────────────────────────────────────
   `.per-sheet__item`'s rows, plus the two things a status list needs that an
   action list does not: the row's own colour, and which one you are on. Both
   are read from the database, so the dot's fill is a computed `:style` and only
   its shape is here. */
.per-status-sheet__dot {
  flex: 0 0 auto;
  inline-size: 10px;
  block-size: 10px;
  border-radius: 999px;
}

.per-status-sheet__item--current {
  font-weight: 600;
}

/* `margin-inline-start: auto` and not a `justify-content` change on the row:
   the row is shared with the action sheet, where the label is the last thing
   in it. */
.per-status-sheet__check {
  margin-inline-start: auto;
  font-size: 0.8rem;
  color: var(--adm-accent-ink);
}

/* Applied ALONGSIDE `.reg-source-badge`, never instead of it - that class is in
   admin.css and five list screens read it. The one thing that has to go is its
   `margin-left: auto`, which exists to push the badge to the end of a table
   cell; in this flex row it would eat all the free space and shove the owner
   chip off the end. A logical property at the same specificity in a later file
   is what cancels it: `margin-inline` maps onto the same two physical sides. */
.per-id__badge {
  /* BACK TO `0 0 auto`, and this time it is not the 2026-08-08 mistake it looks
     like. It was `0 0 auto` when the badge sat inside `.per-id`, where the only
     other shrinkable child was the person's NAME, so a badge that refused to
     shrink was a badge the name paid for. It became `0 1 auto` for that reason,
     and a shrinkable badge is a badge that can be drawn as a coloured stub with
     no word in it to mark as cut - measured at 1280px as 18px and 15px stubs
     and at 1200px as a 4.2px sliver.

     Neither is needed now. The badge is an item of `.per-yield`, whose basis is
     zero, so it is never in the row's shrink pool at all: it is drawn at its own
     size out of slack the row already has, or `flex-wrap` moves it off the line
     whole. One size, and a yes/no about whether there is room for it.

     `min-inline-size` is left at its `auto` default deliberately - the automatic
     minimum of a badge is its content, and content is exactly the size this box
     is now allowed to be. */
  flex: 0 0 auto;
  margin-inline: 0;
  /* THE 14ch CEILING IS GONE. It was the thing truncating "Meta ads · ודאי" and
     a partner's group name mid-word, and it existed because row B had no slack
     to give. It does now: the phone, the email, the owner chip and the
     created-at cell left this row on the same day (owner item 3.3). 22ch is a
     backstop against a staff-authored partner label claiming the whole strip,
     not a clip - the two labels this actually carries measure well inside it. */
  max-inline-size: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* THE TRUNCATION MARK HAS TO BE ON THE TEXT RUN, NOT ON THE PILL (2026-08-09).
   `.reg-source-badge` is `inline-flex`, and `text-overflow` marks the overflow
   of a BLOCK container's inline content - it does nothing to a flex container's
   items. So the `text-overflow: ellipsis` three lines above is inert, and the
   pill above it was free to shrink past its own label and cut it with no mark
   at all.

   Measured in Chromium against these stylesheets, default rail, "Meta ads":
   the pill computed to 19.9px at 1025px, 28.9px at 1100px and 40.9px at 1200px
   against a 70.7px intrinsic, cutting 42.8px / 33.8px / 21.8px off a 47.6px
   text run. THE CUT IS ALWAYS ON THE PHYSICAL LEFT (`cutRight` was 0 in all 96
   measurements), and the row is RTL, so what is lost is the START of a Latin
   label and what survives is its tail: at 1100px the pill read "ds" and the
   partner pill beside it read "ORTH-2026". A truncation that reads as a shorter
   WORD is worse than one that reads as truncation - nothing on screen says the
   channel is not literally called that.

   The span IS a block container (a flex item is blockified), so the mark lands
   where it can be drawn, and it lands at the END of the label rather than the
   start. `min-inline-size: 0` for the same reason it is on the pill: a flex
   item's automatic minimum size is its content, so without it the span cannot
   shrink and the pill goes back to clipping it.

   Scoped to `.per-id__badge`, never to `.reg-source-badge`, which is admin.css's
   and is read by five list screens. */
.per-id__badge > span {
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── The intent badge: what the customer asked for ───
   Third in the rail, after the two that say where this person came FROM.
   `.reg-source-badge` supplies the shape and a transparent border and NO fill,
   so a modifier is required or the badge renders as bare text - the same
   contract every `.reg-source-badge--*` in admin.css works under. These are
   `.per-*` classes, so they are ours to declare here and they reach no other
   screen.

   THREE SWATCHES BECAUSE STAFF SCAN FOR ONE OF THEM. The working question on
   this page is "who is waiting for a phone call", so `call` gets the warm tint
   that reads as a queue (the same amber as the awaiting strip and the overdue
   follow-up flag - one family, one meaning), `chat` gets a cool blue, and
   `info` is the quietest of the three because it asks for a document rather
   than for a person. All three are lifted from the `.reg-chip--*` /
   `.reg-source-badge--*` palette so the page keeps ONE tint vocabulary, and all
   three stay under weight 500 in ink terms - the brand gradient never touches
   this rail.

   GLYPH ONLY, AT EVERY WIDTH, AND THAT IS A MEASUREMENT RATHER THAN A TASTE.
   The badge shipped with its word (`ביקש שיחה`) and cost 87.2px of a row whose
   only shrinkable child is the person's NAME. Measured in a real browser on
   2026-08-08 across the width sweep, worded, against the same page with no
   intent to show:

     1500   81.5 -> 81.5     no cost, the row still has slack
     1440   81.5 -> 47.0
     1366   60.6 ->  4.2     a single glyph where the name was
     1280   14.9 ->  0.0     and `.per-id` spilled 45.5px onto its neighbour

   1366 and 1280 are the two commonest laptop widths in this panel's traffic, so
   the badge was erasing the identity from the sticky header of exactly the
   leads it was added to mark. A width query would be the obvious lever and is
   the one thing this file may not spend: the four breakpoints at the top are
   pinned by `adminPersonRtlMirrors.spec.ts` and a fifth is a decision rather
   than a tweak. Dropping the word instead costs 57.6px and buys the whole
   regression back (1440 -> 80.3, 1366 -> 39.5), and it costs no meaning: the
   three tints and the three glyphs are already two redundant channels, the
   component gives the badge `role="img"` with the full sentence plus the
   timestamp as its accessible name, and the one case that is urgent is spelled
   out in words in the strip directly above this row. */
.per-id__intent--call {
  background: #fffbeb;
  color: #92400e;
  border-color: #fde68a;
}

.per-id__intent--chat {
  background: #eff6ff;
  color: #1d4ed8;
  border-color: #bfdbfe;
}

.per-id__intent--info {
  background: #f3f4f6;
  color: #4b5563;
  border-color: #e5e7eb;
}

/* `.per-id__owner` USED TO BE HERE. The read-only owner chip is gone from this
   row (owner item 3.3): it was hidden outright below 1025px anyway, it was the
   cell measured painting on top of the money value at 390px, and the owner is
   now a PICKER in the general detail panel below - with an "assign to me"
   button beside it, which this row never had room to offer. */

/* ONE cell, where there were five - so this element has nothing left to lay
   out, and since 2026-08-09 it does not try. `display: contents` hands the cell
   itself straight to `.per-yield`, where rung 3 belongs: the money is written
   FIRST in that box, which under wrap ordering means it is the last thing to
   go, after both provenance pills and the intent glyph.

   The group used to carry the ladder's rung-3 shrink weight and a floor read as
   one rule in two selectors with `.per-hl--money`. Both are gone with the whole
   idea of a deficit: a group that can shrink below the one child it holds does
   not clip that child, it lets it OUT (measured at 1025px expanded, 51.5px of
   group around an 80.1px cell, 16.5px of it under the action cluster), and
   there is no longer anything to shrink - `.per-yield`'s basis is zero and the
   cell has one size.

   `gap` is gone with it and is not a loss: this box held five cells when it
   needed a gap and holds one now. Its two per-band overrides (0.85rem in the
   tablet block, 0.7rem on a phone) went at the same time. */
.per-highlights {
  display: contents;
}

/* ── RUNG 3, AND INVARIANT C: THE MONEY IS DRAWN WHOLE OR NOT DRAWN ────────
   `flex: 0 0 auto` is the invariant, not a preference. This cell renders
   `שולם ₪12,000 מתוך ₪18,000` - a Hebrew sentence with two LTR `<bdi>` figures
   in it - and the row is RTL, so a cell one pixel narrower than its content is
   a cell whose PHYSICAL LEFT is cut, which is the middle of the sentence and
   the inside of a numeral run. Measured before this rule, default rail:

     1440   שולם ₪12,000 מתוך 8,000     paid 12,000 of 8,000: overpaid by 4,000
     1366   שולם ₪12,000 מתוך 000
     1075   שולם 00
     1060   שולם 0                      a customer who has paid ₪12,000

   Every one of those is a complete, grammatical sentence containing a complete,
   wrong figure, and none of them carried a truncation mark. A number that is
   absent is honest; a number missing its leading digits is not.

   So the cell has exactly two states. It has ONE size - `0 0 auto`, its own
   content - and `.per-yield`'s `flex-wrap` decides whether the row has room for
   it. Where it does not, the cell moves to a line 56px lower that
   `max-block-size` cuts off, and the reader sees nothing at all rather than a
   wrong amount. The full sentence stays in the cell's `title` at every width,
   and the sale has a whole band of its own one click away on the Enrollment
   tab, which is why rung 3 is a rung and not a floor.

   `min-inline-size` stays at its `auto` default for the same reason as the
   badges: the automatic minimum of this cell is its content, and its content is
   the only size it may have. */
.per-hl {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0;
  flex: 0 0 auto;
  line-height: 1.25;
}

/* THE CAPTION CLIPS TOO, AND NOT ONLY THE VALUE (2026-08-09, invariant B).
   `.per-hl__value` below has carried `overflow: hidden` since it was written;
   this box carried none, and the pair only looked equivalent because at every
   width somebody had checked, the cell was wide enough for both.

   The failure it was written for is gone at its root - the cell is now drawn at
   its own size or not drawn, so neither box is ever narrower than its text -
   and both clips stay as the backstop that says so. `הרשמה` is one unbreakable
   Hebrew word and it once painted 34px OUTSIDE a zero-width parent at 320, 360,
   375 and 390px: a stray caption floating between the person's name and the
   overflow kebab, labelling a figure that had deliberately not been drawn.
   `getBoundingClientRect()` cannot see that (the border box really is 0px); a
   `Range` over the text node can, which is how the geometry spec catches it.

   No ellipsis on either box. On the caption because `הרש…` reads as a different
   label; on the value because it is a NUMBER, and invariant C says a number is
   drawn whole or not drawn - there is no marked-truncation state for it to
   reach. */
.per-hl__label {
  overflow: hidden;
  white-space: nowrap;
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: #8e8f9e;
}

/* `text-overflow: ellipsis` USED TO STAND HERE and its removal is invariant C.
   The mark it promised was never the protection it looked like: the row is RTL
   and both figures are LTR `<bdi>` runs, so the ellipsis is drawn at the line's
   own end while the CUT lands inside a numeral - `שולם ₪12,000 מתוך 8,000` at
   1440px on a customer who has paid 12,000 of 18,000, with the mark nowhere
   near the digits it removed. There is no honest partial state for a figure, so
   there is no mark to draw: the cell is whole or absent (`.per-hl`). The clip
   stays as the backstop. */
.per-hl__value {
  overflow: hidden;
  white-space: nowrap;
  font-size: 0.8rem;
  color: var(--adm-ink);
}

/* `.per-hl__value--empty`, `.per-hl__link` and `.per-hl__link:hover` USED TO BE
   HERE, together with the `.per-hl { position: relative }` / `.per-hl__link::after`
   hit-area pair in section 17. All four belonged to the phone and email cells,
   which are the `tel:` and `mailto:` links this row no longer carries (owner
   item 3.3) - both are now fields in the general detail panel, where the phone
   is still a `tel:` link for a reader who may not edit it. Deleted rather than
   left dormant: a dead rule for a deleted element is the next person's ten
   minutes. */

/* Money is the one figure on this page a person acts on, so it is the only
   highlight at weight 600 - and it is SOLID INK, never the brand gradient.
   The gradient reads cleanly through light letterforms only; on a semi-bold
   numeral the colours muddy. */
.per-hl--money .per-hl__value {
  font-weight: 600;
}

/* `.per-hl--money { min-inline-size: 0 }` USED TO STAND HERE, as one half of a
   pair of floors read as a single rule across two selectors (this cell and the
   group holding it). Both are gone, and the reasoning is worth keeping because
   it is the reasoning the ladder replaced.

   The floor was 9ch above 767px, added because the cell had measured 0px at
   390px, 0px at 768px and 13.9px at 1024px - the screen saying nothing at all
   about whether this person had bought anything. That was a real failure and a
   floor was the wrong instrument for it: a floor does not say "draw this where
   there is room", it says "take this from whoever has no floor", and the boxes
   with no floor were the person's name, the worklist arrows and the status
   control. It was then handed back to 0, and 0 is what let the cell be drawn
   1px at a time through the middle of a figure.

   What decides now is neither: the cell has one size (`.per-hl`) and
   `.per-yield`'s wrap decides whether the row has room for it. */

/* The "(מוסכם, בוטל)" suffix on a person whose every sale is cancelled. Same
   line, quieter ink, and it must not add a pixel of height. */
.per-money__note {
  margin-inline-start: 0.3rem;
  font-size: 0.72rem;
  font-weight: 400;
  color: #8e8f9e;
}

/* The `#actions` slot, and RUNG 4 of the ladder.

   IT USED TO BE `flex: 0 0 auto`, with a comment reading "so the identity group
   is what gives way when the row runs out of room, never the buttons". That is
   the priority inversion this whole section was rewritten to remove. The buttons
   are the ONE thing in this row with a designed second home - every action that
   does not fit is already in the overflow sheet behind the kebab beside them,
   and every visible one keeps its `aria-label` and its glyph - while the things
   that gave way for them were the person's name, the worklist arrows and the
   status control, none of which has anywhere else to be on two of the three
   tabs.

   THE MEASUREMENT THAT SETTLES IT. This cluster is 546.3px for a full-permission
   viewer, against 301.1px for the same viewer in the 768-1024px band, where the
   labels are dropped outright. At 1025px with the sidebar expanded the row has
   697px of content box. 546.3 + the 24px of row gaps leaves 126.7px for the
   arrows, the name and the status control, which need 283.3px between them - so
   the row at 1025px EXPANDED is 156.6px short while the same row at 768px, with
   720px of content box, has 96px to spare. The breakpoint was on the viewport
   and the constraint is on the ROW: expanding the sidebar (D5) spends 200px that
   no `@media` rule in this file can see.

   Shrink weight 100, where it was 2, and the change is the whole of what stops
   this rung and rung 5 paying together. Rungs 1 to 3 are no longer in this pool
   at all - `.per-yield` has a flex basis of zero and is paid out of slack - so
   the pool is exactly two items, both floored, and a weight ratio between two
   items is measurable rather than hopeful. Products at the desktop
   full-permission composition: 100 x 546.3 against `.per-id`'s 1 x 403, so at
   the row's tightest state (1025px, sidebar expanded, 276.3px short) the name
   gives up 1.5px before this cluster reaches its floor and freezes, and every
   pixel after that is the name's. A pixel and a half is not a rung.

   `min-inline-size: 0` IS LOAD-BEARING AND BOTH NEIGHBOURING MISTAKES WERE
   MADE ON THE WAY TO IT, so both are written down.

   Leaving it off does NOT give this box a sensible floor. A flex item's
   automatic minimum is its MIN-CONTENT, and the min-content of a row of
   `white-space: nowrap` buttons is the row with every label at full length -
   546.3px, i.e. exactly the number that could not shrink in the first place.
   Measured with it off: the cluster stayed at 546.3px at every desktop width
   and the row simply overflowed instead.

   Setting it to 0 without the two rules below is the opposite failure. This box
   then shrinks past what its own contents can compress to, and the contents are
   controls: measured at 1025px expanded, it computed to 276.8px around a
   538.2px cluster, so 261px of it - the overflow trigger included - was laid
   out past the physical left edge of the row with nothing clipping it. The
   trigger's own centre hit-tested to the row behind it. An unshrinkable cluster
   was the original bug; a cluster that shrinks below its own controls is the
   same bug pointed the other way, and invariant A forbids both.

   So the zero comes with a floor of its own, and the floor is COUNTED rather
   than guessed - see the four rules below. It has to be counted because CSS
   cannot compute it: what we want is "the width my children can compress to",
   and the only thing the engine offers is min-content, which for a row of
   `white-space: nowrap` buttons is every label at full length. The count is the
   one part of the sum that varies, because `PersonActionBar` drops a button
   outright where the viewer lacks the permission for it rather than greying it.

   The compressed cluster measures 342px for a full-permission viewer against
   the 377.7px this row can offer it at its narrowest (1025px, sidebar expanded,
   rungs 1-3 fully spent). That 35.7px is the whole margin, it is the row's
   tightest number, and it is why `tests/personRowBGeometry.spec.ts` hit-tests
   EVERY control here rather than only the ones that have broken before. */
.per-header__actions {
  display: flex;
  align-items: center;
  flex: 0 100 auto;
  min-inline-size: 0;
  margin-inline-start: auto;
}

/* ── THE CLUSTER'S FLOOR, COUNTED ─────────────────────────────────────────
   `--per-act-btns` is how many `<button>`s the cluster is actually rendering:
   the visible actions plus the overflow trigger. `PersonActionBar` renders
   between one and four of them depending on the viewer's permissions and on
   what the person has, so a fixed reservation would be right for a root user
   and 100px too generous for everybody else - and an over-reservation is not
   free here, it is width taken from the person's name at exactly the widths
   where the name is already at its floor.

   `:has()` cannot add, but it can answer "is there an nth one", which is all
   the sum needs. All four rules carry the same specificity, so the LAST one
   that matches wins and the value lands on the real count. `> button` and not
   `button`: the WhatsApp group is a `<div>` holding three `<a>`/`<button>`
   destinations of its own, and it is reserved for separately below.

   The sum: three 44px WhatsApp segments plus the two 1px hairlines between them
   (the same expression `.per-actions__wa` carries, and it has to stay the same
   expression), then one 44px target and one 8px gap per button. Measured at
   342px for a root user with all three desktop actions, which matches. */
/* ── AND ON A PHONE IT IS NOT THERE AT ALL, SO IT COSTS NOTHING ──────────
   `PersonActionBar` is gated on `!isPhone` at the shell, so below 768px this
   slot renders an empty `<div>` - width 0, and a full 12px of row gap for
   nothing. That gap is not spare change at 320px: the row there is a 44px
   overflow kebab and an identity group already pinned to its reservation, and
   without this rule it was 11.9px over-subscribed and the kebab drew 28 of its
   44px. Removing an empty box from the flow removes its gap with it, which is
   the whole 12px, and the row lands at 295.9px inside a 296px content box.

   `:empty` and not a width query, because the condition really is "the slot has
   nothing in it" - a comment placeholder is what Vue leaves behind for an
   unfilled slot and `:empty` ignores comment nodes. If the phone ever gains a
   header action, the box comes back by itself. */
.per-header__actions:empty {
  display: none;
}

.per-header__actions {
  --per-act-btns: 1;
}

.per-header__actions:has(.per-actions--row > button:nth-of-type(2)) {
  --per-act-btns: 2;
}

.per-header__actions:has(.per-actions--row > button:nth-of-type(3)) {
  --per-act-btns: 3;
}

.per-header__actions:has(.per-actions--row > button:nth-of-type(4)) {
  --per-act-btns: 4;
}

.per-header__actions:has(.per-actions--row) {
  min-inline-size: calc(3 * 44px + 2px + var(--per-act-btns) * (44px + 8px));
}

/* ── THE OWNER CHIP - RUNG 2.5 (owner, 2026-08-10) ────────────────────────

   Between the money and the badges in `.per-yield`, which is both its DOM
   position and its survival position: it outlasts all three provenance marks and
   yields to the money.

   It is the one thing added to this row rather than a second copy of something
   already in it. Every other candidate had a home: the phone, the mail and the
   three WhatsApp routes are buttons in the action cluster at every width, since
   rung 4 ellipsises their LABELS and never removes the buttons. "Who is
   responsible" had none - it is in the band, and the band folds.

   Weight 600 on the initials is solid ink, not the gradient (contract 6). */
.per-owner-chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 24px;
  block-size: 24px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: rgba(0, 113, 227, 0.12);
  color: var(--adm-accent-ink);
  font-size: 0.58rem;
  /* Solid ink at 600, never the gradient - contract 6, numerals and initials
     included. */
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1;
  white-space: nowrap;
}

/* Nobody is responsible - the state most worth acting on, so it is DRAWN rather
   than left blank. Dashed and grey, because an empty slot is what it is. */
.per-owner-chip--empty {
  border-style: dashed;
  border-color: #c9cad4;
  background: none;
  color: #8e8f9e;
  font-size: 0.62rem;
}


/* ══════════════════════════════════════════════════════════════════════════
   7. TAB STRIP
   ══════════════════════════════════════════════════════════════════════════ */

/* EVERY rule for a tab BUTTON is scoped to the strip, and this is not tidiness.
   `.per-tab` is a homonym in the markup: `PersonTabStrip` puts it on each of the
   three <button>s, and `PersonOpenDayTab` / `PersonEnrollmentTab` put the SAME
   class on their panel root (`class="per-tab per-tab--open-day"`). A bare
   `.per-tab { }` here would give both tab bodies an inline-flex row, a pointer
   cursor and a 2px underline. The strip owns the descendant selector; the two
   panels are addressed through their modifiers alone, further down. */
/* The tablist moved INSIDE `.per-tabs` on 2026-08-10 so the fold chevron beside
   it would not be announced as a fourth tab. `.per-tabs` is the sticky BAR; this
   is the three buttons on it. Every rule that styles a tab is a
   `.per-tabs .per-tab` descendant selector, so nothing moved with the nesting. */
.per-tabs__list {
  display: flex;
  align-items: stretch;
  flex: 1 1 auto;
  gap: 0.25rem;
  min-inline-size: 0;
}

/* ── THE DETAIL BAND'S FOLD CHEVRON ───────────────────────────────────────

   At the strip's inline-END, pushed there by the list's own `flex-grow` rather
   than by a margin, so it cannot collide with a fourth tab if one is ever added.

   WHY IT IS ON THIS BAR AND NOT ON THE BAND OR THE IDENTITY ROW. The band folds
   to zero height, so a control inside it has nowhere to be at the exact moment
   somebody wants it back. And the identity row is over budget: measured at
   1280px railed, a 40px chevron there plus its gap took 47px of the 74px of
   slack `.per-yield` lives on, and the money line and all three provenance
   badges wrapped onto the line that row clips away. This bar is sticky too, so
   the control is on screen at every scroll position; it is 48px tall, so it
   takes a touch target without argument; and it costs the row above it nothing.
   The full argument is in `PersonHeader.vue` and in `--per-id-floor`'s comment
   in section 1. */
.per-tabs__fold {
  flex: 0 0 auto;
  align-self: center;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-block-size: 32px;
  padding-inline: 0.7rem;
  border: 1px solid var(--adm-line);
  border-radius: 999px;
  background: #fff;
  font: inherit;
  font-size: 0.72rem;
  font-weight: 500;
  color: #5a5b6a;
  white-space: nowrap;
  cursor: pointer;
}

.per-tabs__fold:hover {
  border-color: rgba(0, 113, 227, 0.35);
  background: rgba(0, 113, 227, 0.06);
  color: var(--adm-accent);
}

.per-tabs__fold:focus-visible {
  outline: 2px solid var(--adm-accent);
  outline-offset: 1px;
}

.per-tabs__fold i {
  font-size: 0.6rem;
  opacity: 0.75;
}

.per-tabs .per-tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  flex: 0 1 auto;
  min-inline-size: 0;
  padding-inline: 0.9rem;
  border: none;
  border-block-end: 2px solid transparent;
  background: none;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 500;
  color: #5a5b6a;
  cursor: pointer;
  white-space: nowrap;
}

.per-tabs .per-tab:hover {
  color: var(--adm-ink);
}

/* The roving tabindex means exactly ONE tab is reachable by Tab, so this ring
   is the only thing telling a keyboard user where they are inside the strip. */
.per-tabs .per-tab:focus-visible {
  outline: 2px solid var(--adm-accent);
  outline-offset: -3px;
  border-radius: 6px;
}

/* Selection is carried by ink weight AND the inset underline, not by colour
   alone: this strip has to read on a greyscale screen and for a colour-blind
   reader. */
.per-tabs .per-tab--active {
  color: var(--adm-ink);
  font-weight: 700;
  border-block-end-color: var(--adm-accent);
}

.per-tab__icon {
  flex: 0 0 auto;
  font-size: 0.8rem;
}

.per-tab__label {
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Never rendered for zero (see `personTabBadge`), so it needs no empty state.
   LTR-embedded in the template, because a bare Latin numeral at the end of a
   Hebrew run resolves to the wrong side of its own label. */
.per-tab__badge {
  flex: 0 0 auto;
  min-inline-size: 1.25rem;
  padding: 0.05rem 0.35rem;
  border-radius: 999px;
  background: #f0f1f5;
  color: #5a5b6a;
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 1.5;
  text-align: center;
}

.per-tabs .per-tab--active .per-tab__badge {
  background: rgba(0, 113, 227, 0.12);
  color: var(--adm-accent-ink);
}

/* ══════════════════════════════════════════════════════════════════════════
   8. ACTION BAR
   ══════════════════════════════════════════════════════════════════════════ */

.per-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

.per-actions--row {
  flex-wrap: nowrap;
  /* The cluster tracks the slot it sits in. Without this its own automatic
     minimum - min-content, i.e. every label at full length - keeps it at 546.3px
     inside a slot that rung 4 has narrowed, and the difference is drawn outside
     the slot. With it, the deficit passes through to the children, each of which
     has a 44px floor of its own. See `.per-header__actions` for both halves. */
  min-inline-size: 0;
}

/* The two other variants are PHONE-ONLY - the shell gates both on `isPhone` -
   so every declaration they own lives inside `@media (max-width: 767px)` and
   there is deliberately nothing for them here:

     `.per-actions--bar`   the fixed toolbar docked to the bottom edge
     `.per-actions--menu`  the destructive overflow, kept up in sticky row B

   Two things about the bar are decided elsewhere and are worth knowing before
   you read its rules further down:

     - It REPLACES `.adm-tabbar` rather than stacking on it, and that is a
       COMPONENT check, not a CSS one: `AdminTabBar.vue`'s
       `visible = canWork && !inThread && !onPerson`. So admin.css's
       `.admin-layout:has(.adm-tabbar) .admin-main` clearance correctly stops
       matching on this route, there is no `padding-block-end` arms race, and
       the tab bar's 90-second unread poller stops with it. A CSS-only
       `display: none` would have left both running.
     - It is an ANCESTOR of `AdminSheet`'s `position: fixed` layer (the sheet
       renders in place, no Teleport), so it may never take `backdrop-filter`,
       `filter`, `transform`, `perspective`, `contain` or `will-change` - any of
       them makes the 64px bar the containing block and drops the sheet inside
       it. The flat opaque fill it carries instead is the same answer
       `.adm-sheet` reached for itself. */

/* 44px, uniformly, and the same 44px the borrowed `.wa-seg` gets below.

   The brief allows the primary set 48px. It is not taken in the `row` variant,
   for a mechanical reason: every button that renders here IS a primary (the
   component's `primaryKeys` for `row` is exactly `call` + `email`; everything
   else is in the sheet), so 48px would not distinguish anything - it would just
   leave a 48px control, a 44px overflow trigger and a 44px WhatsApp group at
   three different heights inside a bar pinned to 56px. 44px across the row
   clears the thumb floor and keeps 6px of breathing room top and bottom. The
   48px option belongs to `--bar`, where there is vertical room for it to mean
   something. */
.per-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  /* RUNG 4: THE LABEL YIELDS, THE BUTTON DOES NOT (2026-08-09). `flex: 0 0 auto`
     stood here, which is what made the whole cluster an immovable 546.3px and
     pushed the row's entire deficit onto the identity group - see
     `.per-header__actions` for the arithmetic.

     `min-inline-size: 44px` is an EXPLICIT minimum and that is the load-bearing
     half. A flex item's automatic minimum size is its min-content, and for a
     `nowrap` button that is the icon plus the whole word - so without a stated
     number this button could not shrink at all and `flex-shrink: 1` would be
     inert. Stating it replaces the automatic minimum, and 44px is the same thumb
     floor every other control in this row keeps.

     `overflow: hidden` so the shrink is taken from the label INSIDE the button's
     border (invariant B) rather than spilling onto the button beside it. */
  flex: 0 1 auto;
  min-inline-size: 44px;
  min-block-size: 44px;
  overflow: hidden;
  white-space: nowrap;
}

/* ── A DISABLED ACTION HAS TO LOOK DISABLED (owner item 15, 2026-08-09) ────
   The email button already carried `:disabled` and a `title` saying why - "לאדם
   אין כתובת Email" - and looked exactly like a live button, because `admin.css`
   declares no `.btn:disabled` at all. So the one thing on the row that could not
   work was the one thing that gave no sign of it, and it was reported as broken.
   The same rule covers `call` with no number, `contract` with no sale and
   `refresh` mid-read.

   `opacity: .45; cursor: not-allowed` is `.reg-action:disabled`'s form
   (`admin.css`), which is this panel's de-facto reference and the majority of
   the five local answers in the repo. A shared `.btn:disabled` in `admin.css`
   would be the tidier fix and is deliberately NOT taken here: that file is read
   by every admin screen and it would restyle disabled buttons on six of them in
   a change nobody reviewed for those screens.

   The hover cancel is the half that is easy to forget: `:hover` still matches a
   disabled button, so `admin.css`'s `.btn:hover { border-color: #bbb }` would
   light the control up under the pointer that has just been told it is off. */
.per-action:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.per-action:disabled:hover {
  border-color: #ddd;
}

.per-action__icon {
  font-size: 0.85rem;
}

/* `fa-spin` is added to the glyph while a refresh is in flight. The fixed box
   stops the label shuffling sideways as the spinner's glyph metrics change. */
.per-action--refresh .per-action__icon {
  display: inline-block;
  inline-size: 1em;
  text-align: center;
}

/* The thing rung 4 actually spends. `min-inline-size: 0` was already here and
   was inert on its own: the button above refused to shrink, so nothing ever
   asked this box to. The ellipsis is what makes the loss honest - `שיחה` cut to
   `ש` with no mark reads as a word, `ש…` reads as a word that did not fit, and
   the button's `aria-label` carries the whole thing either way (the icon beside
   it is the same meaning in a second channel). */
.per-action__label {
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  /* ── AND THE MARK HAS TO LAND AT THE LABEL'S OWN END ──────────────────
     This box inherits `direction: rtl` from the page, so its paragraph
     direction is RTL whatever is written in it - and a Latin label is one LTR
     run inside an RTL line, anchored at the line's start (the physical RIGHT)
     with its overflow spilling LEFT. The clip therefore eats the run's own
     BEGINNING: measured at 1025px with the sidebar expanded, `Email` read
     `ail` and `תבנית WhatsApp` read `תבנית atsApp`. A word that has lost its
     start is a different word, and the ellipsis drawn at the line's far end
     does not say which one is missing.

     `unicode-bidi: plaintext` resolves the paragraph direction from the first
     strong character of each label instead of from the page, which is exactly
     the rule a human reader applies: `Email` becomes an LTR paragraph and is
     cut at its END (`Ema…`), `שיחה` stays RTL and is cut at its end (`שי…`),
     and a mixed label keeps the Hebrew it opens with. The truncation is the
     same size; what changes is which half of the word survives.

     It is the same failure `.per-id__badge > span` was fixed for on the same
     day, one step further along: there the mark was on the wrong BOX, here it
     is on the wrong SIDE. */
  unicode-bidi: plaintext;
}

/* Scoping hook on the WaActionGroup instance. Size and margin nudges only, and
   always through `.per-page .per-actions__wa .wa-seg` - a bare `.wa-actions` or
   `.wa-seg` rule would restyle six other render sites (the leads list, the
   open-day table and card, the inbox thread header, the inbox's blocked
   composer). */
/* RUNG 4 REACHES THE WHATSAPP PILL TOO (2026-08-09). `flex: 0 0 auto` stood
   here, which made 187.3px of segmented control the one part of the action
   cluster that could not participate in the row's budget at all - and it is the
   widest single thing in that cluster, because each of the three destinations
   carries a word.

   The borrowed kit is already built for this and needed only permission:
   `.wa-seg` is `flex: 1 1 auto; min-width: 0` and `.wa-seg__label` already
   carries `overflow: hidden; text-overflow: ellipsis` (admin-wa-actions.css).
   So the labels ellipsise inside their own segments and the group narrows from
   187.3px toward its floor.

   THE FLOOR HAS TO BE STATED, and the reason is the one property this group
   already had: `.wa-actions` carries `overflow: hidden` (its rounded ends
   depend on it, and `.wa-seg:focus-visible` uses an INSET outline because of
   it). A flex item that clips has an automatic minimum size of ZERO, so
   without a number here the group shrank straight past its own segments -
   measured at 1025px expanded the group computed to 88.8px around three 44px
   destinations, and the third one, `wa-seg--app`, hit-tested to the row behind
   it. Hiding a LABEL is a density step and is allowed; hiding a SEGMENT is not
   (owner's call, 2026-08-04), and a segment clipped out of a group is hidden
   however the rule was written.

   `3 * 44px + 2px` is three touch targets and the two hairlines between them
   (`.wa-seg + .wa-seg { border-inline-start: 1px }`). It is a FIXED
   reservation and not a count: the group renders one, two or three segments
   depending on the person and the viewer, and `.wa-seg` is `flex: 1 1 auto`,
   so a group with fewer of them simply grows the ones it has into the same
   134px instead of leaving a gap inside its own border. That keeps the whole
   cluster's floor at one number - 342px - whoever is looking at whom, which is
   the number `.per-header__actions` reasons about. */
.per-actions__wa {
  flex: 0 1 auto;
  min-inline-size: calc(3 * 44px + 2px);
}

.per-page .per-actions__wa .wa-seg {
  /* The inline half is new and is what stops rung 4 eating a destination. An
     explicit minimum also REPLACES the automatic one, which is what lets the
     group's floor be three 44px targets rather than three whole words. */
  min-inline-size: 44px;
  min-block-size: 44px;
}

/* The same bidi fix `.per-action__label` carries, for the same reason, on the
   labels inside the borrowed segmented control: `וואטסאפ ווב` and `אפליקציה`
   are Hebrew, but the group's own wording changes with `labels`, and one Latin
   destination name would be cut at its start exactly as `Email` was.

   Scoped through `.per-page .per-actions__wa`, never to a bare `.wa-seg__label`:
   that class is admin-wa-actions.css's and is rendered by the leads list, the
   open-day table and card, the inbox thread header and the blocked composer,
   none of which has been measured for this. */
.per-page .per-actions__wa .wa-seg__label {
  unicode-bidi: plaintext;
}

.per-actions__more {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  inline-size: 44px;
  min-block-size: 44px;
  padding: 0;
}

/* ── The overflow sheet ─────────────────────────────── */

/* A vertical list with no padding of its own - `.adm-sheet__body` already pays
   it, and doubling it makes the sheet look like it has a frame. */
.per-sheet {
  display: flex;
  flex-direction: column;
}

.per-sheet__item {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  inline-size: 100%;
  min-block-size: 44px;
  padding-inline: 0.25rem;
  border: none;
  background: none;
  font: inherit;
  font-size: 0.9rem;
  color: var(--adm-ink);
  text-align: start;
  cursor: pointer;
}

.per-sheet__item:hover:not(:disabled) {
  background: rgba(0, 113, 227, 0.06);
  border-radius: 10px;
}

.per-sheet__item:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* Fixed column so every label starts on the same line, which is what makes the
   list scannable at a glance instead of a ragged stack. */
.per-sheet__icon {
  flex: 0 0 auto;
  inline-size: 1.25rem;
  text-align: center;
  color: #8e8f9e;
}

.per-sheet__label {
  min-inline-size: 0;
}

/* The separator IS the point. A destructive row one pixel below a routine one
   is a mis-tap waiting to happen, so the band is real clearance and nothing is
   ever added below it. This margin supplies 8px above the rule; the danger row
   below it supplies 1rem of its own (see the next rule and why). */
.per-sheet__sep {
  block-size: 1px;
  margin-block: 0.5rem;
  background: var(--adm-line);
}

/* 1rem, not 0.5rem: on a phone the sheet's ONLY row is this one, so there is no
   separator above it and the 44px delete target lands right under the sheet's
   own close button. Measured 2026-08-08 at 390px: 10.8px of clearance from the
   close button, against the 16px this page requires around anything
   destructive. The separator still carries the desktop case. */
.per-sheet__item--danger {
  margin-block-start: 1rem;
  color: #dc2626;
}

.per-sheet__item--danger .per-sheet__icon {
  color: #dc2626;
}

.per-sheet__item--danger:hover:not(:disabled) {
  background: rgba(220, 38, 38, 0.08);
}

/* ── The HARD delete, one step past the soft one ─────────────────────────────

   Two destructive rows now sit under that separator - `מחיקה` (soft, hides the
   person) and `מחיקה לצמיתות` (destroys the row and its whole timeline) - and
   they are 44px apart in a sheet a thumb opened. Both are already red, so red
   cannot be what tells them apart; the difference has to be visible without
   reading, or the pair is worse than either alone.

   So this row is FILLED rather than merely tinted. It reads as a distinct
   object at a glance, which is the same trick `.per-sheet__sep` plays with
   space: the eye is told there are two kinds here before any word is parsed.

   The margin is the second half, and it is written as a WHOLE VALUE rather than
   as an increment: this rule has the same (0,1,0) specificity as
   `.per-sheet__item--danger` above and simply wins on order, so margins do not
   add up here - 1.5rem REPLACES that rule's 1rem. Getting that backwards would
   have given the harder of the two deletes LESS clearance than the softer one.
   24px, against the 16px this page requires around anything destructive, buys
   the extra because the row above it is also red.

   Deliberately NOT the brand gradient: this is a warning surface, and the
   gradient may never carry weight >= 500 (root CLAUDE.md), which a destructive
   label needs. Solid ink on a solid fill. */
.per-sheet__item--purge {
  margin-block-start: 1.5rem;
  padding-block: 0.35rem;
  padding-inline: 0.6rem;
  border-radius: 10px;
  background: rgba(220, 38, 38, 0.09);
  font-weight: 600;
}

.per-sheet__item--purge:hover:not(:disabled) {
  background: rgba(220, 38, 38, 0.18);
}

/* The blocked state is the common one for root looking at a buyer, so it has to
   read as "not now" rather than as a rendering fault: the fill drops away and
   only `.per-sheet__item:disabled`'s dimming is left, which is what every other
   unavailable row in this sheet already looks like. */
.per-sheet__item--purge:disabled {
  background: none;
}

/* ══════════════════════════════════════════════════════════════════════════
   9. THE DOCKED WHATSAPP PANEL
   ══════════════════════════════════════════════════════════════════════════

   `.per-wa`, never `.wai-page`. See the header. The `--wa-*` palette and the
   `--adm-*` re-point both reach this element because admin-whatsapp.css lists
   `.per-wa` on its two palette selectors - one shared source, no fourth copy of
   thirty-three hex values.

   ── THE THREE BANDS ───────────────────────────────────────────────────────

     > 1024px      docked beside the tab, on the PHYSICAL RIGHT of the pane.
                   `var(--per-wa-w, 380px)`, dragged between 320 and 520 by the
                   handle. THIS block.
     768..1024     `.per-wa--overlay`: over the tab, with a scrim, not beside it.
     <= 767        `.per-wa--full`: the whole scroller, entered and left via
                   `?wa=1` so the browser Back button closes it.
     any height
       <= 480px    FORCE-STACKED, whatever the width. Authored LAST, in section
                   20, so it beats the two above at equal specificity.

   There used to be a FOURTH band, 1025..1280, whose CSS was identical to the
   dock's and whose only difference was that the panel arrived already carrying
   `.per-wa--collapsed`. The owner removed it on 2026-08-09 (decision D8): a
   1366px laptop is one of the two commonest widths in this panel's traffic and
   it was starting every reader on the rail. Nothing in this file changed for
   that - there never was a query - but the band is gone from the ladder and
   1280 no longer means anything on this page.

   It collapses to a 48px rail and NEVER to zero at any of them: a dock that
   disappears teaches staff the conversation is gone.

   ── position: sticky, NOT absolute, and this is a departure from S19 ───────

   S19 says to copy `.wai-lead` exactly - `position: absolute` inside a
   `position: relative` wrapper. The recipe does not transfer, and the reason is
   the wrapper, not the panel. `.wai-lead` lives in a fixed-height shell that
   does not scroll, so `inset-block: 0` is one viewport tall. `.per-panes` is a
   child of a page that scrolls and is routinely thousands of pixels tall, so an
   absolutely positioned overlay with `inset-block: 0` would be as tall as the
   wrapper, putting its header and transcript at the TOP OF THE DOCUMENT - off
   screen the moment the reader has scrolled at all. The panel would be open and
   invisible.

   Sticky keeps every guarantee S19 was actually protecting: local z 30 with the
   scrim at 20, no rung of the global z-ladder consumed, no `position: fixed`,
   and complete immunity to the containing-block trap.

   ── WHICH SIDE, AND HOW IT GETS THERE (changed 2026-08-09) ────────────────

   The dock is anchored at the pane's inline-START, which in this permanently
   RTL island is the PHYSICAL RIGHT of the screen - between the tab content and
   the admin sidebar, which was the owner's instruction (decision D7). It is
   grid track 1; `.per-main` auto-places into track 2, the physical left. Until
   2026-08-09 it was the other way round: `.per-panes` was `1fr auto`, the dock
   was track 2 and therefore the physically LEFT column, and the docblocks here
   said so. Say both halves whenever you rewrite this - the logical property AND
   the physical edge - because "inline-end" and "physically left" name the same
   side here and a reader who knows only one of the two will get it backwards.

   `grid-row: 1` is not decoration. Grid places definitely-positioned items
   FIRST: with a column but no row, the dock would be laid out AFTER `.per-main`
   had already taken cell 1/1 and would land in row 2, stacked under the tab.
   With both axes stated, the dock takes 1/1 and `.per-main` auto-places into
   1/2 - which is the whole swap, with the DOM order untouched. Both are reset
   to `auto` by the landscape block (section 20), so the force-stack there still
   places by DOM order and the open panel still sits BELOW the tab.

   ── the transform sign, if a slide is ever added ──────────────────────────

   None is, and none should be: the panel never fully hides (the rail always
   remains), so there is no closed state to translate. Transition `inline-size`
   and `opacity` instead. If one is added anyway, the sign is the SAME as the
   one S19 quotes, which it was not before this move. `.wai-lead` is anchored at
   `inset-inline-start` and hides with `translateX(100%)`. This dock is now
   anchored at the pane's inline-START too - `.per-panes` is `auto 1fr` in a
   permanently-RTL island, so track 1 is the physically RIGHT column - and
   hiding it means pushing it further RIGHT, which under RTL is
   `translateX(100%)`. Same island, same anchor, same sign. And it would never
   go on `.per-panes`: a transform there makes the wrapper the containing block
   for the phone action bar.

   NEVER write `.admin-main:has(.per-wa) { overflow: hidden }`. That is the
   inbox's shape, not this one: the Person page scrolls, and the dock scrolls
   inside its own box exactly as `.ld-pay-scroll` does. */

.per-wa {
  position: sticky;
  inset-block-start: calc(var(--per-rowb) + var(--per-band) + var(--per-tabs) + 0.75rem);
  z-index: 30;
  /* Track 1 of `.per-panes` = inline-start = the PHYSICAL RIGHT column here.
     See the block above for why the row has to be stated as well. */
  grid-row: 1;
  grid-column: 1;
  display: flex;
  flex-direction: column;
  /* The drag handle publishes `--per-wa-w` as an inline custom property on this
     element, and this is the ONE rule in the file that reads it. That is the
     whole trick: an inline `inline-size` would beat every stylesheet rule short
     of `!important`, so the tablet and phone bands would have to shout to get
     their own size back. A custom property is inert until a rule consumes it,
     and no other band consumes it.

     The bounds are the panel's, not the preference's - the store snaps the
     stored value to seven steps between the same two numbers, so these two
     lines are the second half of one contract and must move together. Below
     320px this stops being a conversation and becomes a column of hyphens. */
  inline-size: var(--per-wa-w, 380px);
  min-inline-size: 320px;
  max-inline-size: 520px;
  /* A definite block-size, so the grid AREA can stay stretched to the row (the
     travel room the sticky needs) while the box itself stays a viewport-tall
     dock instead of a several-thousand-pixel column with nothing to scroll. */
  block-size: calc(100dvh - var(--per-rowb) - var(--per-band) - var(--per-tabs) - 5rem);
  min-block-size: 0;
  overflow: hidden;
  background: var(--wa-panel, #fff);
  border: 1px solid var(--wa-line, rgba(134, 150, 160, 0.15));
  border-radius: 14px;
}

/* 48px, never zero and never `display: none`. A dock that disappears when it is
   closed teaches staff that the conversation is gone.

   All three inline sizes, not just the one: the open panel's 320px floor would
   otherwise inflate the rail to 320px, which is the same bug as no rail at all
   read from the other end. */
.per-wa--collapsed {
  inline-size: 48px;
  min-inline-size: 48px;
  max-inline-size: 48px;
}

/* ══ AND IT STANDS DOWN WHILE A BOTTOM SHEET IS OPEN ══════════════════════
   THE FAILURE, MEASURED. `AdminSheet` renders IN PLACE - there is no Teleport -
   so on this page its layer is a child of `.per-header__row--b`, and that row is
   `position: sticky`, which creates a stacking context UNCONDITIONALLY (it does
   not need a z-index to do it, so there is no way to opt the row out). The
   layer's own `z-index: 1040` is therefore confined inside the row's 12, and
   this dock's 30 paints straight over the top of it. The sheet is drawn - the
   reader can READ the option - and the tap goes to whatever is on top.

   Hit-tested in Chromium on 2026-08-09, sheet open, `elementFromPoint` at three
   x positions across each option row (10%, 50%, 90% of its width). One full
   44px row is dead in every one of these, and every probe on it returned the
   collapsed WhatsApp rail:

     390x844 rail    `ממתין לאישור תקציב...`   y 784-828   -> .per-wa__rail
     414x896 rail    `נרשם לקורס`              y 792-836   -> .per-wa__rail
     430x932 rail    `הגיע ליום פתוח`          y 784-828   -> .per-wa__rail
     767x1000 rail   `חדש`                     y 632-676   -> .per-wa__rail
     900x1000 rail   `נרשם לקורס`              y 606-650   -> .per-wa
     1024x1000 both  `נרשם לקורס`              y 606-650   -> .per-wa

   WHICH row is dead moves with the viewport HEIGHT, because the sheet is
   bottom-anchored (`.adm-sheet-layer` is fixed, `align-items: flex-end`) while
   the rail is sticky inside the document - so no single width or height is
   "the" broken case and a spec that pinned one would pass on the next device.
   `נרשם לקורס` and `שיחת היכרות בוצעה` are among the statuses that land there,
   which is a salesperson setting the status after a call, getting no status
   change, and getting a WhatsApp panel instead.

   WHY LOWERING AND NOT HIDING. `.per-actions--bar` is HIDDEN under the same
   condition three thousand lines down, and that was the owner's call for a
   FIXED bottom bar which is only ever in the way. This dock is different: it is
   the sticky right-hand column of the page's own layout, and `display: none` on
   it would reflow the tab body underneath the sheet for as long as the sheet is
   open. Dropping its z-index changes nothing but paint order, and only while
   the sheet is up.

   `z-index: auto` and not a number: with no z-index the dock still paints above
   the in-flow content it is a sibling of (it is positioned), and below every
   positioned box with a POSITIVE z-index - which is exactly row B's 12 and
   therefore the sheet trapped inside it. A number would be one more rung to
   keep in step with the four in section 3's ladder.

   THE SCRIM GOES WITH IT, and that is not tidiness: `.per-wa-scrim` is z 20 in
   the tablet band, so left alone it would paint over both the dock AND the
   sheet - a grey wash over the modal that a moment ago was the only thing on
   screen. Both back to `auto` puts them in DOM order, and the scrim is written
   first, so the panel stays above its own scrim exactly as it does today.

   AND NOT WHEN THE SHEET IS THE DOCK'S OWN. `PersonWaPanel` and `WaComposer`
   both mount `AdminSheet` INSIDE this panel - the canned replies, the approved
   templates - and those layers are trapped in the dock's stacking context, not
   in row B's. Lowering the dock would take its own sheet down with it, under
   row B's 12, and the modal would have a 56px opaque strip of the page painted
   across the top of its scrim with the status control still live inside it. So
   the condition is "a sheet is open on this page AND it is not one of the
   dock's", which is the `:not(:has())` half. The dock's sheets need no help:
   at z 30 they already cover everything this page draws.

   (0,5,0) against `.per-wa--overlay` and `.per-wa--full` at (0,1,0) - both
   re-declare `z-index: 30` in their own bands - so ONE rule covers all three
   bands and there is no per-band copy to fall out of step. The landscape block
   already resets this dock to `position: static; z-index: auto`, so there it is
   a no-op rather than a fourth answer.

   Pinned by a HIT TEST in `tests/personStatusSheetHit.spec.ts`. A CSS-as-text
   assertion cannot see an overlap, which is exactly how this reached the ship
   gate with `tests/adminPersonMobileGuards.spec.ts` green.

   THE COMMENTS PANE JOINS THIS RULE RATHER THAN GETTING ONE OF ITS OWN. It is
   the same z 30 on the opposite edge of the same wrapper, so it is the same bug
   with a different x coordinate, and one block with four selectors cannot fall
   out of step with itself the way two blocks can. Its own `:not(:has())` half
   costs nothing today (nothing inside it mounts a sheet) and is written anyway,
   because the day something does, the missing half is a modal with a strip of
   page painted across it and nobody grepping for this rule. */
.per-page:has(.adm-sheet-layer):not(:has(.per-wa .adm-sheet-layer)) .per-wa,
.per-page:has(.adm-sheet-layer):not(:has(.per-wa .adm-sheet-layer)) .per-wa-scrim,
.per-page:has(.adm-sheet-layer):not(:has(.per-cm .adm-sheet-layer)) .per-cm,
.per-page:has(.adm-sheet-layer):not(:has(.per-cm .adm-sheet-layer)) .per-cm-scrim {
  z-index: auto;
}

/* ── The drag handle ────────────────────────────────────
   Rendered on the dock alone (`canResize` = docked and open), on the edge that
   actually travels. Since 2026-08-09 the dock is grid track 1 - the pane's
   inline-START, which in this permanently-RTL island is the PHYSICAL RIGHT
   column - so the edge pinned against the pane is its physical RIGHT one and
   the edge that MOVES is its physical LEFT one, the one facing the tab content.
   That is `inset-inline-END: 0` here. It was `inset-inline-start: 0` while the
   dock lived in the left column, and the pointer arithmetic in
   `PersonWaPanel.vue` flipped in the same commit: it now captures the panel's
   `right` and computes `right - clientX`, so the two still agree about which
   edge is under the cursor. `touch-action: none` or a pointer drag scrolls the
   page instead of resizing.

   It is a tabbable `role="separator"`, so it owes a REAL focus ring: an
   invisible focus target is worse than none. The ring is drawn INSET, because
   `.per-wa` clips (`overflow: hidden`) and an outward outline on a box flush
   with the panel edge would be cut in half. */
.per-wa__resize {
  position: absolute;
  inset-inline-end: 0;
  inset-block: 0;
  z-index: 1;
  inline-size: 6px;
  cursor: col-resize;
  touch-action: none;
}

/* The visible line, as a child rather than a border, so the 6px hit area stays
   6px while the mark stays 2px. Transparent until the handle is found.

   2px of offset, a 2px mark and a 6px strip: the mark is exactly CENTRED, so it
   needs no mirror and did not move when the handle changed edges. Keep those
   three numbers adding up if any of them changes. */
.per-wa__resize::after {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 2px;
  inline-size: 2px;
  background: transparent;
}

.per-wa__resize:hover::after,
.per-wa__resize:focus-visible::after {
  background: var(--wa-green-ink, #117335);
}

.per-wa__resize:focus-visible {
  outline: 2px solid var(--adm-accent);
  outline-offset: -2px;
}

/* No conversation at all: quieter surface, no green accent, so an absent thread
   never reads as a live one at a glance. */
.per-wa--empty {
  background: transparent;
  border-style: dashed;
}

/* The whole 48px column is one target, because a 48px-wide panel has no room
   for a second thing to hit by mistake. */
.per-wa__rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  inline-size: 100%;
  block-size: 100%;
  padding-block: 0.75rem;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.per-wa__rail:hover {
  background: var(--wa-hover, #f5f6f6);
}

.per-wa__rail:focus-visible {
  outline: 2px solid var(--wa-green-ink, #117335);
  outline-offset: -3px;
}

.per-wa__rail-glyph {
  position: relative;
  display: inline-flex;
  flex: 0 0 auto;
  font-size: 1.15rem;
  color: var(--wa-green, #25d366);
}

/* A DOT, never a number: the payload carries no per-conversation unread count
   and inventing one would need a WhatsApp call this panel is forbidden to make.
   The 2px ring is the rail's own background, so the dot reads against the
   glyph it is hanging off rather than melting into it. */
.per-wa__dot {
  position: absolute;
  inset-block-start: -2px;
  inset-inline-end: -2px;
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  background: var(--wa-green, #25d366);
  box-shadow: 0 0 0 2px var(--wa-panel, #fff);
}

/* `writing-mode`, NOT `transform: rotate`. A transform on an ancestor of a
   fixed surface creates a containing block for it, and Phase 4's phone action
   bar is fixed - see contract 5. Rotation would also have to have its sign
   reasoned out; writing-mode simply follows the island. */
.per-wa__rail-label {
  writing-mode: vertical-rl;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: var(--wa-ink-2, #54656f);
}

.per-wa__rail-hint {
  margin-block-start: auto;
  font-size: 0.7rem;
  color: var(--wa-ink-3, #667781);
}

/* Mirrors `.wai-lead__header` without borrowing its class, which would drag in
   the inbox's own `.wai-*` behaviour rules. */
/* WHITE, NOT `--wa-header`. The dock stands in a column of white `.ld-card`
   panels, and WhatsApp's own grey chrome bar (#f0f2f5) made its top 55px read as
   a block sitting at a different height from the cards beside it - the two
   columns start at the same pixel, but only one of them announced itself with a
   filled bar. Painting it `--wa-panel` (the palette's own white, the one the
   inbox already uses for its list pane and lead panel) settles the row without
   taking the token away from the inbox, where the grey chrome is correct.

   The hairline STAYS: `.per-wa__body` scrolls underneath this header, and a
   white bar over white scrolling content with no separator loses its edge the
   moment a message reaches it. */
.per-wa__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: 0 0 auto;
  padding: 0.7rem 1rem;
  background: var(--wa-panel, #ffffff);
  border-block-end: 1px solid var(--wa-line, rgba(134, 150, 160, 0.15));
}

/* Solid ink at 600. No brand gradient at this weight, glyph included. */
.per-wa__title {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex: 1 1 auto;
  min-inline-size: 0;
  margin: 0;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--wa-ink, #111b21);
}

.per-wa__title i {
  color: var(--wa-green, #25d366);
}

.per-wa__collapse {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  min-inline-size: 32px;
  min-block-size: 32px;
}

/* The scrolling column. `min-block-size: 0` is the flex-item escape hatch that
   makes `overflow-y: auto` actually scroll instead of stretching the dock. */
.per-wa__body {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  flex: 1 1 auto;
  min-block-size: 0;
  padding: 0.75rem 1rem;
  overflow-y: auto;
}

/* Both sit ON their borrowed class. The only override either needs is losing
   its own bottom margin - the body's `gap` owns spacing here, and paying both
   leaves a double gutter that grows with every block. */
.per-wa__meta {
  margin-bottom: 0;
}

.per-wa__pills {
  margin-bottom: 0;
}

/* The honest truncation line, in the same words the sale page prints. An
   excerpt that says nothing about being an excerpt implies completeness, and a
   staff member reading the top of a 400-message thread as its beginning will
   draw the wrong conclusion about what was promised. Deliberately NOT reusing
   `.edt-wa-cap`: that class belongs to the sale page and the two must be free
   to diverge. */
.per-wa__cap {
  padding: 0.35rem 0.5rem;
  border-radius: 8px;
  background: var(--wa-warn-wash, rgba(255, 171, 0, 0.12));
  font-size: 0.72rem;
  color: var(--wa-warn, #8a5c00);
}

/* The ONE thing that must override the borrowed transcript. `.ld-wa-messages`
   caps itself at 320px, which is right inside a card on a long page and wrong
   inside a dock whose whole job is to be the tall thing. Legal because it is a
   `.per-*` class on the same element, and a later file at equal specificity -
   never a redefinition of `.ld-wa-messages` itself. */
.per-wa__transcript {
  flex: 1 1 auto;
  min-block-size: 140px;
  max-block-size: none;
}

/* ── The embedded live thread ───────────────────────────
   `<WaThread>` renders `.wai-thread` from admin-whatsapp.css: a four-row grid -
   header, lead strip, message list, composer - whose third row is
   `minmax(0, 1fr)` and whose box is `overflow: hidden`. That shape is the whole
   reason the composer stays pinned to the foot of the panel by LAYOUT instead of
   by `position: sticky`, which is what the inbox chose and what S19 forbids
   here. It only works if something hands the grid a definite height to divide.

   In the inbox it is a grid item of `.wai-grid` inside a viewport-locked shell,
   so it gets one for free. Here it is a FLEX item, and the default `flex: 0 1
   auto` sizes it to its CONTENT. Measured at 1440px on a 382px dock: a long
   conversation is fine either way - `flex-shrink: 1` is the default, so 3320px
   of transcript still clamps to the 572px available and the transcript scrolls
   inside itself. It is the SHORT conversation that comes out wrong. Two messages
   make a 340px thread inside a 718px dock, so the composer lands mid-panel with
   325px of blank white below it, and the transcript gets 200px of the 572 it
   could have had.

   That is the exact failure `.wai-thread`'s own header calls out as the reason
   its composer is a grid row rather than `position: sticky` - "on a short thread
   it floats in the middle of the pane" - reintroduced one layer up by the host
   box instead of by the composer. `flex: 1 1 auto` hands the grid the panel's
   height, so the composer is pinned to the foot of the dock at any thread
   length, and the transcript scrolls inside it exactly as `.ld-pay-scroll` does
   on the ledger. Verified at 390 / 900 / 1200 / 1440 with two messages and with
   sixty.

   Written as `.per-wa X` rather than against one named parent because BOTH
   candidate hosts are column flex containers: `.per-wa` itself, and
   `.per-wa__body` if the thread is mounted as a sibling of the pills and the
   "open the full conversation" link. The flex-item minimum size needs nothing
   from us - admin-whatsapp.css already carries `min-height: 0` and
   `min-width: 0` on the class, for the same reason every pane in that file does -
   so it is deliberately not restated.

   AND ON `.wai-thread` RATHER THAN ON `.per-wa__thread`, which is the class
   `PersonWaLiveThread.vue` also puts on this element. Contract 1's idiom would
   normally point the other way, and `.per-wa__transcript` next door is exactly
   that idiom. The exception is earned by the failure mode: a host that forgets
   the `.per-wa__thread` class is a host whose conversation is invisible on every
   phone (see the 767px block), with no error and nothing to grep for. Keying on
   the component's OWN root class cannot be forgotten, and it buys a (0,2,0)
   selector that beats admin-whatsapp.css on specificity rather than on link
   order. If you came here grepping for `.per-wa__thread`, this rule is it.

   And that is ALL the geometry the embedded thread needs, which was established
   by measurement rather than assumed: its bubbles cap at `min(78%, 620px)` and
   so shrink with a 320..520px dock instead of clipping in it; its composer
   already carries `min-width: 0` and wraps its own tab row (both were fixed for
   a 390px phone in July); its one fixed-px child, the audio player at 280px, is
   rescued by its own `max-width: 100%`; and both of its sheets are
   `position: fixed` over the whole VIEWPORT rather than over the dock, which is
   exactly what contract 5 above exists to keep true. This file adds skin to that
   component, never layout. */
.per-wa .wai-thread {
  flex: 1 1 auto;
}

.per-wa__no-messages {
  margin-block: auto;
  text-align: center;
}

/* Full width at the foot of the dock: it is the way OUT of a read-only excerpt
   and into the thread that can be replied to, so it should not look like one
   more inline link. */
.per-wa__open-thread {
  align-self: stretch;
  justify-content: center;
  gap: 0.4rem;
  flex: 0 0 auto;
}

/* Phase 4 seam, written now so that phase is markup-only. The shell renders
   this as a SIBLING of the dock inside `.per-panes` once the dock becomes a
   slide-over; until then no element carries the class and the rule costs
   nothing. Copied from `.wai-scrim`: 35% and not 60%, because over a light UI
   the dark skin's value reads as a blackout. */
.per-wa-scrim {
  display: none;
}

/* ══════════════════════════════════════════════════════════════════════════
   10. FACET EMPTY STATES
   ══════════════════════════════════════════════════════════════════════════

   Three states that must not look alike. `denied` is neutral (nothing is
   broken, this account simply may not see it), `absent` is the funnel's call to
   action (making it look like a failure is the specific mistake to avoid), and
   only `failed` gets warn ink - on the icon and the message, never as a full
   red panel.

   ONE naming scheme, and it is the double-underscore one. `PersonFacetEmpty.vue`
   emits `per-empty__icon` / `__message` / `__hint` / `__actions` / `__cta` /
   `__retry`, and `AdminSaleResolver.vue` reuses the same names. A parallel
   hyphen-form trio (`.per-empty-title`, `.per-empty-text`, `.per-empty-actions`)
   shipped alongside these and was never emitted by anything - two vocabularies
   for one component, one of them invisible. It was deleted on 2026-08-07. If a
   new element is needed here, extend the `__` family; do not start the other
   one again. */

/* `.empty-state` supplies the typography and a `padding: 3rem` shorthand sized
   for a whole page. Both axes are restated here because this block also has to
   live inside a 380px dock, where 3rem of inline padding leaves the hint about
   twenty characters of line. */
.per-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding-block: 1.5rem;
  padding-inline: 1rem;
  text-align: center;
}

.per-empty__icon {
  font-size: 1.5rem;
  opacity: 0.55;
  color: var(--text-tertiary, #8e8f9e);
}

/* Solid ink at 500 - the gradient never comes near this weight. */
.per-empty__message {
  margin: 0;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--text-color, #191a23);
}

.per-empty__hint {
  margin: 0;
  max-inline-size: 42ch;
  font-size: 0.78rem;
  color: var(--text-tertiary, #8e8f9e);
}

.per-empty__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.4rem;
  margin-block-start: 0.35rem;
}

.per-empty--denied .per-empty__icon {
  color: var(--text-tertiary, #8e8f9e);
}

.per-empty--absent .per-empty__icon {
  color: var(--adm-accent);
  opacity: 0.4;
}

.per-empty--failed .per-empty__icon {
  color: #b45309;
  opacity: 0.8;
}

.per-empty--failed .per-empty__message {
  color: #92400e;
}

/* ══════════════════════════════════════════════════════════════════════════
   11. THE COMMENTS PANE
   ══════════════════════════════════════════════════════════════════════════

   One person, one comments list: identical rows, identical order, everywhere,
   no matter which tab, sale or seat is selected. Nothing in this section may
   ever narrow it, and `.lal-*` is NOT mirrored here - LeadActivityLog is RTL by
   default and has three other hosts.

   ── WHAT THIS IS, AND WHERE ──
   Grid track 3 of `.per-panes` = the wrapper's inline-END = the PHYSICAL LEFT
   column, the side facing away from the admin sidebar. It is the MIRROR of the
   WhatsApp dock in section 9, edge for edge, and every rule below that names a
   side names the logical property and the physical edge together, because in
   this permanently-RTL island a reader who knows only one vocabulary gets the
   opposite side.

   Three shapes, and the component decides only whether it is open and which of
   the two floating ones it is in - everything else is a band below:

     > 1024px     a COLUMN beside the tab, 320px by default, 300..460 by drag
     768..1024    an OVERLAY over the content with a scrim, not a column
     <= 767px     FULL SCREEN, entered and left through `?cm=1`
     <= 480px TALL  force-STACKED under the tab, whatever the width (section 20)

   ── FIVE THINGS THAT ARE NOT NEGOTIABLE ──
   1. IT NEVER COLLAPSES TO NOTHING. `.per-cm--collapsed` is a hard 48px rail on
      all THREE inline sizes and may never carry `display: none` or a zero size.
      A pane that vanishes is a pane staff cannot find again, and the rail is
      also where the comment count lives.
   2. THE CARD IS HIDDEN, NOT UNMOUNTED. The component uses `v-show`, because
      three call sites hold live handles to it and `NOTE_DRAFT_SELECTOR` reads
      the composer out of the live DOM to warn about unsaved notes. Nothing here
      may `display: none` the pane in a way that unmounts anything.
   3. `.per-notes` IS THE CONTRACT, not `.per-cm`. Six specs and the person
      store's draft probe anchor on that exact class. `.per-cm` is the pane
      around it and may be renamed; `.per-notes` may not.
   4. BOTH GRID AXES, ALWAYS - see the wrapper's docblock. A `grid-column` with
      no `grid-row` drops this pane into row 2, under the tab, at full desktop
      width, and nothing here looks wrong.
   5. NO `container-type`, no `transform`, no `filter`. A container query is the
      "proper" way to size this pane's internals against the pane instead of the
      viewport, and it is exactly what contract 5 forbids: any of those makes
      this box the containing block for the fixed phone action bar. The answer
      is one narrow width that always works, not a query. */

/* ── The pane box ───────────────────────────────────────
   Sticky and NOT absolute, for the reason section 9 gives at length: this
   wrapper sits in a page that scrolls thousands of pixels, so `inset-block: 0`
   would park the pane's head at the top of the DOCUMENT - open, and invisible to
   anyone who has scrolled.

   It carries the surface (white, hairline, 14px radius) and the card inside it
   is flattened, rather than the other way round. That is what lets the 48px rail
   look like a panel instead of a bare button, and it keeps ONE clipping box, so
   the resize handle's inset focus ring has something to be inset against. */
.per-cm {
  position: sticky;
  inset-block-start: calc(var(--per-rowb) + var(--per-band) + var(--per-tabs) + 0.75rem);
  z-index: 30;
  /* Track 3 = inline-end = the PHYSICAL LEFT column here. */
  grid-row: 1;
  grid-column: 3;
  display: flex;
  flex-direction: column;
  /* `--per-cm-w` is published as an inline custom property by the drag handle,
     and this is the ONE rule in the file that reads it - the same trick the dock
     uses. An inline `inline-size` would beat every stylesheet rule short of
     `!important`, so the tablet and phone bands would have to shout to get their
     own size back; a custom property is inert until a rule consumes it.

     The bounds are the pane's, not the preference's - the store snaps the stored
     value to seven steps between these same two numbers (`CM_PANEL_WIDTHS`), so
     these two lines are the second half of one contract and must move together.
     They are NOT the dock's 320..520: a notes row is an author, a chip and a
     short body, and past 460px it is a wide band with a ragged edge. */
  inline-size: var(--per-cm-w, 320px);
  min-inline-size: 300px;
  max-inline-size: 460px;
  /* A definite block-size, so the grid AREA can stay stretched to the row (the
     travel room the sticky needs) while the box itself stays a viewport-tall
     column instead of a several-thousand-pixel one with nothing to scroll. */
  block-size: calc(100dvh - var(--per-rowb) - var(--per-band) - var(--per-tabs) - 5rem);
  min-block-size: 0;
  overflow: hidden;
  background: #fff;
  border: 1px solid #e8e9f0;
  border-radius: 14px;
}

/* 48px, never zero and never `display: none` - see rule 1 above. All three
   inline sizes, not just the one: the open pane's 300px floor would otherwise
   inflate the rail to 300px, which is the same bug as no rail at all read from
   the other end. */
.per-cm--collapsed {
  inline-size: 48px;
  min-inline-size: 48px;
  max-inline-size: 48px;
}

/* ── The drag handle ────────────────────────────────────
   The MIRROR of `.per-wa__resize`. This pane is grid track 3 - the wrapper's
   inline-END, the PHYSICAL LEFT column - so the edge pinned against the wrapper
   is its physical LEFT one and the edge that MOVES is its physical RIGHT one,
   the one facing the tab content. That is `inset-inline-START: 0` here, exactly
   opposite the dock's `inset-inline-end: 0`.

   `PersonCommentsPane.vue` captures the pane's `left` and computes
   `clientX - left`, and both arrow handlers are flipped to match, so all four
   agree about which edge is under the cursor. They flip together or the handle
   resizes backwards. `touch-action: none` or a pointer drag scrolls the page
   instead of resizing.

   The ring is drawn INSET, because `.per-cm` clips (`overflow: hidden`) and an
   outward outline on a box flush with the pane edge would be cut in half. */
.per-cm__resize {
  position: absolute;
  inset-inline-start: 0;
  inset-block: 0;
  z-index: 1;
  inline-size: 6px;
  cursor: col-resize;
  touch-action: none;
}

/* The visible line, as a child rather than a border, so the 6px hit area stays
   6px while the mark stays 2px. 2px of offset, a 2px mark and a 6px strip: the
   mark is exactly CENTRED, so it needs no mirror. */
.per-cm__resize::after {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 2px;
  inline-size: 2px;
  background: transparent;
}

.per-cm__resize:hover::after,
.per-cm__resize:focus-visible::after {
  background: var(--adm-accent);
}

.per-cm__resize:focus-visible {
  outline: 2px solid var(--adm-accent);
  outline-offset: -2px;
}

/* ── The rail ───────────────────────────────────────────
   The whole 48px column is one target, because a 48px-wide pane has no room for
   a second thing to hit by mistake. */
.per-cm__rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  inline-size: 100%;
  block-size: 100%;
  padding-block: 0.75rem;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.per-cm__rail:hover {
  background: var(--fsp-hover-bg, rgba(0, 113, 227, 0.06));
}

.per-cm__rail:focus-visible {
  outline: 2px solid var(--adm-accent);
  outline-offset: -3px;
}

.per-cm__rail-glyph {
  display: inline-flex;
  flex: 0 0 auto;
  font-size: 1.05rem;
  color: var(--text-secondary, #5a5b6a);
}

/* A NUMBER here, not a dot - and that is the one place this rail deliberately
   parts company with the dock's. The dock shows a dot because the payload
   carries no per-conversation unread count and inventing one would need a
   WhatsApp call it is forbidden to make. This count is a real server-side total
   (`activities.total`), it is the number the fourth tab's badge printed until
   2026-08-10, and it is the reason to open the pane. Losing it with the tab
   would have been the actual regression. */
.per-cm__rail-count {
  flex: 0 0 auto;
  min-inline-size: 1.4rem;
  padding: 0.05rem 0.3rem;
  border-radius: 999px;
  background: var(--primary-light, rgba(0, 113, 227, 0.06));
  font-size: 0.68rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: center;
  color: var(--primary-color, #0071e3);
}

/* `writing-mode`, NOT `transform: rotate`. A transform on an ancestor of a fixed
   surface creates a containing block for it, and the phone action bar is fixed -
   see contract 5. Rotation would also have to have its sign reasoned out;
   writing-mode simply follows the island. */
.per-cm__rail-label {
  writing-mode: vertical-rl;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: var(--text-secondary, #5a5b6a);
}

.per-cm__rail-hint {
  margin-block-start: auto;
  font-size: 0.7rem;
  color: var(--text-tertiary, #8e8f9e);
}

/* ── The card inside the pane ───────────────────────────
   `.per-notes` still renders the borrowed `.ld-card` kit, and that kit is a set
   of BARE single-class rules in `admin-lead-detail.css` which this file may
   never redefine. So the flattening is scoped under `.per-cm`: the pane owns the
   surface now, and a second border, radius and shadow 1px inside the first is
   the "box in a box" every card-in-a-pane layout draws by accident.

   The padding goes with it. 1.25rem each side out of a 320px pane is 40px of the
   278px a note actually gets; the head and the body pay their own, tighter. */
.per-cm .per-notes {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-block-size: 0;
  padding: 0;
  background: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
}

/* NO TOP MARGIN, and the 1.25rem that used to be here is the reason to say so
   out loud. It was correct while this was a FOOTER hanging BELOW the tabpanel:
   the margin was the gap between the tab's last card and this one. It is a
   COLUMN now, in the same grid row as the tab body and the dock, and a top
   margin here would start it 20px lower than both - measured at 1600 and at
   1280 while it was briefly a tab. Do not put one back without checking that
   the three columns still start on the same pixel. */
.per-notes {
  margin-block-start: 0;
}

/* The head is the pane's title bar: it does not scroll, and it carries the
   hairline the body scrolls under. White over white with no separator loses its
   edge the moment a row reaches it - the same reasoning as `.per-wa__head`. */
.per-cm .ld-card-head {
  flex: 0 0 auto;
  padding: 0.7rem 0.85rem;
  border-block-end: 1px solid #e8e9f0;
}

.per-cm .ld-card-title {
  font-size: 0.9rem;
}

.per-cm__collapse {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  min-inline-size: 32px;
  min-block-size: 32px;
}

/* The sentence that stops a reader assuming the list follows the tab.
   It sat INSIDE the head, beside the count, while this was a full-width card. In
   a 300px column it cannot: `.ld-card-head` is a nowrap flex row and this text
   is `flex: 0 0 auto`, so head and sentence fought over 278px and the sentence
   won by overflowing. It is its own block under the head now, which also reads
   better - it is a caption for the list, not a decoration on the title. */
.per-notes__scope-note {
  flex: 0 0 auto;
  display: block;
  padding: 0.45rem 0.85rem 0;
  font-size: 0.72rem;
  font-weight: 400;
  line-height: 1.35;
  color: var(--text-tertiary, #8e8f9e);
}

/* THE PANE'S OWN SCROLLER, and the reason the whole thing works. Until
   2026-08-10 this hook was deliberately empty, with a comment saying a capped
   desktop height would land here rather than on `.ld-card-body` (which two other
   pages read). This is that day.

   `min-block-size: 0` is the flex-item escape hatch that makes `overflow-y`
   actually scroll instead of stretching the pane; `overscroll-behavior-y` stops
   a flick at the end of the timeline scrolling the whole admin shell behind it.
   The top margin `.ld-card-body` carries is cancelled here rather than there. */
.per-notes__body {
  flex: 1 1 auto;
  min-inline-size: 0;
  min-block-size: 0;
  margin-block-start: 0;
  padding: 0.6rem 0.85rem 0.75rem;
  overflow-y: auto;
  overscroll-behavior-y: contain;
}

/* ── The composer, in a column ──────────────────────────
   TEN TEMPLATE PILLS IN 278px WRAP TO SIX ROWS, and the composer is then taller
   than the visible pane before a single note is on screen. The owner's call
   (2026-08-10) is ONE ROW THAT SCROLLS SIDEWAYS rather than a disclosure: the
   first three are the ones staff reach for, and a row that is visibly cut off
   invites the swipe, where a collapsed `תבניות` button has to be learned.

   Scoped under `.per-cm`, and that is not optional: `.lct-*` is shared with the
   WhatsApp inbox's lead panel and with the open-day registration row, both of
   which have the width to wrap and want to. A bare `.lct-row` here would win on
   five other screens, because this file loads last in the admin block. */
.per-cm .lct-row {
  flex-wrap: nowrap;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
}

.per-cm .lct-pill,
.per-cm .lct-label {
  flex: 0 0 auto;
}

/* A long cohort name in the sale chip cannot wrap (`white-space: nowrap` in the
   log's own stylesheet, correct for its three wide hosts) and would otherwise
   push the row's timestamp out of a 234px content box. */
.per-cm .lal-scope-chip {
  max-inline-size: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Off at base, exactly like the dock's. It is painted only in the tablet band
   (section 18) and turned off again in landscape (section 20), where nothing is
   covered. Only one flank can be floating at a time, so only one scrim is ever
   rendered - `usePersonSidePanels` is what makes that true. */
.per-cm-scrim {
  display: none;
}

/* ══════════════════════════════════════════════════════════════════════════
   12. TAB BODIES - SHARED
   ══════════════════════════════════════════════════════════════════════════ */

/* Single column, vertical rhythm between the cards, and no horizontal scroller
   of its own - every wide thing inside carries one.

   The two tab panels are matched by their MODIFIER only. Their roots also carry
   the bare `.per-tab`, which the strip's buttons carry too (see the strip
   section): the modifier is the only half of that pair that means one thing. */
.per-tab--open-day,
.per-tab--enrollment,
.per-lead-tab {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-inline-size: 0;
}

/* A permission limit, stated rather than hidden. Icon column plus one sentence,
   start-aligned, because it is prose and not a status. */
.per-notice {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.6rem 0.8rem;
  border: 1px solid #e3e8f2;
  border-radius: 10px;
  background: #f6f8fc;
  font-size: 0.82rem;
  line-height: 1.55;
  color: #5a5b6a;
  text-align: start;
}

.per-notice > i {
  flex: 0 0 auto;
  margin-block-start: 0.2rem;
  color: #8e8f9e;
}

/* The S33 variant: this viewer CAN read the person, they just cannot see the
   campaign detail. Quieter, because it qualifies data that is on screen rather
   than explaining an absence. */
.per-notice--muted {
  border-color: transparent;
  background: #f7f8fb;
  color: #8e8f9e;
}

/* The WARN variant, and the only place on this page that uses it: the D11
   tombstone banner. It is deliberately louder than `--muted` and quieter than a
   toast - the record is readable and nothing is going wrong, but every write on
   this person is going to be refused, and a reader who does not notice that
   types a note and loses it. Palette matched to `admin-enrollment-detail.css`'s
   own `.edt-notice--warn` so the two amber notices on one screen agree. */
.per-notice--warn {
  border-color: #f0d089;
  background: #fff8e1;
  color: #8a6100;
}

.per-notice--warn > i {
  color: #b8860b;
}

/* ── The D11 tombstone banner ──────────────────────────────────────────────

   Full width, above the tab strip, below the header. It is a fact about the
   whole PERSON rather than about one tab, and a staff member arriving from the
   open-day roster has to meet it before they read anything else - three live
   seats hang off deleted people today, so this is a real arrival and not a
   corner case.

   `margin-block` and nothing else: it sits in the page's own column flow, and
   giving it a box of its own would make it look like a card the reader could
   act on. There is no restore endpoint anywhere in the repo (S63), so there is
   no button here and no space reserved for one. */
.per-tombstone {
  margin-block: 0.25rem 0.75rem;
}

/* One muted explanatory line under a toggle or a list. */
.per-note {
  margin: 0.6rem 0 0;
  font-size: 0.78rem;
  line-height: 1.55;
  color: #8e8f9e;
}

/* ══════════════════════════════════════════════════════════════════════════
   13. LEAD TAB
   ══════════════════════════════════════════════════════════════════════════ */

/* The label, the two inputs and the derived-hour line, stacked. The inputs
   themselves keep the borrowed `.ld-followup*` classes, which is not decoration:
   admin-lead-detail.css gives them `font-size: 16px` below 1025px, UNSCOPED,
   and that is what stops iOS Safari zooming the whole page in on focus and
   never zooming back out. */
.per-followup {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

/* Rendered in the card HEAD, opposite the title, only when the saved follow-up
   is already in the past. The head is a two-end flex, so it needs no placement
   of its own. */
.per-followup-flag {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  flex: 0 0 auto;
  padding: 0.12rem 0.55rem;
  border: 1px solid #fde68a;
  border-radius: 999px;
  background: #fffbeb;
  font-size: 0.72rem;
  font-weight: 600;
  color: #92400e;
  white-space: nowrap;
}

/* ── Marketing card ─────────────────────────────────── */

/* The PROVENANCE MARK, ported from the sale page's `.edt-prov`. It answers "is
   this value frozen on another row, or is it the live record that the next
   visit overwrites", and it keeps all FOUR of that chip's discriminators,
   because staff read these screens in greyscale and the two states sit one line
   apart:
     1. fill - the frozen record is filled, the live one is not
     2. edge - the frozen record is solid, the live one dashed
     3. dot  - solid for the record, a hollow ring for the live one
     4. ink  - the frozen record is the darker of the two
   Drop the colour and three signals still answer the question.

   A rounded RECTANGLE, never a 999px pill: on these pages a pill means STATE,
   and a provenance mark is an annotation about a fact, never the fact. */
.per-prov {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  flex-shrink: 0;
  margin-inline-start: 0.4rem;
  padding: 0.05rem 0.4rem;
  border: 1px solid transparent;
  border-radius: 5px;
  font-size: 0.7rem;
  font-weight: 500;
  line-height: 1.5;
  white-space: nowrap;
  /* `.ld-mk-subtitle` is uppercase, tracked and 700, and this chip sits inside
     it. Left inherited, the annotation would shout louder than its heading. */
  letter-spacing: normal;
  text-transform: none;
  /* An inline-flex box takes its baseline from its first flex item, which here
     is an empty dot with no baseline of its own - the synthesized one would
     hang the chip off that dot's bottom edge. */
  vertical-align: middle;
}

.per-prov::before {
  content: '';
  flex-shrink: 0;
  inline-size: 6px;
  block-size: 6px;
  border-radius: 999px;
}

/* Both modifiers are applied through a DYNAMIC binding
   (`per-prov--${source}`, source being 'record' | 'current'), so a literal grep
   for either class name finds nothing in the templates. They are live. */

/* "נשמר על ההרשמה" - frozen on another row and unable to change. */
.per-prov--record {
  background: #edf0f7;
  border-color: #d6dcea;
  color: var(--adm-ink-2);
}

.per-prov--record::before {
  background: currentColor;
}

/* "מרשומת ה-CRM היום" - the live record, overwritten on the next visit. */
.per-prov--current {
  background: transparent;
  border-color: #c9cdda;
  border-style: dashed;
  color: #8e8f9e;
}

.per-prov--current::before {
  border: 1px solid currentColor;
}

/* In the card head the chip annotates `.ld-ch-chip`, and `.ld-card-head`
   already supplies a 0.5rem flex gap. Its own start margin on top of that reads
   as unattached to the verdict it qualifies, which is the one thing it must
   never do. */
.per-prov--head {
  margin-inline-start: 0;
}

/* The two records DISAGREE about the partner code, and this is what the other
   one says. The 2px dashed edge is the same language `.per-prov--current` uses,
   so it reads as "today's record" before a word of it is read.

   Deliberately NOT an error colour. Anyone who returns through a second
   campaign drifts as a matter of course, and amber on every such row teaches
   staff to stop seeing it. */
.per-drift {
  display: block;
  margin-block-start: 0.3rem;
  padding-inline-start: 0.5rem;
  border-inline-start: 2px dashed #c9cdda;
  font-size: 0.76rem;
  line-height: 1.45;
  color: #8e8f9e;
  overflow-wrap: anywhere;
}

/* The differing value itself, lifted out of the preamble. Weight 500 and solid
   ink - no gradient anywhere near it. */
.per-drift bdi {
  color: var(--adm-ink-2);
  font-weight: 500;
}

/* Said once, at the top of the marketing body, and only when there is another
   record for it to be contrasted against: this card is the PERSON's live
   record, and every seat and every sale keeps a frozen one of its own. */
.per-mk-notice {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  margin: 0 0 0.8rem;
  padding: 0.5rem 0.7rem;
  border: 1px solid #e3e8f2;
  border-radius: 10px;
  background: #f6f8fc;
  font-size: 0.78rem;
  line-height: 1.6;
  color: #5a5b6a;
}

.per-mk-notice > i {
  flex: 0 0 auto;
  margin-block-start: 0.15rem;
  color: #8e8f9e;
}

/* ── Contact messages card ──────────────────────────── */

/* `.ld-msg-head` is `justify-content: space-between` over exactly TWO children.
   The reason label and the course chip share this wrapper rather than becoming
   a third flex item, which would push the date into the middle of the row. */
.per-msg-tags {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  min-inline-size: 0;
}

/* ── WS-D, 2026-08-09: BELOW THE COMMENTS FOOTER (owner items 11 and 6) ─────
   Two Lead-tab panels now render UNDER the shared comments footer: the
   marketing/attribution card and the open-day questionnaire answers. Both are
   reference material read once when a question is asked, while the footer is
   what a reader scrolls to the bottom for.

   `PersonMainColumn` hands the tab a bare `<slot name="below">` with NO wrapper
   of its own, so the two tabs that contribute nothing render nothing and
   reserve nothing. This class is the wrapper the LEAD TAB brings with it, which
   is why the spacing lives here and not on the slot: the container only exists
   when there is something in it.

   `margin-block-start` matches `.per-notes`' own 1.25rem, so the gap above these
   panels equals the gap above the footer and the column keeps one rhythm. */
.per-below {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-block-start: 1.25rem;
  min-inline-size: 0;
}

/* The questionnaire panel's body stacks one `PersonOpenDayAnswers` block per
   occasion (the website request, then every seat that carries answers). Each
   block is its own disclosure with its own count, so they need separating from
   each other and nothing else. */
.per-answers-card .ld-card-body {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* ── WS-D, 2026-08-09: THE LEAD-SOURCE CHIP (owner item 11, second half) ────
   "Leave primary most important data in the profile panel (lead source)." The
   full attribution moved down; this one line stays high on the page and carries
   the server's classified channel verdict.

   `.per-leadsource` AND `.per-leadsource__label` USED TO BE HERE - a two-line
   strip that held the chip under the Lead tab's profile card while the general
   detail panel was still being built. The panel exists now and the chip is one
   of its fields, labelled by the panel's own `.form-label` like every other
   field in it, so the placeholder strip and its label are gone. The chip's own
   rules below are unchanged, which is exactly what the component was made
   standalone for. */

/* The chip itself borrows `.ld-ch-chip` whole - the same shape and the same five
   tier swatches the marketing card's head chip wears, so one person's arrival is
   one colour wherever it is stated. These rules only turn that span into a
   button and say it goes somewhere.

   `max-inline-size` overrides the borrowed `60%`, which is sized for a chip
   sharing a card head with a title; here the chip is the row.

   NO GRADIENT ANYWHERE NEAR IT: `.ld-ch-chip` is `font-weight: 700`, and the
   brand gradient never touches weight >= 500. Solid ink only. */
.per-src-chip {
  max-inline-size: 100%;
  cursor: pointer;
  font-family: inherit;
  text-align: start;
  /* A partner GROUP name is staff-entered free text, so this label can be a
     single unbroken token. Anything that escapes `.admin-main` lands in `#app`,
     which is `overflow-x: clip` - invisible AND unscrollable - so it would eat
     content while passing a "no horizontal scroll" check. The chip grows a
     second line instead. */
  overflow-wrap: anywhere;
}

/* A RING AND NOT A `filter`. The chip wears one of five tier backgrounds, so a
   tier-agnostic hover has to sit outside the fill - and `filter` is one of the
   seven properties this file may never carry: it would make its element the
   containing block for every `position: fixed` descendant, which on this page
   means the phone's docked action bar and `AdminSheet`'s layer. Two specs pin
   that at zero (`adminPersonRtlMirrors`, `personPhase4Gate`). Cool-blue shadow
   ink, never raw black. */
.per-src-chip:hover {
  box-shadow: 0 0 0 3px rgba(0, 60, 130, 0.08);
}

.per-src-chip:focus-visible {
  outline: 2px solid var(--adm-accent);
  outline-offset: 2px;
}

/* Points DOWN the page, at the panel it opens. Quieter than the label it
   follows - it is a hint about what the press does, not a second statement. */
.per-src-chip__go {
  font-size: 0.62rem;
  opacity: 0.6;
}

/* The lead type (סוג פנייה) - the arrival-route pill, in the detail band's own
   field since 2026-08-11. It rode beside the breadcrumb in header row A until
   the owner moved it here, where the rest of the record lives; the per-source
   tints match the leads list's own badge for the same value. A static field:
   the source is set by the form that created the lead and is never edited. */
.per-source {
  flex: 0 0 auto;
  padding: 0.2rem 0.65rem;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.per-source--open_day {
  background: #f3eeff;
  color: #6d3fc4;
}

.per-source--contact_form {
  background: #e8f1fd;
  color: #0b62c4;
}

.per-source--whatsapp {
  background: #e5f8ec;
  color: #1d8a4e;
}

.per-source--manual {
  background: #f0f1f5;
  color: #5a5b6a;
}

.per-source--enrollment_page {
  background: #fdf3e7;
  color: #a35a09;
}

/* ══════════════════════════════════════════════════════════════════════════
   13b. THE DETAIL BAND - THE HEADER'S SECOND TIER
        (WS-E 2026-08-09 as a panel; reshaped into the header 2026-08-10)
   ══════════════════════════════════════════════════════════════════════════

   The band between the sticky strip and the tab strip. It is rendered by the
   SHELL, so it is the same band under all three tabs.

   ── WHAT IT WAS, AND WHY IT IS NOT THAT ANY MORE ──────────────────────────

   A `.ld-card` holding an eleven-cell `auto-fit` grid of permanently-open form
   controls: about 300px of chrome before the tab content started, on every tab,
   for every person, forever. Three things were wrong with it and the owner named
   all three - it read as a data-entry form rather than as a record, it spent a
   third of a laptop screen on fields most readers never touch, and none of it
   was reachable once you had scrolled past it.

   What replaced it: seven fields instead of eleven, each of them TEXT with a
   pencil rather than an input (`.per-fld`), laid out as one wrapping row of
   compact label-over-value cells - and the whole thing folds away on scroll.

   ── THE FOLD IS A GRID ROW, NOT A HEIGHT AND NOT A TRANSFORM ──────────────

   `grid-template-rows: 1fr -> 0fr` is the one technique here that animates, ends
   at a genuine zero, and needs no measured pixel value in the stylesheet.

     - `block-size: auto -> 0` does not transition at all; every value that does
       transition is a number this file would have to guess, and the band is one
       line at 1440px and two at 1024px.
     - `max-block-size` transitions but never reaches a true zero without a magic
       number, and it eases wrongly - the visible motion is over long before the
       declared duration, because the box is smaller than the cap for most of it.
     - `transform: scaleY()` is FORBIDDEN here (contract 5, top of file): a
       transform makes this box the containing block for every `position: fixed`
       DESCENDANT. Nothing in the band is fixed today - its controls are native
       inputs and a native `<select>`, not `AdminSheet` - so this is a rule about
       what may be added rather than a bug that exists, and it is exactly the
       kind of rule that is discovered the hard way: the phone's action bar and
       every bottom sheet on this page are fixed, and this band sits between two
       boxes that already carry the same ban. `filter`, `backdrop-filter`,
       `contain` and `container-type` are out for the same reason, which is also
       why the sticky fill below is a flat opaque colour rather than a blur.

   The `visibility` pair on `.per-gen__inner` is what takes a folded band out of
   the tab order and out of the accessibility tree. It is delayed by exactly the
   fold duration on the way out (so the content does not vanish before it has
   finished moving) and instant on the way in.

   ── AND IT IS STICKY, WHICH IS THE ARITHMETIC TO BE CAREFUL WITH ──────────

   There used to be two sticky boxes on this page and the second one's offset was
   the first one's height. There are three now and the middle one changes size,
   so the tab strip and both docked panes carry a `var(--per-band)` term - see
   the token's own comment in section 1, including the one number that may never
   be derived from it. */

/* The 1px flow element the fold is decided by. It has to be a separate element
   and it has to be OUTSIDE the band: the band is sticky, so once it pins, its
   rect stops moving and an IntersectionObserver watching it would fire once and
   never again - the band would fold and never come back. The negative margin
   cancels its own height, so it moves nothing on the page. */
.per-gen-sentinel {
  block-size: 1px;
  margin-block-end: -1px;
}

.per-gen {
  position: sticky;
  /* Directly under row B. The tab strip is then at row B PLUS this band. */
  inset-block-start: var(--per-rowb);
  /* One rung under row B so the band slides beneath it, and EQUAL to the tab
     strip - which is later in DOM order and therefore paints on top, so the
     strip slides over the band as the band folds. Nothing else on this page
     reads 11 or 12. */
  z-index: 11;
  display: grid;
  grid-template-rows: 1fr;
  /* Near-opaque rather than a blur: see the contract note above. Same fill as
     the two rows it sits between, so the stuck header reads as one object. */
  background: #fff;
  border-block-end: 1px solid var(--adm-line);
  transition: grid-template-rows 170ms ease;
}

.per-gen--collapsed {
  grid-template-rows: 0fr;
  /* The border goes WIDTH AND ALL, not merely transparent. A transparent 1px
     border is still a 1px box, so the ResizeObserver published `--per-band: 1px`
     for a folded band and the tab strip parked one pixel low - invisible, and
     wrong in a number three other rules are arithmetic on. Measured 2026-08-10;
     a folded band is 0px now. */
  border-block-end-width: 0;
}

.per-gen__inner {
  /* `min-block-size: 0` is what lets a grid item actually reach the 0fr track;
     without it the item's automatic minimum size holds the row open. */
  min-block-size: 0;
  overflow: hidden;
  /* THE CONTENT APPEARS WHEN THE BOX HAS FINISHED OPENING, and disappears the
     instant it starts closing. Both delays are on the visibility of the SUBTREE
     and both exist for the same reason: `overflow: hidden` on a row that is
     mid-animation clips most of what is inside it, so any window in which this
     subtree is `visible` while the row is part-height is a window in which the
     band's controls are tabbable and `.focus()`-able while drawing almost
     nothing - invariant A with a timer on it.

     The first build had that window on the way out, the fix for it moved the
     window to the way in, and this is the shape with no window in either
     direction. */
  visibility: visible;
  transition: visibility 0s linear 170ms;
}

.per-gen--collapsed .per-gen__inner {
  /* Instant on the way out - see the base rule for the pair. */
  visibility: hidden;
  transition: visibility 0s;
}

/* One wrapping row of cells. `wrap` and not a grid with a column count: the
   cells are seven very different widths (a two-word owner name, a 24-character
   email, a date with an elapsed label beside it), and a column grid sized for
   the widest of them wastes the width of the narrowest six. This file may
   declare exactly four media blocks, so the layout has to answer every width on
   its own - `flex-wrap` does, a column count does not. */
.per-gen__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  /* The column gap is what decides how many of the eight cells share a line. At
     1.6rem the last was orphaned onto a line of its own at 1440 with ~1100px
     empty beside it, which spends 40px of sticky header on one date. */
  gap: 0.45rem 1.15rem;
  padding-block: 0.5rem 0.55rem;
  min-inline-size: 0;
}

/* ── ONE FIELD ────────────────────────────────────────────────────────────
   Label over value, the same shape as the money cell in the strip above
   (`.per-hl`), because they are the same kind of thing one tier apart. */

.per-fld {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.05rem;
  /* Shrinkable but never a grower: a cell that grew would push the six beside
     it onto a second line for no reason. */
  flex: 0 1 auto;
  min-inline-size: 0;
  line-height: 1.3;
}

/* ── THE CELLS THAT HOLD A NUMBER MAY NOT SHRINK ──────────────────────────

   INVARIANT C, one tier down. Row B's rule is that a number is drawn whole or
   not at all, because the row is RTL and every figure in it is an LTR `<bdi>` -
   so a clip eats the figure's LEADING digits and leaves a shorter,
   complete-looking number behind. The same is true in this band, and it was
   demonstrated here rather than reasoned about: a `max-inline-size: 24ch` added
   to every value to stop long emails hogging a line rendered the follow-up as
   `בעוד 4 ימים · ...8/2026 12:30` and the last-contact stamp as
   `לפני 2 ימים · ...08/2026 15:15` - dates missing their first digits, with an
   ellipsis that a reader scanning a header does not stop to parse.

   So the cap is gone, and the cells whose values are DATES are the ones that
   never give up width. What gives instead is the free text beside them - the
   email and the name ellipsise, which is invariant B's allowed behaviour,
   because a shortened email is visibly shortened and a shortened date is not. */
.per-fld--static,
.per-fld--numeric {
  flex: 0 0 auto;
}

/* An open editor does NOT claim space, and that is a correction. It was
   `flex: 1 1 auto`, which let the edited cell take a third of the row: every
   other cell re-wrapped around it and the band grew from 55.5px to 106.4px the
   instant a pencil was pressed - measured - which shoved the tab strip and both
   docked panes down the page at the same moment, because all three are
   arithmetic on `--per-band`. A field being edited is not a reason to move
   everything else on the screen. */
.per-fld--editing {
  flex: 0 1 auto;
}

.per-fld__label {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  /* These eight labels are the only thing naming eight otherwise bare values, in
     the most-read strip on the page, so they are sized and coloured to be READ.
     They were 0.62rem of #8e8f9e, which computes to 9.92px at 3.19:1 - under the
     4.5:1 AA floor for text this size, and set in uppercase with added tracking
     on top of that. */
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  white-space: nowrap;
  color: #5a5b6a;
}

.per-fld__read {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  min-inline-size: 0;
  /* A floor rather than a height: it keeps the seven cells' baselines level
     whether or not a given cell has a pencil, and stops the band jumping by two
     pixels when a value is cleared. */
  min-block-size: 24px;
}

.per-fld__val {
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.85rem;
  color: var(--adm-ink);
}

.per-fld__strong {
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}

/* "There is none" is a FACT about the person, said in words, and never a hyphen
   or an empty cell - both of those read as "we failed to load this".

   It is quieter than a real value and still READABLE: #8e8f9e on the band's
   white fill is 3.19:1, under the 4.5:1 AA floor for text this size, and this is
   the only thing on screen saying a person has no owner or no follow-up. */
.per-fld__empty {
  color: #6b6c7b;
}

.per-fld__link {
  color: var(--adm-accent);
  text-decoration: none;
}

.per-fld__link:hover {
  text-decoration: underline;
}

/* An overdue follow-up, in the value as well as in the flag on the label. Solid
   ink at 600 - the gradient never touches weight 500 or above (contract 6). */
.per-fld__val--overdue {
  color: #b45309;
}

/* ── THE PENCIL ───────────────────────────────────────────────────────────
   It is FAINT AT REST AND NEVER INVISIBLE, and that is a correction rather than
   a preference. The obvious build is `opacity: 0` until `:hover`, and it fails
   twice over: a control nobody can see is a control nobody discovers, and
   `opacity: 0` does not remove an element from hit-testing or from the tab
   order, so what it actually produces is a button that can be clicked and
   focused while drawing nothing - the exact state invariant A forbids one tier
   up. At 0.45 it is legible enough to find and quiet enough to ignore. */
.per-fld__pen {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 22px;
  block-size: 22px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 6px;
  background: none;
  color: #7c7d8c;
  font-size: 0.68rem;
  cursor: pointer;
  transition:
    color 120ms ease,
    background-color 120ms ease;
}

/* IT IS QUIET, NOT FAINT, and the difference is measured. It rested at
   `opacity: 0.45` over `#8e8f9e`, which composites to #CCCDD3 - 1.59:1 against
   the band, half the 3:1 that WCAG 1.4.11 asks of a control's graphic. The only
   rules that lifted it were `:hover` and `:focus-within`, and a touch screen has
   neither: on a phone the one signal that a field is editable at all sat
   permanently at 1.59:1. Quietness is the COLOUR's job now, at 4.0:1, so it is
   the same in every input mode. */
.per-fld:hover .per-fld__pen,
.per-fld:focus-within .per-fld__pen {
  color: var(--adm-accent);
}

.per-fld__pen:hover {
  border-color: rgba(0, 113, 227, 0.2);
  background: rgba(0, 113, 227, 0.08);
  color: var(--adm-accent);
}

.per-fld__pen:focus-visible {
  outline: 2px solid var(--adm-accent);
  outline-offset: 1px;
}

/* ── THE EDITOR ───────────────────────────────────────────────────────────
   Wraps, because the follow-up's editor is a date, a time, a clear button and a
   sentence about the hour it is going to assume. */
.per-fld__edit {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.3rem;
  min-inline-size: 0;
}

/* Sized to sit as close as possible to the width of the TEXT it replaces. The
   band wraps, so every pixel an editor is wider than its own read view is a
   pixel that can push a neighbour onto a second line - and because `--per-band`
   is arithmetic under the tab strip and both docked panes, that pushes the whole
   page down at the moment somebody starts typing. It cannot be eliminated (an
   input plus a save and a cancel is simply wider than a phone number), but 16ch
   guaranteed it and 13ch does not. */
.per-fld__input {
  inline-size: 13ch;
  min-inline-size: 0;
  min-block-size: 30px;
  padding-block: 0.2rem;
  font-size: 0.85rem;
}

/* The name cell holds TWO of these side by side, so each takes a little over
   half of what a single-value editor gets. It is declared AFTER the rule it
   narrows on purpose: the two selectors have identical specificity, so source
   order is the only thing that decides, and written first this modifier did
   nothing at all. */
.per-fld__input--name {
  inline-size: 9ch;
}

/* The derived-hour sentence takes its own line rather than sitting beside the
   inputs, so it cannot be mistaken for a third field. */
.per-fld__edit .ld-followup-derived {
  flex: 1 0 100%;
}

.per-fld__btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 26px;
  block-size: 26px;
  padding: 0;
  border: 1px solid var(--adm-line);
  border-radius: 8px;
  background: #fff;
  color: #5a5b6a;
  font-size: 0.75rem;
  cursor: pointer;
}

.per-fld__btn:hover {
  border-color: #c9cad4;
  color: var(--adm-ink);
}

.per-fld__btn:focus-visible {
  outline: 2px solid var(--adm-accent);
  outline-offset: 1px;
}

.per-fld__btn:disabled {
  opacity: 0.45;
  cursor: progress;
}

.per-fld__btn--save {
  border-color: rgba(29, 138, 78, 0.35);
  background: rgba(29, 138, 78, 0.08);
  color: #1d8a4e;
}

.per-fld__btn--save:hover {
  border-color: rgba(29, 138, 78, 0.55);
  color: #14663a;
}

/* ── The two controls the band borrows from elsewhere ──────────────────── */

/* `dir="ltr"` on the element is not enough on its own: `.form-input` declares
   `direction: rtl` as an AUTHOR rule, and an author rule beats the `dir`
   attribute, which is only a presentational hint. */
.per-gen__input--ltr {
  direction: ltr;
  text-align: start;
}

.per-gen__owner-select {
  flex: 1 1 auto;
  min-inline-size: 0;
  max-inline-size: 22ch;
  min-block-size: 30px;
  padding-block: 0.2rem;
  font-size: 0.85rem;
}

/* `אליי` stays in the READ view, beside the owner's name, because it is not the
   same action as choosing from a list of eleven: it is the commonest assignment
   there is, it is self-evidently reversible, and D16 existed to make it one
   press. Only the list of OTHER people went behind the pencil. */
.per-gen__take {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  min-block-size: 22px;
  padding-inline: 0.4rem;
  border: 1px solid var(--adm-line);
  border-radius: 999px;
  background: #fff;
  color: #5a5b6a;
  font: inherit;
  font-size: 0.68rem;
  white-space: nowrap;
  cursor: pointer;
}

.per-gen__take:hover {
  border-color: rgba(0, 113, 227, 0.35);
  background: rgba(0, 113, 227, 0.06);
  color: var(--adm-accent);
}

.per-gen__take:focus-visible {
  outline: 2px solid var(--adm-accent);
  outline-offset: 1px;
}

.per-gen__take:disabled {
  opacity: 0.45;
  cursor: progress;
}

.per-gen__take:disabled:hover {
  border-color: var(--adm-line);
  background: #fff;
  color: #5a5b6a;
}

/* The overdue flag rides in the follow-up's LABEL rather than beside the value,
   so it is in the same place whether or not there is a value to carry it, and
   so it survives the value being replaced by the editor. Its colours come from
   `.per-followup-flag` in section 13, unchanged - one word for "you owe this
   person something", one tint, wherever it appears. */

/* ── The wanted-course pill, in the questionnaire panel (owner item 3.6) ───
   It moved out of the deleted profile card and down into the answers card
   below the comments footer, where the rest of what this person told us about
   themselves lives. A labelled line rather than a `.ld-kv` row: that card's
   body is a stack of disclosure blocks, and one two-column row above them would
   read as a heading for the first block. */
.per-answers-course {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  min-inline-size: 0;
}

.per-answers-course__label {
  flex: 0 0 auto;
  font-size: 0.75rem;
  color: #8e8f9e;
}

/* ══════════════════════════════════════════════════════════════════════════
   14. OPEN DAY TAB
   ══════════════════════════════════════════════════════════════════════════ */

/* A hook applied alongside the borrowed card kit (`.ld-card`, plus this page's
   own `.per-od-card--seat`) so the cancelled modifier below has something to
   attach to without redefining the seat block itself. It used to cite
   `.ld-reg`, the lead card's compact registration row; that block was deleted
   with the rest of the lead card's markup, and the seat card has been an
   `.ld-card` since Phase 2. */
.per-seat {
  min-inline-size: 0;
}

/* A revealed cancelled seat must never read as a live one - but it must stay
   READABLE. It is shown to document that the registration existed, so dimming
   is the whole intervention: no strike-through, no collapse, nothing hidden. */
.per-seat--cancelled {
  opacity: 0.62;
  background: #fafbfd;
}

/* ── WS-A, 2026-08-09: THE `?reg=` LANDING MARK ─────────────────────────────
   The reader clicked one seat row on /admin/open-day-registrations and arrived
   here. On a person holding several seats the cards are near-identical and
   stacked one under the other, so "which one did I click" is unanswerable
   without this.

   It answers exactly one question - "where was I" - and that question stops
   being worth answering a few seconds after it is answered, so the mark is
   transient: `PersonOpenDayTab` removes the class after 4s. It is never a
   selection and must never be mistaken for one.

   THREE CHOICES ARE LOAD-BEARING:

   1. A RING, not a tint. The seat card already owns a background, and a revealed
      cancelled one owns a second (`.per-seat--cancelled`); painting a third
      would fight both.
   2. `outline`, NOT `box-shadow`, and this was measured rather than assumed. A
      `box-shadow` here REPLACES the `.ld-card` kit's own resting shadow
      (`rgba(0,60,130,0.05) 0 2px 10px`), so the marked card would visibly lose
      its depth for the four seconds it is marked - the one card the reader is
      looking at, flattened. An outline is a separate property, costs no layout,
      follows the card's border-radius, and sits outside the box so it survives
      the 0.62 opacity of a revealed cancelled card. (The roster's equivalent
      mark cannot use one: it marks a `<tr>`, and a row in a border-collapse
      table is not a paint surface.)
   3. NO ANIMATION, and therefore no `prefers-reduced-motion` query. The roster's
      mark fades itself out over two seconds, which needs a keyframe block AND a
      motion-preference block to switch it off. This file allows exactly four
      media queries (see rule 4 in the header) and a fifth is a decision rather
      than a tweak - so the mark is simply on, then gone, which is also the
      behaviour a reduced-motion reader would have been given. */
.per-od-card--located {
  outline: 2px solid rgba(0, 113, 227, 0.55);
  outline-offset: 2px;
}

/* ── WS-A, 2026-08-09: THE PERMANENT DELETE (owner D6) ──────────────────────
   The only control on this page that destroys anything, so it is fenced off
   from the row of facts and edits above it by a rule and a gap rather than
   sitting among them. `justify-content: start` keeps its hit area the width of
   its own label in this RTL card - a stretched danger button is a bigger target
   than a destructive action deserves. */
.per-od-danger {
  display: flex;
  justify-content: start;
  margin-block-start: 1rem;
  padding-block-start: 0.85rem;
  border-block-start: 1px solid var(--adm-line);
}

/* The BUTTON itself is the shell's `.btn .btn-sm .btn-danger` kit, unchanged and
   undeclared here. That kit is already exactly right for this - red ink and a
   red border on white, filling in on hover, which is loud enough to be read as
   destructive without out-shouting the approval dropdown above it, the control
   staff actually use. Rule 1 of this file's header: borrowed kits are never
   redefined bare. `.per-od-danger-btn` therefore carries NO RULE at all - it is
   the hook a future override would hang on and the handle a test can hold, the
   same job `.reg-crm-tombstone` does in admin.css. */

/* D9's reveal. Borderless and muted, because it is a disclosure control and not
   an action, and it sits BELOW the list so revealing rows never pushes the live
   ones off the screen the reader is looking at. */
.per-cancelled-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-block-size: 44px;
  margin-block-start: 0.5rem;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: 0.8rem;
  font-weight: 500;
  color: #5a5b6a;
  text-align: start;
  cursor: pointer;
}

.per-cancelled-toggle:hover {
  color: var(--adm-accent);
}

.per-cancelled-toggle i {
  font-size: 0.7rem;
  color: #8e8f9e;
}

/* The block below the card list: the reveal toggle plus the muted sentence that
   states the cancelled-seat rule while it is open. A flex column so the sentence
   sits under the control that produced it rather than beside it, and
   `align-items: start` so the toggle's hit area is the width of its own label
   and not the width of the tab. */
.per-od-cancelled {
  display: flex;
  flex-direction: column;
  align-items: start;
  min-inline-size: 0;
}

/* ── 14a. THE TWO CARD TYPES ─────────────────────────

   A SEAT means somebody is expected at the door. A REQUEST means somebody filled
   in a form and no row exists. A staff member standing at the venue must never
   read one as the other, and the markup already carries four signals apart from
   colour - a different title, a different head glyph, the mandatory state strip
   and the head pill. This is the fifth, and it is the only one visible while the
   eye is scanning a column of cards rather than reading them:

     seat    a SOLID inline-start rule in the page's own ink. Solid = committed,
             the same vocabulary `.per-prov--record` speaks one section up.
     request an AMBER, DASHED inline-start rule. Dashed = not a record yet, which
             is exactly what a request is - `.per-prov--current` and
             `.reg-source-badge--unknown` both already use a dashed edge for
             "this is not settled". Amber matches `.ld-odr-state--open`, the
             strip inside the card, so the accent and the strip agree.

   Hue AND edge style, so the pair survives greyscale and a colour-blind reader,
   which is the same two-signal rule the provenance mark and the tab strip
   follow. Both sit on `.ld-card`'s own 1px border, replacing one side of it. */

.per-od-card {
  min-inline-size: 0;
}

.per-od-card--seat {
  border-inline-start: 3px solid var(--adm-ink);
}

.per-od-card--request {
  border-inline-start: 3px dashed #f59e0b;
}

/* The head is title-at-the-start, meta-at-the-end. `.ld-card-head` is already
   `space-between` over a `flex: 1` title, so all that is added is the WRAP: the
   registered-at stamp and the status chip must drop to their own line rather
   than squeezing the title or clipping the chip. `align-items: flex-start` for
   the wrapped case - centring two rows of different heights leaves the title
   floating. */
.per-od-head {
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
}

/* Sits ON `.ld-card-title`, so it must not restate the font: the two card types
   and the Lead tab's cards all have to read as one family. Only the icon gap. */
.per-od-title {
  gap: 0.45rem;
}

.per-od-head-meta {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  flex: 0 0 auto;
  gap: 0.5rem;
  min-inline-size: 0;
}

/* Seat head only: when the row came into being. Muted, because it is provenance
   rather than a fact anybody acts on. No `direction` here - it wraps a Hebrew
   preposition around a `dir="ltr"` timestamp, and forcing a direction on the
   wrapper would put the preposition on the wrong side of it. */
.per-od-registered {
  font-size: 0.75rem;
  color: #8e8f9e;
}

/* Request head only. Amber, and deliberately not any `.reg-chip--*` colourway:
   the five status chips are the seat's vocabulary and this pill must not be read
   as a sixth status. Same pill geometry so it still belongs to the family. */
.per-od-kind {
  display: inline-flex;
  align-items: center;
  padding: 0.15rem 0.6rem;
  border: 1px solid #fde68a;
  border-radius: 999px;
  background: #fffbeb;
  font-size: 0.72rem;
  font-weight: 600;
  color: #92400e;
  white-space: nowrap;
}

/* The body's own rhythm: the fact grid, the contact block, the attribution
   sub-panel and the answers table are four siblings. The gap owns the spacing
   between them, so none of them carries a block margin of its own - paying both
   opens a gutter that grows every time a block is added. */
.per-od-body {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  min-inline-size: 0;
}

/* ── 14b. THE FACT GRID ──────────────────────────────

   `.ld-kv-row`'s key column is 120px, which was sized for `שם`/`טלפון` on the
   Lead tab. The longest key on this tab is `סטטוס אישור הגעה ליום פתוח זה`, and
   at 120px it wraps to three lines and drags every row's baseline apart. Widened
   here and nowhere else - scoped under our own class so the Lead tab, the sale
   page and the enrollment tab keep the column they were designed with. */
.per-od-facts .ld-kv-row {
  grid-template-columns: minmax(0, 11rem) minmax(0, 1fr);
}

/* The relative-time trailer - `(בעוד 27 ימים)` - butts straight against the
   value it qualifies. Vue's `condense` whitespace handling drops a text node
   made of a newline and spaces between two elements, so the space that is in
   the template does not survive into the DOM, and no amount of editing the
   template puts it back without an `&nbsp;`. A start margin says it once, for
   every date line on both card types. */
.per-od-facts .ld-when-rel {
  margin-inline-start: 0.35rem;
}

/* The approval row's value STACKS: the control on the first line, the no-address
   warning underneath it. Side by side, the warning reads as a label for the
   dropdown rather than as a consequence of pressing it. `align-items: start` so
   neither the select nor the chip stretches to the column width. */
.per-od-status-row dd {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: 0.4rem;
}

/* The approval value a CANCELLED seat kept, printed under its tombstone chip so
   whoever restores the row on the registrations screen knows what they are
   restoring it to. Under, not beside: the `dd` above is a column, and two facts
   about one axis on one line read as a single compound value. */
.per-od-kept {
  font-size: 0.75rem;
  color: #8e8f9e;
}

/* "This seat has no address, so approving mails nobody." A footnote under the
   control it qualifies - amber ink, no background box. A filled alert here would
   sit inside a `dd` on 15 of 127 cards and read as an error, and nothing has
   gone wrong: the status change is still going to work. */
.per-od-note {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
  margin: 0;
  font-size: 0.75rem;
  line-height: 1.5;
  color: #92400e;
}

.per-od-note i {
  flex: 0 0 auto;
  font-size: 0.9em;
}

/* The participation row holds TWO things: the resolved mode as a read-only tag,
   and - for a writer - the borrowed `.reg-mode` checkbox that changes it. They
   wrap together on a narrow card and split onto their own lines on a phone. */
.per-od-mode-cell {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
}

/* The resolved mode, as a word and a glyph rather than a ticked box. This is the
   `.per-*` replacement for `.reg-mode-tag`, which is on the post-Phase-4 sweep
   list in admin.css and must not be borrowed into a new page on its way out. The
   two colourways match `.reg-mode-chip--*` exactly, so the same participation
   mode is the same colour on this page and on the registrations screen.
   `white-space: normal` because the phrase is wider than the value column on a
   320px screen: it wraps inside the pill rather than spilling out of it. */
.per-od-mode {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  max-inline-size: 100%;
  padding: 0.14rem 0.55rem;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: 0.76rem;
  font-weight: 600;
  line-height: 1.45;
  white-space: normal;
}

.per-od-mode--zoom {
  background: #ede7f6;
  border-color: #d1c4e9;
  color: #4527a0;
}

.per-od-mode--in-person {
  background: #e8f5e9;
  border-color: #c8e6c9;
  color: #1b5e20;
}

/* ── The seat's calendar invitation: a fact, then a control ────────────────
   Same flex row as `.per-od-mode-cell` because it answers the same shape of
   question, and it wraps for the same reason: at 320px the chip and the button
   do not fit on one line, and a button pushed out of the value column is a
   button nobody presses. */
.per-od-cal {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
}

/* Six states share one pill. The colourways deliberately echo
   `.per-od-mode--*` - same size, same weight, same radius - so a reader scanning
   the facts column sees one vocabulary rather than two. */
.per-od-cal-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  max-inline-size: 100%;
  padding: 0.14rem 0.55rem;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: 0.76rem;
  font-weight: 600;
  line-height: 1.45;
  white-space: normal;
}

/* Nobody has been invited, or there is nothing to invite them to. Grey, because
   neither is a problem - most seats will sit here until somebody acts. */
.per-od-cal-chip--none {
  background: #f1f1f4;
  border-color: #e0e0e6;
  color: #5a5b6a;
}

/* Queued. Blue rather than green: the invitation has not gone yet, and a green
   chip over an unsent message is the one colour that would mislead here. */
.per-od-cal-chip--pending {
  background: #e3f2fd;
  border-color: #bbdefb;
  color: #0d47a1;
}

/* Queued, and the last attempt failed. Amber and never red - the seat is fine,
   the send is retrying by itself, and nothing is lost. Red belongs to things
   somebody must act on. */
.per-od-cal-chip--warn {
  background: #fff8e1;
  border-color: #ffe082;
  color: #8d6e00;
}

/* On the guest list, no answer yet. */
.per-od-cal-chip--invited {
  background: #ede7f6;
  border-color: #d1c4e9;
  color: #4527a0;
}

/* The three RSVP answers. Green/red/amber match how the same three answers are
   coloured on the registrations screen. */
.per-od-cal-chip--yes {
  background: #e8f5e9;
  border-color: #c8e6c9;
  color: #1b5e20;
}

.per-od-cal-chip--no {
  background: #ffebee;
  border-color: #ffcdd2;
  color: #b71c1c;
}

.per-od-cal-chip--maybe {
  background: #fff8e1;
  border-color: #ffe082;
  color: #8d6e00;
}

/* The button carries no colour of its own - it is `.btn.btn-sm` and inherits.
   A disabled one still has to look disabled, which the panel-wide
   `.per-action:disabled` rule does not reach here. */
.per-od-cal-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* Where a control would be for a viewer who may not write - the attendance mark,
   today. Same size and weight as the `.reg-attend-label` a writer sees, so the
   two accounts read the same fact at the same visual level: a read-only screen
   that also shrinks its values reads as a degraded screen rather than a
   permission boundary. No chip and no border - it is a value, not a state. */
.per-od-plain {
  font-size: 0.78rem;
  font-weight: 600;
  color: #5a5b6a;
}

/* "There is none": `לא הוזן`, `לא נקבע`, `לא תועד`, `המועד נמחק מהמערכת`,
   `לא צוין מועד`, `ללא קוד שותף`. Replaces `.reg-detail-empty`, which is on the
   sweep list AND is italic - italics on a Hebrew face render as a synthesised
   slant and read as damaged text rather than as an aside. */
.per-od-empty {
  font-size: 0.82rem;
  color: #8e8f9e;
}

/* ── 14c. THE INLINE CAR-PLATE EDITOR ────────────────
   The row ALWAYS renders, empty or not (S42): the venue needs the plate to let
   somebody through the gate, and a row that vanishes when the value is missing
   makes an absent plate look like a field this screen does not have.

   `align-items: center` overrides `.ld-kv-row`'s baseline alignment for this one
   row - a 30px input has no useful baseline to hang a label off - and the
   minimum height is what stops the whole card jumping when the editor opens. */
.per-od-plate-row {
  align-items: center;
  min-block-size: 2rem;
}

.per-od-plate-edit {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  min-inline-size: 0;
}

/* Narrow: a plate is eight characters and `.form-input` is full width, which
   would put a pane-wide field under a two-word label.

   `direction: ltr` HAS TO BE HERE, and the `dir="ltr"` on the element is not
   enough - measured in a real browser, not assumed. `.form-input` in admin.css
   declares `direction: rtl; text-align: right` for Hebrew form fields, and an
   AUTHOR declaration beats the `dir` attribute, which the browser applies as a
   UA-level presentational hint. Left alone, `12-345-67` renders with its
   segments reordered inside a field whose whole content is a licence plate a
   guard at a gate has to read back. `.reg-inline-edit-input` on the
   registrations screen carries the same two lines and says the same thing.

   `text-align: start` rather than `left`: with the element's OWN direction now
   ltr, `start` IS left, and the logical form keeps the file's rule intact. */
.per-od-plate-input {
  inline-size: 9rem;
  max-inline-size: 100%;
  padding-block: 0.25rem;
  padding-inline: 0.45rem;
  direction: ltr;
  font-size: 0.82rem;
  text-align: start;
}

/* The editor's three glyphs. Borderless and muted, because a card carrying five
   fact rows must not read as a form.

   The pencil is ALWAYS VISIBLE rather than revealed on hover, which is the
   deliberate opposite of what a desktop-only design would do. admin.css's
   `.reg-inline-btn` learned it first and says so: "hover affordances do not
   exist on the phone, and an editable field nobody can find is not editable".
   This page is the one being rebuilt FOR the phone, so the lesson applies twice
   over. Muted ink is what keeps it quiet instead. */
.per-od-plate-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.18rem 0.3rem;
  border: 0;
  border-radius: 5px;
  background: none;
  color: #9aa2af;
  font-size: 0.74rem;
  line-height: 1;
  cursor: pointer;
  transition: color 0.15s, background-color 0.15s;
}

.per-od-plate-btn:hover:not(:disabled) {
  background: rgba(0, 113, 227, 0.08);
  color: var(--adm-accent);
}

.per-od-plate-btn:focus-visible {
  outline: 2px solid var(--adm-accent);
  outline-offset: 1px;
}

/* Disabled means "a save is in flight", which is a state the operator caused and
   should be able to see. Dimmed rather than hidden, so the row does not reflow
   under the cursor at the exact moment somebody is aiming at it. */
.per-od-plate-btn:disabled {
  opacity: 0.45;
  cursor: default;
}

.per-od-plate-btn--save {
  color: #197a4a;
}

.per-od-plate-btn--save:hover:not(:disabled) {
  background: rgba(25, 122, 74, 0.1);
  color: #0f5c37;
}

.per-od-plate-btn--edit {
  margin-inline-start: 0.35rem;
}

/* ── 14d. THE DIFFERING-CONTACT BLOCK (S42) ──────────
   Rendered only when the seat's own snapshot disagrees with the person in the
   header, which is 9 of 115 live production seats. It has to read as "these are
   the SEAT's details" and not as four more facts about the person, so it is a
   tinted panel with its own edge rather than more rows in the same grid. */
.per-od-contact {
  padding-block: 0.7rem;
  padding-inline: 0.85rem;
  border: 1px solid #e3e8f2;
  border-radius: 10px;
  background: #f7f8fb;
  min-inline-size: 0;
}

.per-od-contact-note {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  margin: 0 0 0.6rem;
  font-size: 0.78rem;
  line-height: 1.55;
  color: #5a5b6a;
}

.per-od-contact-note i {
  flex: 0 0 auto;
  color: #8e8f9e;
}

/* The number and its WhatsApp group, stacked. The group is three labelled
   segments and it must never sit beside the digits: on a phone it goes full
   width, and on a desktop a bordered 30px control on a text baseline rides high
   against the numerals. `.reg-phone-wa-actions` solved the same problem on the
   registrations screen with a `flex-basis: 100%` hack inside a wrapping row;
   a column says it once. */
.per-od-phone {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: 0.4rem;
}

/* Replaces `.reg-contact-link`, which is on the sweep list. `overflow-wrap`
   rather than that rule's `word-break: break-all`: an address should break at
   the first opportunity it cannot avoid, not mid-word by preference. */
.per-od-link {
  color: var(--adm-accent);
  text-decoration: none;
  overflow-wrap: anywhere;
}

.per-od-link:hover {
  text-decoration: underline;
}

/* ── 14e. THE PER-SEAT ATTRIBUTION SUB-PANEL ─────────
   A sub-panel INSIDE a seat card, never a card of its own: a card in a card is
   what the seven-card expanded row used to be, and it is what this tab exists to
   undo. A hairline and some breathing room are the whole separation. No block
   margin - `.per-od-body`'s gap already owns it. */
.per-od-attr {
  padding-block-start: 0.75rem;
  border-block-start: 1px solid #f0f1f5;
  min-inline-size: 0;
}

.per-od-attr-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  min-inline-size: 0;
}

/* The disclosure control. 44px tall because it is a real target on a phone, and
   borderless because it opens a panel rather than performing an action. */
.per-od-attr-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-block-size: 44px;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--adm-ink);
  text-align: start;
  cursor: pointer;
}

.per-od-attr-toggle i {
  flex: 0 0 auto;
  font-size: 0.78rem;
  color: #8e8f9e;
}

.per-od-attr-toggle:hover {
  color: var(--adm-accent);
}

/* The same heading rendered as a `<span>` in the four not-ready states, where
   there is no body to open. It keeps the look and loses the affordance: a
   pointer and a hover colour on something that cannot be pressed is the
   dead-control problem this page refuses everywhere else. */
.per-od-attr-toggle--static {
  cursor: default;
}

.per-od-attr-toggle--static:hover {
  color: var(--adm-ink);
}

/* The badge rail: the traffic-channel verdict and the partner code, both of
   which stay readable while the card is collapsed - which is the entire reason
   S43 lets it start closed.

   The `margin-inline` reset is not cosmetic. `.reg-source-badge` carries
   `margin-left: auto` so it can push itself to the end of a table cell, and the
   only thing that cancels it in admin.css is `.reg-source-badges`, a rail this
   page deliberately does NOT borrow (its own margin is physical and its RTL
   mirror is scoped to `.reg-page`). Left uncancelled, two badges in this flex
   row fly to opposite ends of it. Applied through a descendant selector under
   our own class, never as a bare `.reg-source-badge` rule - that class is read
   by five list screens and this is the last stylesheet in the admin block. */
.per-od-badges {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-inline-start: auto;
  min-inline-size: 0;
}

.per-od-badges .reg-source-badge {
  margin-inline: 0;
}

/* The warning glyph inside the channel badge: the verdict left something it
   could not establish. Fixed colour rather than inherited, so it stays a warning
   on all five tier swatches instead of turning green on a referral. */
.per-od-caveat-flag {
  color: #b45309;
  opacity: 0.9;
}

/* The `denied` / `failed` / `loading` / `cancelled` sentence, on the reused
   `.per-notice--muted`. It sits OUTSIDE the collapsible region, so it needs the
   one thing the collapsed body would have given it. */
.per-od-attr-notice {
  margin-block-start: 0.5rem;
}

.per-od-attr-body {
  padding-block-start: 0.6rem;
  min-inline-size: 0;
}

/* A hook on the borrowed `.ld-kv`, and nothing more: the grid, the gaps and the
   dt/dd typography are the kit's and must stay the kit's, or this card stops
   matching the fact grid six inches above it. */
.per-od-kv {
  min-inline-size: 0;
}

/* A caveat's own key. It has to outrank `.ld-kv-row dt` (one class + one type),
   so it is scoped under `.per-od-kv` - which is also what keeps it off every
   other `dt` on the page. */
.per-od-kv .per-od-key--caveat {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  color: #92400e;
}

.per-od-kv .per-od-key--caveat i {
  flex: 0 0 auto;
  color: #92400e;
}

.per-od-verdict {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  min-inline-size: 0;
}

/* Solid ink at 600, like every other figure on this page a reader acts on. The
   brand gradient never comes near this weight. */
.per-od-verdict-label {
  font-weight: 600;
  color: var(--adm-ink);
}

/* The classifier's confidence, as a TAG and never a pill. On these screens a
   pill means state, and a confidence level is an annotation about a verdict -
   the same distinction `.per-prov` makes by being a rounded rectangle. */
.per-od-conf {
  font-size: 0.72rem;
  color: #8e8f9e;
}

/* Evidence and caveats. Both lists carry `dir="ltr"` in the markup because they
   are the classifier's own English sentences with landing URLs and click ids in
   them, so `padding-inline-start` resolves to the LEFT here - the marker side of
   an LTR list. No `direction` rule: the markup owns that, and restating it in
   CSS would put the decision in two places. `overflow-wrap` because there is no
   page-level scroller to catch one unbreakable token. */
.per-od-list {
  margin: 0;
  /* BOTH sides, and the second value is the point. `global.css` gives every
     `<ul>` on the site `padding: 0 30px 0 0`, a physical RTL indent from the
     public pages - and these two lists are `dir="ltr"`, so that 30px lands on
     their TRAILING edge and pulls a wrapping URL 30px short of the card while
     the marker sits somewhere else entirely. Measured in a browser, not guessed:
     the logical start alone left `padding-right: 30px` standing. */
  padding-inline: 1.1rem 0;
  font-size: 0.78rem;
  line-height: 1.5;
  color: #5a5b6a;
  overflow-wrap: anywhere;
}

.per-od-list--caveat {
  color: #92400e;
}

/* Campaign / ad / placement, as chips: a Hebrew key against an English value the
   advertiser wrote, rendered verbatim. */
.per-od-details {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  min-inline-size: 0;
}

.per-od-detail {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  min-inline-size: 0;
  padding: 0.05rem 0.4rem;
  border: 1px solid #e3e8f2;
  border-radius: 5px;
  background: #f6f8fc;
  font-size: 0.75rem;
}

.per-od-detail-key {
  flex: 0 0 auto;
  color: #8e8f9e;
}

.per-od-detail-val {
  min-inline-size: 0;
  color: var(--adm-ink-2);
  overflow-wrap: anywhere;
}

/* The partner code. Monospace so two codes can be compared character by
   character, and green because that is the colour a partner referral already
   wears on `.reg-source-badge--affiliate`. Weight 600 is the ceiling and it is
   solid ink - no gradient, ever, at this weight. */
.per-od-code {
  display: inline-flex;
  align-items: center;
  padding: 0.05rem 0.4rem;
  border-radius: 5px;
  background: #eafaf1;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 600;
  color: #197a4a;
}

/* An EARLIER touch in the path. Greyed, so the newest code is the one that reads
   as current - the path is chronological and the last entry is the one the
   verdict was built from. */
.per-od-code--prev {
  background: #f3f4f6;
  color: #6b7280;
}

.per-od-ref-meta {
  margin-inline-start: 0.4rem;
  font-size: 0.78rem;
  color: #8e8f9e;
}

.per-od-share {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  min-inline-size: 0;
}

.per-od-share-url {
  min-inline-size: 0;
  font-size: 0.76rem;
  color: var(--adm-accent);
  overflow-wrap: anywhere;
}

/* The full touch path, oldest first. An `<ol>` with no `dir` of its own, so it
   inherits the island's RTL and `padding-inline-start` puts the markers on the
   right - which is where a Hebrew reader looks for them. Each `li` is styled
   directly rather than through a class: it is the only child shape this list
   will ever have, and a class for it would be a name with no second reader. */
.per-od-path {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin: 0;
  padding-inline-start: 1.1rem;
  font-size: 0.78rem;
}

.per-od-path li {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.35rem;
  min-inline-size: 0;
}

.per-od-touch-meta {
  font-size: 0.78rem;
  color: #8e8f9e;
}

.per-od-touch-ts {
  color: #5a5b6a;
}

.per-od-ids {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.35rem;
  min-inline-size: 0;
}

.per-od-id {
  min-inline-size: 0;
  padding: 0.05rem 0.4rem;
  border-radius: 5px;
  background: #f3f4f6;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: #4b5563;
  overflow-wrap: anywhere;
}

.per-od-id-tag {
  margin-inline-end: 0.25rem;
  color: #8e8f9e;
}

/* The `fbp` disclaimer, on its own line: it is a sentence about the chip beside
   it, and a sentence wrapped around a mono id at the end of a flex row reads as
   part of the value. */
.per-od-id-note {
  flex-basis: 100%;
  font-size: 0.72rem;
  color: #8e8f9e;
}

/* ── 14f. THE QUESTIONNAIRE ──────────────────────────
   One renderer, two sources, one polarity difference: closed on a seat, open on
   a request. Nothing here knows which - the component takes it as a prop. */
.per-od-answers {
  min-inline-size: 0;
}

/* Deliberately the same shape as `.per-cancelled-toggle` one section up: both
   are disclosure controls on this tab, and two disclosure controls on one screen
   that look different teach a reader they do different things. */
.per-od-answers-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-block-size: 44px;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: 0.8rem;
  font-weight: 500;
  color: #5a5b6a;
  text-align: start;
  cursor: pointer;
}

.per-od-answers-toggle:hover {
  color: var(--adm-accent);
}

.per-od-answers-toggle i {
  flex: 0 0 auto;
  font-size: 0.7rem;
  color: #8e8f9e;
}

/* THE ONE RULE IN THIS BLOCK THAT MATTERS. `.reg-answer-q` is `width: 55%` and
   neither cell wraps unbreakable text, which was harmless in a 1500px table
   inside its own horizontal scroller and is not harmless here: this page has NO
   root scroller by design (contract 2), so one long free-text answer would push
   the card - and the whole page - sideways into `#app`'s `overflow-x: clip`,
   where it is invisible AND unreachable. Scoped under our own class so the
   borrowed atom is never redefined bare. */
.per-od-answer-table {
  margin-block-start: 0.45rem;
}

.per-od-answer-table .reg-answer-q,
.per-od-answer-table .reg-answer-a {
  overflow-wrap: anywhere;
}

/* ══════════════════════════════════════════════════════════════════════════
   15. ENROLLMENT TAB
   ══════════════════════════════════════════════════════════════════════════ */

/* Renders only for a person holding more than one sale. Production has exactly
   one such person and it is a test record, but nothing in the database enforces
   the rule, so the case has to look deliberate rather than accidental. */
.per-sale-picker {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.per-sale-picker-label {
  flex: 0 0 auto;
  font-size: 0.78rem;
  font-weight: 600;
  color: #8e8f9e;
}

.per-sale-pick {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-block-size: 40px;
  padding-inline: 0.7rem;
  border: 1px solid var(--adm-line);
  border-radius: 999px;
  background: #fff;
  font: inherit;
  font-size: 0.82rem;
  color: #5a5b6a;
  cursor: pointer;
}

.per-sale-pick:hover {
  border-color: rgba(0, 113, 227, 0.35);
  color: var(--adm-ink);
}

/* Selected state carried by weight and a filled edge as well as by tint: which
   sale is on screen decides which money the reader is about to act on, so it
   may not depend on colour alone. */
.per-sale-pick--active {
  border-color: var(--adm-accent);
  background: rgba(0, 113, 227, 0.08);
  color: var(--adm-accent-ink);
  font-weight: 600;
}

/* ── The panel's own root ──────────────────────────────────────────────────

   `PersonSalePanel` carries `.per-sale` BESIDE `.edt-page` (S45). `.edt-page`
   is the borrowed kit's root and declares only `direction: rtl; text-align:
   right` - no box properties - which is exactly what makes it safe as a nested
   wrapper here and keeps all twelve existing RTL mirrors working with no CSS
   churn. `.per-sale` is this page's own handle on the same element, and it
   exists so that anything the MERGED page needs to say about the panel can be
   said without touching a selector `admin-enrollment-detail.css` owns.

   It says one thing today, and the one thing is spacing: on the sale's old
   standalone page the panel WAS the page and inherited the shell's padding;
   inside a tab it is one block among several, so it owns the gap between the
   selector above it and the notes footer below. `text-align: start` re-issues
   `.edt-page`'s physical `right` logically, so the panel does not bake a side
   in for a future LTR admin. */
.per-sale {
  display: block;
  margin-block-start: 0.75rem;
  text-align: start;
}

/* ── 15a. PAYMENT LINKS - the sale panel's capability-URL card ─────────────

   `PersonPaymentLinksCard.vue`. Every link in this list is a PUBLIC URL that
   takes a card payment from whoever holds it, so the visual job is different
   from the ledger's beside it: the ledger reports money that has already moved
   and can be scanned; this list is read to decide whether to hand somebody a
   working link, and the state chip is the first thing that has to answer.

   Written in logical properties throughout, like the rest of this file. Nothing
   here redefines a borrowed class: the card's shell is `.ld-card` (already
   answered above), the buttons are the shared `.btn` kit, and everything this
   block declares is its own `.per-plink*` namespace. Two prefixes, one family:
   `per-plinks*` is the CARD, `per-plink-*` is the MODAL that mints one. */

.per-plinks {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A bordered row rather than a table: a link carries five short facts and two
   to four controls, and a table would give the controls a column that is empty
   on three rows out of four for a `payments:read`-shaped reader. */
.per-plinks__row {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.65rem 0.8rem;
  border: 1px solid #eef0f6;
  border-radius: 10px;
  background: #fff;
}

.per-plinks__main {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  font-size: 0.8rem;
  color: var(--adm-ink-2);
}

/* Same shape and the same four swatches as `.ld-ct-status` on the contracts
   card directly below it. Two lists of capability URLs one card apart may not
   speak two visual dialects about whether a link works. */
.per-plinks__state {
  flex: 0 0 auto;
  padding: 0.15rem 0.6rem;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 700;
}

.per-plinks__state--active {
  background: #e5f8ec;
  color: #1d8a4e;
}

.per-plinks__state--paid {
  background: #e8f1fd;
  color: #0b5cc4;
}

/* Expired, disabled and a cancelled sale are ALL "this takes no money right now"
   and all wear the neutral swatch. They are told apart by the words in the chip
   and by the row's own expiry line, not by a colour a reader has to learn. */
.per-plinks__state--expired,
.per-plinks__state--disabled,
.per-plinks__state--sale_cancelled {
  background: #f0f1f5;
  color: #8e8f9e;
}

.per-plinks__terms,
.per-plinks__when {
  flex: 0 0 auto;
  font-weight: 600;
  color: var(--adm-ink);
}

/* What the link charges. It leads the row's facts, so it carries a touch more
   weight than the terms beside it - and a SOLID colour at 700, never the brand
   gradient, which the repo forbids at 500 and above including numerals. */
.per-plinks__amount {
  flex: 0 0 auto;
  font-weight: 700;
  color: var(--adm-ink);
}

/* Money that has already come in through this link. Weight 600 and a solid
   colour, never the brand gradient - the repo forbids it at 500 and above,
   numerals included. */
.per-plinks__paid {
  flex: 0 0 auto;
  font-weight: 600;
  color: #1d8a4e;
}

.per-plinks__by {
  flex: 1 1 auto;
  font-size: 0.75rem;
  color: #8e8f9e;
}

.per-plinks__note {
  margin: 0;
  font-size: 0.78rem;
  color: var(--adm-ink-2);
}

.per-plinks__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

/* The delete is a plain `.btn` wearing danger INK only - no fill, no border
   colour. It sits in a rail with three routine buttons and a filled red target
   beside a "renew" that a reader is aiming for is how the wrong one gets
   pressed; the confirmation dialog is where this action gets its weight. */
.per-plinks__delete {
  color: #c0392b;
}

.per-plinks__retry {
  flex: 0 0 auto;
  margin-inline-start: auto;
}

/* One figure for the whole card - the balance belongs to the SALE, not to a
   link, so it is stated once under the list. */
.per-plinks__balance {
  margin: 0.7rem 0 0;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--adm-ink);
}

.per-plinks__add {
  margin-block-start: 0.7rem;
}

/* ── The create modal ──────────────────────────────────────────────────────

   `PaymentLinkCreateModal.vue`. It reuses the panel's `.modal` / `.form-*` kit
   and adds only what the post-create panel needs, which is the same problem
   `ContractCreateModal` solved for a signing link: a URL the reader must be
   able to copy but must not be tempted to retype. */

.per-plink-hint {
  margin: 0 0 0.9rem;
  font-size: 0.82rem;
  color: var(--adm-ink-2);
}

/* Who decides the amount - the first choice on the mint form. A `fieldset` and
   not a div, so the two radios are announced as one question; its default
   border and padding are dropped because every other group on this form is
   flush. Logical properties throughout, so the RTL island needs no mirror. */
.per-plink-modes {
  border: 0;
  padding: 0;
  margin-block-end: 0.9rem;
}

.per-plink-modes > .form-label {
  padding: 0;
  margin-block-end: 0.4rem;
}

.per-plink-mode {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding-block: 0.35rem;
  cursor: pointer;
}

.per-plink-mode input {
  margin-block-start: 0.2rem;
  flex: 0 0 auto;
}

/* The hint sits UNDER its own option rather than beside it: both wordings run
   to a full line, and inline they wrap into the next option's label. */
.per-plink-mode__text {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  font-size: 0.85rem;
  color: var(--adm-ink-1);
}

.per-plink-created__text {
  margin: 0 0 0.7rem;
  font-size: 0.85rem;
  color: var(--adm-ink-2);
}

/* The field and its copy button on one line, with the field taking every pixel
   the button does not: a 32-hex token in a box that wraps is a URL nobody can
   check by eye before sending it.

   It wraps rather than squeezing, and the field carries a floor: below about a
   phone's modal width the copy button drops to its own line instead of pressing
   the URL into a 40px sliver. No media query - the floor plus `flex-wrap` says
   the same thing at every width and costs this stylesheet no fourth
   breakpoint. */
.per-plink-created__link-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

.per-plink-created__link {
  flex: 1 1 14rem;
  min-inline-size: 0;
  font-family: var(--font-mono, monospace);
  font-size: 0.8rem;
}

.per-plink-created__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-block-start: 0.6rem;
}

.per-plink-created__meta {
  margin: 0.9rem 0 0;
  font-size: 0.78rem;
  color: #8e8f9e;
}

.per-plink-error {
  margin-block-start: 0.8rem;
  padding: 0.5rem 0.7rem;
  border-radius: 8px;
  background: #fdecea;
  font-size: 0.82rem;
  color: #c0392b;
}

/* ══════════════════════════════════════════════════════════════════════════
   15b. THE SALE RESOLVER - /admin/enrollments/:id
   ══════════════════════════════════════════════════════════════════════════

   `AdminSaleResolver.vue` is not a page in any visual sense: it reads who owns
   a sale and `router.replace`s to that person, so for a normal sale it paints a
   single "טוען..." line and is gone. It renders here at all only for the sale
   with NO CRM contact, where it shows the find-or-create interstitial instead.

   So this class does almost nothing on purpose. It caps the measure - the
   interstitial is one paragraph of explanation plus one button, and a paragraph
   run to the full width of a desktop admin screen is unreadable - and it
   centres that column. Everything else it renders (`.per-empty`, `.per-notice`,
   `.btn`) is already answered above. */
.per-resolve {
  max-inline-size: 46rem;
  margin-inline: auto;
  padding-block-start: 1rem;
}

/* ══════════════════════════════════════════════════════════════════════════
   16. MODALS
   ══════════════════════════════════════════════════════════════════════════

   The modals are direct children of `.per-page`, not teleported, so they are
   inside the island and inherit both its direction and the shell's tokens. */

/* Which person or which sale this modal is about, under its title. */
.per-modal-subject {
  margin: 0 0 0.9rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--adm-ink);
}

.per-modal-hint {
  font-size: 0.78rem;
  font-weight: 400;
  color: #8e8f9e;
}

/* "There are no active cohorts / open-day dates" inside a picker. A muted line
   and not an `.empty-state`, which is sized for a whole page. */
.per-modal-empty {
  padding: 0.6rem 0;
  font-size: 0.85rem;
  color: #8e8f9e;
}

/* ── WS-A, 2026-08-09: the register modal's approval note ────────────────────
   The approval dropdown was removed from the create form, so the modal has to
   SAY what the seat is created as - an absent control that explains nothing
   reads as a field somebody forgot. It borrows `.per-note`'s muted type and only
   adds the icon row, because the sentence carries a glyph and `.per-note` is a
   plain paragraph everywhere else it is used. */
.per-register-note {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
}

.per-register-note i {
  flex: 0 0 auto;
  font-size: 0.9em;
}

/* ══════════════════════════════════════════════════════════════════════════
   17. TABLET AND BELOW - max-width: 1024px
   ══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1024px) {
  /* `.admin-main` drops to 1.5rem of padding here, so the full-bleed reach has
     to drop with it. Exactly, never past it. */
  .per-page {
    --per-bleed: 1.5rem;

    /* The shell's own top bar, which appears at exactly this breakpoint and is
       a SIBLING of `.admin-main`, never an ancestor of anything here. It is not
       decoration: `.admin-layout` is `height: 100dvh`, so `.admin-main` - the
       scrollport every sticky on this page measures against - is `100dvh` MINUS
       this number, and a panel that asks for a plain `100dvh` hangs its last
       58px below the fold where nothing can scroll to it.

       Measured off admin.css rather than guessed: `.admin-mobile-bar` is
       `padding: 0.6rem 1rem` (9.6px + 9.6px) around a 38px `.admin-hamburger`,
       plus its 1px bottom border. 58px. Re-derive it if that bar's padding or
       the hamburger's box ever changes - nothing else here depends on it, and
       nothing will tell you it drifted. */
    --per-shellbar: 58px;
  }

  /* One column from here down, so the dock's `grid-column: 1` from section 9
     resolves to the only track there is and the swap of 2026-08-09 is a
     desktop-only affair. The two panes are LAID OVER each other rather than
     placed beside each other, and which one is on top is the panel's own state
     - the three placement rules below are the whole of it. */
  .per-panes {
    grid-template-columns: minmax(0, 1fr);
  }

  /* THE COLLAPSED RAIL SITS ABOVE THE TAB, not below it.

     Row 2 on this page is under the notes footer at the bottom of a document
     that is routinely thousands of pixels tall. "It never collapses to zero" is
     then satisfied in the DOM and defeated in practice: nobody scrolls past a
     whole comments list to find the WhatsApp bar. So the rail is pinned to row
     1 and the tab body follows it in row 2.

     Since 2026-08-09 the base rule in section 9 already places the panel at
     1 / 1, so this line RESTATES rather than establishes - kept because it is
     the rule that carries the invariant's name, because section 20 resets the
     base placement to `auto` and restates this same pair for its own reasons,
     and because the pairing with `.per-main` below has to be read as one thing.

     It is deliberately NOT pinned to the bottom of the viewport. That would be
     a second fixed bottom surface beside the phone action bar, and two of those
     on a 390px screen is the failure the one-bar rule exists to prevent. */
  .per-wa--collapsed {
    grid-row: 1;
  }

  /* THE COMMENTS RAIL SITS UNDER THE WHATSAPP ONE, and above the tab body.
     Same argument, same row-2 trap; the only question this band has to answer
     extra is which of the two flanks goes first, and it is the one with a clock
     on it. A WhatsApp window shuts 24 hours after the customer's last message;
     a note can be read whenever. Two 48px bars is 96px of chrome above the
     fold, which is the honest price of two flanks on a screen that can hold
     neither beside the content. */
  .per-cm--collapsed {
    grid-row: 2;
  }

  .per-panes:has(.per-wa--collapsed):has(.per-cm--collapsed) .per-main {
    grid-row: 3;
  }

  /* OPEN, either of them covers the tab instead of pushing it down, so the two
     have to sit in the same cell. Explicit on BOTH: a grid item with a definite
     position is placed FIRST and auto-placement then routes everything else
     around it, so leaving `.per-main` automatic would push it into row 2 and the
     "overlay" would sit above a gap rather than over the content.

     Four selectors, FOUR STATES AND NOT A COMBINATION: only one flank can float
     below 1025px, and that is structural rather than stylistic -
     `usePersonSidePanels` closes one in the same statement that opens the other,
     which is Fluent's "don't invoke multiple overlay drawers at a time" made
     impossible to get wrong rather than merely documented. */
  .per-panes:has(.per-wa--overlay) .per-main,
  .per-panes:has(.per-wa--full) .per-main,
  .per-panes:has(.per-cm--overlay) .per-main,
  .per-panes:has(.per-cm--full) .per-main {
    grid-row: 1;
    grid-column: 1;
  }

  /* ...and the rail of the flank that did NOT open drops out from under the one
     that did. Without this the WhatsApp rail keeps its row 1 and shares that
     cell with both an open comments pane and the tab body - three boxes in one
     cell, with the rail drawn through the scrim. The comments rail needs no twin
     rule: row 2 is already where it lives. */
  .per-panes:has(.per-cm--overlay) .per-wa--collapsed,
  .per-panes:has(.per-cm--full) .per-wa--collapsed {
    grid-row: 2;
  }

  /* ── THE ON-SCREEN KEYBOARD, AND THE ONE PLACE THIS FILE TOUCHES THE SHELL ──

     Contract 4's neighbours all say the shell "is not this file's to restyle",
     and this rule is the one deliberate exception. It is stated here rather than
     smuggled in, because it is a decision and not a tweak.

     WHY IT HAS TO BE HERE. `.admin-layout` is `height: 100dvh` (admin.css), and
     `dvh` shrinks for browser chrome but NOT for the virtual keyboard - on iOS
     the keyboard is painted OVER the layout viewport. The panel below is a
     viewport-locked box whose LAST row is a text box, so without this the
     keyboard covers the very control being typed into. `useVisualViewport()`
     publishes the actually-visible height as `--app-vvh` on the document
     element; admin-whatsapp.css already consumes it the same way for the inbox
     shell, through the same `:has()` escape, and this is that rule's twin for
     the surface the inbox rule cannot reach. It cannot be shared with the inbox
     rule because the inbox keys on a class this page's DOM is forbidden to
     contain, and it cannot live on `.per-wa--full` alone because the element
     that has to shrink is three levels above this page's root.

     `:has()` on `.admin-layout` and never on `.admin-main`: the shell's global
     scroller stays exactly as it is. The `100dvh` fallback is what a browser
     with no visualViewport, a page with no WhatsApp panel open, and the tick
     before the first measurement all get - so with no holder mounted this rule
     is byte-identical to the shell's own height. */
  .admin-layout:has(.per-wa--full),
  .admin-layout:has(.per-cm--full) {
    height: var(--app-vvh, 100dvh);
  }

  /* The CLOSED shape at every band from here down: a full-width block under the
     content. `min-inline-size` and `max-inline-size` are reset explicitly - the
     dock's 320..520 bounds would otherwise survive into a band that has no
     dock, and the 320px floor alone would inflate the 48px rail. */
  .per-wa {
    position: static;
    inline-size: auto;
    min-inline-size: 0;
    max-inline-size: none;
    block-size: auto;
    max-block-size: 60vh;
  }

  .per-wa--collapsed {
    inline-size: auto;
    min-inline-size: 0;
    max-inline-size: none;
    block-size: auto;
  }

  /* The handle is never rendered below 1025px (`canResize` requires `docked`),
     and a 6px strip is not a touch target in any case. This is the guard for
     the day somebody widens that condition. */
  .per-wa__resize {
    display: none;
  }

  /* The rail runs horizontally once the dock is a full-width block: a vertical
     word down a full-width strip is a 48px-tall band of nothing. 48px and not
     44, because on a phone this rail IS the primary action set. */
  .per-wa__rail {
    flex-direction: row;
    justify-content: flex-start;
    block-size: auto;
    min-block-size: 48px;
    padding-inline: 0.9rem;
  }

  .per-wa__rail-label {
    writing-mode: horizontal-tb;
    font-size: 0.85rem;
  }

  .per-wa__rail-hint {
    margin-block-start: 0;
    margin-inline-start: auto;
  }

  /* Real thumb targets on the panel's own chrome. Both of these live at the TOP
     of the panel, which is where the touch rules want anything consequential
     anyway - never in the thumb zone. */
  .per-wa__collapse {
    min-inline-size: 44px;
    min-block-size: 44px;
  }

  /* The arrow shown while the panel FLOATS (overlay or full screen) is the
     primary action of that view - it is the only way back to the person - so it
     takes 48px on both axes plus 16px of clearance from the window-state chip
     beside it. The head's own `gap` pays 8px of that; this margin pays the
     rest. */
  .per-wa__collapse--close {
    min-inline-size: 48px;
    min-block-size: 48px;
    margin-inline-start: 8px;
  }

  .per-wa__open-thread {
    min-block-size: 44px;
  }

  /* ── The comments pane, same band, same shape ─────────
     The CLOSED shape from here down is a full-width block. `min-inline-size` and
     `max-inline-size` are reset explicitly - the column's 300..460 bounds would
     otherwise survive into a band that has no column, and the 300px floor alone
     would inflate the 48px rail.

     `grid-column: 1` IS THE LOAD-BEARING LINE, and leaving it out is the bug
     this band shipped with for an hour. `.per-panes` declares ONE track here,
     but section 9 places this pane at `grid-column: 3` - so grid mints two
     IMPLICIT tracks to reach it, both sized to their content. The result is not
     a broken-looking layout, which is what makes it dangerous: the WhatsApp rail
     renders as a 764px bar starting a third of the way across the screen and the
     comments rail as a 176px chip beside nothing, and each one looks like a
     deliberate width. The dock needs no twin line because its `grid-column: 1`
     already names the only real track. Measured at 1024x900. */
  .per-cm {
    position: static;
    grid-column: 1;
    inline-size: auto;
    min-inline-size: 0;
    max-inline-size: none;
    block-size: auto;
    max-block-size: 60vh;
  }

  .per-cm--collapsed {
    inline-size: auto;
    min-inline-size: 0;
    max-inline-size: none;
    block-size: auto;
  }

  /* The handle is never rendered below 1025px (`canResize` requires `docked`),
     and a 6px strip is not a touch target in any case. This is the guard for the
     day somebody widens that condition. */
  .per-cm__resize {
    display: none;
  }

  /* The rail runs horizontally once the pane is a full-width block: a vertical
     word down a full-width strip is a 48px-tall band of nothing. 48px and not
     44, matching the dock's rail immediately above it - two bars of different
     heights read as two different kinds of control. */
  .per-cm__rail {
    flex-direction: row;
    justify-content: flex-start;
    block-size: auto;
    min-block-size: 48px;
    padding-inline: 0.9rem;
  }

  .per-cm__rail-label {
    writing-mode: horizontal-tb;
    font-size: 0.85rem;
  }

  .per-cm__rail-hint {
    margin-block-start: 0;
    margin-inline-start: auto;
  }

  /* Real thumb targets on the pane's own chrome, at the TOP of the pane, which
     is where the touch rules want anything consequential anyway. */
  .per-cm__collapse {
    min-inline-size: 44px;
    min-block-size: 44px;
  }

  /* The arrow shown while the pane FLOATS is the primary action of that view -
     it is the only way back to the person - so it takes 48px on both axes. */
  .per-cm__collapse--close {
    min-inline-size: 48px;
    min-block-size: 48px;
    margin-inline-start: 8px;
  }

  /* The tablet overlay's scrim, and a real click target - so never
     `pointer-events: none`. 35% and not the inbox's 60%: over a light UI the
     dark skin's value reads as a blackout rather than a dim. The element is
     only ever RENDERED in the tablet band (a full-screen phone has nothing left
     to dim, and a landscape phone stacks), so this rule is declared for two
     bands and paints on one.

     ONE declaration for both flanks. Only one of them can be floating at a
     time, so only one scrim element is ever rendered, and two rules that had to
     agree on a colour would be two rules that eventually did not. */
  .per-wa-scrim,
  .per-cm-scrim {
    display: block;
    position: absolute;
    inset: 0;
    z-index: 20;
    background: rgba(11, 20, 26, 0.35);
  }

  /* ── THE 16px INPUT FLOOR ──────────────────────────
     16px EXACTLY, on every control this page can render, and the number is not
     a style choice. Below 1025px iOS Safari zooms the whole page in when a
     field with a smaller font takes focus, and it never zooms back out - the
     reader is left on a page they have to pinch their way out of. 15.9px still
     triggers it, and so does `1rem` on any document that has re-based its root.
     Applied at the TABLET breakpoint rather than at 767px because iPad Safari
     is the same engine with the same rule.

     ONE rule for the whole page rather than a list of class names. This page
     renders roughly seventy controls across three tabs, seven modals, the
     open-day seat cards and the inline plate editor, and a class list is a list
     somebody adds a field to without reading this comment - which is exactly
     how the follow-up date, the composer and the plate field each ended up
     needing their own guard on three different screens.

     Element selectors, scoped to `.per-page`. A bare `input`/`select`/
     `textarea` here would restyle every form on the public marketing site,
     because this stylesheet is linked from the same index.html.

     Specificity is (0,1,1), which clears every control rule that can reach
     inside this page: `.form-input`/`.form-select`/`.form-textarea`,
     `.reg-status-select`, `.ld-followup-date`/`-time` and `.per-od-plate-input`
     are all single-class. Anything that ever needs to beat this has to say so
     out loud, at (0,2,0) or better, which is the point. */
  .per-page input,
  .per-page select,
  .per-page textarea {
    font-size: 16px;
  }

  /* ── The name is the one cell that may not be squeezed out ──────────────
     Row B is the ONLY identity on the page since 2026-08-09, full stop: row A's
     `<h1>` and the breadcrumb tail were both deleted when the arrows moved down
     here (owner items 4 and 12). This comment used to note that row B was the
     only identity "once row A has scrolled away"; there is no longer an
     alternative to scroll to, which makes the floor below stricter rather than
     looser.

     Measured in a real browser on 2026-08-08, at 390x844 AND at 768x1024,
     on four different production people: `.per-id__name` computed to a width
     of exactly 0px on every one of them. The sticky header showed two
     campaign-source chips, a clipped email fragment and no human being.

     The cause is a flex deficit, not a typo. Everything else in `.per-id` is
     `flex: 0 0 auto`, so the name - the only shrinkable item - absorbs 100%
     of it. The two source badges are the least identifying thing in the row
     and the only ones repeated verbatim further down the page, so they are
     what yields; the name keeps a floor wide enough to be worth reading.

     `.per-id .ld-nav` carries the same pair at the base, because the name is
     wrapped by the worklist arrows now: a floor on an item inside a nav that
     cannot itself grow is a floor nothing can reach.

     THE RULE THAT USED TO STAND HERE (`flex: 1 1 auto; min-inline-size: 7ch`)
     IS NOW ONE TOKEN, `--per-name-floor`, restated with the other three in the
     block's reservation rule below. It had to move: since 2026-08-09 the floor
     is also the name's flex BASIS (the nav wraps its counter off, and line
     assignment reads the basis), so a band that restated the floor and left the
     basis at the desktop 12ch would have reserved 12 characters of line for a
     7-character promise - and the arrow that does not fit on line 1 is not
     drawn at all. One token, both halves, per band. */

  /* REVISITED ON 2026-08-09 AND KEPT (owner item 12). The merge freed real
     width in row B and the desktop clamp on these two pills is gone with it -
     but at 768px the row still carries the arrows, the name, a status DROPDOWN,
     the enrollment line and the whole desktop action cluster, and the cluster
     alone measured 395.3px there even after its labels were dropped. Re-admitting
     two 90px pills would take 180px straight off a name that has a 7ch floor and
     nothing else to give.
     What changed is the cost of hiding them: the general detail panel below now
     carries the classified channel as a chip on EVERY tab, so a phone reader
     gets the verdict one glance down the page instead of not at all. */
  .per-id__badge {
    display: none;
  }

  /* ... and the one badge that is exempt from it. `.per-id__intent` carries
     `.per-id__badge` for its shape, so the rule above hid it too: below 1025px
     the panel said nothing at all about what the customer asked for once the
     awaiting strip had been cleared by somebody answering. That is the wrong
     one to drop. The two above it say where this person came FROM, are repeated
     verbatim on the Lead tab below, and are the least identifying thing in the
     row; this one says what they ASKED FOR, is the newer and more actionable
     fact, and has no second home on a phone. It is a 29.6px glyph at every
     width (see the base rule), so re-admitting it costs the name 10.3px at
     390px and 36.8px at 1024px, both measured, both inside the 7ch floor the
     name keeps here. Same specificity as the rule above and later in the block,
     which is what makes it win. */
  .per-id__intent {
    display: inline-flex;
  }

  /* ── THE WORKLIST COUNTER IS THE FIRST THING TO GO ──────────────────────
     `LeadListNav` renders "7 / 43" after the next arrow, `white-space: nowrap`
     and unshrinkable. In the merged strip it lives INSIDE `.per-id`, so under
     pressure it does not clip: `.ld-nav` overflows its own box and the counter
     paints ON TOP of the status dropdown beside it. Seen in Chromium at 390px
     and again at 768px on 2026-08-09, which is the same chip-over-chip failure
     the owner chip was taken out of this row for.

     The ARROWS stay - they are the function, and walking a filtered list is the
     reason the name came down into this row at all. The counter is the nicety
     and the widest part of the control. Scoped to `.per-id` so the loading and
     not-found frames, which render the same component in a row that is NOT
     height-capped, keep theirs. */
  .per-id .ld-nav-pos {
    display: none;
  }

  /* THE OWNER CHIP'S `display: none` USED TO BE HERE, and the cell it hid is
     gone from row B entirely (owner item 3.3, 2026-08-09). It was the chip
     measured on 2026-08-08 sitting 23.7px inside the money cell at 390px, with
     `elementFromPoint` over it returning the amount drawn on top. Hiding it
     below 1025px was the right call for a read-only label; making it an
     assignable picker in the panel below is the better one. */

  /* `flex: 0 1 auto` is now the BASE value too, so only the ceiling is this
     band's business. `min-inline-size: 0` USED TO STAND HERE and is gone with
     the rest of the handed-back floors: a zero on the one control this row has
     to keep is how the dropdown became the payer of last resort. The floor
     arrives through `--per-status-floor`, restated for this band in the token
     rule at the top of the block. */
  .per-id__status {
    /* IT WAS 8ch, AND 8ch WAS A NUMBER ABOUT A CONTROL THAT IS NO LONGER HERE.
       The ceiling was sized for a native `<select>`, which this block forces to
       16px for the iOS zoom floor and which Chromium draws with ~20px of its
       own caret: 8ch of 16px is 71.2px, of which roughly 33px was left for
       text. Measured on the real strip at 390px, that drew `ה…` - one Hebrew
       letter, in the only statement of CRM status the Open Day and Enrollment
       tabs carry, on the devices where `title` cannot be reached.

       There is no `<select>` in this row below 1025px any more
       (`PersonStatusSelect.vue`): the reader gets a `<button>` pill at this
       row's own 0.7rem, or the read-only `<span>` at the same size. So the
       ceiling is re-derived for the control that IS here.

       14ch, and the number is a phone budget rather than a preference. The
       ceiling is spent out of the person's NAME here - at 390px rungs 1, 2 and
       3 are already gone, so the name is the only box left with anything above
       its floor to give - which makes this one number the whole trade between
       two things that have no second home on the page.

       RE-MEASURED 2026-08-09, AND THE TABLE THAT STOOD HERE COUNTED WRONG.
       It read the drawn label with a per-character walk - laid out, inside
       every clip, hit-tests as its own element - and under `text-overflow:
       ellipsis` the last one or two characters are all three of those things
       AND ARE PAINTED OVER BY THE MARK. So the old row for 14ch said the pill
       drew `השתתף ביו`, nine characters; screenshotted at 390x844 at 8x, it
       draws `השתתף` and an ellipsis. Every figure in the old table was two to
       four characters generous. The honest one, same viewport, same fixture,
       with the drawn prefix now resolved the way the browser resolves it
       (the longest prefix that fits beside the mark):

         ceiling   `השתתף...` draws   `נרשם לקורס` draws   name draws
         20ch      השתתף ביום (10)    נרשם לקו   (8)       69.5px, its 7ch floor
         16ch      השתתף ביו  (9)     נרשם לקו   (8)       79.2px
         14ch      השתתף      (5)     נרשם לק    (7)       91.6px
         12ch      השת        (3)     נרשם       (4)       104.1px

       SEVEN characters is the number that matters, because the working question
       is not "how much of the label" but "which label": the confusable pairs in
       the shipped taxonomy separate by character 7 at the latest
       (`נרשם ליום פתוח` against `נרשם לקורס`, `מבקש להתכתב עם נציג` against
       `מבקש לשוחח עם רותם`). 14ch draws exactly seven of the pair that needs
       seven, and leaves the name 91.6px.

       THAT IS EXACTLY AT THE BAR AND NOT ABOVE IT, which is a stated residual
       rather than a comfortable margin. 16ch would draw eight, and buys it from
       the name: 79.2px at 390px and its bare 7ch floor at 375px. The name is
       the page's only identity and the row was not asked to pay that, so the
       ceiling stays where it is - and the guard is the thing that carries the
       risk instead. `tests/personRowBGeometry.spec.ts` asserts the drawn prefix
       against `LEAD_LIFECYCLE` itself, so a staff-authored status that needs an
       eighth character to be told from another one fails the suite rather than
       shipping quietly.

       The whole label is one tap away in the sheet, and in `title` and
       `aria-label` either way.

       The tablet band raises it again - see section 18, where the row has the
       width and an iPad is where open-day handling actually happens.

       The clip and the mark it used to restate are on the base rule now, where
       the read-only `<span>` can reach them too. */
    max-inline-size: 14ch;
  }

  /* ── AND THE NAME YIELDS TO THE PILL BEFORE THE PILL YIELDS TO THE NAME ──
     THE FAILURE. The ceiling above decides how much label the control MAY
     draw; it does not decide how much it GETS. Inside `.per-id` the two items
     are this nav (which holds the name, and whose name grows) and the status
     control, both at shrink 1 - so where the row is short they pay in
     proportion to their bases, and the elastic box takes the smaller share.
     Measured in Chromium on a person whose status is `נרשם לקורס` and who
     carries a channel verdict, a partner code and a recorded intent - the
     ordinary person arriving from the leads list:

       375px   the pill drew  `נרשם`   (71.4px)  beside a 92.5px name
       390px   the pill drew  `נרשם`   (74.6px)  beside a 104.2px name

     `נרשם` is a prefix this status SHARES with `נרשם ליום פתוח`, so the one
     status that means THIS PERSON HAS PAID could not be told from the one that
     means they signed up for a free open day. On the Open Day and Enrollment
     tabs nothing else on the screen contradicts the misreading: the lifecycle
     chart is Lead-tab only, `title` cannot be reached without a hover, and rung
     3 has already dropped the money cell at that width. A customer who has
     bought gets worked as a prospect.

     THE FIX IS A WEIGHT, NOT A FLOOR, and the difference is the whole point.
     A floor big enough to hold seven characters (~84px) would have to be
     carried by `--per-status-floor`, which `--per-id-floor` sums - and at 320px
     `.per-id` is handed exactly 216px against a 215.9px reservation, so 40px
     more would over-subscribe the row by 40px with nothing left able to shrink.
     The kebab would be pushed off the physical left edge of an RTL island
     inside an LTR scroller, where nothing can scroll to it.

     A weight has no such cliff: it changes only WHO pays first, and the two
     floors already decide where paying stops. The order it produces is the
     ladder's own - the name shrinks to its 7ch floor, then the pill shrinks to
     its 44px touch floor, then `.per-id` stops at `--per-id-floor` and the row
     is done. Measured after, same person, same sweep: the pill draws `נרשם לק`
     with its own ellipsis after it - seven characters, the number the pair
     needs - from 375px up, and the name stays at or above its floor at every
     width. At 320px and 360px both are on their floors, which is where they
     already were, and the pill draws nothing and four characters: those two
     widths are a stated residual, asserted as one in the spec.

     20 is a weight and not a number with a meaning, exactly like the 100 on
     `.per-header__actions` one section down: what matters is that it is large
     enough that the pill's share of the deficit is a rounding error while the
     name still has anything above its floor to give. The leak is measurable and
     small - 1.2px of pill at 390px before the name reaches its floor - which is
     the same honesty the row's other weight pair is documented with.

     THIS BAND ONLY. Above 1024px the control is a native `<select>` whose text
     the UA draws (no spec can read it), the reader has a hover and therefore a
     `title`, and the desktop row's sums are pinned across eleven widths in
     `tests/personRowBGeometry.spec.ts`. The confusable-prefix problem exists
     there too at 1025-1100px with the sidebar expanded, and it is a stated
     residual rather than a fix nobody measured. */
  .per-id .ld-nav {
    flex-shrink: 20;
  }

  /* `.per-highlights { flex-shrink: 4 }` USED TO STAND HERE and is not gone -
     it moved to the base rule in section 6 on 2026-08-09 and is now rung 3 of
     the row's yield ladder, in force at every width. The argument it was written
     with was always general ("the highlights ellipsise gracefully, `.per-id` has
     nowhere to put the overflow and spills it sideways onto its neighbour"); the
     only thing that was band-specific about it was that this is the band
     somebody had measured. */

  /* `.per-id { overflow: hidden }` USED TO STAND HERE, and it is not gone - it
     moved to the base rule in section 6 on 2026-08-09 and now applies at every
     width. The guarantee it states is unchanged and is quoted there: status
     labels are editable from the admin panel, so no width sum computed here
     stays true, and clipping is the honest failure mode while painting one chip
     over another is not. What changed is that a desktop width stopped being
     exempt, because a reader can now expand the sidebar on this page (D5) and
     row A no longer carries a second copy of the name (D10). */

  /* ── AND THE WORKLIST NAV'S FLOOR IS RE-DERIVED HERE, NOT HANDED BACK ────
     `min-inline-size: 0` STOOD ON `.per-id .ld-nav` HERE until 2026-08-09, with
     a comment explaining that the desktop floor is "BUILT OUT OF DESKTOP VALUES
     - 34px arrows, 8px gaps and a 12ch name - and every one of the three is
     different in this band". Every word of that was true and the conclusion did
     not follow. Handing a floor back to zero does not make the control smaller,
     it makes the control CUTTABLE: `.ld-nav` carries its own `overflow: hidden`
     (the base rule says why - a nav that spills lands on the status dropdown),
     so with no floor the deficit came out of the nav's LAST child. Measured in
     Chromium, the forward "next lead" arrow drew 0 of its 44px at 320px, 10px at
     360px, 24px at 375px and 38px at 390px, and `elementFromPoint` at its centre
     returned the row behind it at 320px and 360px. A worklist pass on a phone
     could only walk backwards - in the band D10 moved the arrows into this row
     to serve. (The block's old comment said the clip "marks the loss" by fading
     the COUNTER; the counter is `display: none` two rules up, so there was
     nothing between the deficit and the arrow.)

     What the band actually needs is the same sum with this band's own terms, so
     the four tokens are restated rather than switched off. The arrows here are
     34px (the 44px touch floor arrives in the ≤767px block with the rest of the
     phone's numbers), the counter is hidden so the nav has two gaps and not
     three, and the name's floor is 7ch of a 1.05rem `<h1>` - which is 7.35ch on
     this box, for the same reason the desktop term is 12.6ch and not 12ch.

     The status floor drops from 72px to 71px, which is what lets the 8ch ceiling
     two rules down keep meaning something: `.per-page select` is forced to 16px
     in this block for iOS, and 8ch of that is 71.2px. A 72px floor over a 71.2px
     ceiling is a floor that has quietly become the width. */
  .per-page {
    --per-nav-floor: calc(2 * 34px + 2 * 8px + 7.35ch);
    --per-name-floor: 7ch;
    --per-status-floor: 71px;
  }

  /* ── Touch targets on the controls staff actually use standing up ───────
     Open-day handling happens at the venue, on a phone, one-handed. These
     three are the whole job - approve the seat, mark the mode, mark who came
     - and they were sized for a mouse: measured 30px, 21px and 13px at both
     390px and 768px against a 44px floor. The 16px font rule above already
     covers the typing half; this is the pointing half. */
  .per-page .reg-status-select {
    min-block-size: 44px;
  }

  .per-page .reg-mode,
  .per-page .reg-attend {
    min-block-size: 44px;
  }

  .per-page .reg-mode input[type='checkbox'],
  .per-page .reg-attend input[type='checkbox'] {
    inline-size: 20px;
    block-size: 20px;
  }

  /* ── The rest of the page's own controls, same 44px floor ───────────────
     Measured 2026-08-08 at 390x844: 31 of 49 interactive elements on the Open
     Day tab were under 44px tall. The seat controls above were the urgent
     ones; these are the rest of what a thumb has to hit.

     `.lct-chip` and `.lal-type-btn` belong to `LeadActivityLog`, which the
     WhatsApp inbox's lead panel ALSO mounts (`WaLeadPanel.vue`), so they are
     scoped to `.per-page` rather than fixed in `admin-activity-log.css`. That
     screen has the same problem and is not measured yet; widening a rule into
     it blind would be a change nobody checked. `.ld-card-toggle` is borrowed
     the same way but is Person-page-only in practice - eight components, all
     under `src/components/admin/person/`.

     `.per-src-chip` joined them on 2026-08-09. It is the `ערוץ הגעה` control in
     the general detail panel, and it is a real target: pressing it moves to the
     Lead tab and scrolls to the attribution card. Measured in Chromium at
     390x844 it was 22.0px tall - half the floor, and half the 44px the owner
     picker two fields above it already gets. It is scoped the same way for the
     same reason: `PersonLeadSourceChip` takes no props and reads the store, so
     it is deliberately droppable anywhere on this page, and the WhatsApp inbox
     mounts several of this page's components without ever having been measured
     at a touch width. */
  .per-page .lct-chip,
  .per-page .lal-type-btn,
  .per-page .ld-card-toggle,
  .per-page .per-src-chip {
    min-block-size: 44px;
  }

  /* ── TWO OPEN-DAY CONTROLS THAT WERE FLOORED ON A PHONE ONLY ────────────
     Both of these carried their 44px inside `@media (max-width: 767px)` until
     2026-08-09, which left the 768-1024px band - an iPad, held in the hand,
     which is where open-day handling actually happens - reading them at the
     desktop sizes. Measured in Chromium at 768x1024: the plate button was
     21.4 x 17.6px and the delete button 29.6px tall. The seven controls above
     were moved into this band for exactly that reason and these two were
     missed; they are the same fix in the same place.

     `.per-od-plate-btn` is the pencil/save/cancel glyph on the licence plate in
     the seat card. It keeps its muted borderless look - only the hit area
     grows, which is the one thing a finger needs and the one thing a cursor
     does not - so it takes the floor on BOTH axes, being a bare glyph with no
     text to widen it.

     `.per-od-danger-btn` is the PERMANENT seat delete (D6). The shell's
     `.btn-sm` computes to 24px tall, which is what every small admin button is
     on a desktop and is nowhere near hittable with a thumb. A bigger target on
     a DESTRUCTIVE control is the right trade rather than the wrong one:
     pressing it destroys nothing on its own, because the dialog behind it is
     irreversible, danger-toned and requires the person's name to be typed. The
     cost of a mis-tap is a dialog to dismiss; the cost of a target too small to
     hit is a staff member stabbing at it repeatedly, a few pixels from the save
     control beside it. It is a hook on THIS page's own class, so the shell's
     `.btn-danger` kit is untouched everywhere else. */
  .per-od-plate-btn {
    min-inline-size: 44px;
    min-block-size: 44px;
  }

  .per-od-danger-btn {
    min-block-size: 44px;
  }

  /* And the chip ALONE needs its line re-centred, which none of its neighbours
     do. It borrows `.ld-ch-chip`, whose `align-items: baseline` puts the label
     and the confidence word on one baseline - correct, and it must stay - but a
     single-line flex container stretches its line to the box, and baseline
     alignment then sits that group flush against the cross-START edge. Measured
     with the floor and without this rule: 3.4px of room above the text and
     25.4px below it, which reads as a chip that lost its bottom half rather than
     as a target.

     THE CENTRING IS PADDING AND NOT `align-items` (corrected 2026-08-09).
     `align-items: center` stood here first, on the reasoning that the label and
     the confidence word "share a font size, so their baselines still agree".
     That holds only while both are ONE line. This chip carries a staff-typed
     partner group name and `overflow-wrap: anywhere` right above, so two lines
     is a case the file already plans for - and when the two items end up with
     different line counts, centring each of them separately pulls them off each
     other's baseline. Measured in Chromium at 390px with a wrapping label:
     the confidence word sat 7.6px ABOVE the label's first line and the arrow
     dropped 10.8px, landing between the label's two lines instead of on the
     first one. `align-items: baseline` in the same fixture: 1px, which is the
     shared baseline the borrowed rule promises.

     Symmetric block padding buys the same centring for the single-line case
     without touching alignment: `(44px - 1.4em - 2px) / 2` is exactly the room
     left over once one line box (`line-height: 1.4`) and the borrowed 1px
     border on each edge are placed inside the floor, so a one-line chip
     measures 45.0px with its text in the middle (44px floor plus a pixel of
     line-box leading), and a wrapped chip simply
     grows with the same margin above and below. `padding-inline` stays the
     borrowed 0.6rem. */
  .per-page .per-src-chip {
    padding-block: calc((44px - 1.4em - 2px) / 2);
  }

  /* ── WS-D, 2026-08-09: the one-click log button, same floor and same scope ──
     `.lct-play` is the new play button on the leading edge of each template
     chip (owner item 7). It writes to the audit trail on ONE press, so it needs
     a target a thumb can hit deliberately - and on BOTH axes, because it is a
     round icon button with no text to widen it. 1.6rem is about 26px at rest.

     Scoped to `.per-page` for exactly the reason stated above this rule: the
     same component is mounted by the WhatsApp inbox's lead panel, which has not
     been measured, and a 44px circle in a 380px sidebar is a layout change
     nobody checked. Beside the 44px `.lct-chip` on this page the two match. */
  .per-page .lct-play {
    min-block-size: 44px;
    min-inline-size: 44px;
  }

  /* The return chip is the crumb line's one link (the trail is gone,
     2026-08-11) and rests at 36px. `inline-flex` because an inline element
     ignores a block-size, and the row is `static` so a taller chip costs
     nothing but a few pixels of a header that scrolls away anyway. */
  .per-crumb a {
    display: inline-flex;
    align-items: center;
    min-block-size: 44px;
  }

  /* ── The two general-detail fields that are also thumb targets ──────────
     The panel below the strip is a form now, and two of its controls are the
     ones a staff member uses standing up: the owner picker and the "assign to
     me" button beside it. `.form-select` and `.btn-sm` are both well under 44px
     at this width, and unlike the seat controls above they are NOT covered by
     `.reg-status-select`'s floor, because they are the shared form kit rather
     than the registrations kit. The 16px font floor at the top of this block
     already covers the typing half of the same problem.

     (`.per-hl { position: relative }` and `.per-hl__link::after` used to sit
     here: a pseudo-element hit area for the `tel:` and `mailto:` links, whose
     16px line box could not be made taller inside a hard 56px row. Both links
     left row B on 2026-08-09 - the panel is where they live now, and there a
     control can simply be 44px tall.) */
  .per-gen__owner-select,
  .per-gen__take,
  .per-fld__pen,
  .per-fld__btn,
  .per-fld__input,
  .per-tabs__fold {
    min-block-size: 44px;
  }

  /* The pencils and the two editor buttons also need the horizontal half of the
     target. The band wraps, so a taller cell costs a phone reader nothing that
     the fold is not already about to reclaim. */
  .per-fld__pen,
  .per-fld__btn,
  .per-tabs__fold {
    min-inline-size: 44px;
  }

  /* AND EVERY READ ROW TAKES THE SAME HEIGHT, whether or not its cell happens to
     contain a control. Without this, the rules above gave a 44px read row to the
     cells holding a pencil, the arrival chip or the `אליי` button and a 24px one
     to the cells holding only text - so within a single wrapped line the values
     sat up to 20px apart and nothing shared a baseline. Measured at 1024 and at
     390. `align-items: center` is what puts the value in the middle of the taller
     box rather than at the top of it. */
  .per-fld__read {
    min-block-size: 44px;
    align-items: center;
  }

  /* The tab strip's fold chevron loses its word and keeps its glyph: at these
     widths the three tabs need every pixel, and the control is beside a band
     that says `פרטים כלליים` in its own `aria-label`. */
  .per-tabs__fold-label {
    display: none;
  }

  /* And the status control in the strip itself. It is deliberately NOT
     `.reg-status-select` (see its base rule), so the floor three rules above
     does not reach it - and it is the one control in row B a staff member uses
     one-handed while the person is on the line. 44px inside a 56px row leaves
     6px above and below, the same budget `.per-action` takes.

     BOTH SPELLINGS, because which element renders here depends on the viewer
     and, since 2026-08-09, on the width: a `<button>` pill for anybody who may
     write, a `<select>` only above 1024px, and a read-only `<span>` for a
     `leads:read` reader. The `<span>` is deliberately NOT in this list - it is
     not a target, and giving a label a 44px height would make it look like one. */
  .per-id__status-select,
  .per-id__status-btn {
    min-block-size: 44px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   18. THE TABLET BAND - 768px to 1024px
   ══════════════════════════════════════════════════════════════════════════

   The one width where there is room for two highlight columns but not for five
   in a 56px row. Precedent: admin-leads.css's two-cards-abreast grid. It is
   also the WhatsApp panel's OVERLAY band: 768px cannot hold a readable tab and
   a 320px-floor conversation side by side, so the panel stops being a dock and
   starts being a thing that covers what is behind it. */

@media (min-width: 768px) and (max-width: 1024px) {
  /* `.per-hl--created { display: none }` used to open this block: at a tablet
     width row B could hold four of its five highlight cells, and the created-at
     stamp was the one a reader could get from the Lead tab without losing
     anything. Row B carries ONE cell now (owner item 3.3) and the created-at
     stamp is a field in the general detail panel, at every width, with its real
     date beside the elapsed label rather than hidden in a `title`. */

  /* `.per-highlights { gap: 0 0.85rem }` USED TO STAND HERE and is gone with
     the box itself: the group is `display: contents` since 2026-08-09 and has
     no gap to set. It held five cells when a density nudge meant something and
     holds one now. */

  /* ── THE STATUS PILL GETS ITS FULL WORDING BACK IN THIS BAND ────────────
     The ≤1024px block caps it at 14ch, and that number is a 390px phone's
     budget: there the ceiling is paid for out of the person's name. Here it is
     not. Measured at 768x900 and 1024x900 with the ceiling at 20ch, the pill
     draws `השתתף ביום פתוח` - fifteen characters, every label in the shipped
     taxonomy but the longest - against a name of 143.1px at 768px and its full
     168.3px at 1024px.

     This band is the one that matters most for this control: an iPad held in
     the hand is where open-day handling actually happens, it is a device with
     no hover and therefore no `title`, and on the Open Day and Enrollment tabs
     this pill is the only statement of CRM status the page carries. */
  .per-id__status {
    max-inline-size: 20ch;
  }

  /* ── THE ACTION CLUSTER GOES ICON-ONLY IN THIS BAND ─────────────────────
     768px is the width where the DESKTOP action bar still renders (`isPhone`
     is <=767) and there is least room for it. Measured in Chromium on
     2026-08-09 at 768x900: `.per-header__actions` was 538.3px of a 720px row -
     three worded buttons plus the WhatsApp pill plus the overflow trigger -
     which left `.per-id` 68px for a name, a status dropdown and an intent
     glyph, and the enrollment cell nothing at all.

     Dropping the three labels is the same trade the phone bar already makes
     one breakpoint down, and it costs no meaning: every button carries the
     same string in `aria-label` AND in `title`, so the accessible name and the
     tooltip are unchanged. The buttons stay 44px tall and become square, which
     is a bigger touch target than the worded pill was. */
  .per-actions--row .per-action {
    inline-size: 44px;
    padding-inline: 0;
  }

  .per-actions--row .per-action__label {
    display: none;
  }

  /* The WhatsApp pill's three words go with them, for the same 168px. Hiding a
     LABEL is a density step and is allowed; hiding a SEGMENT is not (owner's
     call, 2026-08-04), and this rule touches only the label. Every destination
     still renders and still carries its full wording in `title` and
     `aria-label`.

     Prefixed `.per-page .per-actions--row` for the reason the phone block
     states about its own copy: a bare `.wa-seg__label` would restyle six other
     render sites - the leads list, the lead panel, the open-day table and card,
     the inbox thread header and the blocked composer. */
  .per-page .per-actions--row .wa-seg__label {
    display: none;
  }


  /* ── The overlay ───────────────────────────────────
     Set by the component only in this band and only while open. It undoes the
     stacked shape three rules up and re-floats the panel over the tab, sharing
     row 1 with `.per-main` (placed there by the `:has()` rule in section 17).

     `justify-self: start` puts it on the pane's inline-START, which in this
     permanently-RTL island is the physical RIGHT - the same side the desktop
     dock lives on since 2026-08-09, so it appears where the rail the reader
     just pressed was. It was `end` (physical left) until that move, and it had
     to travel with the dock rather than being left behind: a rail pressed on
     one side that opens a panel on the other is the specific half-done outcome
     decision D7 names.

     `position: sticky` and never `fixed` or `absolute`: fixed would take a rung
     of the global z-ladder and collide with the phone action bar, and absolute
     inside a wrapper thousands of pixels tall would park the panel's header at
     the top of the DOCUMENT, off screen for anyone who has scrolled. Local
     z 30 over the scrim's 20, exactly as `.wai-lead` does it, and no rung
     consumed.

     `min(380px, 90vw)` never resolves below 380px in this band (90vw of the
     narrowest tablet is 691px), so the 320px floor the dock promises holds
     here too - the `min()` is the guard for a future narrower band, not a
     shrink this one ever performs. */
  .per-wa--overlay {
    position: sticky;
    inset-block-start: calc(var(--per-rowb) + var(--per-band) + var(--per-tabs) + 0.5rem);
    z-index: 30;
    grid-row: 1;
    grid-column: 1;
    justify-self: start;
    inline-size: min(380px, 90vw);
    max-inline-size: none;
    block-size: calc(
      100dvh - var(--per-shellbar) - var(--per-rowb) - var(--per-band) - var(--per-tabs) -
      1.5rem
    );
    max-block-size: none;
  }

  /* THE COMMENTS OVERLAY, AND IT COMES IN FROM THE OTHER SIDE.
     `justify-self: end` puts it on the pane's inline-END, which in this
     permanently-RTL island is the physical LEFT - the same side its desktop
     column lives on, and the exact opposite of the dock's `start` above. That
     is the whole point of the pair: each flank slides out of the edge the
     reader already associates it with, so the rail they pressed and the panel
     that appears are on the same side of the screen. Getting this one value
     backwards is the half-done outcome decision D7 named for the dock.

     `min(380px, 90vw)` and not the column's 320: an overlay is not paying for a
     tab body beside it, so it may as well be readable. It never resolves below
     380px in this band (90vw of the narrowest tablet is 691px); the `min()` is
     a guard for a future narrower band, not a shrink this one performs. */
  .per-cm--overlay {
    position: sticky;
    inset-block-start: calc(var(--per-rowb) + var(--per-band) + var(--per-tabs) + 0.5rem);
    z-index: 30;
    grid-row: 1;
    grid-column: 1;
    justify-self: end;
    inline-size: min(380px, 90vw);
    max-inline-size: none;
    block-size: calc(
      100dvh - var(--per-shellbar) - var(--per-rowb) - var(--per-band) - var(--per-tabs) -
      1.5rem
    );
    max-block-size: none;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   19. PHONE - max-width: 767px
   ══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 767px) {
  /* `.per-od-danger-btn`'s 44px floor USED TO OPEN THIS BLOCK, and it is not
     gone - it moved up into `@media (max-width: 1024px)` on 2026-08-09, beside
     the seven other touch floors, because a tablet is a touch device too. The
     reasoning that put it here is quoted there in full. */

  /* `.admin-main` is `1rem 0.75rem ...` here. */
  .per-page {
    --per-bleed: 0.75rem;

    /* Re-measured for this band: `.admin-mobile-bar` drops to
       `padding: 0.5rem 0.75rem` (8px + 8px) around the same 38px hamburger,
       plus its 1px bottom border. 55px. See the tablet block for why the number
       has to exist at all. */
    --per-shellbar: 55px;

    /* ── ROW B'S RESERVATION, IN THIS BAND'S OWN TERMS ──────────────────────
       Third and last restatement of the four tokens at the top of this file.
       Every term changes again here: the worklist arrows take the 44px touch
       floor (admin-lead-detail.css), `.per-id` and `.ld-nav` both drop to a
       0.3rem gap two rules down, and the whole desktop action cluster leaves
       this row for the docked bottom bar - so what row B still has to fit
       beside the identity group is a single 44px overflow kebab.

       The status floor drops to 44px, and that number is chosen rather than
       inherited: it is the touch floor, i.e. the width at which the control is
       still a target a thumb can hit and open. The label inside it is 8ch at
       most in this band anyway and the full wording is in `title` and
       `aria-label`, so what 44px costs is characters, and what it buys is the
       ONE status control this page has on two of its three tabs staying on
       screen at 360px. Rung 5 says the control never drops and never goes below
       a usable width; 44px is that width on a phone.

       The name's floor is 7ch of a 1.05rem `<h1>`, which is 7.35ch measured on
       this box - the same 1.05 conversion the desktop term makes for 12ch. */
    --per-nav-floor: calc(2 * 44px + 2 * 0.3rem + 7.35ch);
    --per-status-floor: 44px;
    --per-id-gap: 0.3rem;

  }

  /* NOTHING WAS ADDED TO ROW B ON A PHONE, and that is deliberate rather than
     an omission. The row is 296px of content at 320px, and the four things in it
     that may never yield - two 44px arrows, a 7ch name and a 44px status control
     - plus the overflow kebab already spend 290px of it. Measured, by
     `tests/personRowBGeometry.spec.ts`: a fifth control here did not merely
     crowd the row, it cut the STATUS PILL down to `נרשם ל`, which is also the
     first six characters of another status - so the row stopped being able to
     say whether somebody had paid or only booked a free open day.

     The detail band's fold chevron is on the TAB STRIP, and the shell does not
     ask for it on a phone at all: the band is purely scroll-driven there and
     keeps no stored answer (`usePersonDetailBand`). */

  /* ── THE BOTTOM CLEARANCE FOR THE DOCKED ACTION BAR ──
     8px of bar padding + a 48px target + 8px of bar padding = 64px, which is
     the bar's computed block-size and must stay equal to it. The bar is
     `position: fixed` and therefore out of flow, so without this it parks on
     top of the last row of content - here the comments footer, which is the one
     thing a reader scrolls to the bottom of this page for.

     On `.per-page` and NOT on `.admin-main`, and not through `:has()`.
     `.admin-main` is the shell's shared scroller: it already pays its own
     `calc(1.25rem + env(safe-area-inset-bottom))` at this width, it is read by
     every other admin screen, and reaching up to it from here is how the inbox
     once ended up holding 132px back for a 56px bar. The modifier class is on
     the page root exactly while the bar is rendered, which is the same fact
     stated where it belongs.

     `.adm-tabbar` is NOT rendered on this route (`AdminTabBar.vue`'s
     `visible = canWork && !inThread && !onPerson`), so admin.css's
     `.admin-layout:has(.adm-tabbar) .admin-main` clearance correctly does not
     match and there is no second band underneath this one. */
  .per-page--actionbar {
    padding-block-end: calc(64px + env(safe-area-inset-bottom, 0px));
  }

  /* The kit's own phone rules reset `margin-left` on these three, and our auto
     margin is on the OTHER side - so without this the block keeps an auto
     margin nobody cancelled and refuses to go full width. */
  .per-page .ld-wa-inbound,
  .per-page .ld-journey-ts,
  .per-page .ld-enroll-date {
    margin-inline-start: 0;
    inline-size: 100%;
  }

  /* The lifecycle map's shared phone rules stack the stages into one column.
     Correct here too, but they are spaced for a page that IS the chart; bolted
     under an already long tab, the stage gutters add a screenful of dead air.
     NOT RTL mirrors - density fixes that happen to live in the same block. */
  .per-page .llc-stage + .llc-stage {
    margin-top: 1rem;
  }

  .per-page .llc-stage + .llc-stage::before {
    margin: -0.6rem 0 0.4rem;
    font-size: 0.7rem;
  }

  .per-page .llc-family {
    padding: 0.6rem 0.7rem;
  }

  /* THREE `display: none` RULES USED TO STAND HERE, and all three are gone with
     the cells they hid (owner item 3.3, 2026-08-09): `.per-hl--desk` (the two
     timestamps), `.per-hl--email` and, in section 17, the owner chip. This row
     used to pay for its 56px by dropping one supplementary cell per breakpoint
     until only the money line was left on a phone - which is exactly the row it
     now carries at EVERY width, by construction rather than by subtraction. The
     four cells are fields in the general detail panel below, where nothing is
     hidden and where three of them are editable.

     `--desk` and `--email` no longer exist as class names anywhere in the
     markup. */

  /* THE TWO `min-inline-size: 0` DECLARATIONS THAT USED TO STAND HERE ARE GONE,
     and the behaviour they produced is unchanged: the pair now carries the same
     zero at EVERY width (see `.per-highlights` and `.per-hl--money` in section
     6), so this band no longer has to hand anything back.

     Worth keeping from the note they carried, because it is the sharpest
     statement of the invariant in this file: the phone's `#actions` slot really
     is empty - `PersonActionBar` leaves row B at `isPhone` - but the slot AFTER
     it is not. `AdminLeadDetail` fills `#overflow` in the same row with an
     opaque 44px kebab. A cell that keeps a floor its group has given up hangs
     outside the group, and the only thing out there is that kebab: measured, the
     cell held 85.1px inside a group of 0px and six of twelve `elementFromPoint`
     probes across the value returned `.per-actions__more`. The row is RTL and
     the amount is an LTR `<bdi>`, so what was covered was the LINE'S END - the
     ellipsis and the leading digits - and a person who had paid ₪12,000 of
     ₪18,000 rendered as `שולם 0` with no truncation mark at all.

     The density nudge that used to close this note (`gap: 0 0.7rem`) is gone
     with the box: `.per-highlights` is `display: contents` since 2026-08-09,
     the money cell it holds is its own flex item of `.per-yield`, and there is
     no longer a group with a gap in it. */

  /* ── THE LAST 14px OF A 390px ROW ───────────────────────────────────────
     Row B at 390 holds two 44px arrows (the touch floor, and not negotiable),
     a 7ch name, a status dropdown, the enrollment line and the overflow kebab.
     Measured on 2026-08-09 it was 13px over, which showed as the "next" arrow
     bleeding 5.8px onto the dropdown. Nothing in it can be dropped - every one
     of those is either a control or the only identity on the page - so the
     GAPS pay instead. 0.3rem is still 4.8px of clearance between two boxes that
     are each at least 44px tall, which is well inside the mis-tap budget.

     `.per-id { gap: 0.3rem }` USED TO STAND HERE as a literal. It is now
     `--per-id-gap`, set with the other three reservation tokens at the top of
     this block, because `.per-id`'s floor is built out of this exact number -
     a gap that is narrowed in one place and still summed at its old value in
     another is a reservation that is 2.4px wrong and nothing says so.

     ── AND THE BLOCK CAP IS RESTATED HERE, FOR THE SAME REASON THE FOUR
        RESERVATION TOKENS ARE ─────────────────────────────────────────────
     `.per-id .ld-nav` carries `max-block-size: 2.25rem` in section 6, and 36px
     is a number about the DESKTOP control: it is what wraps the worklist
     counter off the line (`7 / 43` drawn whole or not at all, invariant C) over
     arrows that are 34px there. In this band the counter is `display: none`
     two hundred lines up, so the cap has no counter left to wrap - and the
     arrows take the panel's 44px touch floor (`admin-lead-detail.css:96-99`).
     A 36px cap over a 44px control, on a box that clips, is 8px of the control
     cut off.

     Measured in Chromium on 2026-08-09, every phone width in the sweep and both
     sidebar states: each arrow's border box was 44 x 44px and a hit test down
     its own centre column returned it for 36 of those 44px. The bottom 8px was
     neither painted nor hit-testable - the round buttons rendered with their
     lower arc sliced flat, and a thumb landing there hit row B and did nothing.
     That is invariant A's failure with the clip on the BLOCK axis instead of
     the inline one, in the band D10 moved the arrows into this row to serve.

     44px, which is the arrows' own floor in this band and not a fourth number:
     line 1 is exactly the control, and the `row-gap: var(--per-rowb)` a wrapped
     line would start after is 56px, so a second line still begins 100px down
     and is still cut off unread. The discrete step survives; only the number it
     is measured against is this band's. */
  .per-id .ld-nav {
    gap: var(--per-id-gap);
    max-block-size: 44px;
  }

  /* ── THE GENERAL DETAIL PANEL, TWO UP ON A PHONE ────────────────────────
     The panel sits ABOVE the tab strip on every tab, so its height is height a
     reader scrolls past to reach the tab they came for. At one column its
     eleven fields measured 773.4px at 390x900 in Chromium on 2026-08-09 -
     two screenfuls before the tabs.

     `minmax(150px, 1fr)` puts two fields on a 390px phone (173px each, which
     holds a ten-digit Israeli number at the 16px iOS floor) and brings the
     panel to about 440px. It is the SAME `auto-fit` declaration as the base
     rule with a smaller floor, inside a media block that already exists - this
     file may declare exactly four, and a fifth fails
     `adminPersonRtlMirrors.spec.ts`.

     The follow-up spans both columns: its date input, time input and clear
     button measure about 245px together, so at 173px it would wrap and leave
     the grid ragged around it. */
  /* THE PHONE BAND WRAPS AND IS NEVER CLIPPED, and the second half of that is a
     correction. It shipped with `max-block-size: 7.5rem` + `overflow: hidden`,
     sized for seven cells at two per line. There are EIGHT - the name is a cell
     too - so the cap fell mid-band and took `פולו-אפ`, `ערוץ הגעה`, `נוצר` and
     `קשר אחרון` off the screen entirely. The follow-up date is among them, which
     is the one thing a salesperson looks up mid-call. Screenshotted at 390x844.

     A clip is survivable in the identity row above, where everything it drops
     has a fuller home one glance away and a chevron can bring the band back.
     Here it is neither: this IS that fuller home, and a phone draws no chevron
     at all. So the band takes the height its content needs, and the fold - which
     happens on the first scroll - is what gives the height back. */
  .per-gen__row {
    /* THE ROW GAP IS BIGGER THAN THE GAP INSIDE A FIELD, which at 0.3rem it was
       not: the 44px touch floor on `.per-fld__read` inflates the value box, so
       label-centre to value-centre measured 29.7px and value-centre to the NEXT
       LINE's label-centre measured 33.7px - a 0.88 ratio, close enough to read
       as one four-line list rather than as four labelled pairs. Proximity is the
       only thing grouping them; there is no rule and no panel. */
    gap: 0.7rem 0.9rem;
    padding-block: 0.4rem 0.45rem;
  }

  /* The follow-up takes its own line: its editor is a date input, a time input
     and a clear button, about 245px together, which is most of a 390px screen
     and all of a 320px one. */
  .per-fld--editing {
    flex: 1 0 100%;
  }

  /* ── THE BADGE RAIL, WHICH THE PHONE CANNOT AFFORD ──
     The only child of `.per-id` that can shrink FREELY is the name, so every
     pixel a badge takes is taken off the person's identity.

     Only the two PROVENANCE badges are reached by this rule: section 17 hides
     them outright below 1025px and this is what they would get if that ever
     changed. Ellipsised text is the right failure for them, since each carries
     its full wording in a `title`. The intent badge is exempt from the hide and
     is a bare glyph at every width, so it neither needs this clamp nor is
     touched by it (10ch is 3x its measured 29.6px).

     REVISITED 2026-08-09 (owner item 12) and left at 10ch. The desktop CEILING
     it used to reference - 14ch - is gone; what governs the pills above 1024px
     now is the name's own 12ch floor, which makes them the side that yields.
     This value stays because it is the guard for a hide that might one day be
     lifted, and 10ch is still the right answer at 390px. */
  .per-id__badge {
    max-inline-size: 10ch;
  }

  .per-name {
    font-size: 1.2rem;
  }

  /* The tab labels are what give way, not the badges: the count is the fact
     worth keeping when there is no room for both. */
  .per-tabs .per-tab {
    flex: 1 1 0;
    padding-inline: 0.4rem;
    font-size: 0.8rem;
  }

  .per-tab__label {
    white-space: nowrap;
  }

  /* Full-width targets in the sheet and in every empty state, at the 44px floor
     the rest of the panel uses. */
  .per-empty__cta,
  .per-empty__retry {
    inline-size: 100%;
    min-block-size: 44px;
  }

  /* ══ THE FULL-SCREEN CONVERSATION ════════════════════
     Set by the component only here, and only while `?wa=1` is in the URL - the
     flag is what makes the browser Back button close this, and what makes the
     link shareable between two staff members on one thread.

     It shares row 1 with `.per-main` (placed there by the `:has()` rule in
     section 17) and paints over it, at the panel's own local z 30. That is
     above this page's sticky chrome (row B at 12, the tab strip at 11) and
     below the shell's mobile bar at 1010, which stays visible - covering that
     would need `position: fixed`, which S19 forbids for good reasons that have
     not changed.

     Height is the scrollport's height and not `100dvh`: `.admin-layout` is
     `height: 100dvh` with the shell's bar above `.admin-main`, so a plain
     `100dvh` here would hang the panel's last 55px - which is where the "open
     the full conversation" button lives - below a fold nothing can scroll to.

     And it tracks `--app-vvh` for the same reason the shell rule in section 17
     does, because the shell rule alone does NOT reach it: shrinking
     `.admin-layout` shrinks the scrollport, but a panel still asking for a full
     `100dvh` simply overflows it and puts its composer back under the keyboard.
     The two have to move together. With no holder mounted the fallback makes
     this the same declaration it has always been.

     Full-bleed through the same `--per-bleed` idiom the two sticky header rows
     use: exactly `.admin-main`'s inline padding at this width and never one
     pixel more, because `.admin-main` is `direction: ltr` and an RTL island's
     overflow runs leftward, where that scroller can never reach it. */
  .per-wa--full {
    position: sticky;
    inset-block-start: 0;
    z-index: 30;
    grid-row: 1;
    grid-column: 1;
    margin-inline: calc(-1 * var(--per-bleed));
    inline-size: auto;
    max-inline-size: none;
    block-size: calc(var(--app-vvh, 100dvh) - var(--per-shellbar));
    max-block-size: none;
    border-radius: 0;
  }

  /* `visibility: hidden` and NEVER `display: none`. The box has to stay: it is
     what keeps the document its own height and the reader their own scroll
     offset, which is the entire reason S29 chose a query flag over a route. It
     is also the grid row the sticky panel travels inside - collapse it and the
     panel has nothing to stick to. */
  .per-panes:has(.per-wa--full) .per-main,
  .per-panes:has(.per-cm--full) .per-main {
    visibility: hidden;
  }

  /* THE COMMENTS PANE, FULL SCREEN. Byte for byte the dock's shape above,
     because it is the same view of a different thing and the reader should not
     have to learn two. Set by the component only here, and only while `?cm=1` is
     in the URL - the flag is what makes the browser Back button close this,
     which on a phone is the gesture people actually use.

     Two flags exist on this page now, `?wa=1` and `?cm=1`, and they are mutually
     exclusive by construction: `usePersonSidePanels.openQuery()` strips the
     sibling on the way in, and if a hand-edited URL carries both, WhatsApp wins
     and this pane stays shut. So there is never a state in which both of these
     `--full` rules apply. */
  .per-cm--full {
    position: sticky;
    inset-block-start: 0;
    z-index: 30;
    grid-row: 1;
    grid-column: 1;
    margin-inline: calc(-1 * var(--per-bleed));
    inline-size: auto;
    max-inline-size: none;
    block-size: calc(var(--app-vvh, 100dvh) - var(--per-shellbar));
    max-block-size: none;
    border-radius: 0;
  }

  /* EXACTLY ONE FIXED BOTTOM BAR, EVER. A full-screen conversation and a docked
     action bar are two of them on a 390px screen, which is the failure the
     one-bar rule exists to prevent. This is admin.css's own `.leads-page--bulk`
     precedent - "a contextual action bar REPLACES navigation for the duration
     of the selection" - applied to the panel instead of to a selection. */
  .per-page:has(.per-wa--full) .per-actions--bar,
  .per-page:has(.per-cm--full) .per-actions--bar {
    display: none;
  }

  /* The same rule, for the same reason, against a second offender - and this
     one was not cosmetic. `AdminSheet` renders IN PLACE, so on this page its
     layer is a child of `.per-header__row--b`, which is `sticky; z-index: 12`
     and therefore a stacking context: the layer's own `z-index: 1040` is
     confined inside it and cannot outrank the bar's 1015 in the root context.
     Measured 2026-08-08 at 390x844: the sheet's only row is `מחיקה`, spanning
     y 784-828, and the bar spans 777-844 - 44 of 44px covered.
     `elementFromPoint` at the row's centre returned `.per-action--call`, and a
     real click was refused with "fa-phone intercepts pointer events". A staff
     member reaching for delete was dialling the customer.

     Hiding the bar rather than re-ordering the z-indexes is deliberate: row B
     cannot simply be raised above 1015, because it would then also paint over
     the view modals at 1000, and the sheet is modal anyway - it has its own
     scrim, so nothing under it was meant to be reachable. Owner's call, (a) of
     three options, 2026-08-08.

     ── AND IT MAY NEVER STAND DOWN FOR A SHEET OF ITS OWN (2026-08-09) ──────
     `PersonActionBar variant="bar"` mounts an `AdminSheet` INSIDE this element
     - that is what the bar's `...` button opens - so without the `:not(:has())`
     half the condition matched the bar's OWN layer and `display: none` removed
     the bar and the sheet together, in one step. Five overflow actions (Email,
     the WhatsApp template, the course enrolment, the contract and the refresh)
     were drawn nowhere; the sheet's close button and its scrim went with them,
     so there was nothing left to tap to dismiss it; and a phone has no Escape
     key, so `sheetOpen` stayed true and the bar did not come back for the rest
     of the visit. The reader had to leave the person to get their actions back.

     It is the same guard `.per-wa` carries three thousand lines up, for the
     same reason and against the same component - see the `z-index: auto` rule
     in section 6. The condition both express is "a sheet is open on this page
     AND it is not this element's own"; a sheet raised from inside the bar has
     nothing to fear from the bar, because the bar is its own ancestor and its
     1015 stacking context is what the sheet paints inside.

     Tests: tests/adminPersonMobileGuards.spec.ts declares the rule;
     tests/personStatusSheetHit.spec.ts hit-tests the bar's own five rows in a
     real browser, which is the only kind of assertion that can see this. */
  .per-page:has(.adm-sheet-layer):not(:has(.per-actions--bar .adm-sheet-layer)) .per-actions--bar {
    display: none;
  }

  /* The panel reaches the bottom edge of the viewport in this band, so the home
     indicator eats the last line of the transcript without this. `max()` and
     not `+`: 0.75rem is the padding the body wants anyway, and on a device with
     no inset the safe-area term is 0. */
  .per-wa--full .per-wa__body,
  .per-cm--full .per-notes__body {
    padding-block-end: max(0.75rem, env(safe-area-inset-bottom, 0px));
  }

  /* ── THE EMBEDDED THREAD IS `display: none` ON EVERY PHONE WITHOUT THIS ──────

     `admin-whatsapp.css` shows ONE inbox pane at a time from 767px down, and it
     does it by turning both of them off and then turning one back on:

         .wai-list, .wai-thread          { display: none }
         .wai-page--list   .wai-list     { display: grid }
         .wai-page--thread .wai-thread   { display: grid }

     The first of those three is a BARE selector, so it reaches a `.wai-thread`
     wherever it is mounted - including inside this page's docked panel. The only
     rule that can undo it keys on `.wai-page--thread`, and this page's DOM may
     never contain a `.wai-page` of any kind (the ban at the top of this file, and
     `tests/personPhase1Gate.spec.ts` gate 4). So there is no path back on, and
     without this line the docked conversation is simply GONE on every phone: the
     panel opens full screen, its head and its pills render, and the thread that
     is the entire point of it renders nothing, with no error anywhere.

     That is the worst-shaped defect available on this surface - invisible on the
     desktop the CSS is written on, total on the device the work is actually done
     on. Open-day handling happens standing up, at the venue, on a phone.

     `display: grid` and not `revert`: grid is the value the inbox restores, and
     `.wai-thread`'s four `grid-template-rows` mean nothing without it.

     NO `!important`, and it is not needed. `.per-wa .wai-thread` scores (0,2,0)
     against admin-whatsapp.css's (0,1,0), so it wins on specificity alone - and
     even at a tie this file is the LAST link in index.html's admin block, which
     is a stated contract there (see the comment above the link) and not a
     coincidence of ordering.

     Keyed on `.wai-thread` and NOT on the `.per-wa__thread` class the host also
     applies, for the reason set out where the base rule is stated: a host that
     forgets that class would take this whole defect back, silently, on phones
     only. Verified at a real 390px viewport, both ways round - with this file
     linked the thread computes `display: grid` at 316x339; without it, `display:
     none` and a dock 78px tall containing nothing but its own header. */
  .per-wa .wai-thread {
    display: grid;
  }

  /* THE SCOPE NOTE'S PHONE RULES USED TO SIT HERE and are gone, because the
     problem they solved is gone with them. The sentence that stops a reader
     assuming the comments follow the tab lived INSIDE `.ld-card-head`, a nowrap
     flex row, so at 390px it had to be granted a wrap and a full-width basis or
     it overflowed. It is a block UNDER the head at every width since 2026-08-10
     - see section 11 - which the 300px desktop column forced and which reads
     better anyway: it is a caption for the list, not a decoration on the title.
     Two rules that fire on a structure that no longer exists are worse than no
     rules, so they were deleted rather than left as ballast. */

  /* ══ THE DOCKED ACTION BAR ═══════════════════════════
     `.per-actions--row` is NOT rendered at this width any more - the shell
     swaps it for `variant="bar"` - so the two interim rules that used to stand
     here (label hidden, button squared to 44px) have been deleted rather than
     left to match nothing.

     z-index 1015 is the same rung as `.adm-tabbar` and `.leads-bulk-bar`: above
     content, and BELOW `.modal-overlay` at 1018, which is not arbitrary either
     - a modal footer behind a 1015 bar had its buttons swallowed by nav links
     once already, and that is why 1018 exists.

     THE HARD BAN, and it is the one thing here that fails silently: no
     `backdrop-filter`, `filter`, `transform`, `perspective`, `contain` or
     `will-change` on this element or on any ancestor of it. It is an ancestor
     of `AdminSheet`'s `position: fixed` layer (the sheet renders in place, with
     no Teleport), and any of those properties makes this 64px bar the
     containing block for it - the overflow sheet would open INSIDE the bar. The
     flat opaque fill below is the substitute, and it is the same answer
     `.adm-sheet` reached when it refused a blur for the identical reason.

     Computed block-size: 8 + 48 + 8 = 64px plus the safe area, which is the
     number `.per-page--actionbar` reserves at the top of this block. The two
     move together or the bar covers the last comment. */
  .per-actions--bar {
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: 1015;
    padding-block: 8px;
    padding-inline: 12px;
    padding-block-end: calc(8px + env(safe-area-inset-bottom, 0px));
    background: #fff;
    border-block-start: 1px solid var(--adm-line);
    box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
  }

  /* The WhatsApp pill is one of the four actions and always the first: on this
     screen the question a salesperson is answering is "which client do I answer
     from", and that must not be inside a menu. It takes the slack.

     Width proof at 390px: 390 - 24 (the bar's inline padding) = 366 available.
     Fixed: 3 x 48 (buttons) + 44 (kebab) + 4 x 8 (gaps) = 220. The pill absorbs
     the remaining 146, which is three ~48px segments. `min-inline-size: 0` on
     both the pill and the buttons is what makes a 360px device lose pill width
     instead of wrapping the bar onto a second row. */
  .per-actions--bar .per-actions__wa {
    flex: 1 1 auto;
    min-inline-size: 0;
  }

  /* Both `.wa-seg` rules stay prefixed `.per-page .per-actions--bar`. A bare
     `.wa-seg` would restyle six other render sites (the leads list, the lead
     card, the open-day table and card, the inbox thread header, the inbox's
     blocked composer), and the group's own phone rules are already at (0,2,0)
     - `.wa-actions--nostack .wa-seg { min-height: 40px }` - so this needs the
     third class to win on merit rather than on file order alone. */
  .per-page .per-actions--bar .wa-seg {
    flex: 1 1 0;
    min-inline-size: 0;
    min-block-size: 48px;
    padding-inline: 0;
    justify-content: center;
  }

  /* Icon-only, and no SEGMENT is hidden - the owner's 2026-08-04 rule is that
     all three destinations show on every device, and all three still do. Only
     the WORDS go, and each segment's `title` and `aria-label` still name its
     destination, so the accessible name is unchanged. */
  .per-page .per-actions--bar .wa-seg__label {
    display: none;
  }

  /* 48px for the primary set: it satisfies Apple's 44pt floor and Material's
     48dp one at the same time, and there is vertical room here for it to mean
     something (the desktop row is uniformly 44px because everything in it is
     primary, so 48 would distinguish nothing). Square, because the label is
     carried by `aria-label` rather than by pixels. */
  .per-actions--bar .per-action {
    flex: 0 0 auto;
    inline-size: 48px;
    min-block-size: 48px;
    padding-inline: 0;
  }

  .per-actions--bar .per-action__label {
    display: none;
  }

  .per-actions--bar .per-action__icon {
    font-size: 1rem;
  }

  /* 44px, not 48: the overflow trigger is explicitly not part of the primary
     set. 8px of clearance from the button beside it comes from the bar's own
     `gap`, and nothing destructive is in this bar, so no 16px band is owed
     here - see `.per-actions--menu` for where that debt is actually paid. */
  .per-actions--bar .per-actions__more {
    flex: 0 0 auto;
    inline-size: 44px;
    min-block-size: 44px;
  }

  /* ══ THE DESTRUCTIVE OVERFLOW, UP IN ROW B ═══════════
     Deleting a person may not ride in a bar under the thumb: a sheet raised
     from a bottom bar opens directly beneath the thumb that just tapped it, so
     the one control that empties a salesperson's record would sit exactly where
     a mis-tap lands. It stays at the top of the screen, inside sticky row B,
     which is reachable without scrolling at any offset.

     The 16px is the destructive-isolation gap from the highlights group that
     precedes it. Field data behind that number: 6px between a transfer and a
     cancel button produced a 34% mis-tap rate, and 14px dropped it under 8%.

     ── IT CARRIES ITS OWN ANCHOR NOW, AND THAT REVERSES THE PARAGRAPH THAT
        USED TO STAND HERE (2026-08-09) ────────────────────────────────────
     What stood here said the kebab "sits at the inline-end because
     `.per-header__actions` - zero-width on a phone, holding only a Vue comment
     node - keeps its `margin-inline-start: auto`", and warned in as many words
     against adding `.per-header__actions:empty { display: none }` because "the
     auto margin would go with it, and the kebab would drift into the middle of
     row B". That rule was then added anyway, one band up in section 6, to save
     the empty slot's 12px of row gap - and the drift it predicted is exactly
     what shipped. The file was warning about its own behaviour.

     Measured in Chromium at every phone width in the sweep, both sidebar
     states, the kebab's distance from the row's inline end (the PHYSICAL LEFT
     here, the row being RTL):

       767px  sale + both provenance marks + worklist   90.3px in
       767px  short name, no worklist                  359.8px in
       667px  no sale, no worklist                     259.8px in
       430px  no sale, no worklist                      22.8px in

     ...and it moves with the PERSON, because what is left over is whatever
     `.per-yield` did not need: the same screen puts the one destructive control
     on this page in a different place for two different people, which is the
     one thing a control a thumb reaches for by memory may not do.

     So the anchor moves onto the control that needs it instead of being
     borrowed from a box that may not be there. `margin-inline-start: auto` on a
     flex item absorbs the line's positive free space before `justify-content`
     sees it, which is the same mechanism `.per-header__actions` was providing;
     the difference is that this element is rendered at every width, so nothing
     can take it away.

     THE 16px COMES BACK AS PADDING, not as a margin, and it has to: an `auto`
     margin and a `16px` margin are the same property, so keeping the isolation
     band as a margin would mean choosing between the two. As padding on this
     wrapper the gap is inside the box - the trigger button stays 44px, the box
     becomes 60px - so the measured distance between the enrollment line and the
     button is unchanged at 28px (row B's own 12px gap plus this 16px) wherever
     the row is fully subscribed, and the row's width budget does not move by a
     pixel.

     No `position`, no `order`, nothing that could take it out of the row: row B
     is a hard 56px and this control has to stay inside it. */
  .per-actions--menu {
    flex: 0 0 auto;
    margin-inline-start: auto;
    padding-inline-start: 16px;
  }

  .per-actions--menu .per-actions__more {
    inline-size: 44px;
    min-block-size: 44px;
  }

  .per-panes {
    gap: 0.75rem;
    margin-block-start: 0.85rem;
  }

  /* ── Open Day tab ──────────────────────────────────
     The key column goes away entirely rather than shrinking. At 390px an 11rem
     key leaves the value about fourteen characters, and `סטטוס אישור הגעה ליום
     פתוח זה` sitting beside a dropdown in a 5rem column is unreadable either
     way. Stacked, every row is a label over its value, which is the shape the
     rest of the panel's phone layouts already use.

     The attribution sub-panel's grid stacks WITH it, and that is the reason both
     selectors are here rather than one: the two grids sit inside the same card,
     six inches apart, and one row reading label-beside-value while the row above
     it reads label-over-value looks like a rendering fault rather than a
     decision. Its own labels are short enough to survive either way, so the
     shared rhythm is what settles it. */
  .per-od-facts .ld-kv-row,
  .per-od-kv .ld-kv-row {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.15rem;
  }

  /* The read-only mode tag and the Zoom switch each take their own line. Side by
     side they fit, but the switch's own 44px target then overlaps the tag's tap
     area, and the tag is not a control. */
  .per-od-mode-cell {
    flex-direction: column;
    align-items: start;
  }

  /* `.per-od-plate-btn`'s 44px square USED TO STAND HERE, and it is not gone -
     it moved up into `@media (max-width: 1024px)` on 2026-08-09, for the same
     reason `.per-od-danger-btn` did and in the same commit. See that block. */

  /* The badge rail drops under its heading instead of being pushed to the far
     end of a 390px row, where a two-word channel label and a partner code would
     wrap into a ragged two-line block beside a 44px button. */
  .per-od-badges {
    flex-basis: 100%;
    margin-inline-start: 0;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   20. LANDSCAPE PHONE - max-height: 480px
   ══════════════════════════════════════════════════════════════════════════

   A phone on its side has ~360px of usable height, and the sticky chrome is
   104px of it. Stack nothing, shrink the chrome, and let the dock stop trying
   to be viewport-tall - on this shape it would leave the tab body a two-line
   slot. Height-keyed rather than width-keyed on purpose: a short window on a
   laptop has exactly the same problem, and a phone held sideways is ~700px wide
   - it fails BOTH width queries while having less vertical room than any phone
   in portrait.

   THIS BLOCK IS AUTHORED LAST DELIBERATELY. It has to beat the tablet overlay
   and the phone's full-screen panel, and it does so at equal specificity, on
   source order alone. Moving it above either of them silently un-does the
   force-stack on the one shape that needs it most. */

@media (max-height: 480px) {
  .per-page {
    --per-rowb: 48px;
    --per-tabs: 40px;
  }

  .per-tabs .per-tab {
    font-size: 0.78rem;
  }

  /* The touch block above gives `.per-tabs__fold` a 44px floor, and this block
     shrinks the strip it sits in to 40px while `.per-tabs` keeps
     `max-block-size: var(--per-tabs)` and no `overflow` - so the floor would put
     a 44px control in a 40px bar and let it paint outside it. A landscape phone
     under 768px does not render the chevron at all (the shell withholds it), so
     the widths this reaches are small landscape TABLETS, where a 32px target is
     the same one the desktop draws. */
  .per-tabs__fold {
    min-block-size: 32px;
  }

  /* Row A's name is the first thing worth giving up: the same name is clamped
     in row B one line below it, and on this shape the reader needs the panels. */
  .per-name {
    font-size: 1.05rem;
  }

  .per-header__row--a {
    padding-block: 0.15rem 0.5rem;
  }

  /* FORCE-STACKED, whatever the width. A landscape phone is wide enough to pass
     as a desktop, so the two-column dock survives the width queries and would
     leave both panes unusable on ~360px of height. */
  .per-panes {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The reset, and the belt-and-braces half of it in one rule. The component
     never sets `--overlay` or `--full` at this height (both computeds require
     `!isCompactHeight`), so the last two selectors are a guard rather than a
     working rule - but a guard is what stops a future change to that condition
     from floating a viewport-tall panel over a 360px-tall screen with no way
     back. Equal specificity to the rules in sections 18 and 19; this block wins
     because it is last.

     `grid-row: auto; grid-column: auto` now undoes SECTION 9's base placement as
     well, not only sections 18 and 19. That is what keeps the force-stack in DOM
     order after the 2026-08-09 side swap: with the base 1 / 1 left standing, an
     OPEN panel on a landscape phone would take row 1 and push the tab body under
     it, which is the opposite of what this block is for. */
  .per-wa,
  .per-wa--overlay,
  .per-wa--full {
    position: static;
    z-index: auto;
    grid-row: auto;
    grid-column: auto;
    margin-inline: 0;
    inline-size: auto;
    min-inline-size: 0;
    max-inline-size: none;
    block-size: auto;
    max-block-size: 70vh;
  }

  /* The comments pane gets the identical reset, and forgetting it is the single
     most likely way to break this change: with section 9's `grid-row: 1;
     grid-column: 3` left standing, a landscape phone would carry a
     viewport-tall column floating over a 360px-tall screen, in a track the
     one-column `grid-template-columns` above does not even define. */
  .per-cm,
  .per-cm--overlay,
  .per-cm--full {
    position: static;
    z-index: auto;
    grid-row: auto;
    grid-column: auto;
    margin-inline: 0;
    inline-size: auto;
    min-inline-size: 0;
    max-inline-size: none;
    block-size: auto;
    max-block-size: 70vh;
  }

  .per-panes:has(.per-wa--overlay) .per-main,
  .per-panes:has(.per-wa--full) .per-main,
  .per-panes:has(.per-cm--overlay) .per-main,
  .per-panes:has(.per-cm--full) .per-main {
    grid-row: auto;
    grid-column: auto;
    visibility: visible;
  }

  /* The rails still sit ABOVE the tab, and these rules have to be restated
     rather than inherited for two independent reasons. A landscape phone can be
     wider than 1024px, so section 17 never reaches it at all - and even where it
     does, the `grid-row: auto` in the resets above is a later declaration at
     equal specificity and would otherwise drop the rails back under the comments
     timeline, which is where nobody finds them. */
  .per-wa--collapsed {
    grid-row: 1;
  }

  .per-cm--collapsed {
    grid-row: 2;
  }

  .per-panes:has(.per-wa--collapsed):has(.per-cm--collapsed) .per-main {
    grid-row: 3;
  }

  /* No scrim: nothing is covered here, and a full-page dim over content that is
     not behind anything reads as a rendering fault. */
  .per-wa-scrim,
  .per-cm-scrim {
    display: none;
  }

  /* The stacked rail, restated - a landscape phone can be wider than 1024px, so
     none of section 17's rules reach it. 44px rather than 48: the chrome budget
     is 104px of about 360px, and this is the one shape where four pixels of
     rail are worth more than the extra thumb margin. */
  .per-wa__rail {
    flex-direction: row;
    justify-content: flex-start;
    block-size: auto;
    min-block-size: 44px;
    padding-inline: 0.9rem;
    font-size: 0.8rem;
  }

  .per-wa__rail-label {
    writing-mode: horizontal-tb;
    font-size: 0.8rem;
  }

  .per-wa__rail-hint {
    margin-block-start: 0;
    margin-inline-start: auto;
  }

  /* Vertical room is the scarce axis here, so the transcript's own gutters are
     the first thing to give. */
  .per-wa__body {
    padding-block: 0.5rem;
  }

  .per-wa__collapse {
    min-inline-size: 44px;
    min-block-size: 44px;
  }

  .per-wa__open-thread {
    min-block-size: 44px;
  }

  /* The stacked comments rail, restated for the same two reasons, at the same
     44px. Two rails of different heights stacked on top of each other read as
     two different kinds of control. */
  .per-cm__rail {
    flex-direction: row;
    justify-content: flex-start;
    block-size: auto;
    min-block-size: 44px;
    padding-inline: 0.9rem;
    font-size: 0.8rem;
  }

  .per-cm__rail-label {
    writing-mode: horizontal-tb;
    font-size: 0.8rem;
  }

  .per-cm__rail-hint {
    margin-block-start: 0;
    margin-inline-start: auto;
  }

  /* Vertical room is the scarce axis here, so the timeline's own gutters are the
     first thing to give - the same trade `.per-wa__body` makes above. */
  .per-notes__body {
    padding-block: 0.5rem;
  }

  .per-cm__collapse {
    min-inline-size: 44px;
    min-block-size: 44px;
  }

  /* Never rendered at this height (`canResize` requires `docked`, which
     requires a tall viewport). The guard costs one declaration. */
  .per-wa__resize,
  .per-cm__resize {
    display: none;
  }
}


/* The lead tab opens on the lifecycle card, and `admin-lead-detail.css` gives
   that card a 1.25rem top margin for the standalone screen it was built for.
   Here it stacks on the panel's own spacing and floats the first card away from
   the tab strip, so both are zeroed.

   BOTH SELECTORS ARE SCOPED TO `.per-page`, and that is not decoration. This is
   the LAST admin stylesheet in index.html, so a bare `.ld-lifecycle-card` here
   outranks nothing and follows everything - it would win on
   /admin/enrollments/:id as well, which renders the same borrowed card and
   still wants its margin. `tests/adminPersonRtlMirrors.spec.ts` fails the build
   on a bare `ld-`/`reg-`/`adm-` rule in this file for exactly that reason. */
.per-page #per-tabpanel-lead {
  margin-top: 0;
}

.per-page .ld-lifecycle-card {
  margin-top: 0;
}