/* ADspace Content Review Portal
   Shell + platform mockups. No build step, no framework. */

/* Neutrals are warm, not cool. A grey at low saturation with a blue-green cast
   reads clinical, which is the wrong register for somewhere a team sits all
   day; a few degrees towards yellow reads considered instead, and is what the
   quiet corporate sites this portal takes after actually use. Contrast is not
   lowered to achieve it: every pair here clears WCAG AA and `uxaudit` measures
   it on the real page. What is lowered is the weight of the boundaries, which
   is what made a screen of forms read as a cage.

   Surfaces are flat and separated by a line, not by a shadow on every box;
   depth is spent on the one thing that is raised (a sheet, a menu) and nested
   sections sit on a recessed surface instead.

   Every colour is a token, because a colour written into a rule cannot follow
   the theme. A fill and the text on it are always a pair (`--fill`/`--on-fill`,
   `--ok-solid`/`--on-ok`), since in dark the fill is light and white text on it
   would disappear. */
:root {
  --ink:        #1b1a17;
  --ink-soft:   #524e47;
  --ink-mute:   #6b6760;
  --line-ctl:   #8e8e8e;   /* control boundaries: 3:1 on every page background */
  /* The contact chip's outline, lighter than a field's (the user,
     2026-09-28): its words identify it, so its edge need not hold 3:1. */
  --line-chip:  #a1a1a1;
  --line:       #e2dfd8;
  /* The outline of a bounded card (a directory table, a record's head and
     Overview, a form panel, the booking register, the Team card): one
     token, so every card is the same card. Nothing nests a card in a card
     (checked on 2026-09-22: no panel in a sheet or a panel, no table in a
     panel or an Overview), so a transparent edge never loses a surface. Transparent since 2026-09-22, when the user chose the Team
     card's look for all of them: the card's own ground against the page is
     the edge, and the 1px stays so nothing is re-measured. */
  --card-line:  transparent;
  --line-soft:  #ececec;
  --page:       #f5f5f5;   /* ADspace's own off white, as on the website */
  --card:       #ffffff;
  --sunk:       #f9f9f9;
  /* The solid ink surface (primary button, selected tab, progress, undo bar)
     and the text that sits on it. */
  --fill:       #1b1a17;
  --on-fill:    #ffffff;
  --accent:     #1f7a4d;
  --ok:         #1f7a4d;
  --ok-bg:      #ecf5f0;
  /* The post date on the calendar: a pastel blue of the Pantone 2717 C
     family (2026-10-15, the user: purple to blue, "pantone pastel"), lighter
     and greyer than the action blue so a post date never reads as a button. */
  --pub:        #2a5793;
  --pub-bg:     #e6effb;
  /* Health's own mark (2026-10-07: "highlighted pantone pink with the health
     icons"): Pantone 214 C, on Health's glyph and its chosen answers alone. */
  --health:     #ce0f69;
  --health-bg:  #fcebf3;
  /* A Very Important Client's own mark (2026-10-15, the user chose gold A
     of two): a muted antique gold on its tag, its crown and the ring round
     its logo, nothing else; browner than Medium's yellow and cooler than the
     amber waiting dot. Words in `--vic-ink` hold 6.4:1 on white and 5.6:1 on
     `--vic-bg`. */
  --vic:        #b08a32;
  --vic-ink:    #7a5a0e;
  --vic-bg:     #f7f0dc;
  /* The namecard (js/namecard.js) is the brand's own five tones, light in
     both themes as the post mockups are: never redefined below. */
  --nc-ground:  #ffffff;
  --nc-card:    #f5f5f5;
  --nc-tile:    #ffffff;
  --nc-word:    #515151;
  --nc-ink:     #1a1a1a;
  --nc-line:    #cfd0d4;
  --ok-solid:   #1f7a4d;
  --on-ok:      #ffffff;
  /* Amber reads as caution only while it is plainly orange. #9c5c16 had
     drifted to brown, and a brown chip on a cream tint read as a notice
     board, not a warning; this is the same hue with the red left in. 5.05:1
     on its own tint, 5.59:1 on white. */
  --warn:       #a94d0c;
  --warn-bg:    #fdf1e7;
  --warn-line:  #f1d3b8;
  --warn-solid: #a94d0c;
  --on-warn:    #ffffff;
  --err:        #b3261e;
  --err-bg:     #fdeceb;
  --err-line:   #e9b9b5;
  --err-hi:     #8c1d18;   /* the pressed / heavier step, never a second red */
  /* One colour, one meaning (the user, 2026-10-07, after the colour audit,
     on Apple's guidelines). Blue is the one primary action of a view, as on
     apple.com: Add, Save, Publish and Approve alike, and never a status.
     #0071E3 carries white at 4.70:1; its hover and press step darker so the
     label keeps AA (Apple's own lighter hover is 4.32:1). Blue as text or a
     glyph is `--action-ink` (#0066CC, 5.57:1 on white). #007AFF (4.02:1)
     stays refused. */
  --action:         #0071e3;
  --action-hover:   #0068d1;
  --action-pressed: #005bb8;
  --on-action:      #ffffff;
  --action-ink:     #0066cc;
  /* A status is a dot and its word in ink (App Store Connect): green done or
     live, amber waiting on somebody, red needs acting on now, grey not
     started, paused or closed. The word carries the meaning; the dot is
     Apple's own system colour beside it. */
  --dot-ok:         #34c759;
  --dot-wait:       #ff9f0a;
  --dot-late:       #ff3b30;
  --dot-off:        #aeaeb2;
  /* Medium's yellow, Apple's system yellow, on a task's priority strip, and
     its tag's ground (2026-10-11); nowhere else. */
  --dot-soon:       #ffcc00;
  --soon-bg:        #fdf6d8;
  /* The ring is the focus colour at the weight a ring is drawn, so one token
     still drives every control. */
  --action-ring:    #8ab4f8;
  /* A secondary button is a tonal fill with no outline. Translucent ink, so
     the same token reads on the white card and on the off-white page. A
     field is filled too (--field, below; 2026-10-31), told from a button by
     its label and turning white while typed in. */
  --tonal:        rgba(27,26,23,.065);
  --tonal-hover:  rgba(27,26,23,.105);
  --tonal-press:  rgba(27,26,23,.15);
  /* A box typed into is filled, never outlined (the user, 2026-10-31,
     option B): the tonal shade made opaque, so autofill and a field on a
     shaded block read the same; it turns white while typed in. */
  --field:        #f0f0f0;
  --seg-on:       #ffffff;   /* the chosen option of a segment, raised on its track */
  --focus:      rgba(0,113,227,.22);
  --scrim:      rgba(0,0,0,.42);
  /* The sticky bars are glass as Apple's are (the user, 2026-09-28: "could go
     more transparent, similar as apple"): 72% over a 20px blur. */
  --chrome:     rgba(255,255,255,.72);   /* the translucent sticky bar */
  --veil:       rgba(255,255,255,.88);   /* a label or a question laid over a thumbnail, kept legible */
  --knob:       #ffffff;                 /* a switch's thumb, white on its track in both themes */
  --chrome-solid: #ffffff;               /* the same bar, opaque, where a phone's status bar takes its colour */
  /* Soft Precision. Two corners and nothing between them: 10px on anything a
     finger operates, 14px on a bounded section, 18px on a sheet. A table at
     10px sitting between two panels at 14 is the mismatch nobody can name and
     everybody sees, so `--radius` and `--radius-sm` are those two values
     rather than a third pair beside them. */
  --radius:     14px;
  --radius-sm:  10px;
  --radius-ctl:   var(--radius-sm);
  --radius-panel: var(--radius);
  --radius-lg:    18px;
  /* A control 32px tall takes 8, so its corner is the same share of its
     height as a 38px control's 10 (see THE CORNER FOLLOWS THE SIZE). */
  --radius-ctl-sm: 8px;
  /* One height for every control, so a button, a select and an input standing
     next to each other line up instead of stepping, at Apple's sizes (34
     and 30 at a desk, 36 and 32 under a finger, where a button still takes
     a 44px press; 2026-10-23). */
  /* The chrome bar has a height of its own. It used to have none: 14px of
     padding above and below whatever was tallest inside it, so /admin (a 26px
     wordmark) came out 55px and /client (a 38px button) 67px, both changed
     again at the 640 breakpoint, and the client bar wrapped to two rows at
     390. A bar measured by its contents is a different bar on every page. */
  --head-h:     64px;
  --ctl-h:      34px;
  --ctl-h-sm:   30px;
  /* The press a finger needs, whatever a control draws at, and a box of
     lines' floor (a short note's the smaller): one place each, so a change
     of size is one edit here (the user, 2026-10-23). A section that wants a
     small control drawn at full height sets the token, never the height. */
  --tap:        44px;
  --box-h:      84px;
  --box-h-sm:   72px;
  --state-w:    124px;   /* every status select, every table */
  --ctl-text:   13px;
  --field-text: 14px;
  --shadow:     0 1px 2px rgba(19,24,26,.05);
  --shadow-lift: 0 1px 2px rgba(19,24,26,.08), 0 6px 14px rgba(19,24,26,.08), 0 20px 44px rgba(19,24,26,.16);
  /* What floats over the page (a ⋯ menu, a popover card, the finder) has its
     own ground and a crisper edge than a card's rule, so it never melts into
     the card under it (the user, 2026-10-07: the section's information card
     "blends with the background"). In dark it is one step up from a card. */
  --raised:     #ffffff;
  --line-lift:  rgba(19,24,26,.16);

  /* Motion. Three durations and two curves, the way there are six spaces and
     one accent: nineteen transitions had been typed at their point of use, in
     nine durations and three curves, so no two things on one screen moved
     alike. `--ease` is the long settle a thing takes when it changes place or
     size, which is what reads as unhurried rather than springy; `--ease-out`
     is for something arriving, which decelerates and never overshoots. */
  --t-fast: .12s;   /* a colour, a border: the pointer has not left yet */
  --t:      .2s;    /* the ordinary state change */
  --t-slow: .34s;   /* a size, a position, a surface opening */
  --ease:     cubic-bezier(.32,.72,0,1);
  --ease-out: cubic-bezier(.22,.61,.36,1);
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
          "PingFang SC", "Microsoft YaHei", sans-serif;
  /* Headings only. Chinese has no Slate, so the stack falls through to the
     system face and a zh heading is simply the system face at the same size. */
  --font-head: "ADspace Slate", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          Helvetica, Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
}

/* Dark is the team's, and only ever by choice. It is not taken from the
   system, because a client who happens to have their phone in dark mode would
   be deciding on a proposal in a register nobody chose for that conversation,
   and the mood a decision is made in is not ours to set by accident. So the
   client pages have no dark at all, the console carries a toggle, and the
   choice is this browser's alone.

   Only the colours move; every size, space and shape above is the same screen.
   The ramp is the light one turned over and kept warm, and the fills swap with
   their text, so the primary button stays the highest contrast thing in the
   view rather than vanishing. Depth cannot come from a grey shadow on a dark
   ground, so the shadows go black and the surfaces step instead. */
/* The grounds are neutral and the warmth is on the marks — the same rule the
   light palette states, applied here at last. Dark had never been brought in
   line with it and had drifted onto a different hue axis: measured, every
   non-neutral token sat at **hue 70 to 100 degrees**, which is yellow-green,
   while light's warm marks sit at **42**. Olive at that luminance is what
   reads as muddy brown, which is how the user reported it on 2026-09-21
   against another dark application. `--page` and `--card` are the two largest
   surfaces and are what the eye calls "the colour of the app", so they are
   exactly neutral, as `#f5f5f5` and white are in light; `--sunk` and
   `--line-soft` already were. The ink ramp and the borders keep their warmth
   and keep it at 42. Every value holds the luminance it replaced, so every
   documented contrast pair is preserved (`--ink` on `--page` 15.60 to 15.65,
   `--line-ctl` on `--card` 4.39 to 4.46), and `uxaudit` measures all of them
   on the real page in this theme. */
:root[data-theme="dark"] {
    --ink:        #f2efec;
    --ink-soft:   #cdc8c3;
    --ink-mute:   #ada8a3;
    --line-ctl:   #858585;
    --line-chip:  #5f5f5f;
    --line:       #3e3b38;
    --card-line:  transparent;
    --line-soft:  #2e2e2e;
    --page:       #171717;
    --card:       #1f1f1f;
    --sunk:       #272727;
    --fill:       #f2efec;
    --on-fill:    #1c1a19;
    /* One value per semantic colour, doing both jobs: legible as text on every
       dark surface and deep enough to carry dark text when it is a fill. A
       paler mint passed the same checks and read as a highlighter pen next to
       everything else, which is the register this portal does not have. */
    --accent:     #4aa876;
    --ok:         #4aa876;
    --ok-bg:      #17281f;
    --pub:        #a7c6ed;
    --pub-bg:     #1d2735;
    --health:     #f57eb6;
    --health-bg:  #2e1a24;
    --vic:        #c9a24c;
    --vic-ink:    #e3c272;
    --vic-bg:     #2b2517;
    --ok-solid:   #4aa876;
    --on-ok:      #07150e;
    --warn:       #cf9350;
    --warn-bg:    #2a2217;
    --warn-line:  #4a3520;
    --warn-solid: #cf9350;
    --on-warn:    #1d1408;
    --err:        #e8837a;
    --err-bg:     #2e1d1b;
    --err-line:   #6a3a35;
    --err-hi:     #f2a9a2;
    /* The same blue under white in both registers, as apple.com draws its
       buttons on a dark ground: 4.70:1, and 3.5:1 as a boundary on the card.
       Blue words take the lighter #2997FF (5.47:1 on the card). */
    --action:         #0071e3;
    --action-hover:   #0068d1;
    --action-pressed: #005bb8;
    --on-action:      #ffffff;
    --action-ink:     #2997ff;
    --action-ring:    #8ab4f8;
    --dot-ok:         #30d158;
    --dot-wait:       #ff9f0a;
    --dot-late:       #ff453a;
    --dot-off:        #636366;
    --dot-soon:       #ffd60a;
    --soon-bg:        #2b2614;
    --tonal:        rgba(242,239,236,.09);
    --tonal-hover:  rgba(242,239,236,.135);
    --tonal-press:  rgba(242,239,236,.18);
    --field:        #323231;
    --seg-on:       #57544f;   /* lifted 2026-10-16: #3a3938 matched the track on a shaded block */
    --focus:      rgba(138,180,248,.34);
    --scrim:      rgba(0,0,0,.62);
    --chrome:     rgba(23,23,23,.72);
    --veil:       rgba(23,23,23,.88);
    --chrome-solid: #171717;
    --shadow:     0 1px 2px rgba(0,0,0,.5);
    --shadow-lift: 0 1px 2px rgba(0,0,0,.6), 0 6px 14px rgba(0,0,0,.45), 0 20px 44px rgba(0,0,0,.6);
    --raised:     #272727;
    --line-lift:  rgba(255,255,255,.14);
  color-scheme: dark;
}
/* The wordmark is one flat dark colour on transparent, so it is inverted
   rather than shipped twice. A client logo is not: it is the client's artwork
   and keeps its own colours on the white disc it already sits on. */
:root[data-theme="dark"] img.brand-logo { filter: invert(1) brightness(1.08); }

@media (pointer: coarse) {
  :root { --ctl-h: 36px; --ctl-h-sm: 32px; --ctl-text: 14px; --field-text: 15px; --radius-ctl-sm: 8px; }
  /* A control draws at Apple's size (36 and 32 under a finger; the user,
     2026-10-23: "the buttons seem wrong") and still answers a 44px finger:
     an invisible box above and below it takes the press. */
  .btn { position: relative; }
  .btn::after { content: ''; position: absolute; left: 0; right: 0; top: min(0px, calc(50% - var(--tap) / 2)); bottom: min(0px, calc(50% - var(--tap) / 2)); }
}

/* The one brand face on screen. Slate is already in the repo for the letter,
   so the portal and the paper it prints share a voice without a second asset
   or a webfont service. It is used on headings only: body text stays on the
   system stack, where it is tuned for the reader's own screen and needs no
   download to be legible. swap keeps the heading readable while it loads. */
@font-face {
  font-family: "ADspace Slate";
  /* WOFF2 first at 38KB against the TTF's 110KB; the TTF stays in the repo
     because the letter's pdf-lib fetches it, and doubles as the fallback. */
  src: url("/css/SlateRg.woff2") format("woff2"),
       url("/css/SlateRg.TTF") format("truetype");
  font-weight: 400 700;
  /* `optional`, not `swap`, and preloaded in every page head. Swap paints the
     heading in the system face and then flips it, which is the page visibly
     correcting itself while somebody is already reading; optional gives the
     font a brief window and, if it misses it, simply leaves that one load in
     the fallback and never swaps. The preload means it almost never misses,
     and after the first visit it is cached and always in time. */
  font-display: optional;
}

* { box-sizing: border-box; margin: 0; }
:root { color-scheme: light; }
html { -webkit-text-size-adjust: 100%; }
/* iOS paints a translucent blue box over whatever a finger touched, square
   even on a rounded control, and it stayed over the grown search field (the
   user, 2026-09-25). Every control here answers a press itself — the scale on
   :active and the focus ring — so the platform's box is only noise. */
html, * { -webkit-tap-highlight-color: transparent; }
body {
  font-family: var(--font);
  background: var(--page);
  color: var(--ink);
  font-size: 14px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
/* 14px is the console's, and it is a departure this portal documents: a dense
   operations tool read all day at a desk. A client reads one page once, often
   on a phone, and is being asked to decide something on it, so the four
   client facing pages take the 16px this standard asks for everywhere else.
   Only what inherits moves: controls, labels and chips state their own size,
   so the shapes are the same on both sides and only the reading text grows. */
:root[data-face="client"] body { font-size: 16px; }
img, video { display: block; max-width: 100%; }
/* Author rules beat the UA [hidden] default, so state it explicitly. */
[hidden] { display: none !important; }
button { font: inherit; color: inherit; cursor: pointer; }
a { color: inherit; }

/* ---------- Shell ---------- */
.shell { max-width: 1240px; margin: 0 auto; padding: 0 24px 96px; }

.topbar {
  position: sticky; top: 0; z-index: 40;
  background: var(--chrome);
  -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid var(--line);
}
.topbar-inner {
  max-width: 1240px; margin: 0 auto; padding: 0 24px;
  /* Less the hairline, which sits on .topbar outside this box while
     .console-head carries its own inside: without it the two bars differ by
     exactly the border and the eye still catches the step. */
  min-height: calc(var(--head-h) - 1px);
  display: flex; align-items: center; gap: 16px; flex-wrap: nowrap;
}
/* The actions keep their size and the name gives way, rather than the row
   folding onto a second line and doubling the bar. */
.chrome-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.chrome-actions .btn, .chrome-actions .pill { min-width: 0; }
.brand-kicker, .brand-for { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
/* `height` + `width: auto` + the global `img { max-width: 100% }` is a squash
   waiting for a narrow box: the width is clamped to what is left and the
   height is not, so the mark is compressed horizontally and the wordmark
   stops being the wordmark. Measured in the 244px rail, where the box
   settles at 195px: anything past 8.13:1 drew at 8.13:1, and the wider the
   asset the worse it got. `contain` fits the ink inside whatever box the
   layout gives it with the ratio true, and `left` keeps it on the rail's
   own margin rather than centred in the leftover width. The box still
   measures 24px, so nothing under it moves. */
.brand-logo { height: 26px; width: auto; object-fit: contain; object-position: left center; }
/* In the top bar the mark is the brand's only fixed part, and `.brand` is
   sized by its content: Safari resolves the global `max-width: 100%` against
   that content while measuring it, gets nothing, and on a phone, where the
   label beside the mark is hidden, drew the mark 0px wide. A cap in `vw`
   needs no container to resolve against, and the mark never shrinks. */
.topbar .brand-logo { flex: 0 0 auto; max-width: 60vw; }
/* A first visit holds the brand back until the mark has loaded or failed
   (js/chrome.js), so it arrives in place rather than pushing the label. */
.brand.is-waiting { visibility: hidden; }
.brand-wordmark { font-size: 16px; font-weight: 700; letter-spacing: -.02em; color: var(--ink); }
.brand-divider { width: 1px; height: 26px; background: var(--line); flex: 0 0 auto; }
.brand-meta { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; }
.brand-kicker { font-weight: 600; font-size: 13.5px; letter-spacing: -.01em; }
.brand-for { font-size: 12px; color: var(--ink-mute); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.brand-for b { color: var(--ink-soft); font-weight: 600; }
.topbar-spacer { flex: 1 1 auto; }

.pill {
  border: 1px solid var(--line); background: var(--card); color: var(--ink-soft);
  border-radius: 999px; min-height: var(--ctl-h); padding: 0 14px; font-size: 13px;
  display: inline-flex; align-items: center; gap: 7px;
  transition: border-color var(--t) var(--ease-out), background var(--t) var(--ease-out), color var(--t) var(--ease-out), transform var(--t-fast) var(--ease-out);
}
.pill:hover { border-color: var(--ink); color: var(--ink); }
.pill svg { width: 15px; height: 15px; }

/* One row: two selects that already name themselves, the count, and the safe
   zone switch. The stacked labels above them were a second row saying what the
   selects said. */
.filterbar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 16px 0 14px;
}
.filterbar .count { margin-left: auto; padding-bottom: 0; }
.field-label { display: block; font-size: 12px; color: var(--ink-mute); margin-bottom: 5px; }
.select, .input, .textarea {
  font: inherit; font-size: var(--field-text); color: var(--ink); background: var(--field);
  border: 1px solid var(--field-edge, transparent); border-radius: var(--radius-sm); padding: 8px 12px;
}
.select, .input { min-height: var(--ctl-h); padding-block: 0; }
/* The native control drew its own chrome, so a select sat a few pixels taller
   than the inputs beside it and carried a different arrow on every platform.
   Same box, one chevron, drawn by us. */
.select {
  min-width: 180px; appearance: none; -webkit-appearance: none;
  padding-right: 34px; line-height: 1.5;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b6b6b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center; background-size: 15px;
  /* Every select is a tonal pop-up (the user, 2026-10-11: the same cell
     everywhere): something picked, not typed, so it wears the buttons'
     shade and no outline, its chevron at its end. */
  border-color: var(--field-edge, transparent); background-color: var(--select-ground, var(--tonal));
  /* A value longer than the box (a registered name, Bill to) ends in an
     ellipsis, never a word cut at the edge (2026-10-29, audit REG-1). */
  text-overflow: ellipsis;
}
@media (hover: hover) { .select:not([disabled]):not(.tdetail):hover { background-color: var(--tonal-hover); } }
.select:focus { border-color: transparent; box-shadow: none; }
.select:focus-visible { outline: 2px solid var(--action-ring); outline-offset: 2px; }
/* Date and time fields bring their own chrome too, and it is taller. On iOS
   it is also a pill with the value centred in it: `appearance: none` takes
   the pill away, and the value's own box is told to start at the left like
   every other field, because a date centred in a full width box on a phone
   read as a button and not as a field. `min-height` keeps an empty field at
   the control height, which iOS otherwise collapses to nothing, and the flex
   centring is what puts the value on its middle. This comment said
   `display: block` for some time after the rule had stopped using it, which
   is how `tests/sch.js` came to assert a sentence rather than the mechanism:
   a comment can go stale and a measurement cannot. */
.input[type="date"], .input[type="time"], .input[type="month"] {
  line-height: 1.5; min-height: var(--ctl-h);
  appearance: none; -webkit-appearance: none; text-align: left;
  display: flex; align-items: center;
  /* iOS gives a date field an intrinsic width wider than a half-width cell
     on a phone, so it ran past its column; it takes the cell's width. */
  min-width: 0; max-width: 100%;
}
/* iOS lays the value out as a block at the top of the field, so with the
   pill gone it floated against the top edge while the glyph sat on the
   middle. The value's own box is given the field's inner height and its
   text is centred in it; the flex centring above covers the same case on
   any engine that honours it on an input. */
.input[type="date"]::-webkit-date-and-time-value,
.input[type="time"]::-webkit-date-and-time-value,
.input[type="month"]::-webkit-date-and-time-value {
  text-align: left; min-height: calc(var(--ctl-h) - 2px); line-height: calc(var(--ctl-h) - 2px);
}
/* Chrome's own picker mark, quietened to the mute ink rather than removed:
   it is the pointer's route into the picker at a desk. */
.input[type="date"]::-webkit-calendar-picker-indicator,
.input[type="time"]::-webkit-calendar-picker-indicator,
.input[type="month"]::-webkit-calendar-picker-indicator { opacity: .55; cursor: pointer; }
/* A time field's value is narrower than the box, so its mark followed the
   value into the middle; it keeps to the right edge, as the date's does. */
/* As a block its value sat on the top edge in Safari (Chrome centres it
   itself), so the block is padded to put one line on the field's middle,
   and Safari's own inset around the value goes, so it starts where every
   other field's text does. */
@media (pointer: fine) {
  .input[type="time"] { display: block; padding-block: calc((var(--ctl-h) - 2px - 1.5em) / 2); }
  .input[type="time"]::-webkit-datetime-edit { padding: 0; }
  .input[type="time"]::-webkit-datetime-edit-fields-wrapper { padding: 0; }
}
/* Under a finger there is no picker mark at all, on iOS or on Android, so
   the field names itself with a glyph on the left: a calendar for a date and
   a clock for a time, the same weight as every other glyph in the console.
   At a desk Chrome's own mark already says it, and two calendars in one box
   is noise. */
@media (pointer: coarse) {
  .input[type="date"], .input[type="time"], .input[type="month"] {
    padding-left: 38px; background-repeat: no-repeat;
    background-position: left 12px center; background-size: 16px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b6760' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='5' width='18' height='16' rx='2'/%3E%3Cpath d='M3 10h18M8 3v4M16 3v4'/%3E%3C/svg%3E");
  }
  .input[type="time"] {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b6760' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3 2'/%3E%3C/svg%3E");
  }
  /* A finger opens the picker by tapping the field, so a second mark on the
     right is not a route in, and in a 122px time track it clipped the value. */
  .input[type="date"]::-webkit-calendar-picker-indicator,
  .input[type="time"]::-webkit-calendar-picker-indicator,
  .input[type="month"]::-webkit-calendar-picker-indicator { display: none; }
  :root[data-theme="dark"] .input[type="date"], :root[data-theme="dark"] .input[type="month"] {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a6a89e' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='5' width='18' height='16' rx='2'/%3E%3Cpath d='M3 10h18M8 3v4M16 3v4'/%3E%3C/svg%3E");
  }
  :root[data-theme="dark"] .input[type="time"] {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a6a89e' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3 2'/%3E%3C/svg%3E");
  }
}
/* A file field arrives as the browser's own grey button floating on a baseline
   of its own. Same box as every other field, and the button inside it is one of
   ours: outline, our border, centred on the field's line. */
/* **The button inside the field is shaded, never outlined.** It carried the
   same 1px `--line-ctl` ring as the field around it, on the field's own white
   ground, so one control wore two identical borders and the inner one was told
   apart from the outer by nothing at all. A border groups; a second border
   inside the first groups nothing and only reads as a box that has been drawn
   twice. The account control and the bell were corrected the same way: what
   says a thing is pressable is its fill, not a ring.

   It also fixes a collision. The field's radius is 10px and its inset was 8,
   so the inner corner sat *inside* the outer corner's arc and the two curves
   crossed — which is what "covered by the outer selection" describes. The
   inset is 5px now and the shaded segment's corner is **concentric** with it:
   an inner radius equals the outer radius less the inset, so 10 − 5 is 5, and
   the two curves run parallel instead of crossing. That is not a third corner
   value beside the two this system has — it is the consequence of them, which
   is why it is written as the subtraction rather than as a number.

   And the two labels are one size. `Choose Files` was 12.5px beside the
   field's own 13px `no files selected`, so the pair sat on two baselines and
   read as badly centred however the boxes measured. Reported three times
   before it was looked at properly: the first two passes measured the box
   heights, found them symmetric, and answered the wrong question. */
.input[type="file"] {
  display: flex; align-items: center; gap: 10px; padding-inline: 5px;
  font-size: 13px; color: var(--ink-mute); cursor: pointer;
}
.input[type="file"]::file-selector-button {
  flex: 0 0 auto; height: calc(var(--ctl-h-sm) - 2px); margin: 0; padding: 0 14px;
  font: inherit; font-size: 13px; font-weight: 500;
  /* `--line-soft` and not `--sunk`: at 1.03:1 on the field's white ground a
     sunk fill is invisible, and a control nothing distinguishes is not a
     control. This is the fill the account button and the bell take to say
     they are pressable, which is the same job here. */
  color: var(--ink-soft); background: var(--line-soft);
  border: 0; border-radius: calc(var(--radius-sm) - 5px); cursor: pointer;
  transition: background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
@media (hover: hover) {
  .input[type="file"]::file-selector-button:hover { background: var(--line); color: var(--ink); }
}
.textarea { width: 100%; resize: vertical; min-height: var(--box-h); padding-block: 10px; }
/* A typed box grows downwards only: a sideways drag widened the card past
   the screen (the report's Notes for the draft, 2026-10-05). In a field row
   it runs the row's width, as every other field does. */
textarea.input { resize: vertical; max-width: 100%; }
.row > div > textarea.input { width: 100%; }
/* Every typed box's handle only makes it taller (js/grow.js keeps it above
   its words and short of the screen's foot; the user, 2026-10-07: "just
   expand the textbox input longer larger"). A one-line field that wraps its
   words instead of hiding them (`data-oneline`: a title, a link, a summary
   line) reads as a field one line high and carries no handle. */
textarea { resize: vertical; }
textarea[data-oneline] {
  resize: none; overflow: hidden; line-height: 1.45;
  padding-block: calc((var(--ctl-h) - 2px - 1.45em) / 2);
}
textarea.input-sm[data-oneline] { padding-block: calc((var(--ctl-h-sm) - 2px - 1.45em) / 2); }
.input:focus, .textarea:focus {
  outline: none; border-color: var(--action); box-shadow: 0 0 0 3px var(--focus); background-color: var(--card);
}
/* A placeholder (a format, an example) in the mute ink at full strength:
   the browser's own grey stood at 4.04:1 on the filled field (2026-10-31,
   G-1). Kept low, so a field that hides or tunes its own still wins. */
:where(input, textarea)::placeholder { color: var(--ink-mute); opacity: 1; }
/* A field on a grey card takes the white ground (the user, 2026-10-31:
   "white fields on grey cards", G-2): the filled grey stood at about 1.03:1
   against --sunk and vanished. The card sets the field's ground, so every
   field, the signature pad and an autofilled field follow it; typed in, it
   keeps the blue edge and ring above. A select there is the white pop-up
   (`--select-ground`, read by the select's own rule). White on the grey
   card stood at 1.05:1, so a field there also draws the control's edge
   (`--field-edge`, --line-ctl, 3:1 on --sunk in both themes; the user,
   2026-10-31, option A). Every grey card that holds a field is named here,
   one list. */
:where(.qs-side, .kstep, .postgroup, .zone-draft, .saved.is-editing, .paircard, .sca-row, .rcw-pt.is-cols, .rp-aidraft, .is-narrow .ck-tr:not(.ck-th)) {
  --field: var(--card); --select-ground: var(--card); --field-edge: var(--line-ctl);
}
/* A field the browser filled keeps our ground. Safari paints an autofilled
   field pale yellow and Chrome pale blue whatever the page, so on the dark
   console sign-in the email was a highlighter stripe (reported with a photo,
   2026-09-26). An inset shadow covers the platform's fill and the focus ring
   rides on top of it. */
.input:-webkit-autofill, .input:-webkit-autofill:hover {
  -webkit-text-fill-color: var(--ink); caret-color: var(--ink);
  box-shadow: 0 0 0 100px var(--field) inset;
}
.input:-webkit-autofill:focus { box-shadow: 0 0 0 100px var(--card) inset, 0 0 0 3px var(--focus); }
/* One ring, ours, on everything a keyboard can reach. It used to be the
   browser's own on every button, which meant the console's focus colour was
   whatever the platform happened to draw and changed between two machines
   looking at the same screen. Blue, because focus is interactivity and that is
   what blue means here; `focus-visible` rather than `focus`, so a pointer does
   not leave a ring behind on the last thing it pressed. */
:where(button, [role="button"], a[href], summary, [tabindex]):focus-visible {
  outline: 2px solid var(--action-ring); outline-offset: 2px; border-radius: var(--radius-sm);
}
.input:focus-visible, .textarea:focus-visible { outline: none; }
.count { color: var(--ink-mute); font-size: 13px; }
.crm-group-more {
  width: 100%; min-height: 46px; padding: 0 16px; border: 0;
  border-top: 1px solid var(--line-soft); background: var(--sunk);
  color: var(--ink-soft); font: inherit; font-size: 13px; font-weight: 600;
  text-align: center;
}
/* `--fill` is the solid ink surface, so under ink text it drew a black bar
   with the words gone (the user, 2026-09-26). The row sits on `--sunk`, so
   its hover is the next step, the fill a hovered row takes everywhere. */
@media (hover: hover) { .crm-group-more:hover { color: var(--ink); background: var(--line-soft); } }

/* ---------- Batch ---------- */
.batch { margin-bottom: 56px; }
.batch-head {
  display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
  padding-bottom: 14px; margin-bottom: 22px; border-bottom: 1px solid var(--line);
}
.batch-title { font-size: 20px; font-weight: 600; letter-spacing: -.02em; }
.batch-date { color: var(--ink-mute); font-size: 13px; }
.batch-note {
  background: var(--card); border: 1px solid var(--card-line);
  border-radius: 8px; padding: 12px 16px; margin-bottom: 22px; color: var(--ink-soft);
}

/* ---------- Post card ---------- */
.grid {
  display: grid; gap: 24px;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  align-items: start;
}
/* The review page is a gallery, so it takes a wider column and a card as
   narrow as 300px: four posts across on a desk from about 1400px, three on
   a 1280 laptop, each card no narrower than the old minimum (the user,
   2026-09-28). */
.shell.shell-review { max-width: 1480px; }
.shell-review .grid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
.card {
  background: var(--card); border: 1px solid var(--card-line);
  border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden;
}
/* On a phone the placement name, the pixel size and the status ran into each
   other. Wrapping lets the status drop to its own line instead. */
.card-head {
  display: flex; align-items: center; flex-wrap: wrap; column-gap: 10px; row-gap: 3px;
  padding: 12px 16px; border-bottom: 1px solid var(--line-soft);
}
.card-title { font-weight: 600; font-size: 13px; letter-spacing: -.01em; min-width: 0; }
.card-dims { color: var(--ink-mute); font-size: 12px; white-space: nowrap; }
.card-stage {
  padding: 22px 18px; background:
    linear-gradient(0deg, rgba(0,0,0,.015), rgba(0,0,0,.015)), #fff;
  display: flex; justify-content: center;
}
/* A 9:16 post is the whole screen on a phone, so it gets the whole column here
   rather than sitting inset with padding around it. */
.card.is-vertical .card-stage { padding: 0; background: #000; }
.card.is-vertical .mk-phone { max-width: none; border: 0; border-radius: 0; box-shadow: none; }
.card.is-vertical .mk-coverwrap { max-width: none; }

/* The state hangs off the end of the card head, as it does on every row in
   this portal. The chip shape comes from `.status`. */
.badge { margin-left: auto; }

/* A card's head holds one line at every width: the title gives way first
   (an ellipsis), the ratio and the state never wrap. No platform colour
   (the user, 2026-10-03: the original style). */
.card[data-plat] > .card-head, .cardpair > .card-head { flex-wrap: nowrap; min-width: 0; }
.card[data-plat] > .card-head .card-title, .cardpair > .card-head .card-title {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 1 auto;
}
.card[data-plat] > .card-head .card-dims, .cardpair > .card-head .card-dims { flex: 0 0 auto; }

/* A reel and its cover: one frame, the reel's head, the two halves as the
   view strip with each half's state in its tab, then the half on show. The
   halves are whole cards inside the frame, their own head folded away. */
.cardpair {
  background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow);
  overflow: hidden; display: flex; flex-direction: column;
}
.cardpair[hidden] { display: none; }
.shell-review .cardpair > .card.in-pair { border: 0; border-radius: 0; box-shadow: none; flex: 1 1 auto; align-self: stretch; }
.cardpair > .card.in-pair[hidden] { display: none; }
.cardpair > .card.in-pair > .card-head { display: none; }
.cardpair > .tabrow.pairtabs { margin: 12px 16px; display: flex; width: auto; max-width: none; }
.cardpair > .pairtabs .tab { flex: 1 1 0; display: inline-flex; align-items: center; justify-content: center; gap: 8px; white-space: nowrap; }
/* The half's state in its tab reads at the tab's own weight, the dot and its
   word as every status (2026-10-31, RV-8: the word at 400, its state at 500). */
.pair-chip { font-size: 11.5px; font-weight: 600; padding: 2px 8px; border-radius: 5px; white-space: nowrap; }
.pairtabs .pair-chip { font-weight: 400; }
.pair-chip:empty { display: none; }
/* The name card above the stage strip, the client portal's own. */
.shell-review > .rec-head { margin-top: 16px; }
.camp-namecard { margin-top: 16px; }
.shell-review > .rec-head + .stagestrip { margin-top: 0; }

/* Copy block under the mockup: the caption is half the deliverable. */
.copyblock { border-top: 1px solid var(--line-soft); padding: 16px; }
.copyblock h5 {
  font-size: 12px; letter-spacing: 0;
  color: var(--ink-mute); font-weight: 600; margin-bottom: 7px;
}
/* The section name and the copy button on one line, above the words, so the
   button keeps its place while the text below it grows and shrinks. */
.copyhead {
  display: flex; align-items: center; gap: 12px; margin-bottom: 9px;
}
.copyhead h5 { margin-bottom: 0; flex: 0 1 auto; }
/* Edit text and Copy text, one pair of small tonal buttons at the
   heading's right (the user, 2026-10-04: the pen alone beside the heading
   was not seen). The pair stays on the heading's line at every width,
   320 and an iPhone at Display Zoom included (the user, 2026-10-04). */
.copyhead { gap: 8px; }
.copyhead h5 { min-width: 0; white-space: nowrap; }
.copyacts { display: flex; gap: 6px; flex: 0 0 auto; margin-left: auto; }
.copyacts > .btn { min-width: 0; padding: 0 10px; gap: 6px; }
.copy-pen[hidden] { display: none; }
.copyblock .copyfield { display: block; width: 100%; min-height: 140px; }
.copyblock .copytext + .copyfield + h5 { margin-top: 16px; }
.copytext { white-space: pre-wrap; color: var(--ink-soft); font-size: 13.5px; }
.copytext + h5 { margin-top: 16px; }

/* ---------- Approval ---------- */
.approve { border-top: 1px solid var(--line-soft); padding: 14px 16px; }
.approve-row { display: flex; gap: 10px; }
.btn {
  flex: 1; border-radius: var(--radius-sm); padding: 0 16px;
  min-height: var(--ctl-h); font-size: var(--ctl-text); font-weight: 500;
  /* Tonal: a fill and no outline. The 1px border stays, transparent, so a
     tonal button and a field of the same height are the same height. */
  border: 1px solid transparent; background: var(--tonal); color: var(--ink);
  transition: background var(--t) var(--ease-out), color var(--t) var(--ease-out), border-color var(--t) var(--ease-out), transform var(--t-fast) var(--ease-out);
  /* Width came entirely from the label, so a row of buttons stepped up and
     down with the length of the words. A floor evens them out and centres
     the label in the space rather than leaving it padded to one side. */
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  min-width: 116px; text-align: center;
  /* An <a class="btn"> takes the browser's underline otherwise, so the same
     button reads differently depending on which element drew it. */
  text-decoration: none;
}
.btn svg {
  width: 15px; height: 15px; flex: 0 0 auto;
  /* Most button marks are stroked paths. At the browser's default 1px they
     faded into a filled surface long before the label did, especially at
     zoom. The mark and the word now carry comparable visual weight. */
  stroke-width: 1.8;
}
/* Inside `@media (hover: hover)`, which is where this portal puts every hover
   state so a phone cannot leave one stuck on the last thing tapped. Left
   outside it, a tapped `.btn` kept `.btn:hover` — and `.btn:hover` outranks
   `.btn-primary`, so its ink text could land on the filled primary and lose
   contrast on a touch device. */
@media (hover: hover) {
  .btn:hover { background: var(--tonal-hover); color: var(--ink); }
}
/* At the specificity of `.btn` alone, so every filled variant's own pressed
   step (and a red fill's colour) wins over the tonal one. */
.btn:where(:active) { background: var(--tonal-press); }
/* A tonal toggle held on reads one step deeper (Review n changes). */
.btn:where([aria-pressed="true"]) { background: var(--tonal-press); color: var(--ink); }
/* Approve is the action we want the client to take, so it carries the weight.
   Request changes stays available but visually quiet. Blue while it is still a
   decision to make; the settled state below is green, because by then it is no
   longer an action but a fact about the post. */
.approve-row .btn-approve {
  flex: 1.35; font-weight: 600;
  background: var(--action); border-color: var(--action); color: var(--on-action);
}
@media (hover: hover) {
  .approve-row .btn-approve:hover {
    background: var(--action-hover); border-color: var(--action-hover); color: var(--on-action);
  }
}
.approve-row .btn-approve:active {
  background: var(--action-pressed); border-color: var(--action-pressed); color: var(--on-action);
}
.approve-row .btn-approve[aria-pressed="true"]::before { content: '\2713\00a0'; }

/* Request changes reads in the tonal button's own ink: the mute ink read as
   disabled beside Approve (2026-10-31, RV-1). */
.approve-row .btn-changes { flex: 1; }
/* On the selection page the pair are equal halves, as the review page's are
   (SELECTION-9). */
.booking .approve-row .btn-approve, .booking .approve-row .btn-changes { flex: 1 1 50%; }
@media (hover: hover) { .approve-row .btn-changes:hover { color: var(--ink); } }
.approve-row .btn-changes[aria-pressed="true"] {
  background: var(--tonal-press); color: var(--ink); font-weight: 600;
}
/* Changes requested is waiting on the team: its dot, never an amber slab. */
.approve-row .btn-changes[aria-pressed="true"]::before {
  content: ""; flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; background: var(--dot-wait);
}
/* The one primary action of a view is Apple's blue (the user, 2026-10-07,
   after the colour audit): Add, Create and Save as much as Publish, Release
   and Approve, as apple.com draws every primary. It replaced an ink fill
   for our own operations beside a blue for hand-offs, two primaries a reader
   had to tell apart. A view carries one, two at most (`uxaudit` `accent`
   counts `.btn-primary`, `.btn-go` and `.btn-approve` together). */
.btn-primary, .btn-go {
  background: var(--action); border-color: var(--action);
  color: var(--on-action); flex: 0 0 auto; font-weight: 600;
}
@media (hover: hover) {
  .btn-primary:hover, .btn-go:hover { background: var(--action-hover); border-color: var(--action-hover); color: var(--on-action); }
}
.btn-primary:active, .btn-go:active { background: var(--action-pressed); border-color: var(--action-pressed); color: var(--on-action); }
/* Not a colour of its own: a disabled control is the same control, dimmed and
   inert, and a second grey here is a second thing to keep in step. */
.btn[disabled], .btn[aria-disabled="true"] {
  opacity: .42; cursor: default; pointer-events: none; filter: saturate(.55);
}

/* The name a decision is recorded under, asked inside the button that needs
   it. This is `.pbox` again — a control that creates a need answers it in
   place — and it replaces a `window.prompt`, which is the one shape this
   portal's rules already rule out for something that has to be typed.

   Three mechanics, all of them load bearing:
   · Both ends of the size are stated. `max-width` rather than `width`,
     because the field shares a flex row with the button and has to take
     whatever is left rather than a figure guessed here; a `width: auto` does
     not animate at all, and 0 to 100% does.
   · Request changes leaves while the name is being asked for. The client has
     chosen; this is the confirmation of that choice, not the choice again.
     Without it the field had 135px to grow into on a phone and the row
     wrapped to two lines mid animation, which is a jump, not a transition.
   · The pair reads as one control: the button gives up its right corners and
     the field takes them, with one divider between. */
.namebox { flex: 1.35; display: inline-flex; align-items: stretch; min-width: 0; }
/* The same box around a section head's one action (New content set). The
   1.35 share above is the Approve row's, where the box splits a row with
   Request changes; in a head it made the box take most of the row and put
   the button mid line, 700px short of the edge. Here the box holds its own
   width and sits on the right, and the field opens to a stated width, so
   the button slides left exactly as far as the field grows and no further. */
.viewhead .namebox { flex: 0 0 auto; margin-left: auto; }
.viewhead .namebox .btn { flex: 0 0 auto; }
.viewhead .namebox.is-asking .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.viewhead .namebox-field { width: 360px; }
@media (max-width: 640px) {
  .viewhead .namebox.is-asking { flex: 1 1 100%; }
  .viewhead .namebox-field { width: auto; }
}
/* Beats `.approve-row .btn-approve`'s own flex, which still matches in here. */
.approve-row .namebox .btn-approve { flex: 1 1 auto; min-width: 0; }
.approve-row .namebox.is-asking .btn-approve {
  flex: 0 0 auto;
  border-top-right-radius: 0; border-bottom-right-radius: 0;
}
.namebox-field {
  flex: 1 1 auto; max-width: 0; min-width: 0; opacity: 0;
  padding: 0; border: 0 solid var(--action); border-left-width: 0;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  height: auto; min-height: 0; font-size: var(--field-text);
  transition: max-width var(--t-slow) var(--ease), opacity var(--t) var(--ease-out),
              padding var(--t-slow) var(--ease);
}
.namebox.is-asking .namebox-field {
  max-width: 100%; opacity: 1; padding: 0 12px;
  border-width: 1px; border-left-width: 1px;
}
/* It leaves rather than hides: `display: none` would drop it out of the row
   in one frame and the name field would jump into the gap. */
/* Its words held on one line as it narrows: wrapping a word to a line in
   no width stood the row, Approve and the name field 40 to 43px tall
   (2026-10-31, RV-2). */
.approve-row.is-asking .btn-changes {
  flex: 0 0 0; max-width: 0; min-width: 0; opacity: 0; white-space: nowrap;
  padding-left: 0; padding-right: 0; border-width: 0; overflow: hidden;
  transition: max-width var(--t-slow) var(--ease), opacity var(--t) var(--ease-out),
              padding var(--t-slow) var(--ease), flex-basis var(--t-slow) var(--ease);
}
.approve-row.is-asking { gap: 0; }

/* A title edited where it sits. The field takes the heading's own size and
   weight, so the line does not jump by a pixel when the pen is pressed: a
   rename that reflows the bar around it is a rename you have to find your
   place in again. */
.askfield { min-width: 0; flex: 1 1 260px; max-width: 100%; }
/* A note the console opens under the control that sends it. `.changebox`
   carries the shape; this only says where it sits, because on a card the
   control is in a row of its own and the note belongs under the whole row. */
.asknote { width: 100%; }
.kactions .asknote, .asknote { margin-top: 10px; }

.changebox { margin-top: 12px; display: none; }
.changebox.is-open { display: block; }
.changebox { --box-h: var(--box-h-sm); }
.changebox .textarea { font-size: var(--field-text); resize: vertical; overflow-y: auto; }
/* The note being edited is the request; the standing one steps away until
   it is sent or the request is left. */
.approve.is-requesting .approve-note { display: none; }
/* The name the change request is recorded under. It sits in the box rather
   than growing out of Approve, because by the time this is on screen the box
   is already open and the button is on the row behind it: the same question
   belongs where the client is looking. Drawn only when we do not hold a name
   already, so nobody is asked twice. */
.changebox-who { margin-top: 9px; }
.changebox-actions { display: flex; gap: 8px; margin-top: 9px; justify-content: flex-start; }
.approve-state { margin-top: 10px; font-size: 12.5px; color: var(--ink-mute); }
.approve-state b { color: var(--ink-soft); font-weight: 600; }
.approve-note {
  margin-top: 8px; background: var(--sunk); border-radius: 8px;
  padding: 9px 12px; font-size: 13px; color: var(--ink); white-space: pre-wrap;
}
.approve-note-label { display: block; font-size: 12px; font-weight: 600; color: var(--ink-mute); }
.approve-note > .approve-note-label:not(:first-child) { margin-top: 8px; }
.approve-note-cap { display: block; margin-top: 2px; }

/* ---------- States ---------- */

.demo-strip {
  background: #1a1a1a; color: #f5f5f5; font-size: 12.5px;
  padding: 9px 24px; text-align: center;
}
.demo-strip b { font-weight: 600; }

/* ---------- QR ---------- */
.qr-modal {
  position: fixed; inset: 0; z-index: 90; display: none;
  background: rgba(0,0,0,.45); align-items: center; justify-content: center; padding: 24px;
}
.qr-modal.is-open { display: flex; }
.qr-card {
  background: var(--card); border-radius: var(--radius); padding: 28px; text-align: center;
  max-width: 320px; box-shadow: 0 24px 60px rgba(0,0,0,.25);
}
.qr-card h3 { font-size: 16px; font-weight: 600; margin-bottom: 6px; }
.qr-card p { color: var(--ink-mute); font-size: 13px; margin-bottom: 18px; }
.qr-card canvas, .qr-card img { margin: 0 auto 18px; }

/* =========================================================================
   PLATFORM MOCKUPS
   ========================================================================= */
.mk { width: 100%; max-width: 380px; color: #000; font-size: 13px; }

.mk-media { position: relative; background: #eceff1; overflow: hidden; }
.mk-media img, .mk-media video { width: 100%; height: 100%; object-fit: cover; }
/* In the frame the video is cropped to the placement. Full screen is a
   different job: show the whole thing, letterboxed if it has to be. */
.mk-media video:fullscreen { object-fit: contain; background: #000; }
.mk-media video:-webkit-full-screen { object-fit: contain; background: #000; }
/* Belt and braces: some browsers do not match the pseudo-class on the element
   itself, so the script below sets this class as well. */
.mk-media video.is-fullscreen { object-fit: contain; background: #000; }
/* A video waits on black under one play button, the same in every browser;
   the browser's own bar arrives once it plays (js/mockups.js). */
.mk-media video { background: #000; }
.mk-play {
  position: absolute; left: 50%; top: 50%; z-index: 5; transform: translate(-50%, -50%);
  width: 64px; height: 64px; border: 1.5px solid rgba(255, 255, 255, .5); border-radius: 50%; padding: 0;
  display: grid; place-items: center; cursor: pointer;
  background: rgba(0, 0, 0, .55); color: #fff;
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  transition: background var(--t-fast), transform var(--t-fast);
}
.mk-play svg { width: 28px; height: 28px; margin-left: 3px; fill: currentColor; }
@media (hover: hover) { .mk-play:hover { background: rgba(0, 0, 0, .72); } }
.mk-play:active { transform: translate(-50%, -50%) scale(.97); }
.mk-play:focus-visible { outline: 2px solid var(--action-ring); outline-offset: 3px; }

/* Safe zones. Off unless the page asks for them, so the default view is what
   the audience sees rather than a diagram. The box carries the scrim in its
   own shadow, which dims everything outside it without a second element. */
.mk-safe { position: absolute; inset: 0; z-index: 6; pointer-events: none; display: none; }
.is-safe .mk-safe { display: block; }
.mk-safe-box {
  position: absolute;
  border: 1px dashed rgba(255,255,255,.9);
  box-shadow: 0 0 0 2000px rgba(0,0,0,.42);
}
.mk-safe-box span {
  position: absolute; top: 5px; left: 5px;
  font-size: 10px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: #fff; background: rgba(0,0,0,.55); padding: 2px 8px; border-radius: 999px;
  white-space: nowrap;
}
/* Instagram and Facebook Reels: 250 top, 420 bottom, 60 left, 120 right of 1080 x 1920 */
.mk-safe-reel .mk-safe-box   { top: 13.02%; bottom: 21.88%; left: 5.56%; right: 11.11%; }
/* Stories: 250 top and bottom, 60 each side */
.mk-safe-story .mk-safe-box  { top: 13.02%; bottom: 13.02%; left: 5.56%; right: 5.56%; }
/* TikTok: 108 top, 320 bottom, 60 left, 120 right */
.mk-safe-tiktok .mk-safe-box { top: 5.63%;  bottom: 16.67%; left: 5.56%; right: 11.11%; }

/* The switch that turns them on */
/* Show safe zones is an on or off at the press: the portal's switch, its
   name beside it (ux-audit REVIEWPAGE-6). */
.safetoggle { display: flex; align-items: center; gap: 8px; user-select: none; }
.safetoggle[hidden] { display: none; }
.safetoggle .safetoggle-name { font-size: 13px; color: var(--ink-soft); cursor: pointer; }

.mk-media-empty {
  display: flex; align-items: center; justify-content: center;
  color: #9aa3a8; font-size: 12px; background:
    repeating-linear-gradient(45deg, #f2f4f5 0 10px, #eceff1 10px 20px);
}
.r-916 { aspect-ratio: 9 / 16; }

/* Carousel */
.mk-carousel { position: relative; overflow: hidden; }
/* When the frame is sized from the real file, every slide fills that box. */
.mk-carousel-sized .mk-carousel-track,
.mk-carousel-sized .mk-slide,
.mk-carousel-sized .mk-media { height: 100%; }
.mk-carousel-sized .mk-media { aspect-ratio: auto; }
.mk-carousel-track { display: flex; transition: transform var(--t-slow) var(--ease); }
.mk-slide { flex: 0 0 100%; }
.mk-arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 28px; height: 28px; border-radius: 50%; border: 0;
  background: rgba(255,255,255,.9); color: #333; font-size: 17px; line-height: 1;
  display: grid; place-items: center; box-shadow: 0 1px 4px rgba(0,0,0,.2);
}
.mk-arrow-prev { left: 8px; } .mk-arrow-next { right: 8px; }
.mk-arrow[disabled] { opacity: 0; pointer-events: none; }
.mk-dots {
  position: absolute; bottom: 10px; left: 0; right: 0;
  display: flex; justify-content: center; gap: 5px;
}
.mk-dot {
  width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,.55);
  cursor: pointer; box-shadow: 0 0 2px rgba(0,0,0,.3);
}
.mk-dot.is-on { background: #fff; }
.mk-counter {
  position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,.6); color: #fff;
  font-size: 11px; font-weight: 600; padding: 3px 8px; border-radius: 999px;
}

/* Shared chrome */
.mk-head { display: flex; align-items: center; gap: 10px; padding: 10px 12px; }
/* Without a logo this is the client's face in every preview. A mesh of soft
   colour rather than a grey disc: warm at the top, magenta through the middle,
   blue at the foot, with the edges of each blob dissolved so no band shows.
   A logo, when there is one, covers all of it. */
.mk-avatar {
  width: 32px; height: 32px; border-radius: 50%; flex: 0 0 auto; overflow: hidden;
  background:
    radial-gradient(circle at 18% 12%,  #ffe066 0%, rgba(255,224,102,0) 58%),
    radial-gradient(circle at 80% 10%,  #ff922b 0%, rgba(255,146,43,0)  52%),
    radial-gradient(circle at 10% 52%,  #ff3d7f 0%, rgba(255,61,127,0)  58%),
    radial-gradient(circle at 88% 56%,  #f06bd8 0%, rgba(240,107,216,0) 55%),
    radial-gradient(circle at 26% 96%,  #3b5bdb 0%, rgba(59,91,219,0)   62%),
    radial-gradient(circle at 86% 98%,  #4dabf7 0%, rgba(77,171,247,0)  58%),
    linear-gradient(158deg, #ffb84d 0%, #f0629f 42%, #9775fa 72%, #3b5bdb 100%);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.28);
  display: grid; place-items: center; font-weight: 700; color: #fff;
  font-size: 13px; letter-spacing: -.01em;
  text-shadow: 0 1px 2px rgba(0,0,0,.3);
}
/* A brand mark is rarely square and often carries transparency around it, so
   it is fitted whole on a white ground rather than cropped to fill. A logo
   that is already square fills the circle either way. */
.mk-avatar img { width: 100%; height: 100%; object-fit: contain; }
/* Nothing is drawn around a real profile picture, so nothing is drawn around
   this one: no ring, no inset, no padding. The image runs to the edge of the
   circle. White sits behind it because a mark with transparency needs a ground,
   the same as the client's own profile has. */
.mk-avatar.has-logo { background: #fff; box-shadow: none; padding: 0; }
.mk-who { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.mk-handle { font-weight: 600; font-size: 13px; }
.mk-sub { font-size: 11.5px; color: #737373; }
.mk-more { margin-left: auto; color: #262626; }
.mk-actions { display: flex; align-items: center; padding: 9px 12px 4px; color: #262626; }
.mk-actions-left { display: flex; gap: 14px; }
.mk-act { display: inline-flex; }
/* Save sits on the far right of the row, never against Share. */
.mk-act-save { margin-left: auto; }
.mk-likes { padding: 0 12px; font-weight: 600; font-size: 13px; }
.mk-caption { padding: 4px 12px 0; line-height: 1.45; }
.mk-caption-top { padding: 0 12px; }
.mk-tag { color: #00376b; }
/* Likes, caption and comments are one block of text, so the three gaps that
   separate them are one value: 4px, the same step the action row ends on. */
.mk-time { padding: 4px 12px 12px; color: #737373; font-size: 12px; }

.mk-clampwrap { position: relative; }
.mk-clamp {
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--mk-lines, 2); overflow: hidden;
}
.mk-clamp.is-open { -webkit-line-clamp: 999; }
/* A blank line between paragraphs is a line of the clamp, so a two line
   preview spent one of them on nothing and printed its ellipsis on an empty
   row. The break comes back the moment the caption is opened, which is when
   the client is reading the paragraphs rather than the preview. */
.mk-clamp br + br { display: none; }
.mk-clamp.is-open br + br { display: inline; }
/* -webkit-line-clamp clips at the content box, overflow:hidden at the padding box,
   so any bottom padding lets a partial extra line bleed through. */
.mk-clamp { padding-bottom: 0; }
.mk-fb .mk-clampwrap { padding-bottom: 9px; }
.mk-morebtn {
  border: 0; background: none; padding: 0 12px; color: #737373; font-size: 13px;
}
/* These apps set the toggle at the end of the truncated line. On a row of its
   own it tripled the height of a two line caption and read as a third
   paragraph, which is what made the block look loose. It sits on the last line
   over a short fade, and drops back into the flow once the caption is open. */
.mk-clampwrap.is-clamped > .mk-morebtn {
  position: absolute; right: 0; bottom: 0; padding: 0 12px 0 24px;
  background: linear-gradient(to right, transparent, var(--mk-fade, #fff) 16px);
}
/* The one dark surface: a fade needs a flat ground and the reel foot is over
   the video, so the toggle stays in the flow there. */
.mk-vfoot .mk-clampwrap.is-clamped > .mk-morebtn {
  position: static; padding: 0; background: none;
}

/* Instagram */
.mk-ig { background: #fff; border: 1px solid #dbdbdb; border-radius: 8px; overflow: hidden; }

/* Facebook photo grid for an organic multi-image post */
.fb-grid { display: grid; gap: 2px; background: #fff; }
.fb-grid-2 { grid-template-columns: 1fr 1fr; aspect-ratio: 2 / 1; }
.fb-grid-3,
.fb-grid-4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; aspect-ratio: 1 / 1; }
.fb-grid-3 .fb-cell:first-child { grid-column: 1 / -1; }
.fb-grid-5 {
  grid-template-columns: repeat(6, 1fr); grid-template-rows: 1fr 1fr; aspect-ratio: 1 / 1;
}
.fb-grid-5 .fb-cell:nth-child(-n+2) { grid-column: span 3; }
.fb-grid-5 .fb-cell:nth-child(n+3)  { grid-column: span 2; }
.fb-cell { position: relative; overflow: hidden; min-height: 0; min-width: 0; }
.fb-cell .mk-media { width: 100%; height: 100%; aspect-ratio: auto; }
.fb-more {
  position: absolute; inset: 0; background: rgba(0,0,0,.5);
  display: grid; place-items: center;
  color: #fff; font-size: 26px; font-weight: 600; letter-spacing: -.02em;
}
.mk-sub svg { width: 11px; height: 11px; vertical-align: -1px; }

/* Facebook paid carousel: cards peek in from the right, each with its own
   headline strip and call to action button. */
.fb-ad-carousel { position: relative; overflow: hidden; background: #fff; padding: 0 0 10px; }
.fb-ad-track {
  display: flex; gap: 8px; padding: 0 8px;
  transition: transform var(--t-slow) var(--ease);
}
.fb-ad-card {
  flex: 0 0 86%; min-width: 0;
  border: 1px solid #dfe3e8; border-radius: 8px; overflow: hidden; background: #fff;
}
.fb-ad-shot { position: relative; overflow: hidden; background: #f0f2f5; }
.fb-ad-shot .mk-media { width: 100%; height: 100%; aspect-ratio: auto; }
.fb-ad-foot {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; background: #f0f2f5;
}
.fb-ad-text { min-width: 0; flex: 1; }
.fb-ad-headline {
  font-size: 13px; font-weight: 600; color: #050505; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fb-ad-sub { font-size: 11px; color: #65676b; text-transform: uppercase; letter-spacing: .02em; }
.fb-ad-cta {
  flex: none; background: #e4e6eb; color: #050505;
  font-size: 12px; font-weight: 600; padding: 7px 12px; border-radius: 6px;
}
.fb-ad-carousel .mk-arrow { top: 40%; }

/* Facebook */
.mk-fb {
  background: #fff; border: 1px solid #dfe1e5; border-radius: 8px; overflow: hidden;
  font-family: system-ui, "Segoe UI", Helvetica, Arial, sans-serif;
}
.mk-fb-bar {
  display: flex; border-top: 1px solid #e4e6eb; color: #65676b; font-size: 13px; font-weight: 600;
}
.mk-fb-bar span {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 9px 0;
}

/* Vertical formats: no device, just the true 9:16 proportion with the app's
   own UI drawn on it, so the overlays double as a safe zone guide. */
.mk-phone {
  position: relative;
  width: 100%; max-width: 380px;
  border-radius: 14px; overflow: hidden;
  border: 1px solid var(--line);
  box-shadow: 0 1px 2px rgba(0,0,0,.05), 0 8px 20px rgba(0,0,0,.07);
}
.mk-screen {
  position: relative; overflow: hidden; background: #000; color: #fff;
}
.mk-screen .mk-media { aspect-ratio: 9 / 16; background: #111; }

/* Status bar, island and home indicator */
.mk-statusbar {
  position: absolute; top: 0; left: 0; right: 0; height: 46px; z-index: 3;
  display: flex; align-items: center; justify-content: space-between;
  padding: 2px 20px 0 24px; pointer-events: none;
  font-size: 12.5px; font-weight: 600; letter-spacing: .2px;
  color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.4);
}
.mk-sysicons { display: flex; align-items: center; gap: 5px; }
.mk-island {
  position: absolute; top: 9px; left: 50%; transform: translateX(-50%);
  width: 84px; height: 25px; border-radius: 999px; z-index: 4;
  background: rgba(0,0,0,.72); border: 1px solid rgba(255,255,255,.14);
}
.mk-home {
  position: absolute; bottom: 7px; left: 50%; transform: translateX(-50%);
  width: 108px; height: 4px; border-radius: 999px;
  /* solid white with a soft dark halo, so it stays visible on light and dark
     content alike. mix-blend-mode disappeared against dark video. */
  background: rgba(255,255,255,.92); z-index: 4; pointer-events: none;
  box-shadow: 0 0 6px rgba(0,0,0,.35);
}

/* App chrome at the top of the screen */
.mk-topbar {
  position: absolute; top: 50px; left: 0; right: 0; z-index: 3;
  display: flex; align-items: center; padding: 0 14px; color: #fff;
  text-shadow: 0 1px 3px rgba(0,0,0,.45);
}
.mk-topbar-ig { justify-content: space-between; }
.mk-topbar-ig b { font-size: 17px; font-weight: 700; letter-spacing: -.3px; }
.mk-topbar-tt { justify-content: center; gap: 20px; font-size: 14px; }
.mk-topbar-tt span { color: rgba(255,255,255,.62); }
.mk-topbar-tt b { position: relative; font-weight: 700; }
.mk-topbar-tt b::after {
  content: ''; position: absolute; left: 50%; transform: translateX(-50%);
  bottom: -7px; width: 22px; height: 2.5px; background: #fff; border-radius: 2px;
}

.mk-rail {
  position: absolute; right: 10px; bottom: 150px; z-index: 2;
  display: flex; flex-direction: column; align-items: center; gap: 20px;
  text-shadow: 0 1px 4px rgba(0,0,0,.55);
}
.mk-rail span { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.mk-rail b { font-size: 11px; font-weight: 600; }
/* TikTok carries the account at the top of the rail, with the follow badge
   tucked under it. */
.mk-rail-me { position: relative; margin-bottom: 4px; }
.mk-avatar-rail { width: 34px; height: 34px; }
.mk-rail-plus {
  position: absolute; left: 50%; bottom: -7px; transform: translateX(-50%);
  width: 16px; height: 16px; border-radius: 50%; background: #fe2c55; color: #fff;
  display: grid; place-items: center; font-size: 12px; font-weight: 700;
  line-height: 1; text-shadow: none;
}
.mk-vfoot {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  padding: 14px 58px 30px 16px;
  background: linear-gradient(0deg, rgba(0,0,0,.8) 12%, rgba(0,0,0,0));
  text-shadow: 0 1px 3px rgba(0,0,0,.5);
}
/* Reels show the account next to the handle at the foot. */
.mk-vwho { display: flex; align-items: center; gap: 9px; margin-bottom: 6px; }
.mk-avatar-v { width: 28px; height: 28px; }
.mk-vhandle { font-weight: 600; font-size: 13px; }
.mk-vcaption { font-size: 12.5px; line-height: 1.4; }
.mk-vfoot .mk-morebtn { padding: 0; color: #ddd; font-size: 12.5px; }
.mk-vfoot .mk-tag { color: #fff; font-weight: 600; }
.mk-audio {
  display: flex; align-items: center; gap: 6px; margin-top: 8px; font-size: 11.5px; color: #eee;
}
.mk-tiktok .mk-rail { bottom: 150px; }

/* Stories */
.mk-story .mk-screen { background: #111; }
.mk-story-stage { position: relative; }
.mk-bars { position: absolute; top: 52px; left: 10px; right: 10px; z-index: 3;
  display: flex; gap: 3px; }
.mk-bar { flex: 1; height: 2px; border-radius: 2px; background: rgba(255,255,255,.35); }
.mk-bar.is-on { background: #fff; }
.mk-story-head {
  position: absolute; top: 62px; left: 12px; right: 12px; z-index: 3;
  display: flex; align-items: center; gap: 8px;
  text-shadow: 0 1px 3px rgba(0,0,0,.5);
}
.mk-story-head .mk-avatar { width: 26px; height: 26px; }
.mk-story-time { color: #ddd; font-size: 11.5px; }
.mk-tap { position: absolute; top: 86px; bottom: 26px; width: 34%; border: 0;
  background: none; z-index: 3; }
.mk-tap-l { left: 0; } .mk-tap-r { right: 0; }
.mk-counter-story { top: auto; bottom: 26px; right: 12px; z-index: 3; }

/* XiaoHongShu */
.mk-xhs {
  width: 100%; max-width: 380px; background: #fff; border-radius: 10px; overflow: hidden;
  box-shadow: 0 2px 12px rgba(0,0,0,.08);
  font-family: "PingFang SC", "Microsoft YaHei", var(--font);
}
.mk-xhs-body { padding: 11px 12px 12px; }
.mk-xhs-title { font-size: 14.5px; font-weight: 600; line-height: 1.4; margin-bottom: 6px; color: #333; }
.mk-xhs-text { font-size: 13px; color: #333; line-height: 1.55; }
.mk-xhs .mk-tag { color: #3b5b8c; }
.mk-xhs .mk-morebtn { padding: 0; color: #999; font-size: 12.5px; }
.mk-xhs-foot {
  display: flex; align-items: center; gap: 7px; margin-top: 12px;
  padding-top: 10px; border-top: 1px solid #f2f2f2;
}
.mk-xhs-foot .mk-avatar { width: 22px; height: 22px; }
.mk-xhs-author { font-size: 12px; color: #777; }
.mk-xhs-stats { margin-left: auto; display: flex; align-items: center; gap: 5px; color: #999; }
.mk-xhs-stats b { font-size: 11.5px; font-weight: 400; margin-right: 5px; }

@media print {
  .topbar, .filterbar, .approve, .pill, .qr-modal { display: none !important; }
  .card { break-inside: avoid; box-shadow: none; }
}

@media (max-width: 640px) {
  .shell { padding: 0 16px 72px; }
  :root { --head-h: 56px; }
  .topbar-inner { padding: 0 16px; }
  /* The page label gives way before the bar does. At 390 the mark, the label
     and two actions do not fit, and a label clipped to "Client Po…" reads as
     a fault rather than as a name; the heading under the bar says where you
     are anyway. "Prepared for {client}" stays, because on a client's own page
     that is the one thing the bar is telling them. */
  .brand-kicker { display: none; }
  .brand-meta:empty, .brand-meta:not(:has(.brand-for)) + .brand-divider { display: none; }
  .brand:not(:has(.brand-for:not([hidden]))) .brand-divider { display: none; }
  /* #whoami used to sit in the top bar, where it pushed sign out onto a second
     row. It lives in the sidebar footer now, which has room for it, so hiding
     it only left the drawer saying "Logged in as" and nothing else. */
  .grid { grid-template-columns: 1fr; }
  /* Offering a "view on phone" QR code to someone already on a phone is noise. */
  #qrBtn { display: none; }
  .filterbar { gap: 10px; padding: 14px 0 12px; }
  .filterbar .select { flex: 1 1 140px; min-width: 0; }
  .filterbar .count { flex: 1 1 100%; margin-left: 0; }
  /* The one action keeps its own width and sits at the end of the row,
     as it does in a section head. Stretched across the screen above the
     list, Add lead outweighed every client on it. */
  .filterbar .btn { flex: 0 0 auto; margin-left: auto; }
  .filterbar .safetoggle { flex: 1 1 100%; }
  /* The review page (ux-audit REVIEWPAGE-6): Show safe zones and the count
     share one line, the count at its right edge; the set's approved count
     stands on its title's line, the date under them. */
  #filterbar .safetoggle { flex: 1 1 auto; }
  #filterbar .safetoggle:not([hidden]) ~ .count { flex: 0 0 auto; margin-left: auto; }
  .batch > .batch-head { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: baseline; gap: 2px 10px; }
  .batch > .batch-head .batch-date { grid-column: 2 / -1; grid-row: 2; }
  .batch > .batch-head .batch-progress { grid-column: 3; grid-row: 1; align-self: center; white-space: nowrap; margin-left: 0; }

}

/* =========================================================================
   TEAM ADMIN
   ========================================================================= */
.panel {
  background: var(--card); border: 1px solid var(--card-line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 18px 20px; margin-bottom: 12px;
}
.cards + .panel { margin-top: 24px; }
.panel h3 { font-size: 15px; font-weight: 600; letter-spacing: -.01em; margin-bottom: 4px; }
/* A hint is 13px wherever it is. It was scoped to `.panel`, so the moment a
   form moved into a sheet its hint jumped to the 14px body and came out
   larger than the heading above it. */
.hint { color: var(--ink-mute); font-size: 13px; margin-bottom: 13px; }
.panel code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
  background: var(--line-soft); padding: 1px 5px; border-radius: 4px;
}

.viewhead { display: flex; align-items: center; gap: 12px; margin: 24px 0 12px; }
.viewhead h2, .viewhead h3 { flex: 1 1 auto; }
/* The display sizes carry the brand face; everything smaller stays on the
   system stack, where a hinted system face beats a downloaded one at 12px. */
.viewhead h2, .rec-name h2, .cover-panel h2, .batch-title {
  font-family: var(--font-head);
}
.viewhead h2 { font-size: 19px; font-weight: 600; letter-spacing: -.02em; }
.viewhead h3 { font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
.viewhead .btn { flex: 0 0 auto; }

/* Bottom alignment dropped the label of any field standing next to a taller
   one, which is what made Date and Time sit at two different heights. Each
   pair is now its own column and the row aligns from the top. */
.row { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-start; }
.row > div { flex: 1 1 180px; min-width: 0; display: flex; flex-direction: column; }
.row > div > .input, .row > div > .select, .row > div > .slugfield { margin-top: auto; }
/* A button standing in a row of labelled fields lines up with the fields, not
   with their labels. Top alignment put Save number level with "Invoice no."
   and a full control height above the box it saves. */
.row > .btn { align-self: flex-end; }
/* A field given a fixed basis may still grow into spare room, so a row of
   five never leaves one squashed with its chevron over its text. */
.row > div[style*="flex:0 0"] { flex-grow: 1 !important; }
.row .input, .row .select { width: 100%; }
.row .btn { flex: 0 0 auto; }
/* Fields that share a row share one split, on every row of the form. Each
   field used to carry its own pixel width in a style attribute (220, 200,
   200, 220) and grow from it when the row wrapped, so every row of the
   Billing pane split at a different point and the boxes read as uneven
   (reported 2026-09-24). A grid of equal columns cannot drift: two, or
   three, and `span-all` / `span-2` for the field that needs the room. The
   cell stays the column flex box `.row > div` makes it, so a field still
   sits on the bottom of its cell when a neighbouring label wraps. */
.row.fgrid, .row.fgrid-3 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 12px; }
.row.fgrid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.row.fgrid > .span-all, .row.fgrid-3 > .span-all { grid-column: 1 / -1; }
.row.fgrid-3 > .span-2 { grid-column: span 2; }
/* A date and its time as one cell of a two-column row, so the row keeps the
   columns of the rows above it (2026-10-20, TEAM-9). */
.row > .fpair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 12px; min-width: 0; }
.row > .fpair:has(> [hidden]) { grid-template-columns: minmax(0, 1fr); }
.row > .fpair[hidden] { display: none; }
.fpair > div { min-width: 0; display: flex; flex-direction: column; }
.fpair > div > .input { margin-top: auto; }
.fpair > [hidden] { display: none; }
.row.fgrid:has(> .fpair[hidden]) > :first-child { grid-column: 1 / -1; }
/* A pane that narrow, or a phone, takes one field a line. */
.is-tight .row.fgrid, .is-tight .row.fgrid-3 { grid-template-columns: minmax(0, 1fr); }
.is-tight .row.fgrid-3 > .span-2 { grid-column: auto; }
@media (max-width: 560px) {
  .row.fgrid, .row.fgrid-3 { grid-template-columns: minmax(0, 1fr); }
  .row.fgrid-3 > .span-2 { grid-column: auto; }
}

/* ---------- The form system (js/form.js; the audit of 2026-09-24) ----------
   A form over five fields is sections of two to five, each under a small
   title, in the order the decision is made; a choice of two to four is a
   segment; the fields a person usually leaves as they are fold under More
   details, whose line says what they hold. Asked for site-wide by the user. */
/* Every section after the first is divided from the one before it, however
   the markup wraps them (Bulk add keeps half its sections in a box it hides);
   the first, and one that follows the choice a sheet opens on (the kind of
   task, What in Bulk add), takes no rule. */
.fsec { margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--line-soft); }
.sheet-body > .fsec:first-child, .sheet-body > :first-child > .fsec:first-child, .panelhead + .fsec, .panelhead + .hint + .fsec { margin-top: 0; padding-top: 0; border-top: 0; }
.kindseg + .fsec, .sheet-body > .row:first-child + .fsec { margin-top: 20px; padding-top: 0; border-top: 0; }
.fsec-h {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  margin: 0 0 12px; font-size: 13px; font-weight: 600; letter-spacing: -.005em; color: var(--ink);
}
.fsec-h small { font-size: 12px; font-weight: 400; color: var(--ink-mute); text-align: right; }
/* The section's title is the step above its first field; the sheet's 16px
   between rows starts after it. */
.fsec > .fsec-h + .row { margin-top: 0; }
/* A tick under a section's fields is a field of its own, on the same step. */
.fsec > .tickline, .fsec > .tickstack { margin-top: 16px; }
.fsec > .fsec-h + .tickline, .fsec > .fsec-h + .tickstack { margin-top: 0; }
.sheet-body > .tickline:first-child + .fsec { margin-top: 20px; }
/* The one quiet action a section adds with sits on its own line under it,
   its glyph on the section's own margin. */
.fsec > .fsec-add { margin-top: 8px; min-width: 0; padding-inline: 0; }
/* A segment in a form: a field of its own, the track the height of a field. */
.seg { display: flex; gap: 0; min-height: var(--ctl-h); border-radius: var(--radius-sm); background: var(--tonal); }
.seg-opt {
  flex: 1 1 0; min-width: 0; min-height: var(--ctl-h); padding: 0 8px;
  border-radius: var(--radius-sm); font: inherit; font-size: var(--ctl-text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.seg-opt:disabled { opacity: .45; cursor: default; }
/* A segment whose words do not fit one line: two columns, an odd last option
   across both (js/form.js decides by measuring, never by a count). */
.seg.is-auto > .seg-opt { flex: 1 1 auto; }
.seg.is-wrap { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.seg.is-wrap > .seg-opt:last-child:nth-child(odd) { grid-column: 1 / -1; }
.seg-opt:focus-visible { outline: 2px solid var(--action-ring); outline-offset: 1px; }
.seg.is-disabled { opacity: .6; }
/* The select behind a segment stays in the page for every script that reads
   it, out of sight and out of the tab order. */
.seg-src {
  position: absolute !important; width: 1px !important; height: 1px !important; min-height: 0 !important;
  margin: -1px !important; padding: 0 !important; border: 0 !important; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; opacity: 0; pointer-events: none;
}
/* More details: one line, the fields folded under it, and what they hold. */
.fmore { border-top: 1px solid var(--line-soft); margin-top: 20px; }
.fsec + .fmore { margin-top: 24px; }
.fmore > summary {
  display: flex; align-items: center; gap: 10px; min-height: var(--ctl-h); padding: 10px 0 0;
  list-style: none; cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink);
  white-space: nowrap;
}
.fmore > summary::-webkit-details-marker { display: none; }
.fmore > summary::after {
  content: ""; width: 7px; height: 7px; margin-left: 2px; flex: 0 0 auto;
  border-right: 1.6px solid var(--ink-mute); border-bottom: 1.6px solid var(--ink-mute);
  transform: translateY(-2px) rotate(45deg); transition: transform var(--t) var(--ease);
}
.fmore[open] > summary::after { transform: translateY(1px) rotate(-135deg); }
.fmore-sum { margin-left: auto; order: 2; font-size: 12.5px; font-weight: 400; color: var(--ink-mute);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; }
.fmore > summary::after { order: 3; }
.fmore[open] .fmore-sum { visibility: hidden; }
.fmore > .row:first-of-type { margin-top: 12px; }
/* A fold standing open with its summary hidden reads as the fields alone
   (the Issue sheet's letter body, 2026-10-20). */
.fmore > summary[hidden] { display: none; }
.fmore:has(> summary[hidden]) { border-top: 0; margin-top: 0; }
.muted { color: var(--ink-mute); font-size: 12.5px; }
.msg { font-size: 13px; margin-top: 12px; }
.msg.err { color: var(--err); }
.msg.ok  { color: var(--ok); }
.msg.mute { color: var(--ink-mute); }
/* A value refused in place: its line under the field (js/ask.js, 2026-10-20). */
.msg.askmsg { flex: 1 0 100%; order: 9; margin-top: 4px; }
/* Nothing there, said as a line inside the panel the list would have filled.
   It was a 26px dashed rectangle with the words centred in it, so four empty
   sections on one record drew four large dashed boxes down the page and the
   heading above each one was the smaller mark. A bounded line reads as the
   list that has nothing in it, which is what it is. */
.empty {
  grid-column: 1 / -1;
  background: var(--card); border: 1px solid var(--card-line);
  border-radius: var(--radius-panel);
  padding: 18px 15px; text-align: left; color: var(--ink-mute); font-size: 13.5px;
}
/* Sat flush against the page with almost nothing to aim at. */
.backlink {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: var(--ctl-h); border: 1px solid transparent; background: none; border-radius: 8px;
  margin: 14px 0 4px; padding: 0 12px 0 9px;
  color: var(--ink-mute); font-size: 13px;
}
.backlink svg { width: 15px; height: 15px; }
@media (hover: hover) {
  .backlink:hover { color: var(--ink); background: var(--line-soft); }
}
/* A small act beside what it changes (Change, Remove, Add Chinese caption, a
   suggested figure): a quiet button at the control floor that gives its
   height back to the line, never an underlined word (the user, 2026-09-28:
   no text links in the portal). */
.linkbtn {
  display: inline-flex; align-items: center; gap: 4px;
  min-height: var(--ctl-h); margin-block: calc((20px - var(--ctl-h)) / 2); padding: 0 8px;
  border: 0; border-radius: var(--radius-sm); background: none; cursor: pointer;
  color: var(--ink-soft); font: inherit; font-size: 12.5px; font-weight: 500; text-decoration: none;
}
@media (hover: hover) { .linkbtn:hover { color: var(--ink); } }
.linkbtn.is-danger { color: var(--err); }
@media (hover: hover) { .linkbtn.is-danger:hover { color: var(--err-hi); } .linkbtn.is-danger:hover::before { background: var(--err-bg); } }
.linkbtn .ovgo-chev { width: 14px; height: 14px; flex: 0 0 auto; }
/* A fact's value that opens its record (a supplier's client, a client's
   supplier): the value's own size, its words on the value column's edge. */
dd > .linkbtn { margin-inline-start: -8px; font-size: inherit; }
/* Who this device decides as, under a client page's name card, with Change
   and Forget (js/decide.js, 2026-10-08). */
.decide-as {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px;
  margin: 12px 0 0; padding-top: 12px; border-top: 1px solid var(--line-soft);
  font-size: 13px; color: var(--ink-mute);
}
.decide-as-who { margin-right: auto; }
.decide-as-who b { color: var(--ink); font-weight: 600; }
.decide-as .linkbtn { padding: 0 6px; }
.decide-as-field { flex: 1 1 160px; min-width: 0; max-width: 320px; }
/* On a phone the name takes the row and Cancel and Save share the next as
   equal halves, Cancel on the left. */
@media (max-width: 640px) {
  .decide-as:has(.decide-as-field) { flex-direction: row-reverse; }
  .decide-as .decide-as-field { flex: 1 1 100%; max-width: none; order: -1; }
  .decide-as .btn { flex: 1 1 0; }
}

.linkbox .input {
  flex: 1 1 260px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
}
.linkbox .btn { flex: 0 0 auto; text-align: center; text-decoration: none; line-height: 1.5; }
/* A chip in a row of 38px controls is the one item that is not 38px tall.
   The row stretches its children by default, so the chip grew to the full
   height and printed its word against the top of it, a few pixels above the
   field and the buttons either side. */
.linkbox .tone, .linkbox .chip, .linkbox .chip-state { flex: 0 0 auto; align-self: center; }

/* Upload */
.drop {
  border: 1.5px dashed var(--line); border-radius: var(--radius); padding: 34px 22px;
  text-align: center; cursor: pointer; transition: border-color var(--t) var(--ease-out), background var(--t) var(--ease-out);
  display: flex; flex-direction: column; gap: 4px;
}
.drop b { font-size: 14.5px; font-weight: 600; color: var(--ink); }
.drop > span { color: var(--ink-mute); font-size: 13px; }
/* Under a finger nothing is dragged: the zone reads as the press it is. */
.drop .drop-coarse { display: none; }
@media (pointer: coarse) {
  .drop .drop-fine { display: none; }
  .drop .drop-coarse { display: inline; }
}
.drop:hover, .drop.is-over { border-color: var(--ink); background: var(--sunk); }

.linkrow { display: flex; gap: 8px; margin-top: 12px; }
.linkrow .input { flex: 1; font-size: 13px; }
.linkrow .btn { flex: 0 0 auto; }

/* Google Drive import */

.drive-actions { display: flex; align-items: center; gap: 14px; margin: 16px 0 12px; }
.dfile:hover { border-color: var(--ink); }
.dfile.is-done { opacity: .55; cursor: default; }
.dfile input { flex: 0 0 auto; margin: 0; }
.dfile-img img { width: 100%; height: 100%; object-fit: cover; }
/* No thumbnail from Drive: show what the file is instead of a broken icon. */
.dfile-img i {
  position: absolute; inset: 0; display: none; place-items: center;
  font-style: normal; font-size: 10px; font-weight: 700; letter-spacing: 0;
  color: var(--ink-mute); background: var(--line-soft);
}
.dfile-img.is-blank img { display: none; }
.dfile-img.is-blank i { display: grid; }
.dfile-status { font-size: 11.5px; }
.dfile-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.dfile-meta b {
  font-size: 12.5px; font-weight: 600; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.dfile-meta .muted { font-size: 11.5px; }

.combinebar {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  margin-top: 16px; padding: 13px 16px; border-radius: 10px;
  background: var(--line-soft); font-size: 13px; color: var(--ink-soft);
}
.combinebar span { flex: 1 1 240px; }
.combinebar .btn { flex: 0 0 auto; background: var(--card); }

/* Draft post being written */
/* A box inside a card is shaded, never outlined (2026-10-14): the draft is
   the card's ground on the zone's inset one, with no edge of its own. */
.draft {
  display: flex; gap: 16px; margin-top: 16px; padding: 16px;
  border: 0; border-radius: var(--radius); background: var(--card);
}
.draft-media img, .draft-media video { width: 100%; height: 100%; object-fit: cover; }
.draft-media i {
  position: absolute; bottom: 5px; left: 5px; font-style: normal; font-size: 10px;
  font-weight: 600; color: #fff; background: rgba(0,0,0,.6);
  padding: 2px 6px; border-radius: 999px;
}
.draft-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
.draft-top .select { flex: 0 1 auto; min-width: 0; max-width: 100%; }
.draft-body { --box-h: var(--box-h-sm); }
.draft-body .textarea { font-size: 13.5px; }
.draft-body .linkbtn { align-self: flex-start; }

/* Status, everywhere it appears. A dot and a word, sized like the text around
   it, rather than a grey block in capitals shouting over the thing it labels.
   Colour alone never carries it: the word is always there. */
/* A state is a chip with a word in it, everywhere in this portal. Three
   screens drew a coloured disc beside a word of the same colour instead: the
   disc said nothing the word did not already say, and it was the one shape
   this design system rules out for a status. Same component as the console
   and the client portal now, so one vocabulary has one look. */
.status {
  display: inline-flex; align-items: baseline; gap: 6px; flex: 0 0 auto; white-space: nowrap;
  font-size: 12.5px; font-weight: 500; line-height: 1.5; padding: 2px 0; color: var(--ink);
}
/* The decision's dot (2026-10-07), the status chip's own colours. */
.status::before {
  content: ""; flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; background: var(--dot-off);
}
.status-pending::before  { background: var(--dot-wait); }
.status-approved::before { background: var(--dot-ok); }
.status-changes::before  { background: var(--dot-late); }

/* Posts already saved into the set.

   Three columns on a desktop, two rows on a phone, stated as areas so nothing
   can wrap somewhere it was not meant to. On a phone three marks beside the
   text left it about a hundred pixels, which broke every placement name over
   two lines and cut the pixel count in half, so there the marks take a line of
   their own under a hairline and the words get the width. */
.saved {
  display: grid; gap: 0 13px; padding: 12px 0;
  grid-template-columns: 62px minmax(0, 1fr) auto;
  grid-template-areas: "thumb body actions";
  align-items: start;
  border: 0; border-top: 1px solid var(--line-soft); background: none;
}
.saved:first-child { border-top: 0; padding-top: 4px; }
/* A video and its cover, one linked card: a shaded frame (a box inside a
   card is shaded, never outlined) holding the two rows, a hairline between
   them and a link drawn from the video's picture down to the cover's, so
   the pair reads as one thing at a glance. Each row keeps its own state and
   ⋯. A half outside the chosen stage stays in sight, faded, its ⋯ whole. */
.saved-pair {
  /* The frame reaches into the panel's padding, so its pictures and words
     keep the column every other row's do. */
  position: relative; margin: 8px -12px; padding: 0 12px;
  background: var(--sunk); border-radius: var(--radius-sm);
}
.saved-pair:first-child { margin-top: 0; }
.saved-pair + .saved { border-top: 0; }
.saved-pair > .saved { padding: 12px 0; border-top-color: var(--line); }
.saved-pair > .saved { position: relative; }
.saved-pair > .saved:not(:last-child)::after,
.saved-pair > .saved + .saved::before {
  content: ""; position: absolute; width: 2px; left: calc(var(--pair-th) / 2 - 1px);
  background: var(--line-ctl); border-radius: 1px;
}
.saved-top { display: flex; align-items: center; gap: 8px; min-width: 0; width: 100%; }
.saved-top > b { flex: 1 1 auto; min-width: 0; }
.saved-top > .tone { flex: 0 0 auto; }
.saved-thumb {
  grid-area: thumb; width: 62px; height: 62px; border-radius: 9px; overflow: hidden;
  background: var(--line-soft);
}
.saved-thumb img, .saved-thumb video { width: 100%; height: 100%; object-fit: cover; }
.saved-body {
  grid-area: body; min-width: 0; display: flex; flex-direction: column;
  align-items: flex-start; gap: 2px;
}
.saved-body b {
  font-size: 13.5px; font-weight: 600; letter-spacing: -.01em; line-height: 1.35;
}
.saved-body .muted {
  max-width: 100%; font-size: 12px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* status, then the file, on one quiet line */
.saved-meta {
  display: flex; align-items: center; gap: 6px; max-width: 100%;
  font-size: 12px; color: var(--ink-mute);
}
.saved-meta .sep { opacity: .5; }
/* Select posts (2026-10-07): the tick stands where the row's ⋯ was, the
   whole row its target. */
.saved-pick { justify-content: center; min-width: 38px; min-height: 38px; margin-block: -6px; cursor: pointer; }
.saved.is-picking { cursor: pointer; }
/* A round, quiet mark that only takes colour when you reach for it. */
.iconbtn {
  width: var(--ctl-h); height: var(--ctl-h); flex: 0 0 auto; padding: 0;
  display: inline-grid; place-items: center;
  border: 0; background: none; border-radius: 9px;
  color: var(--ink-mute); transition: background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.iconbtn svg { width: 16px; height: 16px; stroke-width: 1.7; }
/* A thumb needs the whole 44px even where the mark stays this size. */
@media (pointer: coarse) { .iconbtn { width: var(--tap); height: var(--tap); } }
/* Inside the hover media like every other hover here: a tap leaves :hover
   on, and a phone bar with two icon controls showed one of them filled. */
@media (hover: hover) {
  .iconbtn:hover { background: var(--line-soft); color: var(--ink); }
  .iconbtn.is-danger:hover { background: var(--err-bg); color: var(--err); }
  .iconbtn.is-warn:hover { background: var(--tonal-hover); color: var(--ink); }
}

@media (max-width: 640px) {
  .saved { grid-template-columns: 56px minmax(0, 1fr) auto; gap: 0 10px; }
  .saved-thumb { width: 56px; height: 56px; }
}

/* The request is a line, never a pill that reads as a button: who asked
   and when, then the note in ink (2026-10-14). */
.saved-note {
  margin-top: 6px; font-size: 12.5px; color: var(--ink);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.saved-note-who { color: var(--ink-mute); }
/* Edit, under the request it answers, at its own width. */
.saved-body > .saved-edit { margin-top: 8px; flex: 0 0 auto; }

@media (max-width: 640px) {
  .panel { padding: 18px; }
  .cards { grid-template-columns: 1fr; }
  .draft { flex-direction: column; }
}

/* =========================================================================
   BATCH 2 ADDITIONS
   ========================================================================= */

/* Foldable content sets */
.batch-head {
  width: 100%; text-align: left; border: 0; background: none;
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 14px 2px; margin-bottom: 0; border-bottom: 1px solid var(--line);
  cursor: pointer;
}
.batch-head:hover .batch-title { color: var(--ink); }
/* The drawn chevron, as the selection page's fold (2026-10-31, RV-6). */
.fold-caret {
  align-self: center; color: var(--ink); width: 15px; height: 15px; flex: 0 0 auto;
  transition: transform var(--t) var(--ease);
}
.batch.is-folded .fold-caret { transform: rotate(-90deg); }
.batch-progress {
  margin-left: auto; font-size: 12px; color: var(--ink-mute);
  padding: 3px 9px; border-radius: 999px; background: var(--line-soft);
}
.batch.is-done .batch-progress { background: var(--ok-bg); color: var(--ok); }
.batch-body { padding-top: 22px; }
.batch.is-folded .batch-body { display: none; }

/* Equal looking cards: media on top, copy in the middle, buttons pinned down.
   Stretching is what lines the approve rows up across a row, but it also means
   one card growing drags every card beside it with it. So the moment anything
   in the row is expanded the row goes back to sizing each card to its own
   content, and only the card that was opened gets taller. */
.grid { align-items: stretch; }
.grid.has-open { align-items: start; }
.card { display: flex; flex-direction: column; }
.copyblock { flex: 1 1 auto; }
.approve { margin-top: auto; }
.copytext {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 5; overflow: hidden;
}
.copyblock.is-open .copytext { -webkit-line-clamp: 999; }
/* Show full caption is a quiet act with its chevron (`.linkbtn`), never an
   underlined word (DESIGN.md §5); the chevron turns while the caption is
   open. Pulled back to the caption's edge, its fill still hugging the line. */
.copy-more.linkbtn { align-self: flex-start; margin-top: 2px; margin-left: -8px; }
.copy-more[hidden] { display: none; }
.copy-more-chev { transition: transform var(--t) var(--ease); }
.copy-more[aria-expanded="true"] .copy-more-chev { transform: rotate(180deg); }

/* An approved post is settled: one wide outlined button, no second option.
   Green, because this is a status and no longer an action. */
.approve-row.is-settled .btn-approve {
  flex: 1; background: var(--card); border-color: var(--ok); color: var(--ok);
  opacity: 1; cursor: default; font-weight: 600;
}
.approve-row.is-settled .btn-approve::before { content: '\2713\00a0'; }

/* Import progress */
.progress { margin: 16px 0 4px; }
/* The console's own hand-in for a creator, on the card's draft step: the
   file field is a field like its neighbours and the bar under it takes the
   whole row with no margin of its own, so the step keeps its 12px rhythm. */
.kupload { grid-column: span 2; margin: 0; }
.kupload[hidden] { display: none; }
.progress-head {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 12.5px; color: var(--ink-soft); margin-bottom: 6px;
}
.progress-head span:last-child { font-weight: 600; font-variant-numeric: tabular-nums; }
.progress-track { height: 6px; border-radius: 999px; background: var(--line); overflow: hidden; }
.progress-fill {
  height: 100%; width: 0; border-radius: 999px; background: var(--fill);
  transition: width var(--t-slow) var(--ease);
}

/* What the file actually is */
.filetag {
  display: inline-block; align-self: flex-start; font-size: 11px; font-weight: 600; letter-spacing: 0;
  color: var(--ink-soft); background: var(--line-soft);
  padding: 2px 7px; border-radius: 5px; white-space: nowrap;
}

/* Button variants */
.btn-sm { flex: 0 0 auto; padding: 0 13px; min-height: var(--ctl-h-sm); font-size: 12.5px; min-width: 92px; }
.btn-sm svg { width: 13px; height: 13px; }
/* What destroys is the red words on the ordinary tonal button, as Apple's
   destructive button is (2026-10-07); a red slab read as an alarm. */
.btn-danger { background: var(--tonal); border-color: transparent; color: var(--err-ink, var(--err)); flex: 0 0 auto; font-weight: 600; }
@media (hover: hover) {
  .btn-danger:hover { background: var(--err-bg); border-color: transparent; color: var(--err-hi); }
}
.btn-danger:active { background: var(--err-bg); color: var(--err-hi); }
.btn-icon svg { width: 14px; height: 14px; }

.saved.is-editing { align-items: flex-start; background: var(--sunk); }
/* Every field and the action pair run the body's width, so the captions,
   the file and Cancel / Save share one right edge (the user, 2026-10-04:
   the pair sat small at the left and the file ran past the captions). */
.saved.is-editing .saved-body { gap: 10px; align-items: stretch; }
.saved.is-editing .saved-body > .changebox-actions { margin-top: 2px; }
.saved.is-editing { --box-h: var(--box-h-sm); }
.saved.is-editing .textarea { font-size: 13px; }
.capwrite > .btn { flex: 0 0 auto; }
.msg.capmsg { margin-top: 0; }
.msg.capmsg:empty { display: none; }

/* Cover image is an asset, not a post, so it gets a neutral frame. It has no
   copy, so its card ends under its decision rather than stretching to the
   height of a neighbour with copy and leaving a blank. */
.mk-coverwrap { width: 100%; max-width: 380px; }
.mk-coverwrap .mk-phone { max-width: 100%; }
.shell-review .card.is-cover { align-self: start; }

@media (max-width: 640px) {
  .batch-progress { margin-left: 0; }
}

/* Signing in is the whole page, the same as the review notice. A card drawn
   around it only adds an edge to look at. */
.signin-card {
  width: 100%; max-width: 340px; background: none;
  border: 0; border-radius: 0; padding: 0; box-shadow: none;
  text-align: left;
}
.signin-card h2 { font-size: 19px; font-weight: 600; letter-spacing: -.02em; margin-bottom: 6px; }
.signin-card p { color: var(--ink-mute); font-size: 13.5px; margin-bottom: 24px; }
.signin-card .field-label { margin-bottom: 6px; }
.signin-card .input { width: 100%; padding: 11px 13px; }
.signin-btn { width: 100%; margin-top: 14px; padding: 12px; font-size: 14px; }
.signin .msg { margin-top: 14px; }
.signin-alt { width: 100%; margin-top: 8px; }
/* The passkey is the second way in, so it is the tonal button under the
   primary, at the primary's own height. */
.signin-pk { padding: 12px; font-size: 14px; }

/* A question asked about something on a sheet sits above that sheet: every
   sheet is moved to the end of the page on open, so without its own layer
   the question could open underneath the sheet it was about. */
#askSheet { z-index: 95; }

/* The Passkeys sheet: a short list, one row a passkey, the ⋯ at the end. */
.sheet-card.pk-card { width: min(520px, 100%); }
.pk-row + .pk-row { border-top: 1px solid var(--line-soft); }
.pk-ico { width: 16px; height: 16px; color: var(--ink-mute); }
.pk-who { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.pk-who b {
  font-size: 14px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pk-who small { font-size: 12px; color: var(--ink-mute); }
.pk-empty { margin: 0; font-size: 13.5px; color: var(--ink-mute); }
.rp-agesum { margin: 8px 0 0; font-size: 12.5px; color: var(--ink-mute); }
.rp-agesum.is-warn { color: var(--err); }

/* Carousel slide reordering */
.slides { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 4px; }
.slide-chip {
  position: relative; width: 54px; height: 68px; border-radius: 8px; overflow: hidden;
  border: 1px solid var(--line); background: var(--line-soft);
}
.slide-chip img, .slide-chip video { width: 100%; height: 100%; object-fit: cover; }
.slide-chip i {
  position: absolute; top: 3px; left: 4px; font-style: normal; font-size: 10px;
  font-weight: 700; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.8);
}
.slide-move button {
  flex: 1; border: 0; background: none; color: #fff; font-size: 12px;
  line-height: 1; padding: 3px 0;
}
.slide-move button[disabled] { opacity: .3; cursor: default; }
.slide-hint { font-size: 12px; color: var(--ink-mute); margin-top: 8px; }

/* =========================================================================
   REVIEW COVER PAGE
   A client landing here without their link should meet something that
   explains the portal, not something that looks broken.
   ========================================================================= */
.cover {
  display: flex; align-items: center; justify-content: center;
  min-height: calc(100vh - 55px); margin-bottom: -96px; padding: 40px 0;
}
@supports (height: 100dvh) {
  .cover { min-height: calc(100dvh - 55px); }
}
.cover-inner { max-width: 440px; width: 100%; text-align: center; }

/* The notice is the whole page here. An outline and a shadow around it only
   added a box to look at.

   Painting the section white was not enough: the section is only as wide as
   the shell, so the white sat on the grey page with a seam down each side.
   The page carries the white instead, and nothing draws an edge. */
.cover-panel { padding: 38px 8px 34px; }
.cover-panel h2 { font-size: 24px; font-weight: 600; letter-spacing: -.02em; margin-bottom: 10px; }
.cover-panel p { color: var(--ink-soft); font-size: 14px; line-height: 1.65; }
/* A cover is the whole page and its one line is the only thing to read on it,
   so on the client pages it takes the reading size the rest of them do. */
:root[data-face="client"] .cover-panel p { font-size: 16px; }
.cover-panel form { margin-top: 18px; display: flex; gap: 8px; }
.cover-panel form .input { flex: 1; min-width: 0; }
/* The way back under a cover's form: quiet, at its own width, centred with
   the cover's text. */
.cover-panel .cover-alt { margin-top: 8px; min-width: 0; }
/* The quotation's cover offers two ways on: equal halves, 8 apart, each label whole
   (a row each where the halves cannot hold them). */
.qs .cover-alt:has(.btn + .btn) { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.qs .cover-alt:has(.btn + .btn) > .btn { flex: 1 1 172px; max-width: 220px; white-space: nowrap; }
/* The verify page's one answer: the reference, what it is and when, and
   whether it stands, in the chip every other state on this portal wears. */
.verify-out { margin-top: 20px; display: flex; flex-direction: column; align-items: center; gap: 6px; }
.verify-out b { font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
.verify-out span { color: var(--ink-soft); font-size: 14px; }
:root[data-face="client"] .verify-out span { font-size: 16px; }
.verify-out .tone { font-size: 11.5px; color: inherit; }
.verify-out .verify-need { color: var(--err); }
/* The verify page is a form, not a notice (2026-10-31, VER-1 to VER-3): on
   the scale (the title 8 over its line, the field 24 under it, the answer
   24 under that, 32 above and below), its line at the form's 14 as the
   404's, and on a phone the field and Verify each the column's width, so a
   whole reference stays in sight. */
.cover-panel.verify-panel { padding: 32px 0; }
.verify-panel h2 { margin-bottom: 8px; }
:root[data-face="client"] .verify-panel p { font-size: 14px; }
.verify-panel form { margin-top: 24px; }
.verify-panel .verify-out { margin-top: 24px; }
@media (max-width: 640px) {
  .cover-panel.verify-panel { padding: 32px 0; }
  .verify-panel form { flex-direction: column; }
  .verify-panel form > .btn { width: 100%; }
}
/* The sheet that issues a document carries a body per language, so it takes
   a little more width than the rest. */
.sheet-card.sheet-wide { width: min(880px, 100%); }
/* The installed app's PDF viewer, where it has no share sheet for files
   (js/documents.js `viewer`): the file at the sheet's height. */
.pdfview { display: block; width: 100%; height: 70vh; height: 70dvh; border: 0; border-radius: var(--radius-sm); background: var(--sunk); }
@media (max-width: 640px) {
  .cover-panel { padding: 30px 22px 26px; }
  .cover-panel h2 { font-size: 21px; }
}

/* =========================================================================
   BUTTON SYSTEM
   Apple's three roles (2026-10-07), so a glance tells you what a button does.
     primary  solid blue   the one action the view is for (`.btn-primary`,
                           `.btn-go` alike, stated with `.btn-primary`)
     default  tonal        everything else, caution included
     danger   red words    destroys data
     quiet    borderless   incidental, never the point of the section
   ========================================================================= */
/* Caution is not a colour of its own: Unpublish, Withdraw, Stop, Revert and
   Reopen are ordinary buttons, and where one costs something the question it
   opens says what (Apple: no alert for what can be undone). The class stays
   so a page can still name the role. */
.btn-warn { flex: 0 0 auto; }

.btn-quiet { background: none; border-color: transparent; color: var(--ink-mute); flex: 0 0 auto; }
@media (hover: hover) {
  .btn-quiet:hover { background: var(--line-soft); border-color: transparent; color: var(--ink); }
}
.btn-quiet.is-danger { color: var(--err); }
.btn-quiet.is-danger:hover { background: var(--err-bg); color: var(--err-hi); }

.setbar-actions > .btn { flex: 0 0 auto; }
/* On a phone the set's two acts share the row in equal halves. */
@media (max-width: 640px) {
  .setbar-actions:has(> .btn:not([hidden]) + .btn:not([hidden])) > .btn { flex: 1 1 0; min-width: 0; }
}
.setbar .msg { margin: 0; }
.setbar .msg:empty { display: none; }
.rec-id.rec-id-set { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "who ctl"; }
.is-narrow .rec-id.rec-id-set {
  grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name ctl" "meta meta"; gap: 4px 12px;
}
.is-narrow .rec-id-set > .rec-who { display: contents; }
/* The rename pen follows the name's last word, never dropping to a line of
   its own under a wrapped name (2026-10-14): the name runs inline. */
.rec-id-set .rec-name { display: block; }
.rec-id-set .rec-name h2 { display: inline; }
.rec-id-set .rec-name:has(> .askfield) { display: flex; }
/* This month, one line under the person in charge (2026-10-08): the dot
   follows the month (amber running, green all done, red with something
   overdue), the words wrap between their parts, never inside one. */
.rec-month { margin-top: 4px; margin-inline-start: -8px; display: inline-flex; flex-wrap: wrap; align-items: center; gap: 2px 6px;
  font-size: 13px; color: var(--ink-soft); text-align: left; }
.rec-month .is-err { color: var(--err); }
.rec-month-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--dot-wait); flex: 0 0 auto; }
.rec-month-dot.is-ok { background: var(--dot-ok); }
.rec-month-dot.is-late { background: var(--dot-late); }

/* ONE CHIP, TWO SHAPES. `.chip`, `.tone` and `.chip-state` are one component
   (three names from before they were joined). What it says decides its shape
   (the user, 2026-10-07, after the colour audit, on App Store Connect's own
   style):
   · a status is a dot and its word in ink: `.chip-state`, or a chip carrying
     a tone (`is-ok` and `is-live` done or live, `is-warn` waiting on
     somebody, `is-danger` needs acting on now, `is-off` not started, paused
     or closed). The word says it; the dot is the colour beside it, so a busy
     list is ink and dots, never a wall of tinted pills;
   · a fact about a thing (High, Goodwill, White label, You) is a quiet grey
     tag, the bare `.chip` / `.tone`, and never a status colour.
   The dot is an empty inline box, whose foot is its baseline, so it sits on
   the word's baseline and a status beside text shares its line. Never a flex
   box: a word holding a part of its own (`Changes <span>requested</span>`)
   would split into pieces a gap apart. */
.chip, .tone, .chip-state {
  font-size: 11.5px; font-weight: 600; line-height: 1.5;
  padding: 2px 8px; border-radius: 5px; white-space: nowrap;
  background: var(--line-soft); color: var(--ink-soft);
}
.chip { display: inline-flex; align-self: flex-start; }
.tone { flex: 0 0 auto; }
.chip-state,
.chip:is(.is-ok, .is-live, .is-warn, .is-danger, .is-off),
.tone:is(.is-ok, .is-live, .is-warn, .is-danger, .is-off) {
  display: inline-block;
  padding: 2px 0; border-radius: 0; background: none;
  font-size: 12.5px; font-weight: 500; color: var(--ink);
}
.chip-state::before,
.chip:is(.is-ok, .is-live, .is-warn, .is-danger, .is-off)::before,
.tone:is(.is-ok, .is-live, .is-warn, .is-danger, .is-off)::before {
  content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 6px; background: var(--dot-off);
}
:is(.chip, .tone, .chip-state):is(.is-ok, .is-live)::before { background: var(--dot-ok); }
:is(.chip, .tone, .chip-state).is-warn::before { background: var(--dot-wait); }
:is(.chip, .tone, .chip-state).is-danger::before { background: var(--dot-late); }

.zone-mark {
  width: 17px; height: 17px; flex: 0 0 auto; stroke-width: 1.7;
  color: var(--ink-mute); margin-top: 1px;
}
.zone-head > div { display: flex; flex-direction: column; gap: 2px; }
.zone-head b { font-size: 14.5px; font-weight: 600; letter-spacing: -.01em; }
.zone-head .muted { font-size: 12.5px; }
.zone-sub { font-size: 12.5px; font-weight: 600; }

.zone-in .drop { background: var(--card); border-color: var(--line); }
.zone-in .drop:hover, .zone-in .drop.is-over { border-color: var(--ink); }

.saved-note.is-warn { align-self: center; max-width: 260px; }

/* Client side notice that something changed after approval */
/* Sat with no vertical margin at all, so it touched the media above and the
   button divider below and read as part of neither. It is its own thing, set
   apart from both, with the same side inset as the buttons under it. */
.reask {
  display: flex; flex-direction: column; gap: 5px;
  margin: 14px 16px; padding: 12px 14px; border-radius: 9px;
  background: var(--sunk);
}
.reask b { font-size: 12.5px; font-weight: 600; color: var(--ink); }
.reask span {
  font-size: 13px; color: var(--ink-soft); line-height: 1.5;
  white-space: pre-wrap; overflow-wrap: anywhere;
}

/* Disclosure for actions that are needed rarely and are hard to undo */
.panel-collapse { padding: 0; overflow: hidden; }
.disclosure {
  width: 100%; text-align: left; border: 0; background: none;
  display: flex; align-items: center; gap: 10px; padding: 16px 22px;
  font-size: 14px; font-weight: 600; letter-spacing: -.01em; color: var(--ink-soft);
}
.disclosure:hover { color: var(--ink); background: var(--sunk); }
.disclosure .muted { margin-left: auto; font-weight: 400; }
.disclosure-caret { font-size: 10px; color: var(--ink-mute); transition: transform var(--t) var(--ease); }
.disclosure-body .hint { margin: 14px 0 16px; }

/* The summary is the reason to open the fold: "0 of 4" is what says whether
   billing has been filled in. Hiding it on a phone left a row with a title
   and a caret and nothing to decide on. */

/* Activity record */
/* Two lines, named rather than left to auto placement. The old version let
   the browser place five spans across three columns, which reflowed into a
   different shape at every width it was put in. */
/* The record is a table: time, what happened, to what, the detail, and who.
   Columns hold still so a hundred rows read as a list rather than as a
   hundred sentences of different shapes. */
.act {
  display: grid; align-items: center; gap: 0 14px;
  grid-template-columns: 52px 150px minmax(0, 1.2fr) minmax(0, 1.6fr) minmax(0, .9fr);
  padding: 9px 0; border-bottom: 1px solid var(--line-soft); font-size: 13px;
}
.act:last-child { border-bottom: 0; }
.act-subject { font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The confirm sheet's list: the creator's whole name, the rate at the right
   edge on the totals' own line (SELECTION-8). */
#confirmList .act { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: baseline; }
#confirmList .act-subject { white-space: normal; overflow-wrap: anywhere; }
#confirmList .act-when { text-align: right; color: var(--ink); }
.act-when { white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--ink-mute); }

@media (max-width: 560px) {
  /* Two lines on a phone: what and to what, then the rest. */
  .act { grid-template-columns: auto minmax(0, 1fr); gap: 3px 10px; }
  .act-when { order: 3; }
  .act-subject { order: 2; white-space: normal; }
}

/* =========================================================================
   BATCH 11
   ========================================================================= */

/* A title with a mark beside it, used for every panel heading in the internal
   tool so a section is recognised before it is read. */
.panelhead {
  display: flex; align-items: center; gap: 9px; margin-bottom: 4px;
}
.panelhead h3 { margin: 0; }
/* The head's own mark alone: a button in the head keeps its own ink and
   size, or the blue primary drew a grey glyph (the user, 2026-10-09). */
.panelhead > svg {
  width: 17px; height: 17px; flex: 0 0 auto; color: var(--ink-mute);
  stroke-width: 1.7;
}
.viewhead .headmark > svg {
  width: 18px; height: 18px; flex: 0 0 auto; color: var(--ink-mute); stroke-width: 1.7;
}
.viewhead .headmark { display: flex; align-items: center; gap: 9px; flex: 1 1 auto; }
/* Same rule for a record's own head: the title takes the slack so the state it
   carries goes to the end of the line rather than trailing the words. Held at
   `flex: 0 1 auto` the chip sat 222px in and 599px short of the edge, moving
   with every title, and a page of campaign heads could not be read down for
   what state each was in. */
.viewhead .headmark h2 { flex: 1 1 auto; min-width: 0; }
.disclosure svg.disclosure-mark {
  width: 16px; height: 16px; flex: 0 0 auto; color: var(--ink-mute); stroke-width: 1.7;
}

/* A page that is nothing but a notice or a sign in form: white to the edges. */
body.is-plain { background: var(--card); }
/* The console draws its own chrome; the shared footer belongs to the plain
   pages around it (sign in, access not assigned). */
.console:not([hidden]) ~ .portalfoot,
body:has(> .console:not([hidden])) > .portalfoot { display: none; }
body.is-plain .topbar { background: var(--chrome); }
/* The console's first paint, before the session is read (admin/index.html's
   head script). Signed out: the sign-in page's white from the start rather
   than a grey page turning white. Signed in: no sign-in bar and no footer
   drawn for a frame over the console that is about to open. */
:root[data-boot="out"] body { background: var(--card); }
:root[data-boot="in"] body > .topbar,
:root[data-boot="in"] body > .portalfoot { visibility: hidden; }

.logorow .input { flex: 1 1 auto; min-width: 0; }
.logorow .btn { flex: 0 0 auto; min-width: 0; }
/* Nothing around this one either. An empty circle still needs an edge to be
   seen at all, so the checker below supplies it until an address is pasted;
   once there is an image, the image is all there is. */
.logopreview {
  width: 38px; height: 38px; flex: 0 0 auto; border-radius: 50%; overflow: hidden;
  background: #fff; display: grid; place-items: center;
}
.logopreview img { width: 100%; height: 100%; object-fit: contain; }
.logopreview.is-empty {
  background: repeating-conic-gradient(#f2f2f2 0 25%, #fff 0 50%) 0 0 / 10px 10px;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.08);
}
/* A caution is the words in ink after an amber dot (2026-10-07). */
.msg.warn { color: var(--ink); }
.msg.warn:not(:empty)::before { content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 7px; vertical-align: 1px; background: var(--dot-wait); }

/* =========================================================================
   ADMIN CONSOLE
   A shell rather than a page: a fixed rail of sections on the left, the
   section's own work on the right. Content Review is the first of them, and
   nothing about it needs to know that others will arrive.
   ========================================================================= */
.console { display: flex; min-height: 100vh; align-items: stretch; }
@supports (height: 100dvh) { .console { min-height: 100dvh; } }

.sidebar {
  flex: 0 0 244px; display: flex; flex-direction: column;
  background: var(--card); border-right: 1px solid var(--line);
  padding: 22px 14px 16px;
  position: sticky; top: 0; height: 100vh;
}
@supports (height: 100dvh) { .sidebar { height: 100dvh; } }

.sidebar-brand .brand-logo { height: 24px; width: auto; align-self: flex-start; }
.sidebar-kicker {
  font-size: 12px; font-weight: 600; letter-spacing: 0;
  color: var(--ink-mute);
}

/* The foot of the rail, under the two chunks. `.sidebar-nav` takes the
   slack, so the foot sits on the floor whatever the rail holds, the
   hairline setting the build apart from the sections. */
.sidebar-foot {
  flex: 0 0 auto; margin-top: 12px; padding-top: 10px;
  border-top: 1px solid var(--line);
}
/* The build the console runs (`v26.10.05 · 1a2b3c4`), at the type floor in
   the mute ink, on the rail's text's left edge. */
.appver {
  margin: 4px 0 0; padding: 0 10px;
  font-size: 11px; line-height: 1.4; color: var(--ink-mute);
  font-variant-numeric: tabular-nums; letter-spacing: 0;
}
@media (hover: hover) {
  button.appver:hover { color: var(--ink-soft); }
}
/* `.railrow` is the box a row of the rail draws in and `.navitem` is a
   *section* of the console: the first is a shape, the second is a meaning
   with wiring behind it (`navItems()` reads every `.navitem` and asks the
   ladder about its `data-section`). The Activity record took `.navitem` for
   one evening and inherited the other half with it — `sectionAllowed(null)`,
   which threw for everybody who is not an admin, because `level()` answers an
   admin before it ever reads the key. That is this file's own rule about
   borrowing a component for its shape rather than for what it was built to
   say, so the shape is its own class now and the meaning keeps its name. */
.railfoot { color: var(--ink-mute); font-weight: 500; }
.railfoot svg { stroke-width: 1.6; }
@media (hover: hover) {
  .railfoot:hover { color: var(--ink-soft); background: var(--sunk); }
}
.navgroup + .navgroup { margin-top: 12px; }
.sidebar-label {
  font-size: 12px; font-weight: 600; letter-spacing: 0;
  color: var(--ink-mute); padding: 0 10px 8px;
}
/* The box every row of the rail draws in, shared so the column reads as one
   column. `.navitem` adds to it the one thing a section is: a place the
   console navigates to. */
.navitem, .railrow {
  display: flex; align-items: center; gap: 11px;
  width: 100%; min-height: var(--ctl-h); text-align: left; border: 0; background: none;
  padding: 9px 10px; border-radius: 9px;
  font-size: 13.5px; font-weight: 500; color: var(--ink-soft);
  transition: background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.navitem svg, .railrow svg { width: 17px; height: 17px; flex: 0 0 auto; stroke-width: 1.7; color: var(--ink-mute); }
/* Selected is the darker fill and the heavier word; hover is one step
   lighter, so the section you are in never has a twin. Hover exists only
   where a pointer hovers: a phone keeps :hover on the last thing tapped,
   which left two sections looking open at once. */
@media (hover: hover) {
  .navitem:hover { background: var(--sunk); color: var(--ink); }
}
.navitem.is-on, .railfoot.is-on { background: var(--line-soft); color: var(--ink); font-weight: 600; }
.navitem.is-on svg, .railfoot.is-on svg { color: var(--ink); }

/* No outline. It was the last outlined thing in the console bar, beside an
   Activity link and a bell that both carry nothing but ink and a fill on
   hover, so the person's own name read as the one action up there — the same
   fault the bell's own ring had, corrected the same way. The fill on hover and
   while the menu is open is what says it is pressable. */
.acct-btn {
  display: grid; place-items: center; width: var(--ctl-h); height: var(--ctl-h);
  border: 0; border-radius: 50%; background: none; padding: 0;
  transition: background var(--t-fast) var(--ease-out);
}
.acct-mark { font-size: 12.5px; font-weight: 600; letter-spacing: 0; color: var(--ink-soft); }
/* The name, where the bar can hold it. The control is then a pill rather than
   a disc, because a disc is the shape of a picture and there is none: it read
   as an avatar that had failed to load. A long name truncates rather than
   pushing the section's name, which never gives way. Below 640 the name goes
   and the initial comes back, the rule the Activity link already follows. */
.acct-name {
  display: none; font-size: 13px; font-weight: 600; color: var(--ink-soft);
  max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (min-width: 641px) {
  .acct-btn:has(.acct-name:not(:empty)) {
    width: auto; border-radius: var(--radius-ctl);
    grid-auto-flow: column; gap: 0; padding: 0 12px;
  }
  }
@media (hover: hover) {
  .acct-btn:hover .acct-name { color: var(--ink); }
}
@media (hover: hover) {
  .acct-btn:hover { background: var(--line-soft); }
  .acct-btn:hover .acct-mark { color: var(--ink); }
}
.acct-btn[aria-expanded="true"] { background: var(--line-soft); }
/* The account menu is a menu, not a panel. It inherited `.kmenu`'s 320px
   width and, worse, `.kmenu-item`'s `flex-direction: column` — which exists so
   a row menu can stack a bold label over a description — so Theme and Sign out
   came out as an icon above a word above another word, centred, in a 320px
   box. Each of those is overridden explicitly here rather than left to the
   generic rule, because the generic rule is right for the menus it was written
   for. */
.kmenu.acct-menu {
  position: absolute; top: calc(var(--ctl-h) + 6px); right: 0; left: auto;
  width: 272px; min-width: 0; max-width: calc(100vw - 32px);
}
/* Compact rows at a desk (the user, 2026-10-10: "is it necessary to be so
   large?"): a control's height and no more; a finger's under the bar. */
.acct-menu .kmenu-item {
  flex-direction: row; align-items: center; justify-content: flex-start;
  gap: 10px; text-align: left; min-height: var(--ctl-h); padding: 4px 10px;
}
/* The label and its second line read as one row, not as two stacked items. */
.acct-menu .kmenu-item span { margin-left: auto; text-align: right; }
.acct-who-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.acct-who-text b {
  font-size: 14px; font-weight: 600; letter-spacing: -.01em; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.acct-who-text span {
  font-size: 12.5px; color: var(--ink-mute);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Four groups (you, the workspace, this device, the way out), each set off
   by a hairline. The workspace's rows are each drawn only where they may be
   used, and its hairline leaves with the last of them. */
/* A menu's groups are set off by one hairline (DESIGN.md §5 Menus, 4),
   laid by `ADspaceMenu.tidy`; the account menu's are the same rule. */
.kmenu-sep, .acct-sep { flex: 0 0 auto; border: 0; height: 1px; margin: 4px 6px; background: var(--line-soft); }
.acct-theme-word { flex: 1 1 auto; min-width: 0; cursor: default; }
.acct-theme .seg { flex: 0 0 auto; margin-left: auto; }
/* On the menu's raised ground the chosen option takes the menu's own edge,
   so it still reads in dark. */
.acct-theme .seg.has-thumb::before { box-shadow: inset 0 0 0 1px var(--line-lift); }
/* A switch (`button.switch`, role="switch", aria-checked): a setting that
   takes effect at the press, drawn as the iPhone draws one, a track with a
   thumb that slides; off is the control's grey, on the green of live. The
   button is a full control's height (its target, 38 or 44 under a finger)
   and the track sits inside it. */
.switch {
  position: relative; flex: 0 0 auto; width: 46px; height: var(--ctl-h);
  padding: 0; border: 0; background: none; border-radius: 999px; cursor: pointer;
}
.switch::before {
  content: ''; position: absolute; left: 2px; right: 2px; top: 50%; height: 26px; margin-top: -13px;
  border-radius: 13px; background: var(--line-ctl); transition: background var(--t) var(--ease);
}
.switch::after {
  content: ''; position: absolute; left: 4px; top: 50%; width: 22px; height: 22px; margin-top: -11px;
  border-radius: 50%; background: var(--knob); transition: transform var(--t) var(--ease);
}
.switch[aria-checked="true"]::before { background: var(--ok-solid); }
.switch[aria-checked="true"]::after { transform: translateX(16px); }
.switch:disabled { cursor: default; }
.switch:focus-visible { outline: 2px solid var(--action-ring); outline-offset: 2px; }
.switch:active::after { width: 26px; }
.switch[aria-checked="true"]:active::after { transform: translateX(12px); }
/* A switch's state at the row's right edge, read in the mute ink. */
.acct-menu .kmenu-item .acct-val { font-size: 13px; color: var(--ink-mute); }

.acct-menu .kmenu-item svg { width: 15px; height: 15px; stroke-width: 1.8; flex: 0 0 auto; color: var(--ink-mute); }

.console-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.console-head {
  display: flex; align-items: center; gap: 12px;
  padding: 0 28px; height: var(--head-h); flex: 0 0 auto;
  border-bottom: 1px solid var(--line);
  background: var(--chrome); -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px);
  position: sticky; top: 0; z-index: 30;
}
/* Safari on an iPhone takes the colour behind the status bar from the sticky
   bar at the top of the page, and it passes over a translucent one: the
   console's head at 88% white fell through to the grey page behind it, so
   the status bar read as a grey band above a white bar (reported with a
   screenshot on 2026-09-26). Under a finger the bar is opaque, in exactly
   the colour the translucent one already shows over the page, so nothing
   changes on the screen but the band. The theme-color meta tag says the same
   colour for the Safari versions and the installed app that read it. */
@media (pointer: coarse) {
  .topbar, .console-head {
    background: var(--chrome-solid);
    -webkit-backdrop-filter: none; backdrop-filter: none;
  }
}
/* The client pages keep their glass under a finger (the user, 2026-10-10:
   "a bit of glass feel"). The canvas behind the status bar is painted white
   (the root's own background; the body keeps the grey page over the whole
   window), so Safari, passing over a translucent bar, takes white rather
   than the grey band. Confirmed on an iPhone, since Safari cannot run in the
   sandbox. The console keeps its solid head. */
@media (pointer: coarse) {
  :root[data-face="client"] { background: var(--card); }
  :root[data-face="client"] .topbar {
    background: var(--chrome);
    -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px);
  }
}
/* The page label is what gives way when the bar runs out of room, never the
   controls: it clips to an ellipsis rather than pushing a target under 44px. */
/* It is a control, because the section's name is what explains the section:
   pressing it opens the one line that says what the route is for. The glyph
   costs 14px beside the word rather than a 44px mark of its own, which is
   what lets it survive a 320px head where a separate `?` could not. */
.console-title {
  display: inline-flex; align-items: center; gap: 6px; flex: 0 1 auto;
  /* A short route name is a short control: "Team" is 35px of word, and a
     control a finger operates never goes under the floor whatever it says. */
  min-width: var(--ctl-h); min-height: var(--ctl-h); padding: 0 6px; margin-left: -6px;
  border: 0; border-radius: 8px; background: none;
  font-size: 14px; font-weight: 600; letter-spacing: -.01em; color: var(--ink);
  transition: background var(--t-fast) var(--ease-out);
}
.console-title-word { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.console-title-info { width: 14px; height: 14px; flex: 0 0 auto; stroke-width: 1.8; color: var(--ink-mute); }
/* Only the glyph answers. A fill on the open state made the route's name a
   selected chip on every screen the line is open on, which is the first
   three visits to every route: the title is a title. */
@media (hover: hover) { .console-title:hover .console-title-info { color: var(--ink); } }
.console-title[aria-expanded="true"] .console-title-info { color: var(--ink); }
/* While a route is new the glyph carries the action colour, so the invitation
   to read what the section is for sits on the control rather than on a panel
   that opens itself over the command bar. It retires with the line, after
   three visits or on Hide. */
.console-title.is-new .console-title-info { color: var(--action-ink); }
/* Every route starts the same distance under the bar. Three sections opened
   with a `.viewhead` and its 24px, three opened straight on a command bar
   flush against the chrome, so the first thing on the screen sat 24px lower
   on half the console than on the other half — a shell that is a different
   shell on every route. The step belongs to the body, so a route with a head
   and a route without both get it, and the head's own top margin is dropped
   where it is the first thing in the body. */
.console-body { padding: 20px 28px 96px; }
.console-body > .viewhead:first-child,
.console-body > *:first-child > .viewhead:first-child { margin-top: 0; }
/* The section's own views were written for a page with its own margins. */
.console-body .shell { padding: 0; max-width: none; }

/* The rail becomes a drawer on a phone. */
/* `flex: 0 0 auto`, or the bar squeezes it: the account control at the end of
   the row took enough width at 390 to shrink the toggle to 38px, which is
   under the floor a finger needs. */
.navtoggle {
  display: none; flex: 0 0 auto;
  width: var(--ctl-h); height: var(--ctl-h); padding: 0; border: 0; background: none;
  border-radius: 8px; color: var(--ink-soft);
}
.navtoggle svg { width: 19px; height: 19px; stroke-width: 1.7; }
.navtoggle:hover { background: var(--line-soft); color: var(--ink); }

@media (max-width: 860px) {
  .sidebar {
    position: fixed; left: 0; top: 0; z-index: 60; width: 244px; flex-basis: 244px;
    transform: translateX(-100%);
    transition: transform var(--t-slow) var(--ease), box-shadow var(--t-slow) var(--ease);
  }
  /* The shadow belongs to the open drawer: on the closed one it showed as a
     grey band down the left edge of every phone screen. */
  .sidebar.is-open { transform: none; box-shadow: 0 0 40px rgba(0,0,0,.12); }
  .console-body { padding: 16px 16px 72px; }
  /* Starts where the drawer ends. Dimming the area underneath an opaque
     drawer shows nothing, and it left the tappable region overlapping it. */
  .scrim {
    position: fixed; top: 0; right: 0; bottom: 0; left: 244px;
    z-index: 55; background: rgba(0,0,0,.35); border: 0; padding: 0;
  }
}

.headlink {
  display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto;
  min-height: var(--ctl-h); border: 0; background: none; padding: 0 9px; border-radius: 8px;
  font-size: 12.5px; color: var(--ink-mute);
  text-decoration: underline; text-underline-offset: 3px;
  text-decoration-color: var(--line);
}
.headlink svg { width: 15px; height: 15px; stroke-width: 1.7; }
/* On a phone the bar is the toggle, the route's name, the Activity link, the
   bell and the account: five things in 390px. The name is what the bar is
   there to say, so the Activity link gives up its word and keeps its glyph
   and its name for a screen reader. */
@media (max-width: 640px) {
  .headlink .headlink-word { display: none; }
  .headlink { min-width: var(--ctl-h); justify-content: center; padding: 0; text-decoration: none; }
}
.headlink:hover { color: var(--ink); background: var(--line-soft); text-decoration-color: currentColor; }

/* ---------- Sheet ----------
   Opens over the work and hands it straight back, so reading the record never
   costs you the place you were in. */
.sheet {
  position: fixed; inset: 0; z-index: 90; display: flex;
  align-items: center; justify-content: center; padding: 24px;
  background: var(--scrim);
}
.sheet[hidden] { display: none; }
.sheet-card {
  background: var(--card); border-radius: var(--radius);
  width: min(780px, 100%); max-height: min(82vh, 720px);
  display: flex; flex-direction: column;
  padding: 22px 24px 8px; box-shadow: 0 24px 60px rgba(0,0,0,.25);
}
.sheet-head { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }
.sheet-head h3 { flex: 1 1 auto; font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
.sheet-body { overflow: auto; margin: 0 -24px; padding: 0 24px 18px; }
/* A field standing on its own in a sheet takes the sheet's width. Left to the
   browser it sizes itself to its placeholder, which is how Name ended up at a
   quarter of the card with its label floating above nothing. */
.sheet-body > .input, .sheet-body > .select, .sheet-body > textarea.input { width: 100%; }
/* A form in a sheet steps 16px between one field and the next, down the
   sheet and, on a phone, down a row that has wrapped to one field a line.
   The My Work sheets had no step at all, so each label sat on the field
   above it and Bulk add read as one block (reported 2026-09-24); the other
   sheets carried 12px in a style attribute on each row, which is how the two
   came to differ. One rule here, in `:where()` so a component's own rule
   (`.qform .row`, `.perf-plan .row`) still decides for its own rows. A row
   inside a row, and a row that opens the sheet, take no step. */
.sheet-body :where(.row) { margin-top: 16px; row-gap: 16px; }
.sheet-body :where(.row .row), .sheet-body > :where(.row:first-child) { margin-top: 0; }
.sheet-body :where(.row) + :where(.btn) { margin-top: 16px; }

/* ---------- The question every act asks (js/confirm.js) ----------
   Narrower than a sheet that carries a form: this one holds a sentence and
   two answers, and 780px of card for eleven words reads as a dialog that has
   lost its contents. The consequence is a line under the title at the body
   size, never a second paragraph of the title, and the two answers are the
   portal's own Save / Cancel order and weights. */
/* ---------- A form in a sheet (js/sheet.js) ----------
   A colleague, a user group and a creator are all edited in the same card:
   a form on a phone that unfolds at the top of the page is a form a screen
   above the row somebody pressed, so pressing Edit reads as nothing having
   happened. Narrower than the 780px a picker needs, because these are three
   or four fields and not a table. The foot carries the portal's order and
   weights — the primary first and the quiet way out after it, which is the
   order every action row in this portal reads in — and two equal halves on a
   phone rather than a slab and a remnant. */
.sheet-card.formsheet { width: min(620px, 100%); }
/* The card takes focus when a sheet opens, because a field taking focus on a
   phone raises the keyboard and zooms the page over the rest of the form. It
   is a surface and not a control, so it draws no ring; what it is there for is
   Escape, the focus trap and the dialog announcing itself. */
.sheet-card[tabindex]:focus { outline: none; }
@media (max-width: 560px) {
  .formsheet .sheet-foot > .btn,
  .sheet-wide .sheet-foot > .btn { flex: 1 1 0; min-width: 0; }
}
.sheet-card.askcard { width: min(440px, 100%); }
.askline { font-size: 14px; line-height: 1.55; color: var(--ink-soft); margin: 0; white-space: pre-line; }
.askline:not([hidden]) + .askfields:not([hidden]) { margin-top: 14px; }
.askentry + .askentry { margin-top: 12px; }
/* A date beside its time: two halves on one row, every other field the
   row's width, 16px down as in a sheet. */
.askfields.has-half { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px 12px; }
.askfields.has-half > .askentry { grid-column: 1 / -1; margin-top: 0; }
.askfields.has-half > .askentry.is-half { grid-column: auto; }
/* Three figures across (a section of three, 2026-10-15): six tracks, a half
   spanning three, a third two. */
.askfields.has-third { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.askfields.has-third > .askentry.is-half { grid-column: span 3; }
.askfields.has-third > .askentry.is-third { grid-column: span 2; }
/* Fields side by side keep one line when a narrow label wraps: each entry
   stands on the row's foot. */
.askfields.has-half > .askentry:is(.is-half, .is-third) { display: flex; flex-direction: column; justify-content: flex-end; }
.askentry > .input, .askentry > .select, .askentry > textarea.input { width: 100%; }
.askentry .field-label { display: block; margin-bottom: 6px; }
/* A question of more than five figures reads as sections (2026-10-14,
   audit SET-4): each heading 12 above its fields, a hairline and a 24px
   step before it; a mute line under a field says a change already set
   from a later day. */
.askfields > .askhead { margin: 0 0 12px; }
.askfields > .askhead:not(:first-child) { margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--line-soft); }
.askhead + .askentry { margin-top: 0; }
.askfields.has-half > .askhead { grid-column: 1 / -1; margin-bottom: -4px; }
.askfields.has-half > .askhead:not(:first-child) { margin-top: 8px; }
.asknote { margin: 6px 0 0; font-size: 12.5px; line-height: 1.4; color: var(--ink-mute); }
/* A button whose act is out (js/acts.js, 2026-10-10): its width kept, its
   words and mark give way to a turning ring in its own ink. */
.btn.is-busy { position: relative; cursor: progress; }
.btn.is-busy:disabled { opacity: 1; filter: none; }
.btn.is-busy > :not(.btn-spin) { visibility: hidden; }
.btn.is-busy { color: transparent; }
.btn-spin {
  position: absolute; left: 50%; top: 50%; width: 15px; height: 15px; margin: -7.5px 0 0 -7.5px;
  border-radius: 50%; border: 2px solid currentColor; border-right-color: transparent;
  color: var(--ink-soft); animation: btnspin .7s linear infinite;
}
.btn-go .btn-spin, .btn-primary .btn-spin, .btn-approve .btn-spin { color: var(--on-action); }
.btn-danger .btn-spin { color: var(--err); }
@keyframes btnspin { to { transform: rotate(360deg); } }

/* A client page while its link is read (2026-10-15; the user: "can the
   loading be some interactive rotation of glyph instead of boring text"):
   one ring whose arc fades round to its head, turning smoothly (the user,
   the same day: not the stepping spokes, "some modern circular ones"),
   centred where the page will stand. It
   shows only while nothing the page draws (`[data-shown]`) is on show, so no
   script hides it; a browser without :has() draws nothing, as before. */
.pageload { display: none; min-height: 52vh; align-items: center; justify-content: center; }
main:not(:has([data-shown]:not([hidden]))):not(:has(#ncHost:not(:empty))) > .pageload { display: flex; }
.pageload-spin { display: block; width: 32px; height: 32px; border-radius: 50%;
  background: conic-gradient(from 0deg, transparent 0 8%, var(--ink-soft) 100%);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2.5px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2.5px));
  animation: pagespin .9s linear infinite; }
@keyframes pagespin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .pageload-spin { animation-duration: 2.4s; } }
@media (prefers-reduced-motion: reduce) { .btn-spin { animation: none; border-right-color: currentColor; opacity: .6; } }
/* The AI declaration (2026-10-10): its own shaded block under the question,
   the mark and the line over the tick, never run on into the question. */
.askentry.is-declare { background: var(--sunk); border-radius: var(--radius-sm); padding: 12px 14px; display: grid; gap: 10px; }
.askdeclare { display: flex; gap: 8px; align-items: flex-start; margin: 0; font-size: 13px; line-height: 1.5; color: var(--ink-soft); }
.askdeclare svg { flex: 0 0 auto; width: 15px; height: 15px; margin-top: 2px; color: var(--ink); }
.askentry.is-declare > .tickline { margin: 0; color: var(--ink); }
.askentry > .datefield { display: flex; width: 100%; }
@media (max-width: 560px) {
  /* Two actions of the same standing are two halves, never a slab and a
     remnant: the rule the sheet foot already carries everywhere else. */
  .askcard .sheet-foot.askfoot > .btn { flex: 1 1 0; min-width: 0; }
}

/* ---------- Choosing what goes on a letter ----------
   A letter is issued for the services somebody chose, so the choice is a
   screen rather than a predicate nobody can see. Ticked by default where the
   line is free to be quoted; shown and refused where it is already on a live
   letter, because the reason belongs on the screen with the thing it stops. */
/* Every sheet in this console puts its actions in the same place: at the
   start of the foot, on the card's own left margin, primary first. It was two
   places — a form sheet and an ask card anchored their pair right while every
   plain sheet (the task, the letter, the document, every void and delete) left
   it at the start — so the hand learned a position on one screen and pressed
   the wrong control on the next, which is the fault this file already names
   for the button *order*. The serial position effect is not what governs it:
   that is about a list somebody scans, and a pair is not a list. What governs
   it is motor memory and Jakob, and once the order is fixed (primary first,
   enforced by `uxaudit`'s `order` rule) the right edge is the wrong anchor —
   it puts the way out in the corner every platform reserves for the primary.
   The start of the foot is also the x every field label above it starts at. */
.sheet-foot {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  justify-content: flex-start;
  padding-top: 14px; border-top: 1px solid var(--line-soft);
}
/* `.btn` is `flex: 1` at the base, so a bare one in a flex row grows: the
   short link sheet's Cancel was the one button in any foot with no modifier
   class and came out 446px against every other Cancel's 114, which read as a
   slab with the primary stranded beside it. Stated once here so no future
   foot can repeat it; the phone rules below still turn the pair into halves. */
.sheet-foot > .btn { flex: 0 0 auto; }
/* A count in the foot explains the action beside it, so it stays on that
   line — but it follows the actions rather than pushing them off the margin,
   because what must not move is the control somebody presses. */
.lpicksum { order: 1; flex: 0 1 auto; font-size: 13px; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.lpickgroup + .lpickgroup { margin-top: 14px; }
.lpickgroup .svc-cat { padding: 0 0 6px; }
.lpickrow input { width: 18px; height: 18px; accent-color: var(--accent); }
.lpickname { min-width: 0; display: grid; gap: 1px; font-size: 13.5px; }
.lpickname b { font-weight: 600; }
.lpickname small { font-size: 12.5px; color: var(--ink-mute); }
.lpickamt { font-size: 13.5px; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* A line already spoken for reads as a fact, not as something to argue with. */
.lpickrow.is-held { cursor: default; }
.lpickrow.is-held .lpickname b, .lpickrow.is-held .lpickamt { color: var(--ink-mute); }
.lpicknote { margin: 8px 0 0; font-size: 12.5px; color: var(--ink-mute); }
.dueask-line { margin: 0; font-size: 13px; color: var(--ink); }
.dueask-line small { display: block; margin-top: 2px; font-size: 12px; color: var(--ink-mute); }
.dueask-acts:empty { display: none; }
.dueask-acts .btn { flex: 1 1 0; min-width: 0; }
/* One act alone keeps its own width (ux-audit WORK-10). */
.dueask-acts .btn:only-child { flex: 0 0 auto; }

/* ---------- The quality check before a client sees the work ----------
   A list of checks reads like the list it is: one row a check, a hairline
   between, the whole row the target so a tick is never a 14px box to hit.
   It borrows the letter picker's row rather than inventing a second shape. */
.qc-who { margin: 0 0 12px; font-size: 13px; color: var(--ink-mute); }
/* The group's own name, in the label face every other sub-heading uses. */
.qcgroup {
  margin: 0; padding: 14px 0 6px;
  font-size: 12px; font-weight: 600; color: var(--ink-mute); letter-spacing: 0;
}
.qcgroup:first-child { padding-top: 0; }
.qcrow + .qcgroup { border-top: 1px solid var(--line-soft); }
.prlist .tickline { padding: 10px 0; border-bottom: 1px solid var(--line-soft); margin: 0; }
.qcall span { font-weight: 600; }
.qcrow input { width: 18px; height: 18px; accent-color: var(--accent); }
.qcrow span { font-size: 14px; }
@media (hover: hover) { .qcrow:hover { background: var(--sunk); } }
.qcfoot > .btn { margin-left: 0; }
/* Three actions do not divide a phone's width: at a third each, "Ask a
   second reviewer" clips to two words and says nothing. They take a row each
   in the order they are read — the forward action, the one that holds it,
   then the way out — so the rule about a wrapped foot is satisfied line by
   line. Two of them (once somebody has asked for a second, and the control
   leaves) are still two halves. */
@media (max-width: 560px) {
  .qcfoot > .btn { flex: 1 1 0; min-width: 0; margin-left: 0; }
  }
/* What has already happened to this file. It is a fact and reads as one;
   where the reader is the person who asked for a second pair of eyes, it is
   also the reason the blue button will not move, so it takes the caution. */
.qc-note {
  margin: 0 0 12px; font-size: 13px; color: var(--ink-soft); line-height: 1.5;
}
.qc-note.is-blocked { color: var(--ink); font-weight: 600; }
/* What the check reads the file against: the request it answers, and the
   caption, said even when there is none (2026-09-27). */
.qc-cap { display: grid; gap: 4px; margin: 0 0 12px; }
/* The round's files over the checks: a video 9:16 small, a picture its
   thumbnail, anything else an attachment line (2026-10-14). */
.qc-files { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }
.qc-files:empty { display: none; }
.qc-file {
  position: relative; flex: 0 0 auto; width: 120px; aspect-ratio: 9 / 16;
  border-radius: var(--radius-sm); overflow: hidden; background: #000;
  display: block; color: inherit; text-decoration: none;
}
.qc-file video, .qc-file img { width: 100%; height: 100%; object-fit: contain; display: block; }
.qc-file.is-image { background: var(--sunk); }
.qc-file-name {
  position: absolute; left: 0; right: 0; top: 0; padding: 4px 6px;
  font-size: 11px; color: var(--ink); background: var(--veil); pointer-events: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.qc-files .plink { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qc-cap:empty { display: none; }
.qc-cap-lab { margin: 4px 0 0; font-size: 12px; font-weight: 600; color: var(--ink-mute); }
.qc-cap-lab:first-child { margin-top: 0; }
.qc-cap-text { margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
.qc-cap .msg { margin: 0; }
/* The request on a creator's card: what was asked, by whom and when. Open
   (warn) while the booking is back with the creator. */
.kask { margin: 2px 0 12px; padding: 10px 12px; border-radius: var(--radius-sm); background: var(--sunk); }
.kask-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; font-size: 12.5px; }
.kask-head b { font-weight: 600; color: var(--ink); }
.kask.is-open .kask-head b { color: var(--ink); }
.kask-head span { color: var(--ink-mute); }
.kask-note { margin: 6px 0 0; font-size: 13.5px; line-height: 1.55; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
.kask-none { color: var(--ink-mute); }
.handedin .kcap-none { margin: 6px 0 0; }
/* And the same fact on the card, where the button that will not move is. */
.qc-hold { margin: 0 0 10px; }

.lpickref .input { width: 100%; }
.lpickref .select { max-width: 100%; min-width: 0; }
.lpickref input.input { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
@media (max-width: 640px) {
  .lpickrow input { grid-area: tick; }
  /* Equal halves, not proportional ones. At `auto` the basis is each label's
     own width, so Save came out a black slab across most of the row with
     Cancel squeezed beside it — two actions of the same standing drawn at two
     sizes, which the phone checklist has ruled out since it was written. */
  .sheet-foot .btn { flex: 1 1 0; }
  }
/* The activity record keeps one height whatever the filter shows; the list
   scrolls inside it rather than the card growing and shrinking. */
#activitySheet .sheet-card { height: min(82vh, 720px); }
.actfilter .field-label { margin: 0; white-space: nowrap; }
.actfilter .select { flex: 0 1 260px; min-width: 0; }
@media (max-width: 640px) { .actfilter .select { flex: 1 1 auto; } }
/* A view strip standing on its own row (My records, Performance): one
   tonal track, one raised surface sliding to the chosen view, scrolling
   sideways with the far edge faded rather than wrapping, at every width. */
.actstrip { flex: 0 0 auto; min-width: 0; padding: 4px 0 12px; }
.cmdbar-views.actviews {
  display: flex; max-width: 100%; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.cmdbar-views.actviews::-webkit-scrollbar { display: none; }
.cmdbar-views.actviews .acttab { flex: 0 0 auto; white-space: nowrap; }
.cmdbar-views.actviews.is-more-end { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent); }
.cmdbar-views.actviews.is-more-start { -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to left, #000 calc(100% - 32px), transparent); }
/* ---------- Notice ---------- */
.notice {
  display: flex; gap: 11px; align-items: flex-start;
  border: 0; border-radius: var(--radius);
  background: var(--sunk); padding: 13px 15px; margin-bottom: 18px;
}
.notice svg { width: 17px; height: 17px; flex: 0 0 auto; margin-top: 1px; color: var(--ink-mute); stroke-width: 1.7; }
.notice b { font-size: 13px; font-weight: 600; margin-right: 4px; }
.notice span { font-size: 13px; color: var(--ink-mute); }

.slugfield-pre {
  display: inline-flex; align-items: center; padding: 0 0 0 12px; min-height: var(--ctl-h);
  border: 1px solid var(--field-edge, transparent); border-right: 0;
  border-radius: var(--radius-sm) 0 0 var(--radius-sm); background: var(--field);
  font-size: var(--field-text); color: var(--ink-mute); white-space: nowrap;
}
.slugfield .input { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; min-width: 0; flex: 1 1 auto; }
.input.is-pre { padding-left: 6px; }
.input.is-pre:focus { border-left-color: transparent; box-shadow: none; }
/* The prefix is part of the one filled box: while typed in, the box turns
   white and takes the blue edge and ring whole (2026-10-31). */
.slugfield { border-radius: var(--radius-sm); }
.slugfield:focus-within { box-shadow: 0 0 0 3px var(--focus); }
.slugfield:focus-within .slugfield-pre { background: var(--card); border-color: var(--action); }

/* A sheet comes up from the floor and is as tall as what is in it. `height:
   100%` made every one of them full screen, so a confirmation with two lines
   and two buttons drew four hundred pixels of empty white under them. The one
   sheet that filters keeps its own height, stated where it is built. */
@media (max-width: 640px) {
  .sheet { padding: 0; align-items: flex-end; }
  .sheet-card {
    width: 100%; max-height: 92vh;
    border-radius: var(--radius) var(--radius) 0 0;
    /* The card is what moves, so it is what is promoted: a bottom sheet that
       is composited arrives in one piece instead of repainting its rows as
       it travels. */
    will-change: transform;
  }
  /* `vh` on iOS is the tall viewport, the one that assumes the browser's own
     bars have scrolled away, so a sheet measured in it stands taller than the
     screen actually shows and its foot — where Save is — starts below the
     fold. `dvh` is the viewport as it is at this moment, and it follows the
     bars and the keyboard as they come and go. The `vh` line above stays as
     the fallback for anything that does not know the unit. */
  @supports (max-height: 92dvh) { .sheet-card { max-height: 92dvh; } }
  /* The floor the sheet is anchored to is the phone's, not the screen's: on a
     device with a home indicator the last row of a sheet sat under it. */
  .sheet-card { padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px)); }
  /* The grabber: the head is where a sheet is pulled down to close
     (`js/sheet.js`), and the mark says so where every phone puts it. */
  .sheet-card { position: relative; }
  .sheet-card::before {
    content: ""; position: absolute; top: 6px; left: 50%; width: 36px; height: 5px;
    margin-left: -18px; border-radius: 3px; background: var(--line-ctl); opacity: .55;
    pointer-events: none;
  }
  .sheet-head { touch-action: none; }
}
/* A form sheet opened again with what was typed before it was left. */
.draftline {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 8px 0 4px; padding: 4px 4px 4px 12px; border-radius: var(--radius-sm);
  background: var(--sunk); font-size: 13px; color: var(--ink-soft);
}

.iconbtn.is-done { color: var(--ok); background: var(--ok-bg); }

/* =========================================================================
   CREATOR CAMPAIGNS
   ========================================================================= */

/* ---------- Tabs ---------- */
/* The route level strip (Campaigns / Creators) sits where every other
   route's command bar sits, and the bar is one block gap under it: it stood
   18px down and 4px above the bar, so the search on Campaigns opened a
   strip's height lower than on every other route and the gap under the
   strip was off the scale. */
.tabrow {
  display: flex; gap: 4px; border-bottom: 1px solid var(--line);
  margin: 0 0 12px;
}
.tab {
  border: 0; background: none; min-height: var(--ctl-h); padding: 0 14px; border-radius: 9px 9px 0 0;
  font-size: 13.5px; font-weight: 500; color: var(--ink-mute);
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
@media (hover: hover) {
  .tab:hover { color: var(--ink); background: var(--sunk); }
}
.tab.is-on { color: var(--ink); font-weight: 600; border-bottom-color: var(--ink); }

/* ---------- Tally ----------
   Invoiced against booked. Their only job is to make a goodwill replacement a
   decision somebody took, rather than something found at reconciliation. */
/* Cells fill the row on any width: a grid, not a wrap. */
.tally { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-top: 14px; }
/* Five or six cells never leave one alone on a desk either (the iPhone Duo
   pass, 2026-10-18: six ran five across at 792px wide, the sixth on its own
   row). While every cell does not fit one row, the cell's least width is a
   third of the row, so five read three over two and six three over three;
   once all fit (740px for five, 890px for six) they keep one row. The
   `(W - 100%) * 999` term is positive under the width and negative from it,
   so `min()` takes the third only below it. */
@media (min-width: 641px) {
  .tally:has(> :nth-child(5):last-child) {
    grid-template-columns: repeat(auto-fit, minmax(max(140px, min(calc((100% - 23px) / 3), calc((740px - 100%) * 999))), 1fr));
  }
  .tally:has(> :nth-child(6):last-child) {
    grid-template-columns: repeat(auto-fit, minmax(max(140px, min(calc((100% - 23px) / 3), calc((890px - 100%) * 999))), 1fr));
  }
}
/* On a phone the column count follows the cell count so no cell sits alone:
   three, five or six cells run three across; anything else runs two across. */
@media (max-width: 640px) {
  .tally { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  .tally:has(> :nth-child(3):last-child),
  .tally:has(> :nth-child(5):last-child),
  .tally:has(> :nth-child(6):last-child) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  /* Money is too wide for a third of a phone: two across, the total on its
     own full row. A value never wraps. */
  .tally:has(> .tally-cell.is-total) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tally-cell.is-total { grid-column: 1 / -1; }
  .tally-cell b { font-size: 15px; white-space: nowrap; }
  .tally-cell span { font-size: 11px; }
}
.tally-cell {
  min-width: 0;
  border: 0; background: var(--sunk); border-radius: 10px; padding: 10px 13px;
  display: flex; flex-direction: column; gap: 2px;
}
.tally-cell b { font-size: 17px; font-weight: 600; letter-spacing: -.02em; }
.tally-cell span { font-size: 11.5px; color: var(--ink-mute); }
/* Over a limit is the figure in ink with an amber dot before its label. */
.tally-cell.is-warn span::before {
  content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 6px; background: var(--dot-wait);
}
.tally-cell.is-total { border-color: var(--ink); }
.tally-cell.is-total b { color: var(--ink); }
/* Money and totals in a row at the scale's 600, never the browser's bold
   700 (the client pages audit, PO-2, 2026-10-08). */
.svc-amt b, .is-total b, .cp-panes b { font-weight: 600; }

.profrow .input { flex: 1 1 auto; min-width: 0; }
.prof-read { flex: 0 0 auto; font-size: 12px; min-width: 150px; }
.prof-read.ok  { color: var(--ok); }
.prof-read.err { color: var(--err); }

/* The link a tick makes necessary is asked for inside the tick: see .pbox. */

/* .pchip is gone: a 999px grey pill is the costume of a status chip, and the
   only thing wearing it was an outbound profile link. Those are .plink now,
   the component the contact row already uses for a phone and an email. */

/* =========================================================================
   CREATOR SELECTION  (client facing)
   ========================================================================= */

.langtoggle {
  border: 1px solid var(--line); background: var(--card); border-radius: 8px;
  min-height: var(--ctl-h); padding: 0 12px; font-size: 12.5px; color: var(--ink-soft); flex: 0 0 auto;
}
.langtoggle:hover { border-color: var(--ink-mute); color: var(--ink); }

/* ---------- Progress ----------
   Determinate, because the slot count is known and fixed by the invoice. It
   leads with what is left to do; a tally alone makes the reader do the
   subtraction before they know whether to act. */
/* Inside the summary band the progress is a section, not a second card: one
   bounded surface, its parts divided by the hairline this system already uses
   between the sections of a record. */
.engage-band { padding: 16px 18px; }
/* What the campaign is about reads as one block of prose, then the facts.
   Both lines used to be `.camphead p`, which is a caption under a title; inside
   the card they are its content, so they take the card's own quiet body size
   and the first of them sits on the band's own padding rather than 6px below
   it. The `.engage-card` prefix is what makes these outrank `.camphead-purpose`
   and `.engage-band > .booking-facts`, which are single-class rules later in
   the file; one stack rule beats six margins written at their point of use. */
.engage-card .engage-band > .camphead-purpose,
.engage-card .engage-band > p { font-size: 14px; color: var(--ink-soft); }
.engage-card .engage-band > .camphead-purpose { color: var(--ink); }
.engage-card .engage-band > * { margin-top: 0; }
.engage-card .engage-band > * + * { margin-top: 8px; }
.engage-card .engage-band > * + .booking-facts { margin-top: 14px; }
/* The detail folds, and the one fact with a consequence rides the head so it
   is readable while the card is shut. The head is the control, the way every
   other fold in this portal is opened by its own title. */
.engage-head { display: flex; align-items: center; gap: 12px; padding: 14px 18px; }
.engage-toggle {
  display: inline-flex; align-items: center; gap: 8px; min-width: 0;
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; font-size: 15px; font-weight: 600; letter-spacing: -.01em; color: var(--ink);
  /* A bare button is a control and clears the control floor, so the target is
     38px at a desk and 44 under a finger. The negative block margin keeps its
     footprint the label's own line, the way the rate card's ⋯ does: without it
     the card's one-line head stood 66px at a desk and 72 on a phone. */
  min-height: var(--ctl-h); margin-block: calc((20px - var(--ctl-h)) / 2);
}
.engage-toggle svg {
  width: 15px; height: 15px; flex: 0 0 auto; color: inherit; stroke-width: 1.8;
  transition: transform var(--t) var(--ease);
}
.engage-toggle[aria-expanded="true"] svg { transform: rotate(90deg); }
.engage-sum {
  margin-left: auto; flex: 0 1 auto; min-width: 0; text-align: right;
  font-size: 13.5px; color: var(--ink-soft); font-weight: 500;
  /* Its parts wrap between them, never leaving the dot at a line's end
     (SEL-8): each dot hangs into the gap after its part, and the one that
     would end a line overhangs the box and is clipped. */
  display: flex; flex-wrap: wrap; justify-content: flex-end; column-gap: 1em; overflow: hidden;
}
.engage-dot { display: inline-block; width: 1em; margin-right: -1em; text-align: center; font-style: normal; }
/* The label keeps its line; the summary wraps between its parts, never
   inside one (the count joined it, 2026-10-14). */
.engage-toggle { flex: 0 0 auto; white-space: nowrap; }
.engage-sum > span { white-space: nowrap; }
/* The body opens and shuts in place, on the grid row this portal animates
   every fold with (`.crm-group-body`): 0fr to 1fr is the one height CSS can
   move without being told what it is. Driven by a class rather than `hidden`,
   because `hidden` is `display: none` and a display change cannot animate. */
.engage-body {
  display: grid; grid-template-rows: 1fr; opacity: 1;
  transition: grid-template-rows var(--t-slow) var(--ease), opacity var(--t) var(--ease-out);
}
.engage-inner { min-height: 0; overflow: hidden; }
.engage-card.is-shut > .engage-body { grid-template-rows: 0fr; opacity: 0; }
.engage-body .engage-band { border-top: 1px solid var(--line-soft); }
.progress-card {
  padding: 16px 18px; border-top: 1px solid var(--line-soft);
}
.progress-top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.progress-top b { font-size: 14px; font-weight: 600; }
.progress-card .progress-track {
  height: 8px; border-radius: 999px; background: var(--line-soft);
  overflow: hidden; margin: 10px 0 0;
}
.progress-card .progress-fill {
  height: 100%; background: var(--fill); border-radius: 999px;
  transition: width var(--t-slow) var(--ease);
}
.progress-card.is-done .progress-fill { background: var(--ok); }
.progress-say { font-size: 13.5px; margin-top: 10px; }
.progress-card.is-done .progress-say { color: var(--ok); font-weight: 500; }
/* More chosen than the slots left: said in red where the picks are made. */
.progress-say.is-over, .progress-card.is-done .progress-say.is-over, #confirmSummary .is-over { color: var(--err); font-weight: 500; }
.progress-new {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line-soft);
  font-size: 13px; color: var(--ink-mute);
}

/* ---------- Creator list ----------
   A list rather than cards. Ten creators is a page of cards and forty is an
   afternoon of scrolling, and the decision is made by opening profiles and
   comparing rates, which a row puts side by side. */
.listhint { font-size: 13px; color: var(--ink-mute); margin: -4px 0 12px; }
.clist { border: 1px solid var(--card-line); border-radius: var(--radius); overflow: hidden; }
/* A comparison, on a screen wide enough to compare on: the placements a fee
   buys are their own column beside it rather than a line under the name, and
   the columns are named, because a fee with nothing over it is a number the
   client has to work out what to read against. */
.crow {
  display: grid; align-items: center; gap: 4px 14px;
  /* Every track is stated, because a column sized `auto` is sized by its own
     row: the creator with two profile buttons pushed the placements column
     ninety pixels right of the rows under it, and a value that starts
     somewhere different on every line is what reads as unaligned. */
  grid-template-columns: 26px var(--ctl-h) minmax(0, 1.15fr) minmax(0, .95fr) minmax(0, 1.9fr) 84px 92px;
  grid-template-areas: "no tick name plat links rate backup";
  padding: 12px 14px; background: var(--card);
  border-bottom: 1px solid var(--line-soft);
}
.crow-head {
  padding: 12px 14px 8px; background: var(--sunk);
  font-size: 12px; font-weight: 600; letter-spacing: 0;
  color: var(--ink-mute); border-bottom: 1px solid var(--line);
}
/* Every head cell reads 12/600, the column's own rule never outranking it
   (2026-10-20, SEL-3). */
.crow.crow-head > * { font-size: inherit; font-weight: inherit; letter-spacing: inherit;
  text-transform: inherit; color: inherit; }
.crow-head .crow-plat > span { margin: 0; }
/* Left aligned, like the two columns before it. Right aligned, the cell ended
   at the same x but *started* wherever its contents happened to: the creator
   with two profile buttons began at 703px and the three with one began at
   868px, so a column built to be compared down could not be. A column starts
   at the same x on every row or it is not a column. */
.crow-head .crow-links { justify-content: flex-start; }
.crow-tick-cell { grid-area: tick; }
.crow-backup-cell { grid-area: backup; text-align: center; }
.crow:last-child { border-bottom: 0; }
.crow.is-on { background: var(--line-soft); }
.crow.is-backup { background: repeating-linear-gradient(135deg, var(--card) 0 8px, var(--line-soft) 8px 16px); }

.crow-no { grid-area: no; }
/* The box stays 24px; the button around it is a full-size target and the
   negative margin keeps the row's footprint unchanged. */
.crow-tick {
  grid-area: tick; width: var(--ctl-h); height: var(--ctl-h); flex: 0 0 auto; padding: 0;
  margin: calc((24px - var(--ctl-h)) / 2); border: 0; background: none;
  display: inline-grid; place-items: center; color: transparent;
}
.crow-tick::before {
  content: ''; grid-area: 1 / 1; width: 24px; height: 24px; box-sizing: border-box;
  border: 1.5px solid var(--line-ctl); border-radius: 7px; background: var(--card);
  transition: background var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out);
}
.crow-tick svg { grid-area: 1 / 1; width: 14px; height: 14px; position: relative; }
.crow-tick:hover:not([disabled])::before { border-color: var(--ink-mute); }
.crow.is-on .crow-tick::before { background: var(--fill); border-color: var(--fill); }
.crow.is-on .crow-tick { color: var(--on-fill); }
.crow-tick[disabled] { opacity: .35; }

.crow-name { grid-area: name; min-width: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.crow-name b { font-size: 14px; font-weight: 600; letter-spacing: -.01em; }
/* Its own line under the name: the meta a row's money is read against. */
.crow-plat {
  grid-area: plat; min-width: 0; font-size: 12.5px; color: var(--ink-soft);
}
.crow-plat:empty { display: none; }
/* The label belongs to the phone layout, where the value has no column head
   to sit under. */
.crow:not(.crow-head) .crow-plat > span { display: none; }
.crow-plat > span { color: var(--ink-mute); margin-right: 6px; }

/* The thing they came to click, so it looks like something to click. */
.crow-links { grid-area: links; display: flex; gap: 6px; flex-wrap: wrap; min-width: 0; justify-content: flex-start; }
/* The way to reach a person or open their profile: the one outlined chip in
   the portal, on every page (the user, 2026-09-28: "outlined chip is contact
   button and applies sitewide"). Outlined, so it never reads as a state chip
   (tonal) or a plain text link. */
.plink {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--line-chip); border-radius: var(--radius-sm); padding: 4px 10px;
  /* The small control's height and words, as the .btn-sm beside it
     (2026-10-31, POR-7: 31 and 37px chips beside 30 and 36px buttons). */
  box-sizing: border-box; min-height: var(--ctl-h-sm);
  font-size: 12.5px; font-weight: 500; color: var(--ink);
  text-decoration: none; background: transparent; white-space: nowrap;
}
.plink svg { width: 12px; height: 12px; opacity: .65; flex: 0 0 auto; }
@media (hover: hover) { .plink:hover { background: var(--sunk); color: var(--ink); } }

.crow-rate {
  grid-area: rate; font-size: 14px; font-weight: 600; letter-spacing: -.01em;
  font-variant-numeric: tabular-nums; text-align: right; min-width: 76px;
}

/* Was an underlined word, which read as fine print rather than a control. */
/* Same corner as the profile buttons it stands beside: two shapes in one row
   read as two unrelated systems. */
.crow-backup {
  grid-area: backup; flex: 0 0 auto; white-space: nowrap;
  border: 1px solid transparent; border-radius: var(--radius-sm); background: var(--tonal);
  min-height: var(--ctl-h); padding: 0 11px; font-size: 12.5px; font-weight: 500; color: var(--ink-soft);
}
@media (hover: hover) { .crow-backup:hover { background: var(--tonal-hover); color: var(--ink); } }
/* A ticked row is shaded, and the tonal button on it all but vanished
   (#dedede on #ececec): it takes the card's white there (SEL-6). */
.crow.is-on .crow-backup:not(.is-on) { background: var(--card); }
@media (hover: hover) { .crow.is-on .crow-backup:not(.is-on):hover { background: var(--sunk); } }
.crow-backup.is-on { background: var(--fill); border-color: var(--fill); color: var(--on-fill); }

.tag-new {
  font-size: 11px; font-weight: 700; letter-spacing: 0;
  padding: 2px 6px; border-radius: 4px; background: var(--fill); color: var(--on-fill);
}
.tag-rep {
  font-size: 11px; font-weight: 600;
  padding: 2px 7px; border-radius: 4px; background: var(--line-soft); color: var(--ink-soft);
}

/* The client's creator list on a phone (640 and under, the phone line, so
   a folded-open phone at 626 reads it as a 390 one does). Name and rate read
   as one line, the placements under the name with their label, the things
   to tap on the next. Below 640 the desk's seven named tracks no longer fit,
   and the four-track row that stood here kept the header and gave the
   placements no area: the header broke into three lines and an implicit
   track moved the fee and Backup on the rows that held one placement. */
@media (max-width: 640px) {
  .crow {
    grid-template-columns: auto auto minmax(0, 1fr) auto;
    grid-template-areas: "no tick name rate" "no . plat plat" "no . links backup";
    gap: 8px 10px; padding: 12px 14px;
  }
  .crow-tick, .crow-no { align-self: start; }
  .crow-no { padding-top: 3px; }
  /* Four labels over a phone's row is furniture, so the header leaves and
     the placements go back under the name with their label. */
  .crow-head { display: none; }
  .crow:not(.crow-head) .crow-plat > span { display: inline; margin-right: 6px; color: var(--ink-mute); }
  .crow-plat { font-size: 12px; margin-top: -2px; }
  /* The name carries a second line, so the fee sits against the first one
     rather than floating halfway down it. */
  .crow-rate { min-width: 0; align-self: start; }
  .crow-backup { align-self: start; padding: 0 13px; }
  /* Two profile chips stack in the cell: each takes its width, so they
     read as one pair rather than two lengths (SEL-5). */
  .crow-links:has(> .plink + .plink) > .plink { flex: 1 1 100%; justify-content: center; }
}

/* ---------- Confirm bar ---------- */
/* Bleeds to the page edge, so the negative margin has to be the shell's own
   gutter. A guessed 20px sat inside it on desktop and overflowed it on a
   phone, where the gutter is 16. */
.confirmbar {
  position: sticky; bottom: 0; z-index: 40; margin: 18px -24px 0;
  background: var(--chrome); -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px);
  border-top: 1px solid var(--line);
}
.confirmbar-in {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 12px 24px; font-size: 13.5px;
}

@media (max-width: 640px) {
  .tabrow { overflow-x: auto; }
  .prof-read { min-width: 0; flex: 1 1 100%; }
  .confirmbar { margin-left: -16px; margin-right: -16px; }
  .confirmbar-in { flex-direction: column; align-items: stretch; gap: 9px; padding: 12px 16px; }
  .confirmbar-in .btn { width: 100%; }
}

/* The KOC list has always gone out as a printed sheet, so it still can. */
@media print {
  .langtoggle, .confirmbar, .crow-tick, .crow-backup, .progress-new { display: none !important; }
  .crow { break-inside: avoid; }
  .progress-card { border-color: #ccc; }
}

.qrrow.is-off { opacity: .55; }
.qrrow-img canvas, .qrrow-img img { display: block; border-radius: 4px; }
.qrrow-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.qrrow-body b { font-size: 13.5px; font-weight: 600; }
.qrrow-body .muted { font-size: 11.5px; }
.qrrow-acts .btn { min-width: 0; }

@media (max-width: 560px) {
  .qrrow-acts .btn { flex: 1 1 auto; }
}

.bookreg-head, .bookreg .kcard-head {
  display: grid; align-items: center; gap: 0 12px;
  /* Measured against the pane beside a rail at 1280, which is 591px: 44 for
     the number, 124 for the widest step chip ("Changes requested"), 82 for the
     two actions and 48 of gaps leaves 293 for the name and the summary. The
     name takes the larger share and never less than 140px (2026-10-14, audit
     CAMP-7: at .9fr it was 102px and cut "Ah Beng Reviews" while the summary
     had room); the summary gives way first, ending in an ellipsis. */
  grid-template-columns: 44px minmax(140px, 1fr) minmax(0, 176px) var(--state-w) 82px;
}
/* In a narrow pane the number's track gives back 12px to the name. */
.is-narrow .bookreg-head, .is-narrow .bookreg .kcard-head {
  grid-template-columns: 32px minmax(120px, 1fr) minmax(0, 176px) var(--state-w) 82px;
}
.bookreg .kcard {
  border: 0; border-radius: 0; margin: 0;
  border-bottom: 1px solid var(--line-soft);
}
.kcard-steps .cx-journey { margin: 0; max-width: 560px; }
.is-tight .kcard-steps { grid-template-columns: minmax(0, 1fr); padding: 0 8px 10px 14px; }
.is-tight .kcard-steps > span { display: none; }
.bookreg .kcard > *:last-child { border-radius: 0; }
/* The options the client did not take, folded under the bookings once the
   team has confirmed creators: one register row naming them and their count,
   on the same first column as the rows it opens. */
.bookreg-fold {
  display: flex; align-items: center; gap: 12px; width: 100%;
  min-height: calc(var(--ctl-h) + 22px); padding: 11px 14px 11px 16px;
  border: 0; background: none; text-align: left; color: var(--ink-soft);
}
.bookreg-fold .kfold { color: var(--ink-mute); }
@media (hover: hover) { .bookreg-fold:hover { background: var(--sunk); } }
.is-tight .bookreg-fold { padding: 10px 14px; gap: 10px; }
.kcard-lead { display: flex; align-items: center; gap: 4px; min-width: 0; }
.kcard-tags { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; min-width: 0; }
.kcard-act .kmenu-btn { margin-left: 0; }

/* A booking waiting on us reads so from its own step chip, the amber dot
   (2026-10-07); the card carries no amber edge of its own. */

.kcard.is-off { opacity: .62; }
/* Running numbers, so a card or a row can be referred to in a call. */
.kcard-no, .rowno {
  flex: 0 0 auto; min-width: 18px; font-size: 12px; font-weight: 600;
  color: var(--ink-mute); font-variant-numeric: tabular-nums;
}

.kcard-name {
  font-size: 15px; font-weight: 600; letter-spacing: -.012em; color: var(--ink);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The shape and the tones are stated once, up by `.chip`. */

.kmenu-btn {
  margin-left: auto; flex: 0 0 auto;
  width: var(--ctl-h); height: var(--ctl-h); display: grid; place-items: center;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; color: var(--ink-mute);
}
.kmenu-btn svg { width: 17px; height: 17px; }
/* The lightest control: a fill on hover and while open, never an outline
   (DESIGN.md; the audit's `btnline` found the ring on 2026-09-30). */
.kmenu-btn[aria-expanded="true"] { background: var(--line-soft); color: var(--ink); }
@media (hover: hover) {
  .kmenu-btn:hover { background: var(--line-soft); color: var(--ink); }
}
/* Raised, because it is the one thing on the page that floats above it. */
/* An open menu is the top layer of the page it opens on: above every sticky
   bar under the console head (the select bar is 20, the confirm bar 40), and
   below the drawer, the sheets and their scrims (55 and up). At 20 it tied
   with My Work's select bar, which comes later on the page and so drew over
   the bar's own ⋯ menu (reported 2026-09-24). */
.kmenu {
  position: absolute; top: 44px; right: 12px; z-index: 50;
  width: min(320px, calc(100vw - 48px));
  background: var(--raised); border: 1px solid var(--line-lift);
  border-radius: var(--radius); box-shadow: var(--shadow-lift);
  padding: 5px; display: flex; flex-direction: column;
}
/* A menu row is a control, so it clears the control floor like every other
   one: a control's height on a desktop, `--tap` under a finger. Padding alone left it at 43px
   on a phone, which nothing caught until the ⋯ was opened during the walk. */
.kmenu-item {
  display: flex; flex-direction: column; gap: 1px; justify-content: center;
  text-align: left; min-height: var(--ctl-h);
  padding: 9px 11px; border: 0; border-radius: var(--radius-sm); background: none;
}
.kmenu-item { font-size: 13.5px; font-weight: 400; color: var(--ink); }
/* One look for every item (DESIGN.md §5 Menus, 10; MENU-4, 2026-10-22): a
   bold label in some menus and a plain one in others read as two kinds. */
.kmenu-item b { font: inherit; color: inherit; }
.kmenu-item span { font-size: 12px; line-height: 1.45; color: var(--ink-mute); }
.kmenu-item:hover { background: var(--line-soft); }
/* A destructive item is red whether or not it carries a <b>. The colour hung
   off the bold child alone, so Delete on the client record — the one such
   item written as plain text — read in ordinary ink beside Edit and said
   nothing about what it does. */
.kmenu-item.is-danger, .kmenu-item.is-danger b { color: var(--err); }

.kstep-title {
  font-size: 12px; font-weight: 600; letter-spacing: 0;
  color: var(--ink-mute); margin-bottom: 9px;
}

.kstep-mark {
  flex: 0 0 auto; width: 17px; height: 17px; display: grid; place-items: center;
  border: 1.5px solid var(--line); border-radius: 50%; color: transparent;
}
.kstep-mark svg { width: 11px; height: 11px; }
.kstep-terms.is-done .kstep-mark {
  border-color: var(--ok-solid); background: var(--ok-solid); color: var(--on-ok);
}
.kstep-sum { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
.kstep-sum b { font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; }
.kstep-sum span { font-size: 12.5px; color: var(--ink-mute); }
.kstep-terms .btn, .kstep-note { margin-left: auto; flex: 0 0 auto; }
.kstep-terms .rate-edit { flex: 1 1 100%; margin-left: 27px; }

.kfield { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.kfield > span {
  font-size: 12px; font-weight: 600; letter-spacing: 0;
  color: var(--ink-mute);
}
/* A booking's value read where the reader may not change it (2026-10-14):
   words under the label, never a disabled field. */
.kcard .kread {
  margin: 0; font-size: 14px; line-height: 1.55; color: var(--ink);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.kactions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.kstep-work .msg:empty { display: none; }
.kstep-ended .hint { margin: 0; flex: 1 1 220px; }
/* The plan as the Schedule holds it: its title with the way there beside
   it, the set facts label beside value, then the notes. */
.kstep-titlerow {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: -6px 0 6px;
}
.kplan > div { grid-template-columns: 104px minmax(0, 1fr); gap: 0 12px; }
.kapproved { margin: 0 0 12px; font-size: 13px; color: var(--ink-soft); }
.kplan-none { margin: 0 0 12px; font-size: 13px; color: var(--ink-mute); }
@media (max-width: 520px) {
  .kstep-terms .btn, .kstep-note { margin-left: 0; }
}

/* Folded, a production card is its header: the fold control, the name, the
   state, and one line of what matters. The header is the target; the
   buttons in it keep their own jobs. */
.kfold {
  flex: 0 0 auto; width: var(--ctl-h); height: var(--ctl-h); margin-left: -10px;
  display: grid; place-items: center; border: 0; border-radius: var(--radius-sm);
  background: none; color: var(--ink-mute);
}
.kfold svg { width: 16px; height: 16px; transition: transform var(--t) var(--ease); }
@media (pointer: coarse) { .kfold { width: var(--tap); height: var(--tap); margin-left: -12px; } }
.kfold[aria-expanded="true"] svg { transform: rotate(90deg); }
.kfold:hover { background: var(--line-soft); color: var(--ink); }
.kcard-sum {
  min-width: 0; font-size: 12.5px; color: var(--ink-mute);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.kactions .btn svg { width: 14px; height: 14px; }
/* ---------- Client-facing bookings ----------
   One card per confirmed creator, in the same language as the console's
   cards: flat, hairline, a state chip, the live one marked on its edge. */
/* 24 above, on the scale: a new section in a list. It was 26, which is not a
   step this portal has, and against the card's own 12px foot it read as a gap
   nobody had chosen. */
.sectionhead { font-size: 16px; font-weight: 600; letter-spacing: -.01em; margin: 24px 0 12px; }
.booking {
  border: 1px solid var(--card-line); border-radius: var(--radius); background: var(--card);
  padding: 14px 16px; margin-bottom: 9px;
}
/* A creator's own booking is the page's panel, padded as every client
   panel is, at a desk and on a phone alike (CR-2). */
.cwork .booking { padding: 18px 20px; }
@media (max-width: 640px) { .cwork .booking { padding: 15px 16px; } }
.booking.is-off { opacity: .6; }
.booking-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.booking-head b { font-size: 15px; font-weight: 600; letter-spacing: -.01em; min-width: 0; }
/* The state hangs off the right of the card, not off the end of the name.
   Trailing the name it started at a different x on every card — 恩比 put it at
   205px and Vini168小队长 at 325px — so a column of cards could not be read
   down for where each one has got to, which is the one thing the chip is for.
   Whatever follows it (a Replacement tag) travels with it. */
.booking-head > .chip-state, .booking-head > .tone { margin-left: auto; }
.booking-head > .chip-state ~ *, .booking-head > .tone ~ * { margin-left: 0; }
/* The same words and the same colours as the console's state chips; the
   shape and the tones are stated once, up by `.chip`. */
.booking-meta { font-size: 13px; color: var(--ink-mute); margin-top: 5px; }

/* A live post's button (`.postlink`, inside `.cx-post-item`), named for
   where it opens (the user, 2026-09-27: the platform's name alone was "too
   not obvious"); the mark says it leaves the page. On a phone it runs the
   card's width (`.cx-postbtn`), every card alike. */
.postlink { gap: 7px; }
.postlink svg { width: 13px; height: 13px; flex: 0 0 auto; opacity: .7; }
/* One placement's results on the Results step, a hairline between two. */
.postrow + .postrow { border-top: 1px solid var(--line-soft); margin-top: 12px; padding-top: 12px; }
.postrow-head b { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.postrow-head span { font-size: 12px; color: var(--ink-mute); }
.postrow .msg:empty { display: none; }

/* The engagement, above everything: how many creators, when it is due, the
   invoice and the money. "Your campaign" below is then only the creators. */
.engage-card {
  border: 1px solid var(--card-line); border-radius: var(--radius); background: var(--card);
  padding: 0; margin: 12px 0;
}
.engage-card .booking-facts { margin: 0; }
.engage-card .amountfold {
  border: 0; border-top: 1px solid var(--line-soft); border-radius: 0;
  /* The hairline is 12px under the facts and the Amount row's ink is 16px
     under the hairline, which is what the band's own floor gives it below.
     It was 4px above and 26 below: the row is a 44px target holding a 20px
     line, so the slack inside the button was counted only at the bottom. The
     toggle takes the negative block margin every other bare control in this
     portal takes, so its footprint is its label's line and its target is
     untouched. */
  margin: 12px 0 0; padding: 16px 0 0;
}
.engage-card .amountfold-btn { margin-block: calc((20px - var(--ctl-h)) / 2); }
.rollup-card {
  border: 1px solid var(--card-line); border-radius: var(--radius); background: var(--card);
  padding: 14px 16px; margin: 0 0 12px;
}
.rollup-head { font-size: 14px; font-weight: 600; letter-spacing: -.01em; }
.rollup-card .tally { margin-top: 10px; }

/* The facts that make the card worth looking at: when, where, what next.
   A grid so they fill the width instead of trailing off in one grey line. */
.booking-facts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: 12px 24px; margin: 12px 0 0;
}
.booking-facts dt {
  font-size: 12px; font-weight: 600; letter-spacing: 0;
  color: var(--ink-mute);
}
.booking-facts dd { margin: 2px 0 0; font-size: 14px; color: var(--ink); }
/* The columns follow the count, so no fact sits alone under a full row
   ("Next" and "Draft due" did, on `auto-fit`). At a desk, up to three facts
   are one row and four are two pairs (four abreast wrapped a date in the
   creator's pane at 900); five or more are three across, and a short last
   row is filled by its last fact. Below the phone line they are two across,
   and an odd count's last fact takes the row, as `dl.facts` does. */
@media (min-width: 641px) {
  .booking-facts:not(:has(> :nth-child(4))) {
    grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  }
  .booking-facts:has(> :nth-child(4):last-child) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .booking-facts:has(> :nth-child(5)) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .booking-facts:has(> :nth-child(5)) > :last-child:nth-child(3n+1) { grid-column: 1 / -1; }
  .booking-facts:has(> :nth-child(5)) > :last-child:nth-child(3n+2) { grid-column: span 2; }
}
/* Every booking on the selection page lays its facts two across, however
   many it holds, so the cards read alike (2026-10-20, SEL-4). */
@media (min-width: 641px) {
  #bookingList .booking-facts, #needBox .booking-facts,
  #bookingList .booking-facts:not(:has(> :nth-child(4))), #needBox .booking-facts:not(:has(> :nth-child(4))) {
    grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-flow: row;
  }
  #bookingList .booking-facts > :last-child, #needBox .booking-facts > :last-child { grid-column: auto; }
  #bookingList .booking-facts > :last-child:nth-child(odd), #needBox .booking-facts > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  .booking-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 16px; }
  .booking-facts > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}

/* The amount, folded until asked for. */
/* The invoice is a card of its own, folded to its number. */
.amountfold {
  margin: 0 0 12px; padding: 6px 16px;
  border: 1px solid var(--card-line); border-radius: var(--radius); background: var(--card);
}
.amountfold-head { display: flex; align-items: center; gap: 12px; }
.amountfold-btn {
  display: inline-flex; align-items: center; gap: 7px; flex: 1 1 auto; min-width: 0;
  min-height: var(--ctl-h); background: none; border: 0; padding: 0; cursor: pointer;
  font: 600 14px/1.3 inherit; color: var(--ink); text-align: left;
}
.amountfold-btn svg { flex: 0 0 auto; width: 14px; height: 14px; color: var(--ink-mute); transition: transform var(--t) var(--ease); }
.amountfold-btn.is-open svg { transform: rotate(90deg); }
.amountfold-btn:hover svg { color: var(--ink); }
.amountfold-link { flex: 0 0 auto; font-size: 13px; font-weight: 500; color: var(--ink-soft); text-decoration: none; }
.amountfold-link:hover { color: var(--ink); text-decoration: underline; }
.amountfold .totals { margin: 6px 0 8px; max-width: none; }

/* Backups are part of the selection, so their count sits with the count. */
.progress-backup { color: var(--ink-mute); margin-top: 4px; }
.progress-backup.is-ok { color: var(--ok); }
.listhint.is-priority { color: var(--ink); font-weight: 500; }
.tag-pri {
  font-size: 11px; font-weight: 600; letter-spacing: 0;
  padding: 2px 7px; border-radius: 4px; background: var(--line-soft); color: var(--ink-soft);
}

.rate-edit .slugfield { width: 150px; }
.rate-edit .input { min-width: 0; width: 100%; }

/* The form inside the campaign's own card: no second border, no second
   padding. Deletion is at its foot, behind the edit, and looks like what
   it is. */
.btn-quiet.is-danger { color: #a3312a; }
.btn-quiet.is-danger:hover { color: #a3312a; border-color: #a3312a; }

/* ---------- Add creators (a sheet, 2026-10-10) ----------
   The Creators List in its fee bands, a tick a creator; a tick opens the
   rate and platforms under its row on the shaded ground. The strip and the
   search stay above the list as it scrolls. */
.ac-sheet .ac-tabs { flex: 0 0 auto; margin: 8px 0 12px; }
.ac-bar .input, .ac-bar .select { width: 100%; min-width: 0; }
.ac-row:first-child { border-top: 0; }
.ac-row.is-on { background: var(--sunk); }
.ac-row.is-in .ac-pick { grid-template-columns: minmax(0, 1fr) auto; cursor: default; }
.ac-pick input[type="checkbox"] { width: 18px; height: 18px; margin: 0; accent-color: var(--ink); }
.ac-who { min-width: 0; }
.ac-more .field-label { display: block; }
.ac-more .slugfield { width: 100%; }
.ac-more .pickrate { min-width: 0; width: 100%; }
.is-narrow .ac-bar { grid-template-columns: minmax(0, 1fr) 156px; }
.is-narrow .ac-more { grid-template-columns: minmax(0, 1fr); padding-left: 15px; }
/* Which platforms an offer covers, ticked by us. */
/* A flex item will not shrink below its content unless it is told it may, and
   an opened box is wide enough to push the rate and Add off a phone. */
.pboxes { display: inline-flex; gap: 6px; flex-wrap: wrap; align-items: center; min-width: 0; max-width: 100%; }
.pbox {
  display: inline-flex; align-items: stretch;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  min-height: var(--ctl-h-sm);
  font-size: 12px; color: var(--ink-soft); background: var(--card);
  transition: border-color var(--t) var(--ease-out), color var(--t) var(--ease-out),
              background var(--t) var(--ease-out);
}
.pbox input[type="checkbox"] { margin: 0; accent-color: var(--ink); }
.pbox:has(.pbox-tick input:checked) { border-color: var(--ink); color: var(--ink); }
.pbox:active { transform: scale(.97); }

/* The tick opens into the field it makes necessary, inside its own box. The
   field used to arrive as a labelled row after the Add button: downstream of
   the button that sends it, and reflowing the whole row on every tick. Both
   ends of the width are stated, because a width of `auto` does not animate. */
/* Two classes, because `.input-sm` sets the 32px floor further down the file
   and the box itself is what carries the height here. */
.pbox .pbox-link {
  flex: 0 0 auto; width: 0; min-width: 0; opacity: 0;
  padding: 0; border: 0; border-left: 1px solid transparent; border-radius: 0;
  background: none; min-height: 0; font-size: 12px;
  transition: width var(--t-slow) var(--ease), opacity var(--t) var(--ease-out),
              padding var(--t-slow) var(--ease), border-color var(--t) var(--ease-out);
}
.pbox.needs-link { border-color: var(--ink); color: var(--ink); }
.pbox.needs-link .pbox-link {
  width: 190px; opacity: 1; padding: 0 10px; border-left-color: var(--line);
}
/* On a phone the pill takes the line it needs rather than pushing the row
   sideways: the boxes already wrap. */
@media (max-width: 640px) {
  .pbox.needs-link .pbox-link { flex: 1 1 auto; width: auto; min-width: 0; }
}

/* ---------- Facts ----------
   Label over value, in columns. Reads as a record rather than a caption. */
.facts {
  display: grid; gap: 14px 24px;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  margin: 6px 0 12px;
}
/* The column count follows the cell count, so no fact sits alone. */
.facts:has(> :nth-child(6)) { grid-template-columns: repeat(6, minmax(0, 1fr)); }
@media (max-width: 640px) {
  .facts:has(> :nth-child(6)) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .facts:has(> :nth-child(5):last-child) > :last-child { grid-column: 1 / -1; }
}
.facts dt {
  font-size: 12px; font-weight: 600; letter-spacing: 0;
  color: var(--ink-mute); margin-bottom: 3px;
}
.facts dd { font-size: 14px; color: var(--ink); margin: 0; overflow-wrap: anywhere; }

/* ---------- Money ----------
   Subtotal, tax, total, the way an invoice lays them out, because that is
   what this becomes. */
.totals { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 16px; max-width: 420px; }
.totals > div { display: flex; justify-content: space-between; gap: 16px; font-size: 13.5px; color: var(--ink-soft); }
.totals > div span:last-child { font-variant-numeric: tabular-nums; }
.totals .is-total { padding-top: 6px; border-top: 1px solid var(--line); font-weight: 600; color: var(--ink); font-size: 14.5px; }
#confirmTotals { margin-top: 10px; }
.confirmbar-in > span { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.confirmbar-in > span b { font-size: 14px; }
.confirmbar-in > span .muted { font-size: 12.5px; }

/* The invoice, once there is one. */
.topbar .headlink { margin-right: 8px; text-decoration: none; }
.invcurrent .btn { flex: 0 0 auto; }

/* ---------- Campaign header, in order ----------
   Name, then what it is for, then the facts, then the numbers. */
/* The second of the three lines in the campaign's identity, between the name
   and the meta, on the same margin as both. */
.camp-purpose { font-size: 14px; color: var(--ink-soft); margin: 0; min-width: 0; overflow-wrap: anywhere; }
.camp-purpose[hidden] { display: none; }

.stat b { font-size: 17px; font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.stat span { font-size: 12px; font-weight: 600; letter-spacing: 0; color: var(--ink-mute); }
.stat.is-total b { color: var(--ink); }
.stat.is-warn b, .stat.is-warn span { color: var(--ink); }

/* The one date on the client's page with a consequence. */
.camphead-purpose { font-size: 15px; color: var(--ink-soft); margin-top: 6px; }

/* A profile link reads as an instruction now, not a tag. */
.plink { padding-inline: 11px; font-weight: 500; }

/* =========================================================================
   PHONE
   One pass, last in the file, so it is the final word on layout at phone
   width rather than a correction scattered through eighteen breakpoints.
   Three rules govern it: nothing is narrower than the thumb that taps it,
   a row either fits or becomes a column, and a button never sits at a
   different height from the button beside it.
   ========================================================================= */
@media (max-width: 560px) {

  /* A heading and its actions: the heading takes the line, the actions share
     the next one equally. Two buttons of different widths and heights side by
     side was the thing that read as unfinished. */
  .viewhead { flex-wrap: wrap; gap: 10px; margin: 22px 0 12px; }
  .viewhead > h2, .viewhead > h3 { flex: 1 1 100%; min-width: 0; }
  /* A heading and its one action share the line where they fit, the way the
     desktop head does. Taking the whole row for the heading spent a line on
     four words and put four actions on four lines of their own down the
     record. The heading still wraps under a wide pair of buttons. */
  .viewhead .headmark { flex: 1 1 auto; min-width: 0; }
  /* A hidden button is not a second action. Both `:only-of-type` and
     `:has(.btn + .btn)` counted one, so the Creators pane — whose Bulk dates
     button is hidden until somebody is booked — gave its heading the whole row
     and stretched Add creators into a full width slab across the phone, which
     is the banner this rule exists to stop. Now that the primary is a filled
     blue that slab was the loudest thing on the screen. */
  .viewhead:has(.btn:not([hidden]) ~ .btn:not([hidden])) .headmark { flex: 1 1 100%; }
  /* Two visible actions share the line. One action is an action, not a banner:
     it keeps its own width and sits at the end of the row. */
  .viewhead .btn { flex: 1 1 0; min-width: 0; }
  .viewhead:not(:has(.btn:not([hidden]) ~ .btn:not([hidden]))) .btn {
    flex: 0 0 auto; margin-left: auto;
  }
  /* Issue Letter of Offer does not fit half a phone's row: the Documents
     head's two acts take a row each, never a label broken (MENU-14). */
  .viewhead:has(#crmCover:not([hidden])) > .btn:not([hidden]) { flex-basis: 100%; }
  .viewhead .btn-sm { min-height: var(--ctl-h); font-size: var(--ctl-text); }

  /* Action rows: a button keeps a readable width and wraps to the next line
     rather than squeezing its label onto three. */
  .kactions .btn,
  .linkbox .btn,
  .row > .btn { flex: 1 1 138px; min-width: 0; }
  .crm-engage-actions .btn { flex: 1 1 100%; min-width: 0; }
  /* A borderless tertiary button left alone at the end of a wrapped row read
     as a stray word. It takes the whole last line instead. */
  .kactions > .btn-quiet:last-child,
  .row > .btn-quiet:last-child { flex: 1 1 100%; }
  /* A width pinned in a style attribute is what left Cancel a 70px stub beside
     a full-width Confirm. On a phone the row decides, not the attribute. */
  .row > .btn[style] { flex: 1 1 138px !important; }
  .row > .btn-quiet[style]:last-child { flex: 1 1 100% !important; }
  .btn { padding-inline: 12px; }

  /* Fields stack rather than squeeze. A fixed flex-basis in a style attribute
     is what produced the half-width date box next to a narrower time box. */
  .row > div[style] { flex: 1 1 100% !important; }
  .row > div { flex: 1 1 100%; }
  /* Panels give back the side gutter the phone needs. A folded panel is a row,
     not a box: it was standing 70px tall to hold one word. */
  .panel { padding: 15px 16px; margin-bottom: 12px; }
  .disclosure .muted { font-size: 12px; }
  .hint { font-size: 12.5px; line-height: 1.5; }
  .empty { padding: 16px 15px; }

  /* A ⋯ menu is as wide as its words on a phone too (2026-10-11): drawn
     the screen's width, the task card's covered the card's whole head. */
  .kmenu { right: 6px; top: 46px; width: min(260px, calc(100vw - 32px)); }
  .kactions { gap: 8px; }

  /* Full label on a phone too: the client has to know which profile they are
     opening. They wrap to their own lines rather than being abbreviated. */
  .plink { padding-inline: 11px; }
  .crow-links { flex: 1 1 100%; }

  /* A sheet sits on the floor of a phone rather than floating in a dark room;
     the shape is set at 640, and this is the gutter it takes below that. */
  .sheet-card { padding: 18px 16px 8px; }
}

/* A tap target is never smaller than a finger, whatever the width. */
@media (pointer: coarse) {
  .kmenu-btn { width: var(--tap); height: var(--tap); }
  .kmenu-item { padding: 11px 12px; }
  }

/* ---------- Activity record ----------
   One record for the whole portal, so it needs a way in other than scrolling:
   the tabs answer "what happened in campaigns" and the day headings give the
   eye somewhere to stop. */
.acttab {
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; padding: 6px 11px; font-size: 12.5px; font-weight: 500;
  color: var(--ink-mute); white-space: nowrap;
}
@media (hover: hover) {
  .acttab:hover { background: var(--line-soft); color: var(--ink); }
}
.acttab.is-on { background: var(--fill); border-color: var(--fill); color: var(--on-fill); }
@media (max-width: 560px) {
  .acttab { padding: 8px 10px; font-size: 12px; }
}

/* ---------- CRM ----------
   A client list is a table, because that is the shape of every CRM the team
   has already used and the eye reads a column faster than it hunts across a
   wide row. Columns align down the page; everything about one client sits
   together on its line. */
.crm-head, .crm-row { display: grid; align-items: center; gap: 0 16px; }
/* The pad above the first row belongs to the header, not to the table, so it
   leaves with the header when a phone hides it. Padding the table instead
   left a 12px gap over the first row on a phone, with nothing in it. */
/* A label is 12px, 600, mute and sentence case. It was an 11px uppercase
   eyebrow with .06em of tracking on every table header, band, fact label
   and step title; the user sent that face back on 2026-09-22 ("weird
   spacing, wide visual effect") and it is retired portal-wide, the
   platform mockups excepted because they reproduce another product. */
.crm-head {
  padding: 12px 15px 8px; border-bottom: 1px solid var(--line);
  font-size: 12px; font-weight: 600; letter-spacing: 0;
  color: var(--ink-mute);
}
.crm-row {
  width: 100%; text-align: left; background: var(--card);
  border: 0; border-bottom: 1px solid var(--line-soft); border-radius: 0;
  padding: 13px 15px; min-height: 52px;
}
@media (hover: hover) {
  .crm-row:hover { background: var(--sunk); }
}
/* A whole row is the control, so its ring sits inside its own edge rather than
   over the row above it. */
.crm-row:focus-visible { outline: 2px solid var(--action-ring); outline-offset: -3px; border-radius: 0; }
.crm-c { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 13.5px; color: var(--ink-mute); }
/* A stage that has run past its limit. Red, the late colour, and the
   word Overdue beside it, because the mark has to survive a reader who
   cannot tell the two apart. */
.crm-age.is-late { color: var(--err); font-weight: 600; }
.crm-reach { display: flex; gap: 6px; flex-wrap: wrap; min-width: 0; }

.cmonth-table.has-file .cmonth-row { grid-template-columns: 32px minmax(0, 1fr) var(--state-w) 116px; }
.cmonth-tile {
  width: 32px; height: 32px; border-radius: var(--radius-ctl-sm); background: var(--tonal); color: var(--ink-soft);
  display: grid; place-items: center;
}
.cmonth-tile svg { width: 16px; height: 16px; stroke-width: 1.8; }
.cmonth-what { display: grid; gap: 2px; min-width: 0; }
.cmonth-what b {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; min-width: 0;
  font-size: 14px; font-weight: 600; letter-spacing: -.01em; color: var(--ink);
}
.cmonth-name { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cmonth-what small { font-size: 12.5px; color: var(--ink-mute); }
.cmonth-what small .part { white-space: nowrap; }
.cmonth-state { min-width: 0; }
.cmonth-end > svg { width: 16px; height: 16px; }
.cmonth-end .btn svg { width: 14px; height: 14px; }
.is-narrow .cmonth-row,
.is-narrow .cmonth-table.has-file .cmonth-row { grid-template-columns: 32px minmax(0, 1fr) var(--state-w); align-items: start; }
.is-narrow .cmonth-state { grid-column: 3; grid-row: 1; justify-self: stretch; text-align: right; }
.is-narrow .cmonth-row.is-file > .cmonth-end { grid-column: 3; grid-row: 2; }

/* A checkbox with its label, sized to be tapped rather than aimed at. */
.tickline {
  display: inline-flex; align-items: center; gap: 9px; cursor: pointer;
  font-size: 13.5px; color: var(--ink-soft); min-height: var(--ctl-h);
}
.tickline input { width: 18px; height: 18px; accent-color: var(--accent); flex: 0 0 auto; }

/* Below the column widths, the same cells become two lines: who they are,
   then everything else. */
@media (max-width: 760px) {
  .crm-head { display: none; }
  .crm-row.client-row {
    grid-template-columns: minmax(0, 1fr) 120px;
    grid-template-areas: "name stage" "meta stage";
  }
  }
@media (max-width: 560px) {
  .crm-reach .plink { flex: 1 1 auto; justify-content: center; }
}

/* ---------- Shared footer ----------
   Drawn by js/chrome.js, so every page that includes the chrome gets it and a
   new page does not have to remember it. Two things, one at each end, and
   nothing in the middle competing with them. */
/* The foot stands on the page's own ground (2026-09-27, the user: "footer
   not optimized, spacings and color"): as a white band it was a stripe
   between a grey page and the grey a phone shows under the last pixel. The
   rule above it is the boundary; the ground is the page's, white or grey. */
.portalfoot {
  flex: 0 0 auto; margin-top: auto;          /* see the sticky-footer note below */
  order: 1;   /* drawn with the header, and last on the screen from the first paint */
  border-top: 1px solid var(--line); background: transparent;
}
.portalfoot-in {
  max-width: 1240px; margin: 0 auto; padding: 16px 24px;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  font-size: 12.5px; color: var(--ink-mute);
}
/* The bar and the foot run to the screen's edges on every page: their
   contents sit on the 24px gutter at every width, so the mark is at the
   left edge and the actions at the right on a wide screen rather than both
   stopping 1240px apart in the middle of it and sliding as the window
   widens. The console's sign-in page was the last to keep the centred box
   (the user, 2026-09-26); its mark now lands at x=24, where the rail's mark
   sits once the console opens. The content stays centred, because a page
   of text has a width it reads at and a bar does not. */
.topbar-inner, .portalfoot-in { max-width: none; }
.portalfoot-copy { min-width: 0; }
.portalfoot-link { flex: 0 0 auto; color: var(--ink-mute); text-decoration: none; }
.portalfoot-link:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.chrome-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* A short page used to leave the footer floating halfway up with dead space
   under it. The page is a column at least as tall as the window, the content
   takes the slack, and the footer's auto top margin pushes it to the floor. */
body { min-height: 100vh; display: flex; flex-direction: column; }
@supports (min-height: 100dvh) { body { min-height: 100dvh; } }
body > main, body > .shell, body > .console { flex: 1 0 auto; }
/* In a flex column an item with auto side margins shrinks to its content
   instead of stretching; the client pages were a 450px column on a desktop. */
body > .shell { width: 100%; }

/* The review cover claimed a full viewport of its own, which put the footer
   just below the fold on a page with nothing else on it. It now fills what is
   left between the bar and the footer instead of asking for the lot. */
.shell:has(> .cover:not([hidden])), .shell:has(> .signin:not([hidden])) {
  display: flex; flex-direction: column; padding-bottom: 0;
}
.shell:has(> .cover:not([hidden])) > .cover, .shell:has(> .signin:not([hidden])) > .signin {
  flex: 1 1 auto; min-height: 0; margin-bottom: 0;
}

@media (max-width: 560px) {
  .portalfoot-in { padding-inline: 16px; gap: 10px; font-size: 12px; }
  .filterbar .select { flex: 1 1 calc(50% - 5px); min-width: 0; }
}

/* The card arrives from its heading and leaves into it: a grid row from 0fr
   to 1fr is the one height CSS can animate without being told the height,
   and the inner box clips while it moves. Nothing is clipped at rest: every
   row ⋯ is placed on the viewport by `ADspaceMenu.place()`. */
.crm-group-body {
  display: grid; grid-template-rows: 1fr; opacity: 1;
  transition: grid-template-rows var(--t-slow) var(--ease), opacity var(--t) var(--ease-out);
}
.crm-group.is-shut > .crm-group-body { grid-template-rows: 0fr; opacity: 0; }
.crm-group-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin: 0 0 12px; }
.crm-group-head h3 { font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
.crm-group-fold .crm-band-fold { width: 14px; height: 14px; color: var(--ink-mute); transform: rotate(90deg); transition: transform var(--t) var(--ease); }
.crm-group-head .crm-band-worth { margin-left: auto; font-size: 12.5px; color: var(--ink-mute); }
.crm-group.is-shut { margin-bottom: 24px; }
.crm-group-head .hint { margin: 0; flex: 1 1 260px; }
.crm-table {
  background: var(--card); border: 1px solid var(--card-line); border-radius: var(--radius);
  overflow: hidden; margin-bottom: 12px;
}
/* Blocks stacked in a section sit 12px apart whatever they are: a table, a
   panel, a fold, an empty state, a card. The section head carries 24 above. */
#crmWork .empty, #campWork .empty, #svcList .empty, .crm-removed-toggle { margin-bottom: 12px; }
/* The client portal stacks the same blocks under the same heads. */
#app > .empty, #app .empty, #crmRequests .empty { margin-bottom: 12px; }
.viewhead > .select { flex: 0 0 auto; width: auto; min-width: 180px; }
.sheet-card > .facts { margin: 8px 0 16px; }
/* The journey reads as one sentence of facts, so it is one mute line under
   them, not a row of cells competing with the facts above it. */

/* ---------- Services ----------
   The rate card, and the lines a client has asked for. One row per line:
   name, the numbers, a ⋯. */
.svc-row {
  display: grid; align-items: center; gap: 0 12px;
  grid-template-columns: minmax(0, 1fr) 130px minmax(0, 1fr) var(--ctl-h);
  padding: 10px 15px; border-bottom: 1px solid var(--line-soft);
}
.svc-row:last-child { border-bottom: 0; }
/* A retired or voided row fades its content, never its ⋯: fading the row
   itself put the menu inside a faded stacking context, behind the next row. */
.svc-row.is-off > :not(.team-act), .team-row.is-off > :not(.team-act), .group-row.is-off > :not(.team-act) { opacity: .55; }
.svc-row.crm-head { padding: 12px 15px 8px; border-bottom: 1px solid var(--line); }
/* A header cell keeps the header's type whatever column class it carries. */
.svc-row.crm-head > * { font-size: inherit; font-weight: inherit; color: inherit; }
.svc-name { display: flex; flex-direction: column; min-width: 0; }
.svc-name b { font-size: 14px; font-weight: 600; letter-spacing: -.01em; }
.svc-name b .tone { margin-left: 6px; vertical-align: 1px; }
.svc-name small { font-size: 12px; color: var(--ink-mute); }
/* A line's Bukku item code, in the token face as a client's ID (2026-10-15). */
.svc-name small.svc-sku { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; letter-spacing: 0; }
.svc-name small.svc-said { margin: 2px 0 0; color: var(--ok); font-size: 12.5px; }
.svc-rate { font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; font-size: 13.5px; }
.svc-unit { font-size: 13px; color: var(--ink-mute); min-width: 0; }
.svc-calc { font-size: 12.5px; color: var(--ink-mute); }
.svc-calc small { display: block; font-size: 12px; }
/* The state column is one width everywhere, so the selects line up down a
   table and across sections. It sits last before the ⋯. */
.svc-state { text-align: left; }
/* The name track states a real minimum rather than `minmax(0, 1fr)`, which is
   a track that will shrink to nothing: at the 690px pane a 1440px window
   gives it, the name had 158px and "Social media management for Instagram,
   Facebook and TikTok" broke one word per line into a 210px tall row. 180 is
   the floor a service name is readable at; the two money tracks give up 20px
   between them so the sum of the fixed tracks and the gaps (672) still fits
   that 690. Below it the row stacks on purpose — see `.is-narrow` — rather
   than overflowing, because a declared minimum that cannot be honoured is
   worse than no minimum at all. */
.csv-row { grid-template-columns: minmax(180px, 1fr) 150px 120px var(--state-w) var(--ctl-h); }
/* The client portal writes Qty × rate as one phrase that never breaks
   ("1 × RM 2,830.00 × 6 months"), so its track holds the phrase whole; the
   console's months keep their own line under the rate (2026-10-31, POR-4). */
.cp-card .csv-row { grid-template-columns: minmax(180px, 1fr) 200px 120px var(--state-w) var(--ctl-h); }
.doc-row { grid-template-columns: minmax(0, 1fr) 130px var(--state-w) var(--ctl-h); }
/* A meeting's date and time is a sentence, not an amount: its own track,
   measured on "Wed 30 Sept 2026, 10:30pm to 11:45pm" at the client pages'
   16px (the span says "to", never a dash: PORTAL-10, 2026-10-14), and never
   wrapped into the state beside it. */
/* Join's track is one width on every row and the head, measured on Join meeting and
   加入 with the leaving mark at a desk (2026-10-21: an `auto` track sized each
   row to its own Join and the head to nothing, so no column lined up). */
.doc-row.meet-row { grid-template-columns: minmax(0, 1fr) 296px var(--state-w) 128px; }
/* A request's Reply stands on its row (MENU-6). */
.doc-row.rq-row { grid-template-columns: minmax(0, 1fr) 130px var(--state-w) 92px; }
.doc-row.rq-row > .team-act { justify-self: end; }
/* A Letter of Offer or a performance record on the Register opens on a
   press of its row (MENU-7). */
.reg-row.is-go { cursor: pointer; }
.reg-go svg { width: 15px; height: 15px; color: var(--ink-mute); }
@media (hover: hover) { .reg-row.is-go:hover { background: var(--sunk); } }
/* A signed letter's Verify stands before its ⋯ (MENU-9). */
.doc-row.ltr-row { grid-template-columns: minmax(0, 1fr) 130px var(--state-w) 140px; }
.doc-row.ltr-row > .team-act { display: flex; justify-content: flex-end; align-items: center; gap: 8px; }
.meet-row .svc-amt { white-space: nowrap; }
/* The client record's pane has no Brand column: every row there is that
   client's. */
.reg-row.is-own { grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) 110px var(--ctl-h); }
.reg-brand, .reg-who { font-size: 13px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-open {
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  min-width: 0; align-self: stretch; margin: -10px 0; padding: 10px 0;
  border: 0; background: none; font: inherit; color: inherit;
  text-align: left; cursor: pointer;
}
.task-open b { font-size: 14px; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-open small { font-size: 12px; color: var(--ink-mute); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-stage { min-width: 0; }
/* Two classes on the select, because the global `.select-sm.state-select`
   later in the file sets 124px and a one-class rule here lost to it: the
   track widened and the select inside it did not. */
.task-stage .state-select, .task-stage .select-sm.state-select { width: 100%; }
/* Take (Open to take) stands where the stage would, at the stage's width. */
.task-stage > .task-take { width: 100%; min-width: 0; }
/* The refusal a stage move met, under the row it was made on. */
.task-note { margin: 0; padding: 0 15px 10px; }
.task-owner { font-size: 13px; color: var(--ink-soft); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-due {
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
  font-size: 13px; color: var(--ink-soft); white-space: nowrap; font-variant-numeric: tabular-nums;
}
.task-due > b { font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.task-due > small { font-size: 12px; color: var(--ink-mute); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* Overdue carries the word as well as the colour, so the mark survives
   greyscale and a reader who cannot tell red from mute. */
.task-due.is-over > b { color: var(--err); font-weight: 600; }
/* Priority is a chip at the right end of the title line, only where it is
   the exception. */
.task-top { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* At a desk the priority chip follows the name, never the far edge of a
   wide cell, where it read as the Task Owner's (the user, 2026-09-28); on a
   narrow row it ends the title line beside the ⋯, as a card's chip does. */
.task-top > b { flex: 0 1 auto; }
/* PRIORITY DEADLINES (2026-10-11, the user's D3 + B): a task with a
   deadline carries a strip down its left edge in the level's colour (Urgent
   red, High amber, Medium yellow), red once the deadline has passed, and the
   deadline in words as a filled tag in the same colour where the due date
   stands (the row's Due cell, a board card's foot) until it is met,
   its words in ink while it counts down and red once over (the user,
   2026-10-11): the one status drawn filled. Normal draws neither. The strip is an inset shade, so
   it takes no room and moves nothing. */
.task-row.pri-urgent, .bcard.pri-urgent, .task-deadline.pri-urgent,
.task-row.pri-over, .bcard.pri-over, .task-deadline.pri-over { box-shadow: inset 4px 0 0 var(--dot-late); }
.task-row.pri-high, .bcard.pri-high, .task-deadline.pri-high { box-shadow: inset 4px 0 0 var(--dot-wait); }
.task-row.pri-medium, .bcard.pri-medium, .task-deadline.pri-medium { box-shadow: inset 4px 0 0 var(--dot-soon); }
.task-pri:is(.is-urgent, .is-high, .is-medium, .is-over) {
  font-size: 12px; font-weight: 600; line-height: 1.5; padding: 2px 8px; border-radius: 5px; white-space: nowrap;
}
.task-pri:is(.is-urgent, .is-over) { background: var(--err-bg); }
.task-pri.is-high { background: var(--warn-bg); }
.task-pri.is-medium { background: var(--soon-bg); }
.task-pri:is(.is-urgent, .is-high, .is-medium) { color: var(--ink); }
.task-pri.is-over { color: var(--err); }
.task-due.is-pri { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; min-width: 0; }
.task-deadline {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px;
  margin: 8px 0 0; padding: 8px 12px 8px 14px; border-radius: var(--radius-sm);
  background: var(--sunk); font-size: 13px; color: var(--ink-soft);
}
.task-deadline[hidden] { display: none; }
.is-narrow .task-row > .task-due > small { display: none; }

.tstage-row.is-total { border-top: 1px solid var(--line); }
/* The Time pane holds two measures that are never summed — how long the work
   sat in each stage, and the sessions somebody started — so each is named at
   the pane's own section size, and the second is one block step under the
   first like every other pair of blocks in a section. */
.ovsec-title { font-size: 15px; font-weight: 600; letter-spacing: -.01em; margin: 0 0 8px; }
.crm-table + .ovsec-title, .empty + .ovsec-title { margin-top: 24px; }

/* The task number is the mark, because that is what an invoice, a message and
   a spreadsheet row all name a task by; a monogram of a title says nothing. */
/* A record with no mark: the task. Its number heads the left part in the
   token face and copies on a press, the way a serial does on the Register,
   and the right part centres on the card as it does on every other record.
   On a phone the ⋯ and the chip share the token's line, so the first row is
   the number on the left and the state on the right, and the title and the
   meta follow under them full width. */
.rec-id.rec-id-plain {
  grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "token ctl" "who ctl";
  row-gap: 3px;
}
.rec-token {
  grid-area: token; justify-self: start; min-height: var(--ctl-h-sm);
  margin: calc((18px - var(--ctl-h-sm)) / 2) 0 calc((18px - var(--ctl-h-sm)) / 2) -6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px; font-weight: 600; letter-spacing: 0; color: var(--ink-mute);
}
/* On a phone only the number shares its line with the state and the ⋯; the
   title and the meta take the card's full width under them, or a chip beside
   the meta line wrapped the owner's name onto a line of its own. */
.is-narrow .rec-id.rec-id-plain { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "token ctl" "who who"; row-gap: 4px; }
/* The pen that edits the description and the control that copies the name
   sit on the name's line at the icon button's floor, taking the negative
   block margin the rate card's ⋯ takes so a 38px target does not set the
   height of a 25px line. */
.rec-name .rec-pen {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: var(--ctl-h); height: var(--ctl-h); margin-block: calc((25px - var(--ctl-h)) / 2);
  border: 0; border-radius: var(--radius-ctl); background: none; color: var(--ink-mute); cursor: pointer;
}
.rec-name .rec-pen svg { width: 15px; height: 15px; }
.rec-name .rec-pen[hidden] { display: none; }
@media (hover: hover) { .rec-name .rec-pen:hover { background: var(--line-soft); color: var(--ink); } }
.rec-name .rec-pen.is-saving { color: var(--action-ink); }
.rec-name .rec-pen.is-done { color: var(--ok); background: var(--ok-bg); }
/* The field takes the heading's size; its box is the small control's, so the
   head grows by as little as a field can when the pen is pressed. */
/* A hint under its own field sits on the field, not a block step away. */
.field > .hint { margin: 6px 0 0; }
/* The New sheet's posts (2026-10-12): one shaded group holding every post
   and Add another; each post is one task and names on its own lines what it
   is and its format; its week and three dates; who takes it, how soon and
   what kind, the deadline a priority sets under them. Posts sit a hairline
   apart, each headed Post n with its remove mark once there are two. The
   tracks of the week and dates line are the post's own, so the lines above
   and below share their edges. On a narrow card the fields go two across. */
.ntfor { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px 16px; margin: 0; }
.ntfor + .row { margin-top: 16px; }
.ntfor-pick .field-label { margin: 0; }
.ntfor .tickline { margin: 0; }
.ntnomonth > span { font-size: 13px; color: var(--ink-soft); }
.ntnomonth[hidden] { display: none; }
.postgroup-head { display: flex; align-items: baseline; gap: 8px; margin: 0 0 4px; }
.postgroup-head .fsec-h { margin: 0; }
.postgroup-n[hidden] { display: none; }
.piece {
  display: grid; gap: 12px; align-items: end; padding: 12px 0 16px;
  grid-template-columns: minmax(0, .8fr) repeat(3, minmax(0, 1fr));
  grid-template-areas: "desc desc desc fmt" "week draft due post" "owner owner pri type";
}
.piece:last-child { padding-bottom: 4px; }
.piece + .piece { border-top: 1px solid var(--line); }
.piece-head .piece-x { margin: -10px -8px -10px 0; }
/* The deadline line takes a row of its own only while it is said, so a
   Normal post keeps no empty row. */
.piece-note { grid-column: 1 / -1; margin: -6px 0 0; }
.piece-note[hidden] { display: none; }
.pieces.is-many .piece { grid-template-areas: "head head head head" "desc desc desc fmt" "week draft due post" "owner owner pri type"; }
.pieces.no-week .piece { grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-areas: "desc desc fmt" "draft due post" "owner pri type"; }
.pieces.no-week .piece-week, .pieces:not(.is-many) .piece-head { display: none; }
/* A post's fields fill their tracks: a select's own 180px floor would push
   the week over the dates. */
.piece .input, .piece .select, .piece .dmy { width: 100%; min-width: 0; max-width: none; }
/* A one-line box is a block, so no line box's descent drops it below the
   Format beside it. */
.piece-desc textarea { display: block; }
.is-narrow .postgroup { padding: 12px 14px 14px; }
.is-narrow .piece { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-areas: "desc desc" "fmt fmt" "week draft" "due post" "owner owner" "pri type"; }
/* Several ticks in a sheet stack one to a line; inline they flowed two to a
   line with the third wrapping under them. */
.tickstack { display: grid; gap: 2px; margin: 0 0 12px; }
.pctfield .field-label { margin: 0; }
.pctbox .input { width: 104px; padding-right: 26px; }
.tickstack .tickline { display: flex; }

/* The engagement is the month's own record, so it sits in the month's card
   above the tasks as a bounded block with the same edge the tables carry. */
.engcard { border: 1px solid var(--card-line); border-radius: var(--radius); background: var(--card); padding: 16px 18px; margin: 0 0 12px; }
.is-narrow .engcard { padding: 14px; }
.eng-who { min-width: 0; }
/* The head's facts are rows on the card's grid (label track, value), the
   state and ⋯ beside the first of them; a tight card keeps each on one line,
   where the rows below stack (ux-audit WORK-7, 2026-10-15). */
.eng-meta { display: grid; gap: 8px; min-width: 0; }
.eng-meta > .eng-row { grid-template-columns: 150px minmax(0, 1fr); grid-template-areas: "lab val"; }
.is-narrow .eng-meta > .eng-row { grid-template-columns: 104px minmax(0, 1fr); }
/* A tight card gives its facts the card's width, one label track for all,
   the state and ⋯ beside the first fact alone, so a date or a report's name
   keeps its line (2026-11-02, the month's Runs and Reports). */
.is-tight .eng-head { gap: 8px 12px; }
.is-tight .eng-who, .is-tight .eng-meta { display: contents; }
.is-tight .eng-meta > .eng-row { grid-column: 1 / -1; grid-template-columns: 64px minmax(0, 1fr); grid-template-areas: "lab val"; gap: 12px; }
.is-tight .eng-meta > .eng-row:first-child { grid-column: 1; grid-row: 1; }
.is-tight .eng-head > .eng-ctl { grid-column: 2; grid-row: 1; }
.eng-row > .eng-val { grid-area: val; min-width: 0; overflow-wrap: anywhere; color: var(--ink); }
.eng-meetword.is-held { color: var(--ok); }
/* The meeting's link is the contact chip, cut to the cell with an ellipsis. */
.eng-meetlink .plink { max-width: 100%; }
.plink-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.is-narrow .eng-row { grid-template-columns: 104px minmax(0, 1fr) auto; gap: 4px 12px; }
/* A narrow card puts the label and the controls on the first line and the
   value under them at the full width, so a button never stands alone. */
.is-tight .eng-row { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "lab act" "val val"; }
.eng-meetwarn { margin: 8px 0 0; }
/* The message to the client is the section's client-facing act, so it is
   set off from the meeting's own facts by the card's hairline, 12 on each
   side as under the head (the user, 2026-09-28). */
.eng-facts > .eng-meetmsg { margin-top: 4px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
.eng-meetmsg-text { margin: 8px 0 0; padding: 12px 14px; background: var(--sunk); border-radius: var(--radius-sm); font: inherit; font-size: 13px; line-height: 1.55; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
.engcard .eng-checks { margin-top: 12px; }
.mcheck .tcheck { margin: calc((18px - var(--ctl-h)) / 2); }
.mcheck span.tcheck { display: block; }
.mcheck-lab { display: grid; gap: 1px; min-width: 0; font-size: 14px; }
.mcheck-lab small { font-size: 12px; color: var(--ink-mute); }
.mcheck-na { min-width: 0; margin-block: calc((20px - var(--ctl-h-sm)) / 2); }
/* The head keeps its control on the right wherever the name has 200px
   beside it, which the narrow pane does; only the tight one stacks. */
/* The state and its ⋯ keep the head's right edge on a phone too; the facts
   wrap in the column beside them. */
.is-tight .eng-head { align-items: start; }
@media (hover: hover) { .checkrow:hover { background: var(--sunk); } }
.checkrow input { width: 18px; height: 18px; accent-color: var(--action); cursor: pointer; }
.checkrow-label { font-size: 13.5px; color: var(--ink); min-width: 0; }
.checkrow.is-done .checkrow-label { color: var(--ink-mute); text-decoration: line-through; }

.ovnote { margin: 0 0 8px; font-size: 13.5px; color: var(--ink); overflow-wrap: anywhere; }
.mute { color: var(--ink-mute); }
/* The reference is a copy control in the row's own face: the target clears
   the control floor, the negative block margin keeps the line the height it
   was, and Copied is said the way every copy button says it. */
.serial-copy {
  display: inline-flex; align-items: center; min-height: var(--ctl-h);
  margin: calc((20px - var(--ctl-h)) / 2) 0 calc((20px - var(--ctl-h)) / 2) -6px;
  padding: 0 6px; border: 0; background: none; border-radius: var(--radius-sm);
  font: inherit; font-weight: 600; letter-spacing: -.01em; color: inherit; cursor: pointer;
  transition: background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}

.serial-copy.is-done { color: var(--ok); background: var(--ok-bg); }
/* A long reference in a register row is one line ending in an ellipsis,
   left aligned under the row's edge; the whole of it is the label, the
   hover and what Copy copies (DOC-3, 2026-10-22). */
.reg-row .serial-copy { max-width: 100%; text-align: left; justify-content: flex-start; }
.reg-row .serial-copy > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cat-row > .svc-unit {
  color: var(--ink-mute); font-size: 13px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cat-row.crm-head > .svc-unit { color: inherit; }
/* The price as its own control (ux-audit SVC-1): the figure keeps its right
   edge and its size; the target overflows the line evenly, as a due date's
   does. Pressed, the field takes the figure's place and the tick ends the
   cell. */
/* A version's row ends in its own Download (ux-audit SVC-9); its date names
   itself only where the heading is hidden (SVC-4). */
/* The Download track is fixed, so the heading's empty cell keeps the
   rows' columns (audit R2, 2026-10-15). */
.cat-row.rcv-row { grid-template-columns: minmax(0, 1fr) 118px 232px 112px; }
.is-narrow .rcv-row .rcv-k { display: inline; }
.cat-row > .svc-rate:has(> .askfield) { display: flex; align-items: center; justify-content: flex-end; gap: 2px; }
.cat-row > .svc-rate > .askfield { flex: 1 1 0; min-width: 0; text-align: right; -moz-appearance: textfield; appearance: textfield; }
/* Rate card versions (2026-10-13): which version is live, and what the
   draft holds that it does not, with Publish at the line's right edge. */
/* It stands in the command bar between the search and the actions, so the
   page keeps one row above the tabs (the user, 2026-10-14). */
.svc-live {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
  gap: 8px 12px; font-size: 13px; color: var(--ink-mute); min-height: var(--ctl-h-sm);
  flex: 1 1 auto; min-width: 0;
}
.svc-live-now > * { white-space: nowrap; }
.svc-live b { color: var(--ink); font-weight: 600; }
.rcw-head.is-fixed { padding-right: 0; }
.rcw-cell { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.sheet-body:has(> .rcw-rows) { display: flex; flex-direction: column; }
.cr-links { display: flex; flex-wrap: wrap; gap: 4px 14px; min-width: 0; }
.cr-record {
  font-size: 12.5px; color: var(--ink-mute); font-variant-numeric: tabular-nums;
  display: flex; align-items: baseline; min-width: 0;
}
/* The name truncates and the tail never does: `+2 · Sept 2026` is the part a
   clipped cell would make meaningless, while a shortened campaign name is
   still the campaign. */
.cr-record-n { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.crcamp {
  display: flex; align-items: center; gap: 12px; width: 100%;
  min-height: var(--ctl-h); padding: 8px 10px; text-align: left;
  background: none; border: 0; border-bottom: 1px solid var(--line-soft);
  border-radius: var(--radius-ctl);
  transition: background var(--t-fast) var(--ease-out);
}
.crcamp-name {
  flex: 1 1 auto; min-width: 0; font-size: 13.5px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (hover: hover) { .crcamp:hover { background: var(--sunk); } }
.cr-who > b { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* The Creators List's profile links are the same outlined chip (they were
   bare text until 2026-09-28: no text links); the handle rides in weight. */
.plink-bare b { font-weight: 600; }
.ct-row  { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) var(--ctl-h); }
.ct-row .crm-reach { justify-content: flex-start; }
/* A category heading inside the table, between its lines. */
.svc-cat {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 14px 15px 6px; font-size: 12px; font-weight: 600; letter-spacing: 0;
  color: var(--ink-mute); background: var(--sunk);
  border-bottom: 1px solid var(--line-soft);
}
/* The count is a number, not part of the band's name. */
.svc-cat > span { letter-spacing: 0; font-variant-numeric: tabular-nums; }
/* A status that moves as the work is a select on the row. It reads as the
   status it holds (2026-10-07): the field's own box, the word in ink and the
   status dot before it (`--sdot`), never a tinted box. The ⋯ beside it holds
   only what is not a status. */
.state-select, .select.state-select, .select-sm.state-select {
  width: var(--state-w); min-width: 0; height: var(--ctl-h-sm); min-height: var(--ctl-h-sm);
  /* The dot 8px in and the word 6px after it, as a status chip spaces them;
     the longest stage (Performance review) keeps its whole word in My Work's
     170px column, measured in the widest system face (Roboto, 120px). */
  padding: 0 24px 0 22px; font-size: 12.5px; font-weight: 500; line-height: 1.4; color: var(--ink);
  background-image: radial-gradient(circle, var(--sdot, var(--dot-off)) 3.5px, transparent 4px), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b6b6b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-size: 8px 8px, 13px; background-position: 8px center, right 8px center;
  background-repeat: no-repeat;
  /* A state longer than its column ends in an ellipsis, never a cut: a word
     sliced mid letter reads as a fault, three dots as a name that goes on. */
  text-overflow: ellipsis;
  /* A tonal pop-up, never a field's outline (the user, 2026-10-11): it acts
     as the row's buttons do, so it wears their shade; an outline is left to
     what is typed into. Never blue: blue is the view's one next step. */
  border-color: transparent; background-color: var(--tonal);
}
@media (hover: hover) {
  .state-select:not([disabled]):hover, .select.state-select:not([disabled]):hover { background-color: var(--tonal-hover); }
}
/* A press leaves no field's blue edge behind; the keyboard's ring is the
   buttons' own. */
.state-select:focus, .select.state-select:focus, .select-sm.state-select:focus { border-color: transparent; box-shadow: none; }
.state-select:focus-visible, .select.state-select:focus-visible, .select-sm.state-select:focus-visible {
  outline: 2px solid var(--action-ring); outline-offset: 2px;
}
.state-select.is-ok     { --sdot: var(--dot-ok); }
.state-select.is-warn   { --sdot: var(--dot-wait); }
.state-select.is-danger { --sdot: var(--dot-late); }
.state-select.is-off    { --sdot: var(--dot-off); }
/* In hand is a stage with no tone: the select with no dot. */
.state-select:not(.is-ok):not(.is-warn):not(.is-danger):not(.is-off) { --sdot: transparent; }
.ring { width: 18px; height: 18px; flex: 0 0 auto; transform: rotate(-90deg); }
.ring circle { fill: none; stroke-width: 2.6; }
/* Complete: a filled green disc with a white tick, never a full ring, which
   reads as an empty circle. The same size as the ring it replaces. */
.ring-done { width: 18px; height: 18px; flex: 0 0 auto; }
.ring-done circle { fill: var(--ok-solid); }
.ring-done path { fill: none; stroke: var(--on-ok); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.csv-total {
  display: flex; justify-content: flex-end; gap: 24px; flex-wrap: wrap;
  padding: 11px 15px; border-top: 1px solid var(--line); font-size: 13px; color: var(--ink-mute);
}
.csv-total b { color: var(--ink); font-variant-numeric: tabular-nums; margin-left: 6px; }
.csv-total small { margin-left: 6px; font-size: 12px; font-variant-numeric: tabular-nums; }
/* On a phone a row is two or three lines: the name with its ⋯, the small
   facts, then the money on the left and the state on the right. */
/* ---- Rows, keyed on the width of the thing that holds them ----
   Not `@media (max-width: 640px)`: the record pane is about 591px wide at a
   1280px window, so every one of these rules was false exactly where it was
   needed and the Services row kept a desktop grid in 591px — the name track
   collapsed to 59px and a description came out one word per line. `is-narrow`
   is set from the element's own width by `ADspaceState.fit`, so one copy of
   each template serves the phone and the narrow pane alike. */
.is-narrow .svc-row.crm-head { display: none; }
.is-narrow .svc-row, .is-narrow .csv-row, .is-narrow .doc-row, .is-narrow .cat-row, .is-narrow .ct-row, .is-narrow .cr-row {
  grid-template-columns: minmax(0, 1fr) auto; gap: 6px 12px; padding: 12px 15px;
  grid-template-areas: "name act" "calc calc" "amt state";
}
/* A service line draws a select while it can still move and a chip once a
   verified letter has confirmed it, and the two are not the same width. On
   an `auto` track that put the state column 51px further left on the
   confirmed rows, which is the fault this portal calls a column that is not
   a column. The track is the state's own width on every row, and what sits
   in it ends at the same edge whichever shape it is. */
.is-narrow .csv-row { grid-template-columns: minmax(0, 1fr) var(--state-w); }
/* The two things a person came for sit on the first line, and what qualifies
   the money goes under it. Three lines with a control on the last one was a
   row 310px tall: two and a half services on a screen out of thirty four. */
.is-narrow .cat-row {
  grid-template-columns: minmax(0, 1fr) 92px auto;
  grid-template-areas: "name rate act" "meta meta meta";
}
/* An action cell with nothing in it gives up its track. The rate card draws
   the ⋯ only for an admin, so for everybody else a fixed `var(--ctl-h)`
   third column held 44px of nothing and the whole column of prices stopped
   44px short of a right edge nobody could see — the row looked mis-centred
   and the cause was invisible. The track is `auto` above so it can collapse,
   and it only collapses on a phone: on a desktop the column is shared with
   rows that do draw the ⋯, and a cell that leaves the grid takes its column
   with it and slides the cells after it one track left. */
/* A block of the card's words holds no price: its rate track is the Changed
   chip's width or nothing, so the words take the row (ux-audit SVC-3). */
.is-narrow .cat-row.rcw-row { grid-template-columns: minmax(0, 1fr) auto auto; }
/* A version: its name and state, then its date and Download (ux-audit SVC-9). */
.is-narrow .cat-row.rcv-row { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name rate" "meta act"; }
.is-narrow .cat-row > .team-act:empty,
.is-narrow .cr-row > .team-act:empty { display: none; }
/* A header the phone hides is not a header, so it leaves and takes its own
   padding with it. The row becomes three lines under the number: who, where
   it has got to, and what the booking is. The state keeps a line it can be
   read down rather than sharing one with a summary that would squeeze it. */
.is-tight .bookreg-head { display: none; }
/* Two lines, as a phone list row reads: the name with its link and ⋯ at
   the right edge, then the state (its dot and whole word) at the left and
   its date at the right (the user's ruling, 2026-10-15). The state
   left the name's line (2026-10-15, geom): beside the chip, the link and
   the ⋯ a name had 90px at 320 and wrapped to three and four lines (audit
   CAMP-7 had let it wrap rather than cut its end). With the line to itself
   a name holds one line and every collapsed booking is one height. */
.is-tight .bookreg .kcard-head {
  grid-template-columns: auto max-content minmax(0, 1fr);
  grid-template-areas: "lead name name" "lead tags sum";
  gap: 2px 8px; align-items: center; padding: 10px 8px 10px 14px;
}
.is-tight .bookreg .kcard-name { grid-area: name; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.is-tight .bookreg .kcard-sum {
  grid-area: sum; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; text-align: right; color: var(--ink-mute);
}
/* On a phone these are named areas, so an empty one is a blank line held
   open by the gap and can safely leave. */
.is-tight .bookreg .kcard-sum:empty { visibility: hidden; }
/* Two lines, not four: who and what they cost, then where to find them with
   their record at the end of the same line. A grid row each for the links
   and the record put a 120px row on the screen, which is five creators on a
   phone. */
/* The last track is stated, not left to `auto`. The record cell spans the
   second and third tracks, and a spanning item sizes an intrinsic track it
   reaches: its tail never shrinks (`+2 · Sept 2026` is the part a clipped
   cell would make meaningless), so a creator with a second campaign grew
   that track by the width of `+2` and every cell after the name started
   15px left of the rows around it. The track is the control's own width,
   which is the only thing that is ever drawn in it. */
/* **Two lines, and one right margin.** The row drew three different right
   edges: the ⋯ spanned the first line only, so the record line ran to the
   card's padding at 358 while the fee above it stopped at 302, and `.cr-record`
   is a flex box, so the `text-align: right` it was given moved nothing at all
   and `—` sat at the *left* of a 148px track while `1 · Sept 2026` filled it
   and looked right aligned. Three edges on a two line row is what reads as
   uneven spacing without anybody being able to name it.
   The ⋯ spans both lines now, so the meta track ends where the money track
   ends; 104px is measured against `12 · Sept 2026` at 12.5px, which is the
   widest thing either cell can draw. */
.is-narrow .cr-row {
  grid-template-columns: minmax(0, 1fr) 104px var(--ctl-h);
  grid-template-areas: "name rate act" "links rec act";
  row-gap: 4px; align-items: start;
}
/* The ⋯ acts on the whole row, so it spans the whole row. Left on the first
   line it set that line's height to its own 38px and pushed the row to
   100px for the sake of a 22px name. */
.is-narrow .doc-row { grid-template-areas: "name act" "amt state"; }
/* A meeting's date and time is the longest value a row holds (Sun 4 Oct
   2026, 10:30pm – 11:15pm), so on a phone the state and the ⋯ end the
   agenda's line and the date and time take the second line whole. An
   empty ⋯ cell keeps no gap, so the state still ends at the card's edge. */
.is-narrow .doc-row.meet-row {
  grid-template-columns: minmax(0, 1fr) auto auto; column-gap: 0;
  grid-template-areas: "name state act" "amt amt amt";
}
.is-narrow .doc-row.meet-row > .svc-state { margin-left: 12px; }
.is-narrow .doc-row.meet-row > .team-act:not(:empty) { margin-left: 8px; }
/* A request with no fee says nothing on a phone (2026-10-20, POR-1). */
.is-narrow .doc-row > .svc-amt.is-none { display: none; }
/* Two lines: the reference and its ⋯, then who and when, the date ending at
   the card's right edge. */
/* The right track is fixed, measured against `30 Sept 2026`, because an
   `auto` track is sized by its own row: a row with no date drew it at the
   width of the ⋯ and the dates of the rows around it started elsewhere. */
/* On a phone the brand shares its line with the date and the registered
   name takes a line of its own, because side by side at 110px each the
   longer of the two was cut to its first word. */
/* Three lines on a phone (ux-audit DOC-3): the reference over its kind, then
   the recipient (the registered name, or the colleague) beside the date; the
   brand, which the recipient names again, is left out. */
.is-narrow .reg-row { grid-template-columns: minmax(0, 1fr) 100px; grid-template-areas: "name act" "who date"; }
.is-narrow .reg-row > .reg-brand { display: none; }
.is-narrow .reg-row.is-own { grid-template-columns: minmax(0, 1fr) 100px; grid-template-areas: "name act" "who date"; }
.is-narrow .reg-row > .svc-name { grid-area: name; }
.is-narrow .reg-row > .team-act { grid-area: act; justify-self: end; }
/* A narrow task row is the tick, the task and its ⋯ on the first line, and
   the Task Owner, the due date and the stage under the task, the stage at a
   stated width ending at the card's right padding on every row. The meta
   line runs under the ⋯, so the stage ends where the ⋯ does: one right
   margin. It used to wrap wherever the words ran out, which on one card put
   two stages at two heights and three different x: a select is as wide as
   its longest option, and under a finger its text is 16px so iOS does not
   zoom, so which rows fitted was decided by the words in them. Now nothing
   on the line wraps: the Task Owner gives way with an ellipsis, the date
   never does, and on a list a phone's width (tight) the stage takes a line
   of its own on every row instead. 170px is the desk stage track; 200px is
   `Performance review` at 16px. */
.is-narrow .task-row {
  grid-template-columns: var(--ctl-h) minmax(0, 1fr) var(--ctl-h);
  grid-template-areas: "check name act" ". meta meta";
  row-gap: 4px; column-gap: 8px; align-items: center;
}
.is-narrow .task-row > .trow-meta {
  grid-area: meta; display: flex; flex-wrap: nowrap; align-items: center; gap: 4px 12px; min-width: 0;
}
.is-narrow .trow-meta > .task-owner { flex: 0 1 auto; min-width: 0; max-width: 34%; }
.is-narrow .trow-meta > .task-due { flex: 0 0 auto; }
.is-narrow .trow-meta > .task-stage { flex: 0 0 auto; margin-left: auto; display: flex; justify-content: flex-end; }
.is-narrow .trow-meta .state-select { width: 170px; max-width: 100%; }
/* Under a finger the stage is 13.5px, and `Performance review` needs 176px
   with its chevron at that size. */
@media (pointer: coarse) { .is-narrow .trow-meta .state-select { width: 180px; } }
.is-narrow .trow-meta .task-take { width: 170px; max-width: 100%; }
@media (pointer: coarse) { .is-narrow .trow-meta .task-take { width: 180px; } }
.is-tight .trow-meta > .task-stage { flex: 0 0 100%; margin-left: 0; }
.is-narrow .task-row > .team-act { grid-area: act; align-self: start; }
/* The ring, the title with its priority chip, and the ⋯ share one centre:
   the title line's (22px). The 44px target overflows that line evenly
   instead of pushing the ⋯ 12px below it (2026-09-28). */
.is-narrow .task-row > .team-act,
.is-narrow .task-row > .trow-check { height: 22px; display: flex; align-items: center; }
.is-narrow .task-row.crm-head { display: none; }
.is-narrow .tstage-row { grid-template-columns: minmax(0, 1fr) 56px 96px; }
/* What happened owns the first line; when and who share the second, which is
   the shape the client record's own activity rows take. */
.is-narrow .ct-row { grid-template-areas: "name act" "reach reach"; }
.is-narrow .svc-row > .svc-name { grid-area: name; }
.is-narrow .svc-row > .team-act { grid-area: act; align-self: start; }
.is-narrow .svc-row > .svc-calc { grid-area: calc; text-align: left; }
.is-narrow .svc-row > .svc-amt { text-align: left; align-self: center; grid-area: amt; }
.is-narrow .cat-row > .svc-rate {
  grid-area: rate; text-align: right; align-self: center; white-space: nowrap;
}
/* Against a name over two meta lines the fee belongs on the name's own line,
   not floating halfway down the row. */
.is-narrow .cr-row > .svc-rate {
  grid-area: rate; text-align: right; align-self: start; white-space: nowrap;
}
.is-narrow .svc-row > .svc-state { grid-area: state; justify-self: end; }
/* A service line draws a select while it can still move and a chip once a
   verified letter has confirmed it, and the two are not the same width. End
   justified in an `auto` track that put the state column 51px further left
   on the confirmed rows, which is the fault this portal calls a column that
   is not a column. The track above is the state's own width; the cell fills
   it, so it starts at the same x on every row, and the chip is what ends at
   the edge. Restated here because `.svc-row > .svc-state` comes later in the
   file and would otherwise win on the same specificity. */
.is-narrow .svc-row.csv-row > .svc-state { justify-self: stretch; text-align: right; }
.is-narrow .ct-row > .crm-reach { grid-area: reach; }
/* Three ways to reach one person share the row at equal widths, which is
   what the law is for. One link is not a row of links: Engagements borrows
   this row for a name and a single Open, and at `flex: 1 1 0` that one
   outlined button became a slab across the phone — the banner every section
   head in this portal has refused for months. */
/* A phone's width alone (`is-tight`): a pane merely narrower than a desk's
   (a record's pane beside its rail) keeps each chip its own width, as the
   Overview's contact details do; stretched there they read as fields (the
   user, 2026-10-30). */
.is-tight .ct-row .plink { flex: 1 1 0; justify-content: center; }
.is-tight .ct-row .crm-reach .plink:only-child { flex: 0 0 auto; justify-content: center; }
/* Two chips or more share equal tracks (DESIGN §4: equal widths on a phone
   unless alone): the phone and WhatsApp as halves, an address on a row of
   its own, so no chip is left a width of its own beside the rest (CP-14). */
.is-tight .ct-row .crm-reach:has(> .plink + .plink) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.is-tight .ct-row .crm-reach:has(> .plink + .plink) > [href^="mailto:"],
.is-tight .ct-row .crm-reach > .plink:first-child:nth-last-child(2):has(+ [href^="mailto:"]) { grid-column: 1 / -1; justify-self: start; }
/* A chip alone on its row keeps its own width, whether it is a contact's
   only chip or the address under two halves (2026-10-31, POR-8: one
   stretched across the card and the other did not). */
.is-narrow .csv-total { justify-content: space-between; gap: 12px; }
.crm-links:empty { display: none; }
.notice.is-warn { border-color: transparent; background: var(--sunk); margin: 12px 0 0; }
.notice.is-warn svg { color: var(--ink-mute); }
.notice.is-warn b { color: var(--ink); }
/* One call, one visit, one row. Date and kind on the left hold still so the
   eye reads down; the summary carries the width; what happens next is the
   one line that can be overdue, and says so. */
.touch {
  display: grid; align-items: start; gap: 4px 16px;
  grid-template-columns: 128px minmax(0, 1fr) auto;
  grid-template-areas: "when body del";
  background: var(--card); border: 1px solid var(--card-line); border-radius: var(--radius);
  padding: 13px 15px; margin-bottom: 8px;
}
.touch.is-due { box-shadow: none; }
.touch-when b { font-size: 13.5px; font-weight: 600; font-variant-numeric: tabular-nums; }
.touch-body { grid-area: body; min-width: 0; }
.touch-next.is-due { color: var(--err); }
.touch-meet > span, .touch-msgrow > span { margin-right: 4px; font-variant-numeric: tabular-nums; }
@media (max-width: 640px) {
  .touch { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "when del" "body body"; }
  }

/* ---------- CRM: next actions, undo, deal value ---------- */
/* The one screen a sales person opens in the morning: what is due, whose it
   is, and a Done that clears it. Overdue leads and is marked; this week is
   marked more quietly. */
.due {
  background: var(--card); border: 1px solid var(--card-line); border-radius: var(--radius);
  padding: 14px 16px 6px; margin-bottom: 22px;
}
.due-head h3 { font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
.due-row.is-due .due-when, .due-row.is-due .due-when em { color: var(--err); }
/* The client's name opens the record: the in-line button's whole target
   (38, 44 under a finger) given back to the row by its margins, and the
   fill hugging the line, never an underlined word. */
.due-client {
  display: block; justify-self: start; max-width: calc(100% + 12px);
  min-height: var(--ctl-h); line-height: var(--ctl-h); margin: calc((20px - var(--ctl-h)) / 2) -6px;
  background: none; border: 0; padding: 0 6px; text-align: left; font: inherit;
  font-weight: 600; color: var(--ink); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.due-what { font-size: 13.5px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.due-what small { display: block; font-size: 12px; color: var(--ink-mute); }
@media (max-width: 640px) {
  .due-row .btn { grid-area: done; justify-self: end; flex: 0 0 auto; min-width: 96px; }
}

/* A single line with Undo on it, for the few seconds when a person realises. */
/* Something was undone and there is a way back. That is a message, not an
   action to take, so the bar is a quiet strip: the ink fill belongs to the
   primary button and to nothing else, and a full width black slab sitting a
   few pixels above a black "Add contact" read as one enormous call to
   action. Neutral ground, a hairline, ordinary text, and Undo as the outline
   button it always was. */
.undobar {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 0 0 12px; padding: 9px 14px;
  background: var(--sunk); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: 13px;
}
.crm-removed-toggle { display: flex; width: max-content; margin: 0 0 12px; }
.touch-next.is-done { color: var(--ink-mute); text-decoration: line-through; }
/* The same sub-heading the rate card, the Team page and the creators list
   use, carrying what the separate card heads used to: the stage, the count,
   how many have run over, and what the group is worth. */
/* The count belongs to the stage it counts, so it sits against the name; the
   worth ends the line, where a money column ends everywhere else. */
/* A band opens and shuts, so it is a button and says which it is. Past clients
   start shut: nobody opens this page to read an ended engagement, but they are
   still clients and still hold their number, so the band keeps its place and
   its count rather than disappearing. `font: inherit` and `color: inherit`
   because a native button otherwise takes the platform's own face and the
   platform's own `buttontext`, which is the fault this portal already fixed
   once for every other button. */
.crm-band {
  display: flex; align-items: center; justify-content: flex-start; gap: 8px;
  width: 100%; background: var(--sunk); border: 0; text-align: left;
  cursor: pointer; color: inherit; font: inherit;
  /* It is a control now, so it clears the control floor like every other one:
     `.svc-cat`'s own padding left it at 42px, two short of what a finger is
     owed, and the audit is what said so. */
  min-height: var(--ctl-h-sm);
  /* And it states its own padding rather than taking `.svc-cat`'s 14 above
     and 6 below. That asymmetry is right for a heading that hugs the list
     under it; this is a divider row inside one register, with a hairline of
     its own, so its word sits on the middle of its band. Left as it was, the
     band read as a gap above the first client with the label fallen to the
     floor of it. */
  padding: 9px 15px;
}
.crm-band-fold {
  width: 14px; height: 14px; flex: 0 0 auto; color: var(--ink-mute);
  transform: rotate(90deg); transition: transform var(--t) var(--ease);
}
@media (hover: hover) { .crm-band:hover .crm-band-fold { color: var(--ink); } }
.crm-band > span:first-of-type { font-weight: 500; }
/* The client list's columns, hung off the list's own row class rather than off
   every `.crm-head` in the console. `.crm-head:not(.svc-row)` was a template
   that claimed any header not wearing one particular class, and Short Links
   wore another: its header took the seven client columns while its rows took
   their own five, so DESTINATION sat 91px right of every destination under it
   and LABEL 103px left of every label. Nothing caught it, because the `cols`
   check only compares a header against a row it believes share a grid.
   Every table states its own tracks now, on both the header and the row. */
.crm-head.client-row, .crm-row.client-row {
  grid-template-columns:
    minmax(0, 1.9fr) 128px minmax(0, .9fr) minmax(0, 1fr) 104px 18px;
}
/* The client's logo before the name, the record head's mark at row size:
   white disc and hairline for a logo (light in both themes, as a logo is),
   initials on the quiet fill while none is held. */
.crm-row.client-row .crm-c-name { display: flex; align-items: center; gap: 12px; min-width: 0; }
.cl-mark {
  flex: 0 0 32px; width: 32px; height: 32px; border-radius: 50%; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--line-soft); color: var(--ink-soft);
  font-size: 12px; font-weight: 600; letter-spacing: -.01em;
}
.cl-mark.has-logo { background: #fff; border: 1px solid var(--line); }
.cl-mark img { width: 100%; height: 100%; object-fit: contain; }
.cl-who { display: flex; flex-direction: column; min-width: 0; overflow-wrap: anywhere; }
/* A part of a line of parts that must stay whole (a date, a name). */
.crm-c-meta .nb { white-space: nowrap; }
.crm-seen-kind {
  font-size: 12px; color: var(--ink-mute);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The mark that says the row goes somewhere, in the column the header leaves
   empty — the same chevron an engagement row on the record already uses. */
.crm-c-go { width: 15px; height: 15px; color: var(--ink-mute); justify-self: end; }
.crm-row.client-row.is-picking { cursor: pointer; }
.crm-row.client-row.is-picked { background: var(--line-soft); }
/* Restated after the desktop template above, or the five columns win on a
   phone and the name is squeezed to a letter. The state track is fixed here
   too: this rule is the later one, so an `auto` here undid the column the
   phone layout above had just set, and the chips went back to drifting.

   760, not 640, because that is where the header hides and where the two line
   row takes over: between the two the header was gone and the row still had
   five desktop columns, so a 720px window — which is what a 1440px one is at
   200% zoom — scrolled sideways. A header the phone hides is not a header,
   and the row it heads leaves with it. */
@media (max-width: 760px) {
  .crm-head.client-row, .crm-row.client-row { grid-template-columns: minmax(0, 1fr) 120px; }
  /* The mark at 28px beside the name; the meta line under it starts where
     the name does. */
  .cl-mark { flex-basis: 28px; width: 28px; height: 28px; font-size: 11px; }
  .crm-row.client-row .crm-c-name { gap: 10px; align-items: flex-start; }
  }

/* A standing fact about a whole route: true every day, nothing anybody can do
   about it here, and therefore the quietest thing on the screen rather than a
   full width card above the controls. A message is a line, not a card. */
.routenote {
  margin: 12px 0 0; font-size: 12.5px; color: var(--ink-mute);
}
.routenote b { font-weight: 600; color: var(--ink-soft); }
/* ---------- Content Review's client register ----------
   A tile per client answered "which clients are there" and nothing else. The
   sets, how many are live and whether an access code is on are columns now,
   which is what lets a book of business be scanned rather than read. */
.crm-head.cr-client-row, .crm-row.cr-client-row {
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) 132px 18px;
}
.crm-c-sets .is-warn { color: var(--ink); font-weight: 600; }
@media (max-width: 760px) {
  /* The sets line moves under the name and the access mark keeps its track,
     which is the two column record row every other list here takes. */
  .crm-row.cr-client-row {
    grid-template-columns: minmax(0, 1fr) 120px;
    grid-template-areas: "name code" "sets code";
  }
  /* A lone dash says nothing on a phone, so it is not drawn. */
  .crm-row.cr-client-row .crm-c-code.is-none { display: none; }
}

/* ---------- The campaign register ----------
   The same surface the clients directory is, with the campaign states as its
   divider rows. A card grid answered "how many campaigns are there" and
   nothing else: which one is waiting on somebody, what it is worth and who it
   is for all needed opening each tile in turn. Columns do that by being read
   down. The tracks are stated on both sides of the breakpoint, because a
   `.crm-row.camp-row` outranks the phone rule for `.crm-row` whatever order
   they sit in, and an unstated template is the desktop one at 390. */
.crm-head.camp-row, .crm-row.camp-row {
  grid-template-columns:
    minmax(0, 1.7fr) minmax(0, 1.1fr) 84px 116px 110px 18px;
}
.crm-row.camp-row .camp-c-upd { display: block; color: var(--ink-soft); font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right; }
/* A heading over a money column ends where the figures end, and is still the
   same eyebrow as the headings beside it. */
.crm-head .is-end { text-align: right; }
.camp-c-client { color: var(--ink-soft); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 760px) {
  /* Who it is for and what it is worth on one line under the name, the state
     in its own fixed track: the two column record row this console gives every
     other list, measured against its worst case. "Open for selection" is the
     widest chip a campaign can draw. */
  .crm-row.camp-row {
    grid-template-columns: minmax(0, 1fr) 110px;
    grid-template-areas: "name stage" "meta stage";
  }
  .crm-row.camp-row .camp-c-client,
  .crm-row.camp-row .camp-c-slots,
  .crm-row.camp-row .svc-rate { display: none; }
}

/* ---------- Team ----------
   Two tables. People, under the group they belong to, and the groups, each
   stating in words what it opens. Permissions are a property of the group,
   never of the person, so they are read on three rows rather than on nine. */
.team-table {
  background: var(--card); border: 1px solid var(--card-line); border-radius: var(--radius);
  padding: 12px 0 4px;
}
/* Soft Precision: the panel draws the boundary, so the table inside stops
   drawing a second one and stops padding the top — the header row has its
   own, and two of them read as a blank band above the first column name. */
/* The panel draws the boundary; the table inside keeps the shared card
   outline (one token, one card) and stops padding the top. */
.team-table.softpanel { padding: 0; }
.group-name { display: flex; flex-direction: column; min-width: 0; }
.group-name b { font-size: 14px; font-weight: 600; letter-spacing: -.01em; }
.group-name small { font-size: 12px; color: var(--ink-mute); }
/* The ⋯ menu hangs from its own cell, not from the table. */
.team-act { position: relative; display: flex; justify-content: flex-end; }
.team-act .kmenu { top: calc(var(--ctl-h) + 4px); right: 0; }
.group-name .input { width: 100%; }
.input-sm { min-height: var(--ctl-h-sm); font-size: 13px; }
.team-who { display: flex; flex-direction: column; min-width: 0; }
.team-who b { font-size: 14px; font-weight: 600; letter-spacing: -.01em; }
/* Your own row is marked with the neutral chip, not the accent: being you is
   a designation, and green is spent on a live state. */
.team-who b .tone { margin-left: 6px; vertical-align: 1px; }
/* The Employee ID beside the name, read after it. */
.team-who b .team-eid { margin-left: 6px; font-size: 13px; font-weight: 400; letter-spacing: 0; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.team-who small { font-size: 12px; color: var(--ink-mute); }
.permchunk > .fsec-h { margin: 0 0 4px; }
/* The Dashboard's one row stands above the chunks, with no heading (2026-10-11). */
.permdash > .permsec { border-top: 0; padding-top: 0; padding-bottom: 0; }
/* A row with nothing in it takes no room: the Dashboard's block holds no
   Customise, and an empty line stood 44px over Work (2026-10-27, TEAM-7). */
.permsec-desc:empty { display: none; }
.permsec-name { grid-area: name; align-self: center; min-width: 0; font-size: 14px; font-weight: 600; color: var(--ink); }
.permsec-desc { grid-area: desc; align-self: center; margin: 0; font-size: 12.5px; line-height: 1.45; color: var(--ink-mute); }
/* What differs from the section, said while its parts are shut. */
.permsec-diff { grid-area: diff; margin: 0; font-size: 12.5px; line-height: 1.45; color: var(--ink-soft); }
.is-narrow .permsec { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name tog" "seg seg" "desc desc" "diff diff" "parts parts"; row-gap: 8px; }
/* Four levels across a phone's panel: four pixels each side keep every
   level whole (Team audit, 2026-10-03); narrower still, the segment's own
   measure wraps it into two columns. */
.is-narrow .permsec .seg-opt { padding: 0 4px; }
/* Admin tools: one fold, its switches in the Settings page's groups. */
.permtools .permsec { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name tog" "diff diff" "parts parts"; border-top: 0; padding-top: 0; }
.permtool-group + .permtool-group { margin-top: 8px; }
/* The fold names how many differ; it clears the control floor with the
   negative block margin the ⋯ uses, so its line keeps the words' height. */
.permsec-toggle {
  grid-area: tog; justify-self: end; align-self: center;
  display: inline-flex; align-items: center; gap: 6px; min-height: var(--ctl-h); min-width: var(--ctl-h);
  margin-block: calc((18px - var(--ctl-h)) / 2); padding: 0; border: 0; background: none;
  color: var(--ink-soft); font: inherit; font-size: 13px; text-align: right; cursor: pointer; white-space: nowrap;
}
.permsec-n:empty { display: none; }
.permsec-toggle .disclosure-caret { display: inline-block; color: var(--ink-mute); transition: transform var(--t) var(--ease); }
/* The sentence at the top reads the group back as it stands. */
.permsum {
  margin: 0; padding: 12px 14px; border-radius: var(--radius-sm); background: var(--sunk);
  font-size: 13.5px; line-height: 1.5; color: var(--ink);
}
.permpreset-note::after {
  content: ""; width: 6px; height: 6px; margin-left: 2px; flex: 0 0 auto;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg); transition: transform var(--t) var(--ease);
}
.permpreset-note[aria-expanded="true"]::after { transform: translateY(1px) rotate(-135deg); }
/* What separates the panel from the role it is nearest. */
.permchanges { margin: 8px 0 0; font-size: 12.5px; line-height: 1.45; color: var(--ink-soft); }
/* Every part select under a fold is one width and ends on one right edge, so
   the levels read down a single column (2026-09-24). */
.permpart .select { width: 200px; flex: 0 0 auto; }
.is-narrow .permpart .select { width: 160px; }
.permpart-name { min-width: 0; font-size: 13.5px; color: var(--ink-soft); }
.permcmp-sec { min-width: 0; font-size: 13.5px; color: var(--ink); }
.permcmp-cell { display: flex; flex-direction: column; min-width: 0; font-size: 13px; }
.permcmp-cell b { font-weight: 500; color: var(--ink); }
.permcmp-cell.is-off b { font-weight: 400; color: var(--ink-mute); }
.permcmp-cell small { font-size: 12px; color: var(--ink-mute); }
.is-narrow .permcmp-head { display: none; }
.is-narrow .permcmp-row { grid-template-columns: minmax(0, 1fr); row-gap: 4px; }
.is-narrow .permcmp-sec { font-weight: 600; }
.is-narrow .permcmp-cell { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 12px; align-items: baseline; }
.is-narrow .permcmp-cell::before { content: attr(data-g); color: var(--ink-soft); }
.team-act { text-align: right; }
.select-sm { min-height: var(--ctl-h-sm); height: var(--ctl-h-sm); min-width: 0; padding-right: 28px; font-size: 13px; width: 100%; }

/* ---------- Access ----------
   What a person may not do is not drawn. The database refuses it anyway;
   this stops the screen offering what will be refused. */
/* A destructive control names the section it destroys in, and is drawn only
   where that section is managed.

   This used to be one `no-remove` class over a list of ids plus
   `.kmenu-item[data-a="del"].is-danger:not([data-soft])`, which is a rule
   that says "any danger item that is not a soft remove" — so one switch
   covered clients, contacts, letters, rate card lines, short links, creators
   and content sets alike, and granting it so a group could delete a content
   set also let them delete a client. The section is on the control now
   (`data-need="review:manage"`), so the gate is per section and the selector
   no longer has to guess from what a control is *not*. */
/* A part names its section and itself (`data-need="clients.billing:work"`),
   and the class hyphenates the key, since a dot is not a class token. The
   list is every section and part the console has at every level it gates,
   written out rather than generated, so a control that names a part nobody
   defined is a control that is always drawn and therefore seen. */
body.no-manage-clients [data-need="clients:manage"],
body.no-work-clients [data-need="clients:work"],
body.no-manage-whatsapp [data-need="whatsapp:manage"],
body.no-work-whatsapp [data-need="whatsapp:work"],
body.no-view-whatsapp [data-need="whatsapp:view"],
body.no-manage-review [data-need="review:manage"],
body.no-work-review [data-need="review:work"],
body.no-manage-campaigns [data-need="campaigns:manage"],
body.no-work-campaigns [data-need="campaigns:work"],
body.no-manage-links [data-need="links:manage"],
body.no-work-links [data-need="links:work"],
body.no-manage-register [data-need="register:manage"],
body.no-work-register [data-need="register:work"],
body.no-manage-services [data-need="services:manage"],
body.no-work-services [data-need="services:work"],
body.no-manage-team [data-need="team:manage"],
body.no-manage-clients-contacts [data-need="clients.contacts:manage"],
body.no-work-clients-contacts [data-need="clients.contacts:work"],
body.no-view-clients-contacts [data-need="clients.contacts:view"],
body.no-manage-clients-billing [data-need="clients.billing:manage"],
body.no-work-clients-billing [data-need="clients.billing:work"],
body.no-view-clients-billing [data-need="clients.billing:view"],
body.no-manage-clients-services [data-need="clients.services:manage"],
body.no-work-clients-services [data-need="clients.services:work"],
body.no-view-clients-services [data-need="clients.services:view"],
body.no-manage-clients-documents [data-need="clients.documents:manage"],
body.no-work-clients-documents [data-need="clients.documents:work"],
body.no-view-clients-documents [data-need="clients.documents:view"],
body.no-manage-clients-requests [data-need="clients.requests:manage"],
body.no-work-clients-requests [data-need="clients.requests:work"],
body.no-view-clients-requests [data-need="clients.requests:view"],
body.no-manage-clients-calls [data-need="clients.calls:manage"],
body.no-work-clients-calls [data-need="clients.calls:work"],
body.no-work-clients-leads [data-need="clients.leads:work"],
body.no-view-clients-leads [data-need="clients.leads:view"],
body.no-work-clients-past [data-need="clients.past:work"],
body.no-view-clients-past [data-need="clients.past:view"],
body.no-view-clients-calls [data-need="clients.calls:view"],
body.no-manage-clients-quotes [data-need="clients.quotes:manage"],
body.no-work-clients-quotes [data-need="clients.quotes:work"],
body.no-view-clients-quotes [data-need="clients.quotes:view"],
body.no-manage-clients-brand [data-need="clients.brand:manage"],
body.no-work-clients-brand [data-need="clients.brand:work"],
body.no-view-clients-brand [data-need="clients.brand:view"],
body.no-manage-clients-checklists [data-need="clients.checklists:manage"],
body.no-work-clients-checklists [data-need="clients.checklists:work"],
body.no-view-clients-checklists [data-need="clients.checklists:view"],
body.no-manage-reports [data-need="reports:manage"],
body.no-work-reports [data-need="reports:work"],
body.no-view-reports [data-need="reports:view"],
body.no-manage-review-sets [data-need="review.sets:manage"],
body.no-work-review-sets [data-need="review.sets:work"],
body.no-view-review-sets [data-need="review.sets:view"],
body.no-manage-review-settings [data-need="review.settings:manage"],
body.no-work-review-settings [data-need="review.settings:work"],
body.no-view-review-settings [data-need="review.settings:view"],
body.no-manage-campaigns-campaigns [data-need="campaigns.campaigns:manage"],
body.no-work-campaigns-campaigns [data-need="campaigns.campaigns:work"],
body.no-view-campaigns-campaigns [data-need="campaigns.campaigns:view"],
body.no-manage-campaigns-creators [data-need="campaigns.creators:manage"],
body.no-work-campaigns-creators [data-need="campaigns.creators:work"],
body.no-view-campaigns-creators [data-need="campaigns.creators:view"],
body.no-manage-campaigns-finance [data-need="campaigns.finance:manage"],
body.no-work-campaigns-finance [data-need="campaigns.finance:work"],
body.no-view-campaigns-finance [data-need="campaigns.finance:view"],
body.no-manage-register-documents [data-need="register.documents:manage"],
body.no-work-register-documents [data-need="register.documents:work"],
body.no-view-register-documents [data-need="register.documents:view"],
body.no-manage-register-hr [data-need="register.hr:manage"],
body.no-work-register-hr [data-need="register.hr:work"],
body.no-view-register-hr [data-need="register.hr:view"],
body.no-work-register-types [data-need="register.types:work"],
body.no-manage-suppliers [data-need="suppliers:manage"],
body.no-work-suppliers [data-need="suppliers:work"],
body.no-view-suppliers [data-need="suppliers:view"],
/* The Activity record is read a tab at a time, so each tab is a part and
   draws only where that part is readable. The database decides which rows
   arrive (`activity_section()` and the read policy), so a tab a group cannot
   open would be a tab over an empty list. */
body.no-view-activity-clients [data-need="activity.clients:view"],
body.no-view-activity-whatsapp [data-need="activity.whatsapp:view"],
body.no-view-activity-ops [data-need="activity.ops:view"],
body.no-view-activity-team [data-need="activity.team:view"],
body.no-view-activity-review [data-need="activity.review:view"],
body.no-view-activity-campaigns [data-need="activity.campaigns:view"],
body.no-view-activity-links [data-need="activity.links:view"],
body.no-view-activity-register [data-need="activity.register:view"],
body.no-view-activity-reports [data-need="activity.reports:view"],
body.no-view-activity-handbook [data-need="activity.handbook:view"],
body.no-view-activity-services [data-need="activity.services:view"],
body.no-view-team-performance [data-need="team.performance:view"],
body.no-work-team-performance [data-need="team.performance:work"],
/* Operations. The route is called My Work on the screen and `?s=work` in the
   address; the ladder's key is `ops`, which is the word the database uses, so
   the two are mapped once in `sectionAllowed()` and never spelled twice. */
body.no-manage-ops [data-need="ops:manage"],
body.no-work-ops [data-need="ops:work"],
body.no-view-ops [data-need="ops:view"],
body.no-view-ops-all [data-need="ops.all:view"],
body.no-view-ops-reports [data-need="ops.reports:view"],
body.no-work-ops-workflows [data-need="ops.workflows:work"],
body.no-manage-ops-time [data-need="ops.time:manage"] { display: none !important; }
@media (pointer: coarse) {
  /* The whole row is the label, so the target is already the row's height;
     the box itself grows so a finger has something to aim at. */
  .checkrow input { width: 22px; height: 22px; }
}
/* A phone takes two lines a row: who or which group with the ⋯ beside it,
   then the state or what the group opens underneath. */
@media (max-width: 640px) {
  .team-row > .team-act, .group-row > .team-act { grid-column: 2; grid-row: 1; align-items: start; }
  .team-act { display: flex; gap: 8px; margin-top: 4px; }
  .team-act { --ctl-h-sm: var(--ctl-h); }
  .team-act .btn { flex: 1 1 0; }
}

/* =========================================================================
   MOTION
   One place, because motion drifts exactly the way colour does: nineteen
   transitions had been written at their point of use in nine durations and
   three curves, so nothing on one screen moved like anything else. The
   durations and curves are tokens in :root; what follows is where they apply.
   ========================================================================= */

/* A press is felt, not just seen. Every control gives the same small give
   under the finger, on the fast curve, so the whole portal answers alike. */
.btn:active:not([disabled]),
.iconbtn:active:not([disabled]),
.kmenu-btn:active,
.kmenu-item:active,
.navitem:active,
.tab:active,
.acttab:active,
.crow-tick:active:not([disabled]),
.crow-backup:active,
.langtoggle:active,
.plink:active,
.state-select:active { transform: scale(.97); }
.kmenu-item, .navitem, .tab, .acttab, .plink, .langtoggle, .crow-backup, .state-select {
  transition: background var(--t) var(--ease-out), color var(--t) var(--ease-out),
              border-color var(--t) var(--ease-out), transform var(--t-fast) var(--ease-out);
}
/* A full width control on a phone is a slab, and a slab that shrinks reads as
   a wobble rather than a press. It dims instead. */
@media (max-width: 640px) {
  .viewhead:not(:has(.btn:not([hidden]) ~ .btn:not([hidden]))) .btn:active,
  .team-act .btn:active { transform: none; opacity: .72; }
}

/* A surface arrives; it does not blink into place. The menu comes from its
   button, the sheet from under the scrim, and on a phone the sheet comes up
   from the floor it is anchored to. */
@keyframes mk-menu-in { from { opacity: 0; transform: translateY(-5px) scale(.97); } }
@keyframes mk-scrim-in { from { opacity: 0; } }
@keyframes mk-sheet-in { from { opacity: 0; transform: translateY(10px) scale(.985); } }
@keyframes mk-sheet-up { from { transform: translateY(100%); } }
.kmenu { transform-origin: top right; animation: mk-menu-in var(--t) var(--ease-out); }
.sheet { animation: mk-scrim-in var(--t) var(--ease-out); }
.sheet-card { animation: mk-sheet-in var(--t-slow) var(--ease); }
@media (max-width: 640px) {
  .sheet-card { animation: mk-sheet-up var(--t-slow) var(--ease); }
}
/* The Undo bar is an answer to something that just happened, so it slides in
   under the row it belongs to rather than appearing over it. */
@keyframes mk-undo-in { from { opacity: 0; transform: translateY(-6px); } }
.undobar { animation: mk-undo-in var(--t) var(--ease-out); }

/* Nothing moves for a reader who has asked for stillness. This replaces three
   scattered rules that only ever quietened two carets, while DESIGN.md said
   the whole portal obeyed it. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
  .btn:active, .iconbtn:active, .kmenu-item:active, .navitem:active, .tab:active,
  .acttab:active, .crow-tick:active, .crow-backup:active, .langtoggle:active,
  .plink:active, .state-select:active, .pbox:active { transform: none; }
}

/* =========================================================================
   CREATOR PORTAL  (/creator/)
   A creator's own page: their bookings, and where they hand work in. Built
   from the components the client pages already use, so nothing here invents
   a shape. Only what the client pages had no need for is below.
   ========================================================================= */

.booking-brand { font-size: 12.5px; color: var(--ink-mute); margin-top: 2px; }
.booking-next { font-size: 13px; color: var(--ink-soft); margin-top: 10px; }
/* Where a creator's booking stands, in ink above its facts, where the
   hand-in stood (the client pages audit, CR-1, 2026-10-08). */
.booking-next.cx-status { margin: 12px 0 0; font-size: 14px; color: var(--ink); }
/* The head's Request change: which change, and the confirmed line it is
   about (PO-1). */
#reqSheet .req-pick { margin: 0 0 12px; }
#reqSheet .sheet-body > .facts { margin: 8px 0 16px; }
/* A client page's line of parts holds each short part whole (js/portal.js
   `parts`), so a phone wraps between them: never "5 Oct | 2026". */
.cp-panes .nb { white-space: nowrap; }
/* A letter's or a report's one act, Download, in the row (the client pages
   audit, PORTAL-3, 2026-10-14): its own 116px track at a desk; on a phone
   the name and its state on the first line, the figure and Download on the
   second, Download ending at the card's edge. */
.doc-row.is-file { grid-template-columns: minmax(0, 1fr) 130px var(--state-w) 116px; }
.doc-dl { display: flex; justify-content: flex-end; min-width: 0; }
.is-narrow .doc-row.is-file { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name state" "amt act"; align-items: center; }
.is-narrow .doc-row.is-file > .doc-dl { grid-area: act; }

/* The brief, and the changes asked for, read as quoted matter rather than as
   more of the card: an inset ground and a rule down the side. */
.booking-brief, .booking-pay, .booking-files, .booking-deliver {
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line-soft);
}
.booking-brief p { font-size: 13.5px; line-height: 1.55; color: var(--ink-soft); margin-top: 4px; }
.booking-brief.is-warn p { color: var(--ink); }
.booking-pay p { font-size: 13.5px; color: var(--ink-soft); margin: 4px 0 10px; }

/* What has been handed in, as thumbnails rather than a list of file names: a
   creator checking they sent the right cut should not have to read. */
.filegrid { display: grid; gap: 8px; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); margin-top: 8px; }
.filecard {
  position: relative; border: 1px solid var(--line); border-radius: var(--radius-sm);
  overflow: hidden; background: var(--sunk); aspect-ratio: 1; display: flex;
  flex-direction: column; align-items: center; justify-content: center; gap: 4px; padding: 6px;
}
.filecard img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1; }
/* The kind sits under the thumbnail and shows through when the image cannot
   load, so a failed preview reads as the file it is rather than as a broken
   image mark telling a creator their work is gone. */
.filecard-kind {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-size: 12px; font-weight: 600; color: var(--ink-mute);
}
.filecard-name {
  position: relative; z-index: 2; align-self: stretch; margin-top: auto; font-size: 11px;
  color: var(--ink-soft); background: var(--veil); border-radius: 4px; padding: 2px 4px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Anything a person can attach, a person can remove. `--ctl-h`, not the small
   control's height: this is an icon button, and the floor for one is 38px at a
   desk and 44 under a finger whatever the glyph inside it measures. It sat at
   32 for months on a card nothing walked. */
.filecard-x {
  position: absolute; top: 4px; right: 4px; z-index: 2;
  width: var(--ctl-h); height: var(--ctl-h); border-radius: 50%;
  border: 1px solid var(--line); background: var(--card); color: var(--ink-soft);
  font-size: 15px; line-height: 1; display: grid; place-items: center;
  transition: background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.filecard-x:hover { background: var(--err-bg); color: var(--err); border-color: var(--err-line); }
.filecard-x:active { transform: scale(.94); }

@keyframes armin { from { opacity: 0; } to { opacity: 1; } }
.filearm-acts .btn { min-width: 0; }
.filepin-row .filearm {
  position: static; flex-direction: row; justify-content: flex-end;
  gap: 8px; padding: 0 0 0 8px; background: none; backdrop-filter: none;
  text-align: right; flex: 1 1 auto; min-width: 0;
}
.filepin-row .filearm-ask { max-width: none; font-size: 12px; color: var(--ink-soft); }
.filepin-row.is-arming .filepin-open { flex: 0 1 auto; }

.booking-deliver .field-label { display: block; margin: 12px 0 8px; }
.booking-deliver .input { width: 100%; display: block; }
/* The hand-in block's steps: 8 from a label to its field, 12 between
   blocks, 16 before Submit (CR-3). */
.booking-deliver .kactions { margin-top: 16px; }
/* Post and results: one block a placement, a hairline between two; the
   three figures side by side under the period they are counted over. */
.post-entry + .post-entry { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--line-soft); }
.post-entry-head { margin-top: 8px; }
.post-entry-head b { font-size: 14px; font-weight: 600; color: var(--ink); }
.post-entry-period { font-size: 13px; color: var(--ink-soft); margin: 12px 0 0; }
.post-entry-link { font-size: 13px; margin: 6px 0 0; overflow-wrap: anywhere; }
.post-nums-in { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 12px; }
.post-nums { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 12px; margin: 10px 0 0; }
.post-nums dt { font-size: 12px; font-weight: 600; color: var(--ink-mute); }
.post-nums dd { margin: 2px 0 0; font-size: 14px; color: var(--ink); font-variant-numeric: tabular-nums; }
.booking-posts .msg:empty { display: none; }
.post-entry-day { font-size: 13px; color: var(--ink-soft); margin: 6px 0 0; }
.post-entry-day[hidden] { display: none; }
.booking-posts > .kactions { margin-top: 16px; }
/* What the team released, in the sheet the client decides in: the creator's
   own files and the caption that goes out with them. The caption is read, not
   typed, so it is text on the sunk ground and not a field. */
.draft-caption {
  margin: 0; white-space: pre-wrap; font-size: 14px; line-height: 1.55;
  color: var(--ink); background: var(--sunk); border: 0;
  border-radius: var(--radius-sm); padding: 10px 12px;
}

/* The same progress the console draws on Content Review, so a creator sending
   a 300 MB video is told what is moving and how far, and the label can say it
   is saving once every byte is out and S3 has still to answer. */
.booking-deliver .progress { margin: 12px 0 0; }
.booking-deliver .progress-head { gap: 12px; }
.booking-deliver .progress-head span:first-child {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The code field is the whole sign-in, so it is set like one thing to type:
   wide letter spacing, the monospace figures, and upper case as you go. */
#codeRow { display: flex; gap: 10px; margin-top: 14px; }
#codeInput {
  flex: 1 1 auto; min-width: 0; text-align: center; letter-spacing: .18em;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; text-transform: uppercase;
}

/* What the creator handed in, on the campaign card. The same thumbnails their
   own page draws, because it is the same files and a reviewer should recognise
   what they were sent. */
.handedin { margin: 2px 0 12px; }
/* 92px was a thumbnail grid, which is the right size for a list of documents
   and the wrong one for the screen where somebody watches a creator's work
   before a client does. A 9:16 player needs a column it can be read in, so the
   track is wide enough for one and the row still fits two side by side in the
   pane at 1280. */
.handedin .filegrid { grid-template-columns: repeat(auto-fill, minmax(148px, 200px)); }
/* The card's own ground is the letterbox's, not `--sunk`: the player paints
   black over the whole card, so a light ground is a flash before the first
   frame arrives and a light ground behind a white file name the moment it
   does not. The name below is white, and what it is white on has to be
   something a reader — and the contrast check — can see. */
.filecard.filecard-video {
  width: auto; aspect-ratio: 9 / 16; padding-bottom: 0; background: #000;
}
.filecard.filecard-video video {
  position: absolute; inset: 0; z-index: 1;
  width: 100%; height: 100%; object-fit: contain; background: #000;
}
/* The name sits over the foot of the player, so it does not cost a row and
   does not sit under the scrubber a finger reaches for. */
.filecard.filecard-video .filecard-name {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  padding: 16px 8px 6px; pointer-events: none;
  background: linear-gradient(to top, rgba(0, 0, 0, .72), transparent);
  color: #fff;
}
.handedin .kfield { margin-top: 10px; }

/* Copied, said on the button that did it. */
.kcard-link.is-done { background: var(--ok-bg); color: var(--ok); }

.readfield:focus {
  border-color: transparent; background: var(--sunk);
}
/* An access code is a string of characters somebody reads out, so it is set
   the way this portal sets every other token. */
.readfield.is-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: .08em; color: var(--ink);
}

/* An instruction, for as long as it is still needed.
   A line that explains a step is right the first few times somebody meets it
   and furniture ever after, which is why this portal carries no explanatory
   copy. It opens by itself while the step is new, retires behind its own mark
   once it has been read three times, and is a button rather than a `title`,
   because a hover tooltip cannot be reached on a phone. */
.hintline { display: flex; align-items: flex-start; gap: 8px; margin: 2px 0 10px; }
/* The mark is an 18px ring; the target is the control floor (38px, 44 under a
   finger), drawn transparent around it with negative margins so the line
   keeps its height. It was the 18px ring alone, which the matrix caught the
   day the mark reached the console's command bars. */
.hintbtn {
  position: relative; isolation: isolate; flex: 0 0 auto;
  width: var(--ctl-h); height: var(--ctl-h);
  margin: calc((20px - var(--ctl-h)) / 2) 0 calc((20px - var(--ctl-h)) / 2) calc((18px - var(--ctl-h)) / 2);
  padding: 0; border: 0; background: none; color: var(--ink-mute);
  font-size: 11px; font-weight: 600; line-height: 1; display: grid; place-items: center; cursor: pointer;
  transition: color var(--t-fast) var(--ease-out);
}
.hintbtn::before {
  content: ""; position: absolute; z-index: -1; left: 50%; top: 50%; width: 18px; height: 18px;
  margin: -9px 0 0 -9px; border-radius: 50%; border: 1px solid var(--line-ctl); background: var(--card);
  transition: background var(--t-fast) var(--ease-out);
}
@media (hover: hover) {
  .hintbtn:hover { color: var(--ink); }
  .hintbtn:hover::before { background: var(--sunk); }
}
.hinttext { font-size: 12.5px; line-height: 1.5; color: var(--ink-mute); margin: 0; }
/* What a section is for, under its command bar, while the team is new to it:
   one sentence in the body face with Hide at its end, on the stack's 12px
   step above and below; the ? in the bar brings it back. It was the 18px
   ring with 12.5px mute text against the table's edge, which the user sent
   back on 2026-09-22 as badly sized and spaced. */
/* Retired 2026-09-22 as a block under the command bar: it could only be
   reached from a route's directory, so on a record — where the bar is off the
   screen — the control that explains the section did nothing. It is a surface
   that comes from the control instead, in the panel family this console
   already uses, placed by `ADspaceMenu.place(btn, pop, 'left')` because the
   title sits at the start of its line and not at the end of a row. */
.aboutpop {
  display: block; width: min(340px, calc(100vw - 16px)); padding: 14px 16px 10px;
}
.aboutpop-text {
  margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft);
}
/* A deadline is a date, not an action: a neutral box while it is still
   ahead, and red words only once it has gone (2026-10-07: amber is a dot,
   never a box). */
.due-countdown {
  margin: 10px 0 0; padding: 8px 10px; border-radius: var(--radius-sm);
  background: var(--sunk); color: var(--ink); font-size: 13px; font-weight: 600;
}
.due-countdown.is-late { background: var(--err-bg); color: var(--err); }
/* A submitted file is a receipt, not a thing to handle: one line, the mark
   that says it is an attachment, the name and how big it was. */
.filepins { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.filepin {
  display: flex; align-items: center; gap: 9px; min-height: var(--ctl-h);
  padding: 0 12px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--card); color: var(--ink); text-decoration: none;
  transition: border-color var(--t) var(--ease-out);
}
@media (hover: hover) { .filepin:hover { border-color: var(--ink); } }
.filepin:active { transform: scale(.97); }
.filepin svg { width: 16px; height: 16px; flex: 0 0 auto; color: var(--ink-mute); }
.filepin-name { flex: 1 1 auto; min-width: 0; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filepin-size { flex: 0 0 auto; font-size: 12px; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.filepin-open {
  display: flex; align-items: center; gap: 9px; flex: 1 1 auto; min-width: 0;
  color: inherit; text-decoration: none; min-height: var(--ctl-h);
}
.filepin-x {
  flex: 0 0 auto; width: var(--ctl-h-sm); height: var(--ctl-h-sm); border-radius: 50%;
  border: 1px solid transparent; background: none; color: var(--ink-mute);
  font-size: 15px; line-height: 1; display: grid; place-items: center;
  transition: background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
@media (hover: hover) {
  .filepin-x:hover { background: var(--err-bg); color: var(--err); border-color: var(--err-line); }
}
/* After the client's approval the file's control hides it from the client
   (audit S6): the struck eye, never the red of a removal. Hidden files wait
   under their own fold with Restore, and Delete in the ⋯ for the part. */
.filecard-x.is-hide svg, .filepin-x.is-hide svg { width: 15px; height: 15px; }
@media (hover: hover) {
  .filecard-x.is-hide:hover, .filepin-x.is-hide:hover { background: var(--sunk); color: var(--ink); border-color: var(--line); }
}
.handedin .filehidden { margin-top: 12px; }
.filehidden > .filepins { margin-top: 8px; }
.filepin-row.is-hidden { gap: 8px; }
.filepin-row.is-hidden .filepin-name { color: var(--ink-soft); }

/* How the job went. Five targets, nothing to type, and the filled state is
   the accent this portal keeps for something that is true rather than
   something to press. */
.booking-rate { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
.stars { display: flex; gap: 2px; margin-top: 6px; }
.star {
  width: var(--ctl-h); height: var(--ctl-h); padding: 0; border: 0; background: none;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--line-ctl); cursor: pointer;
  transition: color var(--t) var(--ease-out), transform var(--t-fast) var(--ease-out);
}
.star svg { width: 24px; height: 24px; fill: currentColor; }
.star.is-on { color: var(--ink); }
/* A pointer over the third star fills the first three, as a rating reads
   (the user, 2026-10-08: it lit one star at a time). */
@media (hover: hover) {
  .stars:hover .star { color: var(--ink); }
  .stars .star:hover ~ .star { color: var(--line-ctl); }
}
.star:active { transform: scale(.9); }
.booking-rate .msg:empty { display: none; }

.booking-caption { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
.booking-caption p, .client-draft-preview .draft-caption p { margin: 4px 0 0; line-height: 1.55; white-space: normal; }
/* Quoted matter, as the brief above it is: the caption was body size under
   a 13.5px brief on the same card. */
.booking-caption p { font-size: 13.5px; color: var(--ink-soft); }
.client-draft-preview { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; margin-top: 12px; }
/* Text a person typed (a caption, a brief, a note, a place) may hold a link
   or a run of tags with no space in it. It breaks where it has to, inside its
   own box: on 27 Sept 2026 a creator's caption carrying a 120-character form
   link widened the draft, the card and the page on both client pages, so the
   bar and foot stopped short of the screen's edge and the player ran off the
   card. `anywhere`, not `break-word`: only `anywhere` lowers the width a grid
   track is sized to, which is what pushed the card out. */
.booking-caption p, .booking-brief p, .booking-facts dd, .draft-caption,
.approve-note, .booking-decided, .camphead-purpose, #campBrief {
  overflow-wrap: anywhere;
}
.client-draft-media {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 360px));
  gap: 10px; align-items: start;
}
.client-draft-media video, .client-draft-media img {
  width: min(100%, 360px); aspect-ratio: 9 / 16; object-fit: contain;
  background: #000; border-radius: var(--radius-sm);
}
/* A draft waiting on the client, at a desk: the file at the left, its
   caption and the decision beside it, so Approve is in the first screen with
   the video rather than under it (UX audit SELECTION-7, 2026-10-14). */
@media (min-width: 761px) {
  .draft-decide.has-media {
    display: grid; grid-template-columns: minmax(220px, 360px) minmax(0, 1fr);
    column-gap: 16px; row-gap: 12px; align-items: start; margin-top: 12px;
  }
  .draft-decide.has-media > .client-draft-preview { display: contents; }
  .draft-decide.has-media .client-draft-media { grid-column: 1; grid-row: 1 / span 4; grid-template-columns: minmax(0, 1fr); }
  .draft-decide.has-media .client-draft-media video, .draft-decide.has-media .client-draft-media img { width: 100%; }
  .draft-decide.has-media > .client-draft-preview > :not(.client-draft-media), .booking > .draft-decide.has-media > .approve { grid-column: 2; margin-top: 0; }
}
/* `.filecard-video` was styled here for a console player that was never
   written, so these two rules matched nothing for as long as they existed and
   the console kept drawing a square with MP4 on it. The player exists now and
   the rules that size it sit with the rest of `.filecard`, above. */
@media (max-width: 520px) {
  .client-draft-media { grid-template-columns: minmax(0, 1fr); }
  /* About three fifths of the screen, so Approve is in reach of the draft
     (2026-10-20, SEL-6); a picture keeps its shape inside it. */
  .client-draft-media video, .client-draft-media img { width: 100%; max-height: 60vh; max-height: 60dvh; object-fit: contain; }
}

/* The decision sits on the card it belongs to, so it takes the card's own
   gutter rather than the approval component's 16px inset, and the hairline is
   what separates the work above from the answer below. Everything else about
   it — the blue Approve, the quiet Request changes, the note that opens under
   them — is the component Content Review already uses. */
.booking > .approve, .booking > .draft-decide > .approve { padding: 14px 0 0; margin-top: 12px; }
.approve-state.is-err { color: var(--err); }
/* What the client decided, kept on the card once the step has moved on. The
   same line Content Review leaves under a post, so a decision is recorded the
   same way on both client pages. */
.booking-decided { margin: 12px 0 0; padding-top: 12px; border-top: 1px solid var(--line-soft); }
.booking > .approve-note { margin-top: 8px; }

/* ==========================================================================
   SOFT PRECISION PRIMITIVES
   Built for the routes being migrated one at a time. Nothing here restyles an
   existing component: a legacy screen keeps its own classes until its turn.
   ========================================================================== */

/* The command bar. One row: what you are looking for on the left, how you
   are cutting the list in the middle, the one action at the end. It replaces
   the section head's lone black slab, which on a phone stretched the full
   width above the records somebody came to read. */
.cmdbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0 0 12px;
}
/* It wraps at any width it has to, not only below the phone line: at 720 —
   which is what a 1440px window is at 200% zoom — the search, two filters, a
   count and the action came to eleven pixels more than the bar, and the page
   scrolled sideways. */
/* `width: 100%` is set on these controls elsewhere, so a select with
   `flex: 0 0 auto` took the whole bar and crushed the search box to nothing.
   Both ends are stated here. */
/* Bounded at both ends. With nothing beside it the box grew to the width of
   the console, which is a nine hundred pixel field for a client's name. */
.cmdbar-find { position: relative; flex: 1 1 240px; min-width: 190px; max-width: 420px; }
.cmdbar-find .input { width: 100%; padding-left: 34px; border-radius: var(--radius-ctl); }
/* The field sits in a box that clips it while it grows, so an outer focus
   ring came out as a square blue patch behind a round field. The ring is
   drawn inside the field instead: the blue edge, one pixel heavier. */
.cmdbar-find .input:focus { box-shadow: inset 0 0 0 1px var(--action); }
.cmdbar-find svg {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; stroke: var(--ink-mute); stroke-width: 1.8; fill: none;
  pointer-events: none;
}
/* 164px is measured against the longest option any command bar draws,
   `Waiting or blocked` at 157px with its padding and chevron. It was 172,
   which is eight pixels of slack every bar paid for and which put My Work's
   third select four pixels past the row at 1280, wrapping the one action
   onto a line of its own at the left margin. A bar's width is shared, so the
   fix is the shared value and not an exception for the crowded route. */
.cmdbar .select {
  flex: 0 0 auto; width: auto; min-width: 164px; max-width: 220px;
  border-radius: var(--radius-ctl);
}
.cmdbar .btn { flex: 0 0 auto; width: auto; border-radius: var(--radius-ctl); }
/* What the list holds and what to do about it: the count, the `?` and the
   route's one or two actions, as ONE element rather than three to five loose
   ones. The bar is a wrapping flex row of unlike things, and loose items wrap
   differently on every route — Services put the count and the `?` mid-row
   with the action stranded underneath, Documents crammed four things onto the
   last line, My Work pushed its one action onto a fourth row at the left
   margin. A group cannot be split by a wrap, so the composition is the same
   on all nine bars at every width, and it is stated in the markup rather than
   guessed at with auto margins and `:nth-of-type` counting. */
.cmdbar-end {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
  justify-content: flex-end; flex: 0 1 auto; margin-left: auto; min-width: 0;
  min-height: var(--ctl-h-sm); /* the bar's own row, whether or not an action stands in it */
}
.cmdbar-count:empty { display: none; }
@media (max-width: 640px) {
  /* Two to a row at equal halves, so two read as two and four read as four
     rather than as whatever was left over. A filter with no partner grows to
     the full width by itself, because it is alone on its line and every
     select may grow; nothing has to count them. */
  .cmdbar .select { flex: 1 1 calc(50% - 5px); max-width: none; }
  /* Never full width. An action stretched across a phone is the heaviest
     thing on the screen, sitting on top of the records somebody came to
     read; this portal's section heads have refused that for months. */
  .cmdbar .btn { flex: 0 0 auto; }
}

/* A bounded section, softly. One 1px hairline and a 14px corner, no shadow:
   a border and a shadow together is a thing pretending to float. */
.softpanel {
  background: var(--card); border: 1px solid var(--card-line);
  border-radius: var(--radius-panel); overflow: hidden;
}
.softpanel + .softpanel { margin-top: 12px; }

/* Loading, as the shape of what is coming rather than the word for it. */
.skel { padding: 4px 0; }
/* The console while its team row is being read: the shell and the rail are
   the console's own, the nav's names and the bar's controls are not drawn,
   and the body is the shape of what is coming. Without it the in-between was
   the shared page bar over an empty page, which is the client pages' own
   composition, so a refresh read for a second as somebody else's portal. */
.console.is-booting .sidebar-nav,
.console.is-booting .sidebar-foot,
.console.is-booting .console-head > * { visibility: hidden; }
.console.is-booting .console-body { display: none; }
.skel-row {
  height: 14px; margin: 14px 15px; border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--line-soft) 0%, var(--sunk) 50%, var(--line-soft) 100%);
  background-size: 200% 100%;
  animation: skel 1.4s var(--ease) infinite;
}
.skel-row:nth-child(2n) { width: 62%; }
.skel-row:nth-child(3n) { width: 78%; }
@keyframes skel { from { background-position: 200% 0; } to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .skel-row { animation: none; } }

/* Nothing there, and nothing there after a filter. A line and the way out,
   never a large dashed rectangle. */
.emptyline {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 18px 15px; font-size: 13.5px; color: var(--ink-mute);
}
.emptyline b { color: var(--ink); font-weight: 600; }
.emptyline .btn { flex: 0 0 auto; }

/* Something failed, said where it failed rather than where the page ends. */
.errline {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 16px 15px; font-size: 13.5px; color: var(--err);
  background: var(--err-bg); border-bottom: 1px solid var(--err-line);
}
.errline .btn { flex: 0 0 auto; }

/* The Short Links row. It was a bordered card per link, each holding the same
   four icons, so fifty links were fifty boxes and the slug — the one thing
   anybody scans for — had no column to sit in. */
/* Every track is stated, the last one included. An `auto` final track is
   sized by its own row, so the header — whose last cell is empty over the
   actions — sized it at nothing and the three columns before it slid 76px
   right of the rows beneath them. 124px is the three icon buttons the actions
   cell actually holds. */
/* Four columns, not five. A STATUS heading stood over a cell that is empty on
   every live row, which is nearly all of them, so the column read as a fault
   rather than as the accent rule working. The rate card had already settled
   this: where a state is the default for its list, the row says nothing while
   it holds and names the exception beside the name. `Paused` is a chip next to
   the slug now and the row fades, exactly as an inactive service does. */
.link-row, .crm-head.link-row {
  display: grid; align-items: center; gap: 0 12px;
  grid-template-columns: minmax(110px, 200px) minmax(0, 1.6fr) minmax(0, 1fr) calc(var(--ctl-h) * 4 + 12px);
  padding: 10px 15px; border-bottom: 1px solid var(--line-soft);
}
.link-row.is-off > :not(.link-act) { opacity: .55; }
@media (max-width: 640px) {
  /* Four fixed tracks, one a control (Copy, Open, QR codes, ⋯; LNK-2), so
     a namecard's Copy and Open stand under a link's and the ⋯ never wraps
     alone (ux-audit LNK-1, 2026-10-14; LNK-8, 2026-10-27). */
  .link-row > .link-act { display: grid; grid-template-columns: repeat(4, var(--ctl-h)); justify-content: end; justify-items: center; }
  .link-row > .link-label:empty { display: none; }
  .crm-head.link-row { display: none; }
}

.team-head.team-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) 96px 18px; }
.team-state { min-width: 0; }

@media (max-width: 640px) {
  .team-head.team-row { display: none; }
}

/* ---------- The client record as a workspace ----------
   Seven sections in one column meant Documents was a scroll away from the
   services it quotes and Billing a scroll away from the contact it names.
   Identity stays at the top, the panes carry the work, and the rail carries
   the facts that are true whichever pane you are in. */
/* The facts moved to the rail, so the head is the identity line and nothing
   else: its old 14px bottom margin left a band of empty panel under it. */
.rec-head { margin-bottom: 12px; }
.rec-head .msg:empty { display: none; }
#crmListMsg:empty { display: none; }
/* Identity, in two parts. Left: the mark, then who this is — the name with
   its state on one line and under them the facts that identify rather than
   describe. Right: the one ⋯ that acts on the record itself. The two parts
   centre on each other, so the control sits on the middle of the card and
   not on the name line: drawn on the name line it stood above the card's
   centre on every record that carried a meta line, and the head read as top
   heavy. Edit lives in the ⋯ with Delete, because a record is read a hundred
   times for every time it is edited, and a control drawn beside the stage on
   every record spent the head's second position on its rarest act. */
.rec-id {
  display: grid; align-items: center; gap: 14px;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "mark who ctl";
}
.rec-who { grid-area: who; min-width: 0; display: grid; gap: 4px; }
/* The name and its state share a line; a long name wraps under itself and
   the state follows it down, so nothing here ever pushes the ⋯ off the row. */
.rec-name { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; min-width: 0; }
.rec-name h2, .rec-name h1 {
  margin: 0; font-size: 19px; font-weight: 600; letter-spacing: -.02em;
  line-height: 1.3; min-width: 0; overflow-wrap: anywhere;
}
.rec-ctl .state-select, .rec-ctl .chip { flex: 0 0 auto; }
/* The head's state stands alone, in no column, so it is as wide as its
   longest stage (Proposal sent beside its dot), never cut to an ellipsis. */
.rec-ctl .state-select { width: auto; min-width: var(--state-w); }
/* Meta at 12px against a 19px name: the two sizes stand a golden ratio
   apart, which is what keeps the line reading as a note under the name and
   not as a second line of it. */
.rec-id-meta { margin: 0; font-size: 12px; color: var(--ink-mute); min-width: 0; }
.rec-id-meta[hidden] { display: none; }
.rec-ctl { grid-area: ctl; display: flex; align-items: center; gap: 8px; align-self: center; }
/* The shared .chip rule pins every chip to the top of its flex row, which is
   right in a card head where it sits beside a name; beside a 38px ⋯ (44px
   under a finger) it put the campaign's state 11px above the control's
   centre, so the two things on the right of the head were on two lines. */
.rec-ctl .chip { align-self: center; }
/* The client's own logo where we hold one, their initials where we do not.
   The disc stays light in both themes for the same reason the review mockups
   do: the logo is the client's artwork and is not ours to invert. */
.rec-mark {
  grid-area: mark; width: 46px; height: 46px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; overflow: hidden;
  background: var(--line-soft); color: var(--ink-soft);
  font-size: 15px; font-weight: 600; letter-spacing: -.01em;
}
.rec-mark.has-logo { background: #fff; border: 1px solid var(--line); }
.rec-mark img { width: 100%; height: 100%; object-fit: contain; }
/* On a phone the same two parts, closer: the mark takes less of the row so
   the name keeps its width, and the ⋯ stays where the thumb is. Keyed on the
   console's own width, like every other rule about something inside it. */
/* On a phone the two parts become two rows: the mark at the top left and
   the state with the ⋯ at the top right, then who this is under them on the
   card's own margin, full width. Beside a 38px mark the name had 130px and
   a campaign's state wrapped under it while its purpose line, drawn outside
   the grid, sat on a different margin from the name and the meta: three
   lines on two margins, which is what reads as unaligned. */
.is-narrow .rec-id {
  gap: 10px 12px; align-items: center;
  grid-template-areas: "mark . ctl" "who who who";
}
.is-narrow .rec-mark { width: 38px; height: 38px; font-size: 13px; }
.is-narrow .rec-name { gap: 6px 10px; }
.is-narrow .rec-who { gap: 4px; }
.lhist-row:first-child { border-top: 0; padding-top: 0; }
.lhist-who { flex: 1 1 auto; min-width: 0; font-size: 12.5px; color: var(--ink-mute); }
.lhist-diff { margin: 8px 0 0; padding: 0; list-style: none; display: grid; gap: 6px; font-size: 13px; }
.lhist-diff li { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; min-width: 0; color: var(--ink-soft); }
.lhist-diff .plink { min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
/* The creator's head: who they are and one control, no mark (a creator has
   no artwork on file), and their profile links under a hairline. On a phone
   the control keeps its line beside the name rather than taking a row. */
.crhead { margin-top: 24px; }
.rec-id.rec-id-solo { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "who ctl"; }
.rec-name h1 {
  margin: 0; font-family: var(--font-head); font-size: 19px; font-weight: 600; letter-spacing: -.02em;
  line-height: 1.3; min-width: 0; overflow-wrap: anywhere;
}
.crlinks {
  display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; padding-top: 14px;
  border-top: 1px solid var(--line-soft);
}
.crlinks-none { margin: 0; font-size: 13.5px; color: var(--ink-mute); }
.crhead > .msg { margin-top: 10px; }
.input[aria-invalid="true"] { border-color: var(--err); }
@media (max-width: 640px) {
  /* The chrome's "Forget this device" gives up its word on a phone, the way
     the command bar's primary does, so the name beside the mark is not cut
     to "S..". The word stays as the button's name. */
  #signOutBtn > span { display: none; }
  #cpHead .rec-id {
    gap: 10px 12px; align-items: center;
    grid-template-areas: "mark . ctl" "who who who";
  }
  #cpHead .rec-mark { width: 38px; height: 38px; font-size: 13px; }
  #cpHead .rec-name { gap: 6px 10px; }
  #cpHead .rec-who { gap: 4px; }
  /* Two rows on a phone, so the action gives up its word beside the state the
     way the command bar's primary does rather than wrapping the pair. */
  #cpHead .rec-ctl { flex-wrap: wrap; justify-content: flex-end; }
}

/* ---------- The record's rail ----------
   One block per question, each with its own title, divided by the same
   hairline the Overview uses. A block leaves entirely when the data behind it
   is not there, so the rail never carries a heading over nothing. */
.railblock {
  padding-bottom: 16px; margin: 0 0 16px; border-bottom: 1px solid var(--line);
}
/* Set in `paintRail`, because `:last-child` counts a hidden sibling and every
   one of these blocks leaves when its data is not there. */
.railblock.is-last { border-bottom: 0; margin-bottom: 0; padding-bottom: 0; }
.railblock[hidden] { display: none; }
/* The rail's titles are the same size as the pane's section titles, because
   they head the same kind of thing: a block of the record. As 11px eyebrows
   the rail read as a footnote to the pane rather than as its other half. */
/* A rail block's title with its way to the whole (Recent activity's View
   all), the control's target overflowing the title's line evenly. */
.railhead { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 0 0 8px; }
.railtitle {
  margin: 0 0 8px; font-size: 15px; font-weight: 600; letter-spacing: -.01em; color: var(--ink);
}
/* One glyph per row, so a list of dates or events can be scanned by shape
   before it is read: a calendar is a date, a file is a letter, a person is a
   contact. Neutral, never coloured, because the word beside it is the fact. */
.railico { width: 16px; height: 16px; flex: 0 0 auto; color: var(--ink-mute); stroke-width: 1.8; }
/* Where the record stands: the stage as the chip every list draws, and how
   long it has stood there, in one line. */
/* What stands between this record and Active. Red, because it is a refusal
   and not a caution: amber is for something pending, and this is something
   the database will not allow until the fields are filled. The whole block
   is the control that opens Billing, where they are. */
.railgate {
  display: grid; align-items: center; gap: 2px 10px; width: 100%; text-align: left;
  grid-template-columns: auto minmax(0, 1fr) auto;
  margin: 0 0 16px; padding: 12px 13px; font: inherit; cursor: pointer;
  border: 1px solid var(--err-line); border-radius: var(--radius-sm);
  background: var(--err-bg); color: var(--ink);
}
.railgate[hidden] { display: none; }
.railgate > svg:first-child { grid-row: 1 / -1; width: 18px; height: 18px; color: var(--err); stroke-width: 1.8; align-self: start; margin-top: 1px; }
.railgate b { grid-column: 2; font-size: 13.5px; font-weight: 600; color: var(--err); }
.railgate > span { grid-column: 2; font-size: 12.5px; color: var(--ink-soft); }
.railgate > .ovgo-chev { grid-column: 3; grid-row: 1 / -1; width: 15px; height: 15px; color: var(--err); }
/* Without a Billing pane to open there is nothing to press. */
.railgate:not([data-go]) { cursor: default; }
/* A restricted account (2026-10-10): the same red block, its Clear at the
   right edge in place of the chevron. */
.railgate.is-restricted { cursor: default; }
.railgate > .railgate-act { grid-column: 3; grid-row: 1 / -1; flex: 0 0 auto; }
.railgate:not([data-go]) > .ovgo-chev { display: none; }
/* A next action is a thing to go and do, so the whole block is the control
   that opens where it is done. */
.railnext, .railmiss {
  display: grid; align-items: center; gap: 2px 10px; width: 100%; text-align: left;
  grid-template-columns: auto minmax(0, 1fr) auto;
  /* A row that opens something is a white row with no edge, as every card
     is (the user, 2026-09-28, replacing the faint edge of 2026-09-22); its
     chevron and its hover say it opens. The billing gate keeps its red. */
  border: 1px solid var(--card-line); border-radius: var(--radius-sm);
  background: var(--card); padding: 10px 11px; min-height: var(--ctl-h);
  font-size: 13.5px; color: var(--ink);
}
@media (hover: hover) { .railnext:hover, .railmiss:hover { background: var(--sunk); } }
.railnext > .railico { grid-column: 1; grid-row: 1 / -1; align-self: start; margin-top: 2px; }
.railnext-text { grid-column: 2; min-width: 0; font-weight: 600; }
.railnext-when.is-late { color: var(--err); font-weight: 600; }
.railnext > .ovgo-chev, .railmiss > .ovgo-chev {
  grid-column: 3; grid-row: 1 / -1; width: 15px; height: 15px; color: var(--ink-mute);
}
/* How much of the record is filled in. The bar is never the only thing said:
   the line under it names what is still missing and opens it. */
.railpct { display: flex; align-items: baseline; gap: 8px; margin: 0 0 7px; font-size: 13px; }
.railpct b { font-weight: 600; }
.railpct span { margin-left: auto; color: var(--ink-mute); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.railbar-fill.is-done { background: var(--ok-solid); }

/* The campaign rail's own blocks. Money is right aligned on one grid so the
   total sits directly under the figures it is the sum of, which is the whole
   point of putting it there rather than in a card of its own. */
.railmoney { display: grid; gap: 8px; margin: 0; }
.railmoney > div { display: flex; align-items: baseline; gap: 12px; }
.railmoney dt { font-size: 13px; color: var(--ink-mute); }
.railmoney dd {
  margin: 0 0 0 auto; font-size: 13px; color: var(--ink);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* The one bold amount in the block, over the hairline that separates it from
   the figures it totals. */
.railmoney > .is-total {
  padding-top: 7px; border-top: 1px solid var(--line-soft);
}
.railmoney > .is-total dt, .railmoney > .is-total dd { font-weight: 600; color: var(--ink); }

.railfoot { margin: 7px 0 0; font-size: 12.5px; color: var(--ink-mute); }
.railwarn { margin: 7px 0 0; font-size: 12.5px; font-weight: 600; color: var(--ink); }

/* The campaign and task rails still list plain dates. Glyph, label, then the
   date on the right, tabular so a column of them lines up on the day. */
.raildates { display: grid; gap: 8px; margin: 0; }
.raildates > div { display: flex; align-items: center; gap: 10px; }
.raildates dt { font-size: 13px; color: var(--ink-mute); min-width: 0; }
.raildates dd {
  margin: 0 0 0 auto; font-size: 13px; color: var(--ink);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* An overdue date is the one anybody scans for: the late colour and the
   weight together, so the mark survives greyscale. */
.raildates dd.is-late { color: var(--err); font-weight: 600; }

/* Every date and every duration in one block: the stage journey above a rule
   and the dates the record holds below it.

   TWO tracks, not three. A name, a date and a duration are all `nowrap`, and
   three of them in a 365px rail measured at exactly 365px — no slack at all,
   so the first longer stage name or the first `3 days · Overdue` pushed the
   page sideways, which is what the walk caught. What each row is read for is
   the duration, so that ends the row on its own margin and the date rides
   quietly beside the name, where the pair can ellipsis if it ever has to. */
.tline { display: grid; gap: 10px; }
.tl-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: 4px 10px;
}
.tl-lead { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.tl-what {
  font-size: 13px; color: var(--ink-soft); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The stage the record is in now is the live one, so it carries the weight;
   the stages behind it are history and read quiet. */
.tl-row.is-now .tl-what { color: var(--ink); font-weight: 600; }
/* Overdue is the one thing anybody scans this block for: the late colour
   and the weight together, so the mark survives greyscale. */
.tl-span.is-late, .tl-when.is-late { color: var(--err); font-weight: 600; }
.tl-why-btn svg { width: 13px; height: 13px; flex: 0 0 auto; }
.tl-why-btn::before {
  content: ''; position: absolute; inset: -3px -6px; border-radius: var(--radius-ctl-sm);
}
@media (hover: hover) { .tl-why-btn:hover { color: var(--ink); } .tl-why-btn:hover::before { background: var(--sunk); z-index: -1; } }
/* Who did a report's step, and to whom, on its own line under the date
   (2026-10-06), so two names never push the row past its card. */
.rp-keydates .tl-lead { flex-wrap: wrap; row-gap: 2px; }
.tl-who {
  flex-basis: 100%; min-width: 0; font-size: 12.5px; color: var(--ink-mute);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A dated row is read, so its date carries the ink the stage rows give the
   duration; the stage rows' own dates stay mute beside the stage. */
.tl-date .tl-when { font-size: 13px; color: var(--ink); }
/* Client since is the one value here a person corrects, so the pen ends the
   row where a duration would and the value itself becomes the field. It takes
   the negative block margin the rate card's ⋯ takes, so a 38px target does
   not set the height of a 20px row. */
.tl-pen {
  justify-self: end; width: var(--ctl-h); height: var(--ctl-h);
  margin-block: calc((20px - var(--ctl-h)) / 2);
  border: 0; border-radius: var(--radius-sm); background: none; color: var(--ink-mute);
}
.tl-pen svg { width: 15px; height: 15px; }
@media (hover: hover) { .tl-pen:hover { background: var(--line-soft); color: var(--ink); } }
.tl-pen.is-saving { color: var(--action-ink); }
/* A stage row ends with its duration and the pen that corrects its date, so
   every pen on the block stands in one column with Client since's. */
.tl-end { display: inline-flex; align-items: center; gap: 2px; justify-self: end; min-width: 0; }
/* The field takes the value's place inside the row's own left cell, so the
   picker grows where the date was rather than in the duration's track. */
/* A value edited where it sits (js/ask.js; the user, 2026-10-04: a box
   dropped into a line is not how Apple or Aesop edit). The field takes the
   value's own type (set from the value), sits on a quiet tonal ground with
   no box line and no glyph, and is padded 6px and pulled back 6px, so the
   words stay where they were and the line keeps its height. A date is a
   date's width and opens the system's own picker under a finger. The focus
   ring stays. No screen sizes its own. */
.input.askfield {
  min-height: 0; height: auto; padding: 2px 6px; margin: -2px 0 -2px -6px;
  border-color: transparent; border-radius: 6px; background-color: var(--tonal);
}
.input.askfield:not(.dmy) { background-image: none; }
/* Words are a one-line box that wraps (js/ask.js): a textarea holds its
   whole line inside its box, 2px more than a field drew, so it pulls back
   3px each way, and it hangs from the top of its line rather than standing
   on the text's baseline (which added the line's descent below it), so the
   line keeps its height and the words their place. */
textarea.input.askfield { margin-top: -3px; margin-bottom: -3px; vertical-align: top; }
.input.askfield.is-date { flex: 0 0 auto; width: 9.5em; }
.input.askfield.is-date::-webkit-date-and-time-value { min-height: 0; line-height: inherit; text-align: left; }
.input.askfield.is-date::-webkit-calendar-picker-indicator { display: none; }
.input.askfield.dmy { width: 9.5em; padding-right: 26px; background-position: right 6px center; background-size: 14px; }
@media (pointer: coarse) {
  .input.askfield, .input.askfield.is-date { padding: 2px 6px; background-image: none; }
}

/* One row, always. Wrapping put Activity alone on a second line and moved the
   pane down by a tab's height; below the width where seven of them fit, the
   strip scrolls sideways instead, which is what every tab strip on a phone
   does. `flex: 0 0 auto` on the tabs, so none of them is squeezed to make the
   row fit rather than scrolling. */
/* The strip never gives up its height. `overflow-x: auto` makes it a scroll
   container, whose automatic minimum size is 0, so as a flex item in the
   Activity sheet's fixed-height card it was squeezed to nothing by the
   scrolling body beside it: the tabs were in the markup, took no height and
   drew nothing at all. In the record it is a grid item and this changes
   nothing. */
.rectabs {
  grid-area: tabs; display: flex; gap: 2px; flex-wrap: nowrap; flex: 0 0 auto;
  overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
  /* One block gap under the strip, the same step `.tabrow` takes at the top
     of Campaigns: two tab strips in one console standing 4px and 12px above
     their content is the kind of difference nobody can name and everybody
     sees. */
  border-bottom: 1px solid var(--line); margin-bottom: 12px;
}
.rectabs::-webkit-scrollbar { display: none; }
.rectabs .tab { flex: 0 0 auto; }
/* A strip that scrolls sideways must not scroll up and down. `overflow-x:
   auto` makes the other axis auto too, and each tab hangs 1px below the strip
   so its underline can sit on the rule: that one pixel was a vertical scroll
   a finger found on every swipe. The rule is drawn inside the strip instead,
   the tab sits on it, and the other axis is shut. */
.rectabs, .tabrow { overflow-y: hidden; overscroll-behavior-x: contain; border-bottom: 0; box-shadow: inset 0 -1px 0 var(--line); }
.rectabs .tab, .tabrow .tab { margin-bottom: 0; }
.rec-pane { grid-area: pane; min-width: 0; }
/* The client record's parts (2026-10-15): a tab holding two or three parts
   draws them one under the other, 24px apart, in the grid's one pane area. */
.rec-panes { grid-area: pane; min-width: 0; display: flex; flex-direction: column; gap: 24px; }
.rec-panes > .rec-pane[hidden] { display: none; }
.pane-form { padding: 12px 0 0; }
.pane-form-head { margin: 0 0 12px; }
.camp-client-tools .panelhead { display: none; }
.camp-client-tools > .row, .rec-linktools > .row { margin: 0 !important; flex-wrap: nowrap; }
@media (max-width: 760px) {
  .camp-client-tools > .row, .rec-linktools > .row { flex-wrap: wrap; }
}
/* The first block in a pane sits as far from the tabs as two blocks sit from
   each other: the law of the stack does not stop at a tab strip. THE STEP IS
   THE STRIP'S, and only the strip's (`.rectabs { margin-bottom }`), the way
   `.tabrow` already owned it at the top of Campaigns. It used to be split —
   4px on the strip and 12px on whichever first child remembered to ask for
   it — so the client record stood 16px off its tabs, the task record 4px,
   and Campaigns 12px: three strips, three gaps, and nothing on the page
   saying which was meant. One owner, one value, every pane. */
.rec-pane > :first-child { margin-top: 0; }
.rec-pane > .viewhead:first-child { margin-top: 0; }
#crmSummary:first-child { display: block; margin-top: 0; }
.rec-rail { grid-area: rail; min-width: 0; }
.rec-rail > .facts { margin: 0 0 12px; }
/* A 320px rail is two columns of facts, not six; an odd last one spans, or a
   cell sits alone on its row with an empty half beside it. */
.rec-rail .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.rec-rail .facts > div:nth-child(odd):last-child { grid-column: 1 / -1; }
.rec-rail .notice { margin-bottom: 12px; }

/* Seven panes beside a rail is 633px of tabs in a 591px strip at 1280, so the
   strip wrapped to two rows and Activity sat alone on the second. The campaign
   tab reads `Selection` — the pane's own heading still says Client selection,
   which is where that word means something — and the tabs in a workspace strip
   sit 2px tighter, which buys the row back with room to spare rather than
   exactly enough. */
.rectabs .tab { padding: 0 12px; }

/* Below the workspace width the rail is not a rail: it is what you read
   before the pane, so it comes first and the panes follow. */
@media (max-width: 1100px) {
  /* Stacked, the rail is not one block: what needs acting on (the stage and
     its clock, the next action, the billing gate, the profile bar) is read
     before the pane, and what is only looked up (the dates, the details, the
     recent activity) is read after it. One rail above the tabs put the
     record's own content a screen and a half down on a phone, behind a log
     nobody opens a record to read. The rail dissolves into the page grid
     (`display: contents`) and its blocks take their place by `order`; the
     `.rec-rail` element stays in the tree, so nothing that queries it
     changes. `ADspaceState.fit` measures it at 0 and leaves it alone, which
     is why this is the console's record only (`:not(.cportal)`): the client
     portal's rail carries contact rows keyed on the rail's own width class,
     and a rail that is not a box has no width to key on. */
  .rec:not(.cportal) > .rec-rail { display: contents; }
  .rec-rail .facts { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
}
/* The tab strip scrolls sideways rather than wrapping to three rows: seven
   words at 390 is two and a half lines of chrome above the work. */
@media (max-width: 640px) {
  /* The strip sits inside the page gutter like every other block, and its
     rule therefore lands on the same two margins as the rail's dividers and
     the card below it. It used to bleed (`margin-inline: -16px`) so a tab
     scrolling out faded to the screen edge rather than being cut at the
     gutter — a nicety that cost the one thing a hairline is for: at 390 the
     rule ran 0 to 390 while every sibling ran 16 to 374, so the page had two
     margins and the line under the tabs had none. `.tabrow` never bled, and
     a scroller cut at the gutter is what the rest of this portal does. */
  .rec-rail .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---------- The campaign command centre ----------
   One campaign is a booking desk, a schedule, a set of deliverables, a
   conversation with the client and an invoice. All five used to be one column,
   with the invoice above the creators and the dates inside each card. */

/* What it is waiting on us for, derived on every repaint and never stored, so
   reverting a step cannot leave it lying. */
.camp-next {
  margin: 8px 0 0; padding: 8px 12px; border-radius: var(--radius-sm);
  background: var(--sunk); color: var(--ink);
  font-size: 13px; font-weight: 600;
}
.camp-next:has(.camp-next-go) { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; }
/* The Schedule is the Creators tab's register (2026-10-03): a row a booking
   with its shoot and publish dates, folded; the plan opens under its row.
   The fields line up under the name, so the body starts on the name's x. */
.sched-reg .kcard-head { cursor: pointer; }
.sched-lab { display: none; }
.is-tight .sched-reg .sched-lab { display: inline; color: var(--ink-mute); }
/* On a phone each date of a schedule row takes its own line under the
   name, its word before it: beside the post date the shoot's date and time
   had 130px and was cut (2026-10-23, the day's gate). */
.is-tight .sched-reg .kcard-head {
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "lead name" "lead sum" "lead tags";
}
.is-tight .sched-reg .kcard-sum { text-align: left; }
.sched-reg .sched-pubday {
  font-size: 12.5px; color: var(--ink-soft); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.sched-body { padding: 0 16px 16px 72px; }
.is-narrow .sched-body { padding-left: 60px; }
.sched-said:empty { display: none; }
/* Add creators: a refusal on the creator's own row (2026-10-14). */
.camp-rowmsg { margin: 8px 0 0; }
.kcard .postrow .msg:empty { display: none; }
.sched-edit { display: flex; flex-direction: column; align-items: stretch; gap: 4px; min-width: 0; }
.sched-edit .input { min-width: 0; width: 100%; }
/* The label above its field, as every label in the portal is. */
.sched-when::before, .sched-f::before {
  content: attr(data-label);
  font-size: 12px; font-weight: 600; letter-spacing: 0; color: var(--ink-mute);
}
/* The shoot keeps its date and its time on one line under the one label. */
.sched-grid > .sched-shoot { display: grid; grid-template-columns: minmax(0, 1fr) 122px; gap: 4px 8px; }
.sched-grid > .sched-shoot::before { grid-column: 1 / -1; }
.sched-log {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; min-width: 0;
}
.sched-f { display: flex; flex-direction: column; align-items: stretch; gap: 4px; min-width: 0; }
.sched-f > .input { width: 100%; }
/* On a phone the body runs the card's width; Draft due and Publish still
   share a line, so five creators are not fifteen stacked boxes. */
.is-tight .sched-body { padding: 0 14px 14px; }
.pick-plat { font-size: 13px; color: var(--ink-mute); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-state { min-width: 0; }
.pick-row { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.1fr) 96px 124px; }

/* The room js/form.js §6 makes under a date field at the foot of its
   scroller, so the calendar opens inside the window. Present only while the
   field is in use. */
.pick-room { flex: none; margin: 0; padding: 0; pointer-events: none; }

/* A date box at a desk reads DD/MM/YYYY (js/form.js §7): a text box with the
   calendar mark at its right edge, which opens the browser's own picker. The
   picker itself is one hidden native field laid under the box it serves. */
.input.dmy {
  padding-right: 38px; background-repeat: no-repeat; background-position: right 12px center; background-size: 16px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b6760' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='5' width='18' height='16' rx='2'/%3E%3Cpath d='M3 10h18M8 3v4M16 3v4'/%3E%3C/svg%3E");
  font-variant-numeric: tabular-nums;
}
:root[data-theme="dark"] .input.dmy {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a6a89e' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='5' width='18' height='16' rx='2'/%3E%3Cpath d='M3 10h18M8 3v4M16 3v4'/%3E%3C/svg%3E");
}
.dmy-pick { position: fixed; left: 0; top: 0; width: 1px; height: 1px; margin: 0; padding: 0; border: 0; opacity: 0; pointer-events: none; }
/* Under its own hint an empty box keeps its DD/MM/YYYY until it is reached. */
.datefield.is-empty:not(:focus-within) > .dmy::placeholder { color: transparent; }

/* A date nobody has set yet. An empty `input[type=date]` draws nothing at all
   on iOS — no mm/dd/yyyy, no caret — so a Publish date that has not been
   chosen was a blank pill under a label, with nothing saying it was a date
   field. The field says it itself, over the box and out of the pointer's way,
   so a tap anywhere still opens the picker.

   The hint hangs off `.sched-field`, a wrapper around the field and nothing
   else, because an overlay is centred on the box it explains. Hung off the
   cell it was centred on the label as well: on a phone `top: 50%` of a 65px
   cell is 10px above the centre of the 44px field inside it, so the word
   printed across the box's own top border. The phone rule written to correct
   that re-anchored to `bottom: 0` and restated the height from `--ctl-h-sm` —
   a second guess at a box already on the page, 6px out at the desk where the
   field is 38px and the token is 32. A wrapper needs neither guess, and one
   rule is then true at every width. */
.sched-field { position: relative; display: flex; min-width: 0; }
.sched-field > .input { width: 100%; }
.sched-one.is-unset .sched-field::after {
  content: 'Not set';
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  font-size: 13px; color: var(--ink-mute); pointer-events: none;
}
/* The hint starts where the value would, which under a finger is after the
   glyph, and at the size the value is drawn at. */
@media (pointer: coarse) {
  .sched-one.is-unset .sched-field::after { left: 38px; font-size: var(--field-text); }
}
/* A column is a column on every row, or it is not a column. The second track
   was `auto`, so it was sized by its own row's chip: "Pending draft" opened it
   at 76px and "Submitted" at 72, and the state and the fee under it started
   4px apart on every line. It is `--state-w`, the width this portal already
   gives a state column, measured against the widest word the vocabulary has
   ("Changes requested", 123px) and against the fee beneath it (RM 12,000.00,
   85px). Both cells then end at the card's own padding rather than wherever
   their own words happen to stop. */
.is-narrow .pick-row {
  grid-template-columns: minmax(0, 1fr) var(--state-w);
  grid-template-areas: "who state" "plat rate";
  gap: 2px 10px;
}
.is-narrow .pick-row > .pick-who { grid-area: who; }
.is-narrow .pick-row > .pick-state { grid-area: state; justify-self: stretch; text-align: right; }
.is-narrow .pick-row > .pick-plat { grid-area: plat; }
.is-narrow .pick-row > .svc-rate { grid-area: rate; text-align: right; }
.is-narrow .crm-head.pick-row { display: none; }

.deliv-head b { font-size: 14px; font-weight: 600; letter-spacing: -.01em; }
@media (hover: hover) { .deliv-file:hover { border-color: var(--ink-mute); } }
.deliv-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.deliv-caption {
  margin: 10px 0 0; padding-top: 10px; border-top: 1px solid var(--line-soft);
  font-size: 13.5px; color: var(--ink-soft); line-height: 1.55; white-space: pre-wrap;
}

/* ---------- The creator's work queue ----------
   Four bookings used to be four full cards stacked down the page, each with
   its own upload box, and the one that needed them today was wherever it
   happened to fall. */
/* One column at its own width, never its widest word's: an `auto` track
   grows to the longest unbroken run inside it, and a caption holding a form
   link made the open booking 105px wider than a phone. */
.cwork { display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; gap: 16px; }
@media (min-width: 900px) {
  .cwork { grid-template-columns: minmax(240px, 1fr) minmax(0, 1.618fr); gap: 0 24px; }
  /* One booking has no queue, and its card took the queue's narrow column
     with the pane's width empty beside it. Alone, it runs the head card's
     width. */
  .cwork:has(> .queue[hidden]) { grid-template-columns: minmax(0, 1fr); }
}
/* The queue is one list, so it is one surface with hairlines between its rows,
   not four floating cards with page ground showing between bookings that
   belong to each other. The open booking beside it is the object being worked
   on and keeps its own boundary. */
.queue {
  display: flex; flex-direction: column;
  background: var(--card); border: 1px solid var(--card-line); border-radius: var(--radius);
  overflow: hidden;
}
.qrow {
  display: grid; align-items: center; gap: 2px 10px; width: 100%; text-align: left;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "name flag" "meta meta";
  padding: 12px 14px; border: 0; border-bottom: 1px solid var(--line-soft);
  border-radius: 0; background: var(--card);
  transition: background var(--t-fast) var(--ease-out);
}
.qrow:last-child { border-bottom: 0; }
/* The one waiting on them says so with its own amber dot (`.qrow-flag`);
   the row itself is not tinted (2026-10-07). */
.qrow-name {
  grid-area: name; font-size: 14px; font-weight: 600; letter-spacing: -.01em; color: var(--ink);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.qrow-flag { grid-area: flag; }
.qrow-meta { grid-area: meta; font-size: 12.5px; color: var(--ink-mute); }
@media (hover: hover) { .qrow:hover { background: var(--sunk); } }
/* Chosen is a fill and a border, never a shadow, like every other selected
   thing in this portal. */
.qrow.is-on { background: var(--line-soft); }
/* Inside one surface a selected row is a fill and an edge on the side it is
   read from, never a border that would double the hairline above it. */
.qrow.is-on::after {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--ink);
}
.qrow { position: relative; }
.qrow.is-off { opacity: .6; }
.qrow:active { transform: scale(.97); }
@media (prefers-reduced-motion: reduce) { .qrow:active { transform: none; } }

/* ---------- The Review Canvas ----------
   The gallery is how a client sees the month; the canvas is how they decide on
   one post. The mockup at the size it deserves, and everything the decision
   rests on beside it: what it is, the copy in full, what was said last time,
   where it stands, and the one place to decide. */
.canvas {
  position: fixed; inset: 0; z-index: 95; display: flex;
  align-items: center; justify-content: center; padding: 24px;
  background: var(--scrim);
  animation: mk-scrim-in var(--t) var(--ease-out);
}
.canvas[hidden] { display: none; }
body.is-canvas { overflow: hidden; }
.canvas-card {
  display: flex; flex-direction: column;
  width: min(1180px, 100%); max-height: min(92vh, 900px);
  background: var(--card); border-radius: var(--radius-lg);
  box-shadow: 0 24px 60px rgba(0,0,0,.25); overflow: hidden;
  animation: mk-sheet-in var(--t-slow) var(--ease);
}
.canvas-head {
  display: flex; align-items: center; gap: 10px; flex: 0 0 auto;
  padding: 0 12px 0 20px; min-height: var(--head-h);
  border-bottom: 1px solid var(--line);
}
.canvas-what { display: flex; align-items: baseline; gap: 10px; flex: 0 1 auto; min-width: 0; }
.canvas-title { font-size: 14px; font-weight: 600; letter-spacing: -.01em; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.canvas-dims { font-size: 12.5px; color: var(--ink-mute); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Previous, where you are in the set, Next: one group, so stepping through
   the set reads as one control. The three buttons are filled (the tonal
   secondary), because the quiet icon mark read as a glyph on the bar and not
   as something to press (the user, 2026-09-28, on an iPhone). Close stands
   apart at the right edge. */
.canvas-nav { margin-left: auto; display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.canvas-pos { font-size: 12.5px; color: var(--ink-mute); font-variant-numeric: tabular-nums; white-space: nowrap; min-width: 4.5em; text-align: center; }
.canvas-head .iconbtn { background: var(--tonal); color: var(--ink); border-radius: var(--radius-sm); }
.canvas-head .iconbtn svg { width: 18px; height: 18px; stroke-width: 2; }
.canvas-head .canvas-close { margin-left: 8px; }
@media (hover: hover) {
  .canvas-head .iconbtn:hover:not([disabled]) { background: var(--tonal-hover); color: var(--ink); }
}
.canvas-head .iconbtn:active:not([disabled]) { background: var(--tonal-press); }
.canvas-head .iconbtn[disabled] { opacity: .35; }
.canvas-body {
  flex: 1 1 auto; min-height: 0; display: grid;
  grid-template-columns: minmax(0, 1.618fr) minmax(320px, 1fr);
}
/* The mockups reproduce each platform's own UI, so the stage stays light in
   dark exactly as the cards behind it do. */
.canvas-stage {
  min-width: 0; overflow: auto; padding: 20px;
  display: flex; align-items: flex-start; justify-content: center;
  background: var(--sunk); border-right: 1px solid var(--line);
}
.canvas-stage > .card-stage { width: 100%; max-width: 460px; }
.canvas-rail { min-width: 0; overflow: auto; padding: 18px 20px; }
.canvas-rail > .badge { display: inline-flex; margin: 0 0 12px; }
.canvas-rail > .copyblock { border-top: 0; padding: 0 0 14px; }
.canvas-rail > .reask { margin: 0 0 14px; }
/* The decision is the last thing in the rail and the only green on it. */
.canvas-rail > .approve { border-top: 1px solid var(--line-soft); padding: 14px 0 0; }
/* The decision on the rail's one edge, and said once: a decided post's
   state line leaves the rail, the decision line saying it (ux-audit
   REVIEWPAGE-7). */
.canvas-rail > .badge.is-ok, .canvas-rail > .badge.is-changes { display: none; }

@media (max-width: 860px) {
  /* One column: the mockup, then what the decision rests on, then the
     decision. A rail beside a 9:16 mockup on a phone is two columns of
     nothing. And one scroll (2026-09-28, a client's complaint): the stage
     and the rail were two scroll areas sharing the screen, so the copy and
     the decision sat over the lower third of every post, a Reel's own
     caption included. The post is shown whole; its copy and the decision
     follow it as the page scrolls. */
  .canvas { padding: 0; align-items: flex-end; }
  .canvas-card { width: 100%; max-height: 100vh; max-height: 100dvh; height: 100%; border-radius: 0; }
  /* The name over its size, so the three buttons keep their room. */
  .canvas-head { gap: 8px; padding: 0 12px 0 16px; }
  .canvas-what { flex-direction: column; align-items: stretch; gap: 0; }
  .canvas-dims { font-size: 12px; }
  .canvas-body { display: block; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
  .canvas-stage { overflow: visible; border-right: 0; border-bottom: 1px solid var(--line); padding: 14px; }
  .canvas-rail { overflow: visible; padding: 14px 16px calc(24px + env(safe-area-inset-bottom, 0px)); }
}
@media (prefers-reduced-motion: reduce) {
  .canvas, .canvas-card { animation: none; }
}

/* The client's own page: the company card, one tab strip, and a pane at a
   time, every section a card with its title inside it (the user, 2026-09-28:
   "the client portal is a whole mess, everything scattered", choosing tabs
   like the console's records). It had no tab strip, on the reading that a
   client reads the page once; seven sections down one column, some cards
   and some loose on the ground, read as scattered, and the rail fell above
   the work below 1100. */
/* Head, tabs and panes 12px apart at every width, as every client page
   stacks its blocks (2026-10-31, POR-10). */
#cpHead { margin-top: 24px; margin-bottom: 12px; }
#cpHead .rec-ctl .select { flex: 0 0 auto; width: auto; min-width: 150px; }
#cpHead .rec-ctl .btn { flex: 0 0 auto; }
.cp-tabs { margin-bottom: 12px; }
.cp-pane { display: grid; gap: 12px; align-content: start; min-width: 0; }
.cp-pane[hidden] { display: none; }
/* Overview: the summary on the left, who to reach and the company on the
   right, on the record's own proportion; one column once the pane is narrow. */
.cp-overview { grid-template-columns: minmax(0, 1.618fr) minmax(280px, 1fr); gap: 12px 16px; }
.cp-col { display: grid; gap: 12px; align-content: start; min-width: 0; }
@media (max-width: 900px) { .cp-overview { grid-template-columns: minmax(0, 1fr); } }

/* One card: the title and its one action on the first line, the content
   under it. A table inside a card is the card's rows, never a card of its
   own (a border inside a border groups nothing). */
.cp-card { margin: 0; min-width: 0; }
.cp-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 12px; min-height: 24px; }
.cp-card-head h3 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
.cp-card .crm-table { background: none; border: 0; border-radius: 0; box-shadow: none; margin: 0; }
.cp-card .crm-head, .cp-card .svc-row { padding-inline: 0; }
.cp-card .svc-row:last-child { border-bottom: 0; padding-bottom: 0; }
.cp-card .csv-total { padding-inline: 0; padding-bottom: 0; }

/* The overview's lines: a label beside its figure. */
.cp-lines { display: grid; gap: 8px; margin: 0; }
.cp-lines > div { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0 16px; align-items: baseline; }
.cp-lines dt { font-size: 13px; color: var(--ink-mute); }
/* Money in a row is 13.5px, under the card's 15px title, never above it
   (2026-10-31, POR-1: lines and money read at 16px under 15px titles). */
.cp-lines dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; font-size: 13.5px; }
.cp-lines .is-total dd { font-weight: 600; }
.cp-line { margin: 0; line-height: 1.5; font-size: 14px; }
.cp-line + .cp-line { margin-top: 4px; }
.cp-sub { margin: 0; font-size: 13px; color: var(--ink-mute); }
/* A line's sub-line sits 4px under it, as a record's meta does. */
.cp-line + .cp-sub { margin-top: 4px; }
.cp-card-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
/* The account manager: the name, then the two ways to reach them. */
.cp-person { margin: 0 0 8px; font-size: 14px; font-weight: 600; }
.cp-reach { display: flex; flex-wrap: wrap; gap: 8px; }
.cp-reach .plink { flex: 0 0 auto; }
/* On a phone the chips share the card's width equally, as every reach row
   does (DESIGN.md: `.plink`). */
@media (max-width: 640px) { .cp-reach .plink { flex: 1 1 0; justify-content: center; } }
/* Company facts sit label over value, one to a row, in the narrow column. */
.cp-card .facts { grid-template-columns: minmax(0, 1fr); gap: 12px; }
.cp-card .facts dd { white-space: pre-line; }
.acc-email { font-size: 13px; color: var(--ink-soft); overflow-wrap: anywhere; }
/* A contact's marks sit on a line of their own under the name. */
.ct-top { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 6px; min-width: 0; }
.ct-top > b { flex: 1 1 auto; min-width: 0; }
.ct-top > .tone { flex: 0 0 auto; }
/* A contact only read (the client's portal) has no ⋯, so its row is the name
   over the ways to reach them, and the chip ends at the card's edge. */
.is-narrow .ct-row.is-read { grid-template-columns: minmax(0, 1fr); grid-template-areas: "name" "reach"; }
.is-narrow .ct-row.is-read > .team-act { display: none; }
/* One of the client's own pages: its name, and Open at the right edge. */
.cp-eng-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: center; }
.cp-open { flex: 0 0 auto; gap: 6px; }
/* Every glyph on the client portal's buttons is the system's 15px
   (2026-10-31, POR-9: 13 and 14px beside one another). */
.cp-open svg, .cp-panes .btn-sm svg, .cp-panes .ovgo-chev { width: 15px; height: 15px; }
@media (max-width: 640px) {
  #cpHead { margin-top: 16px; }
}

.ovsec-head h3 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
/* The chevron 6px after its words, as every glyph sits (2026-10-31, POR-9). */
.ovgo { margin-left: auto; min-width: 0; padding: 0 6px 0 10px; gap: 6px; }
.ovgo-chev { width: 14px; height: 14px; flex: 0 0 auto; }
/* An Edit carries the pen, like every Edit in the console, and no chevron:
   it opens the pane the facts are edited in rather than going somewhere. */
.ovgo.is-edit { padding: 0 10px; gap: 6px; }
.ovgo.is-edit svg { width: 14px; height: 14px; stroke-width: 1.8; }

/* Label beside value, one fact a line, the label in a stated track so every
   value starts at the same x. Two across with eyebrow labels read as six
   small cards; a list of rows reads as one record. The label track is the
   row's smaller share: 150px against a pane of roughly 590 is close to the
   short side of a golden cut, which is what keeps the labels quiet. */
.ovfacts { display: grid; gap: 8px; margin: 0; }
.ovfacts > div { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 0 16px; align-items: baseline; }
.ovfacts dt { font-size: 13px; color: var(--ink-mute); min-width: 0; }
.ovfacts dd { margin: 0; font-size: 13.5px; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
/* The number and the way to reach on it, with something between them. The
   WhatsApp link used to be an underlined word printed hard against the last
   digit, so the row read as one long string. */
.ovreach { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; min-width: 0; }
.ovnone { margin: 0; font-size: 13.5px; color: var(--ink-mute); }
.ovmore { margin: 8px 0 0; font-size: 12.5px; color: var(--ink-mute); }

.ovname { font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
a.ovrow:focus-visible { outline: 2px solid var(--action-ring); outline-offset: -2px; border-radius: 0; }
.ovdim { color: var(--ink-mute); font-size: 12.5px; min-width: 0; }
/* A call is a glyph for what kind it was, what was said under what it was,
   and when and with whom on the right: the shape every activity feed uses. */
.ovlog { list-style: none; margin: 0; padding: 0; }
.ovlog-ico {
  width: 30px; height: 30px; border-radius: 50%; background: var(--sunk);
  display: grid; place-items: center; color: var(--ink-soft);
}
.ovlog-ico svg { width: 15px; height: 15px; stroke-width: 1.8; }
.ovlog-main { display: grid; gap: 1px; min-width: 0; }
/* What was said is the point of the row, so it wraps rather than being cut
   off mid-word: clipped text is a value the reader cannot have. */
.ovlog-text { min-width: 0; color: var(--ink-soft); font-size: 13px; overflow-wrap: anywhere; }
.ovlog-when .ovmeta { display: block; margin: 0; }

/* The Overview's rows are inside the pane as well, so they take the same
   component-width switch rather than the window's. */
.is-narrow .ovsec { padding: 14px 15px; }
.readgroup:first-child { border-top: 0; padding-top: 4px; }
.readgroup .fsec-h { margin-bottom: 12px; }
.ovfacts dd.is-missing { color: var(--err); font-weight: 600; }
.ovfacts dd.is-empty { color: var(--ink-mute); }
.readlogo { display: inline-grid; place-items: center; width: 22px; height: 22px; margin-right: 8px; vertical-align: middle;
  border-radius: 50%; overflow: hidden; background: #fff; box-shadow: inset 0 0 0 1px var(--line); }
.readlogo img { width: 100%; height: 100%; object-fit: cover; }
/* When and with whom drop under what was said, rather than squeezing it. */
/* A header the phone hides is not a header, so the row carries its own
   labels instead: name and money on the first line, what qualifies them on
   the second — the shape every other list in this console takes. */
.is-narrow .ovhead { display: none; }
.is-narrow .ovrow-svc, .is-narrow .ovrow-doc {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "name amt" "dim state"; gap: 3px 12px;
}
.is-narrow .ovrow-svc > :nth-child(1), .is-narrow .ovrow-doc > :nth-child(1) { grid-area: name; }
.is-narrow .ovrow-svc > :nth-child(2), .is-narrow .ovrow-doc > :nth-child(2) { grid-area: dim; }
.is-narrow .ovrow-svc > :nth-child(3), .is-narrow .ovrow-doc > :nth-child(3) { grid-area: amt; justify-self: end; }
.is-narrow .ovrow-svc > :nth-child(4), .is-narrow .ovrow-doc > :nth-child(4) { grid-area: state; justify-self: end; }
/* Bookings: name and where the work has got to on the first line, where
   they post and the fee on the second, as the Selection pane reads. */
.is-narrow .ovrow-book {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "name state" "dim amt"; gap: 3px 12px;
}
.is-narrow .ovrow-book > :nth-child(1) { grid-area: name; }
.is-narrow .ovrow-book > :nth-child(2) { grid-area: dim; }
.is-narrow .ovrow-book > :nth-child(3) { grid-area: state; justify-self: end; }
.is-narrow .ovrow-book > :nth-child(4) { grid-area: amt; justify-self: end; }
/* A task: its name and stage on the first line, whose and when on the
   second, the last column the right edge. */
.is-narrow .ovrow-task {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "name state" "dim amt"; gap: 3px 12px;
}
.is-narrow .ovrow-task > :nth-child(1) { grid-area: name; }
.is-narrow .ovrow-task > :nth-child(2) { grid-area: dim; }
.is-narrow .ovrow-task > :nth-child(3) { grid-area: state; justify-self: end; }
.is-narrow .ovrow-task > :nth-child(4) { grid-area: amt; justify-self: end; font-size: 12.5px; color: var(--ink-mute); }
/* The schedule keeps both dates, each under the caption the hidden header
   would otherwise have given it. */
.is-narrow .ovrow-date {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-areas: "name name" "shoot pub"; gap: 6px 12px;
}
.is-narrow .ovrow-date > :nth-child(1) { grid-area: name; }
.is-narrow .ovrow-date > :nth-child(2) { grid-area: shoot; }
.is-narrow .ovrow-date > :nth-child(3) { grid-area: pub; }
.is-narrow .ovcap {
  display: block; font-size: 12px; font-weight: 600; letter-spacing: 0;
  color: var(--ink-mute); margin-bottom: 2px;
}
/* The kind and what was said are the main cell's own lines, so on a narrow
   pane that cell gives them to the row's grid: the glyph, the kind beside
   when, what was said under both. (The areas were named on lines the row
   did not hold, and the kind was drawn over the words; 2026-10-11.) */
.is-narrow .ovlog-row {
  grid-template-columns: 30px minmax(0, 1fr) auto;
  grid-template-areas: "ico kind when" "ico text text"; gap: 3px 12px;
}
.is-narrow .ovlog-main { display: contents; }
/* ---------- My Work, phase 3: three views of one set of rows ----------
   The list bands by when the work is owed; the board lays one workflow's
   stages side by side; the calendar puts the commitments on their days. One
   read, one filter, one search, one scope: what changes is the question the
   rows are asked. */
.cmdbar-views { display: inline-flex; gap: 0; flex: 0 0 auto; padding: 0; border: 0; border-radius: var(--radius-ctl); background: var(--tonal); }
/* A segment: the grey track, and the chosen option raised in white inside
   it. A 2px transparent edge insets the white without making the control
   taller than the bar it sits in, so the group stays the height of a small
   control (the form audit of 2026-09-24, approved by the user). The same
   look draws `.seg` (js/form.js), the choice of two to four in a form. */
.cmdbar-views .acttab, .seg-opt {
  /* Unchosen options are the track itself: without a stated fill a <button>
     paints the browser's own grey inside the 2px edge and every option
     showed a seam. */
  border: 2px solid transparent; background-color: transparent; background-clip: padding-box;
  border-radius: var(--radius-ctl); color: var(--ink-soft); font-weight: 500;
}
.cmdbar-views .acttab.is-on, .seg-opt.is-on {
  background-color: var(--seg-on); border-color: transparent; color: var(--ink); font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--line), 0 0 0 0 transparent;
}
@media (hover: hover) {
  .cmdbar-views .acttab:not(.is-on):hover, .seg-opt:not(.is-on):not(:disabled):hover { background-color: var(--tonal); color: var(--ink); }
}
/* THE CHOICE SLIDES (2026-09-24, asked for by the user: "smooth sliding
   instead of very lagging click"). Once js/form.js has measured a track
   (`.has-thumb`), the raised white is one surface drawn by the track and
   moved under the option chosen, rather than a fill that blinks off one
   button and on another. It is a pseudo-element, so it is never a child: a
   segment's options are redrawn with innerHTML, counted by nth-child and
   walked by the arrow keys, and none of that sees it. The option keeps the
   2px inset the fill had, so the thumb sits where the white always sat.
   Before the script measures, and without it, each option paints its own
   fill as it did. The move is `transform` (the compositor's, smooth while
   the page repaints the view behind it) with the size following on the same
   curve; the first placement and a resize snap (`.is-snap`), so only a
   choice moves. Reduced motion zeroes it with every other transition. */
.cmdbar-views.has-thumb, .seg.has-thumb, .rectabs.has-thumb, .tabrow.has-thumb { position: relative; isolation: isolate; }
.cmdbar-views.has-thumb::before, .seg.has-thumb::before, .rectabs.has-thumb::before, .tabrow.has-thumb::before {
  content: ""; position: absolute; left: 0; top: 0; z-index: 0; pointer-events: none;
  width: var(--thumb-w, 0); height: var(--thumb-h, 0);
  transform: translate3d(var(--thumb-x, 0), var(--thumb-y, 0), 0);
  border-radius: calc(var(--radius-ctl) - 2px);
  background: var(--seg-on); box-shadow: inset 0 0 0 1px var(--line);
  opacity: var(--thumb-o, 0);
  transition: transform var(--t-slow) var(--ease), width var(--t-slow) var(--ease),
              height var(--t-slow) var(--ease), opacity var(--t-fast) var(--ease-out);
  will-change: transform;
}
.seg.has-thumb::before { border-radius: calc(var(--radius-sm) - 2px); }
/* A CHOICE IS A PILL (the user, 2026-10-04): a value picked from two to four
   is round, track, options and thumb alike, so it never reads as one of the
   rectangular buttons that act beside it. Tab strips and view strips move
   between screens and keep their corners. A segment wrapped onto two rows
   rounds its track by one row's half height. */
.seg, .seg-opt, .seg.has-thumb::before { border-radius: 999px; }
.seg.is-wrap { border-radius: calc(var(--ctl-h) / 2); }
/* A wrapped segment is separate pills (the user, 2026-10-13, after Apple's
   own choice pills: four on a phone read as one block stuck together): the
   track gives its ground to each option, 8px apart. */
.seg.is-wrap { gap: 8px; background: transparent; border-radius: 0; }
.seg.is-wrap > .seg-opt { background-color: var(--tonal); }
/* Apart from its track the chosen pill is the ink fill, as a chosen
   `.acttab` outside a strip is: the thumb slides to it in ink, and the pill
   paints the same ink itself once chosen (2026-10-14), so its words stand
   on their own ground however they are read. One still cut at two across
   stacks. */
.seg.is-wrap.has-thumb::before { background: var(--fill); box-shadow: none; }
.seg.is-wrap > .seg-opt.is-on { color: var(--on-fill); background-color: var(--fill); }
.seg.is-wrap.is-stack { grid-template-columns: minmax(0, 1fr); }
@media (hover: hover) {
  .seg.is-wrap > .seg-opt:not(.is-on):not(:disabled):hover { background-color: var(--tonal-hover); }
}
/* A round end takes room from the words, so a pill's option keeps 14px
   either side of them (8 sat the word against the curve). */
.seg-opt { padding-inline: 14px; }
/* The small segment: the small control's height and type, beside the small
   button it belongs with; its options as wide as their words. */
.seg.is-sm { display: inline-flex; min-height: var(--ctl-h-sm); }
.seg.is-sm > .seg-opt { flex: 0 0 auto; min-height: var(--ctl-h-sm); font-size: 12.5px; }
.cmdbar-views.has-thumb.is-snap::before, .seg.has-thumb.is-snap::before,
.rectabs.has-thumb.is-snap::before, .tabrow.has-thumb.is-snap::before { transition: none; }
.cmdbar-views.has-thumb > .acttab, .seg.has-thumb > .seg-opt,
.rectabs.has-thumb > .tab, .tabrow.has-thumb > .tab { position: relative; z-index: 1; }
.cmdbar-views.has-thumb > .acttab.is-on, .seg.has-thumb > .seg-opt.is-on,
.rectabs.has-thumb > .tab.is-on, .tabrow.has-thumb > .tab.is-on { background-color: transparent; box-shadow: none; }
.seg.is-wrap.has-thumb > .seg-opt.is-on { background-color: var(--fill); }
/* The word follows the thumb on the short curve, so the chosen word darkens
   as the surface arrives under it. */
.cmdbar-views .acttab { transition: color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out); }
/* Each segment is a control and clears the small control floor on its own
   (32px at a desk, 44 under a finger); the group's hairline sits outside it.
   Shrunk to fit inside a 32px box the segments were 26px, which the audit's
   target rule is there to catch. */
.cmdbar-views .acttab { min-height: var(--ctl-h-sm); min-width: 0; padding: 0 11px; font-size: 12.5px; }
/* On a phone the strip scrolls sideways rather than squeezing six views
   into 358px (the brief of 2026-09-26): every view keeps the same padding
   and its whole word, a view grows to fill the strip only where the strip
   has room to spare, and the edge with more beyond it fades
   (`is-more-start` / `is-more-end`, set by js/form.js, which also scrolls the
   chosen view into sight). The fade is a mask on the track, so the thumb and
   the words under it fade together and nothing is drawn over the options. */
@media (max-width: 640px) {
  .cmdbar-views {
    flex: 1 1 calc(50% - 5px); justify-content: flex-start; min-width: 0;
    overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
  }
  .cmdbar-views::-webkit-scrollbar { display: none; }
  .cmdbar-views .acttab { flex: 1 0 auto; padding: 0 14px; text-align: center; white-space: nowrap; }
  .cmdbar-views.is-more-end { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent); }
  .cmdbar-views.is-more-start { -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to left, #000 calc(100% - 32px), transparent); }
  .cmdbar-views.is-more-start.is-more-end { -webkit-mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent); }
}

/* THE BOARD. One column per stage of one workflow, in the workflow's own
   order, and the lanes beside the line (blocked, waiting, KIV) gathered in one
   column at the end: a board is where the flow is read, and a stage a task
   has stepped out of the flow into is one place, not three. It scrolls
   sideways at a desk, because eight columns at a legible width are wider
   than any screen and a board narrowed to fit is a board nobody can read;
   the rule against sideways tables is a rule about tables. On a phone the
   columns stack, so it reads as the list grouped by stage, which is what a
   board is on a screen one column wide. */
/* Every column runs the board's full height, so the whole column is where a
   card lands, not the height of the cards already in it (the user,
   2026-09-28); its cards stay packed at the top. */
.board { position: relative; display: flex; gap: 12px; align-items: stretch; overflow-x: auto; padding-bottom: 8px; scrollbar-width: thin; }
.board.is-more-end { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent); }
.board.is-more-start { -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to left, #000 calc(100% - 32px), transparent); }
.board.is-more-start.is-more-end { -webkit-mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent); }
.bcol { flex: 0 0 248px; min-width: 0; display: grid; grid-template-rows: auto 1fr; gap: 8px; }
.bcol-head h3 { margin: 0; font-size: 13px; font-weight: 600; letter-spacing: -.01em; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bcol.is-over .bcol-n { color: var(--ink); font-weight: 600; }
.bcol.is-over .bcol-n::before { content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px; vertical-align: 1px; background: var(--dot-wait); }
/* While a card is in hand the board does not also scroll under it, and the
   text under the finger is not selected. Only while: a board wider than the
   screen has to stay scrollable the rest of the time, which is why a finger
   holds before a card lifts at all. */
.board.is-dragging { touch-action: none; user-select: none; }
/* The card that follows the hand is a clone, never the card itself: taking
   the real one out of its column reflows the board mid gesture. It is out of
   the way of `elementFromPoint`, which is how the column under the finger is
   found at all. */
.bcard-ghost {
  position: fixed; z-index: 90; pointer-events: none; margin: 0;
  box-shadow: var(--shadow-lift); opacity: .96;
  transform: rotate(1.2deg);
}
/* The grip a finger drags from. `touch-action: none` on the handle itself is
   the whole mechanism: a phone decides whether a touch is a scroll the moment
   it lands, so a class this page sets afterwards is always too late, and the
   board cancelled every drag as soon as the finger moved. Declared here, the
   browser never claims a gesture that starts on the grip, and the rest of the
   card goes on scrolling the board. 44px under a finger like every other
   target; quiet, because it is furniture until it is wanted. */
.bcard-grip {
  touch-action: none; margin-left: auto; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 20px; margin-block: -2px; color: var(--ink-mute); opacity: .55;
}
.bcard-grip svg { width: 16px; height: 16px; }
@media (pointer: coarse) {
  .bcard-grip { width: 44px; height: 44px; margin-block: calc((20px - 44px) / 2); margin-right: -8px; }
}
@media (hover: hover) and (pointer: fine) {
  .bcard.is-dragging { cursor: grabbing; }
  .bcard-grip { cursor: grab; }
  .bcard-title, .bcard .state-select { cursor: pointer; }
}
/* THE CARD IS READ IN THE ORDER A BOARD IS SCANNED: what it is, whose it is,
   when it is owed. The serial led the card in the token face at the same size
   as the client beside it, which put the one fact nobody scans for at the top
   of every card; it is mute and one step smaller than the line it shares, and
   the title above the fold is what carries the weight. */
.bcard-top {
  display: flex; align-items: baseline; gap: 6px; min-width: 0;
  font-size: 11.5px; color: var(--ink-mute); font-variant-numeric: tabular-nums;
}
.bcard-top .bcard-no {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-weight: 500; letter-spacing: 0; flex: 0 0 auto;
}
.bcard-top .bcard-no::after { content: "·"; margin-left: 6px; }
.bcard-top .bcard-no:only-child::after { content: none; }
/* The title is what opens the task: the widest thing on the card, where the
   eye already is, and nothing else on the card is nested inside it. */
/* It is what opens the task, so it clears the control floor like every other
   control: at `display: block` it was its own 19px line of text and the walk
   measured it as a 19px target. */
.bcard-title {
  display: flex; align-items: center; width: 100%; min-height: var(--ctl-h);
  padding: 0; border: 0; background: none;
  text-align: left; font: inherit; font-size: 14px; font-weight: 600; line-height: 1.35; color: var(--ink);
  cursor: pointer; border-radius: 6px; overflow-wrap: anywhere;
}
@media (hover: hover) { .bcard-title:hover { color: var(--action-ink); } }
.bcard-stage .state-select { width: 100%; }
/* Whose it is and when it is owed, on one line under the control. The owner
   was missing from the card altogether, which is the fact a board is read
   for. It arrived as a name behind a 20px initials disc, and the disc went
   the same day the log's did: it stands in for a profile picture this portal
   has none of, and a dashed empty one on every unassigned card is a
   placeholder for a placeholder. The name is the fact. */
.bcard-foot {
  display: flex; align-items: center; gap: 8px; min-width: 0;
  font-size: 12px; color: var(--ink-soft);
}
.bcard-who { display: flex; align-items: center; min-width: 0; }
.bcard-due.is-over { color: var(--err); font-weight: 600; }
.is-narrow .board { flex-direction: column; align-items: stretch; overflow-x: visible; padding-bottom: 0; gap: 16px; }
.is-narrow .bcol { flex: none; width: auto; }
/* A card that holds nothing on a phone is a heading over nothing, and the
   phone has no room for eight of them. */
.is-narrow .bcol.is-empty { display: none; }

/* THE CALENDAR. Seven columns at a desk, the commitment's day the cell it is
   on, three chips a day and the rest counted. On a phone a seven column month
   is forty-two cells of nothing with a word in a few of them, so it is a list
   of the days that hold something, each named. */
.calbar { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }
.calbar h3 { margin: 0 4px; font-size: 15px; font-weight: 600; letter-spacing: -.01em; min-width: 132px; flex: 0 1 auto; white-space: nowrap; }
/* At 320 the month, its two arrows and Today ran 40px past the gutter, and
   Today wrapped to a line of its own when the bar was let wrap. The bar stays
   one line: Today gives up the small button's minimum width, and under 360px
   the month is printed short (`Sept 2026`), which is how the day labels
   already name it. */
.calbar .btn-quiet { min-width: 0; margin-left: auto; }
.cal-day { background: var(--card); min-height: 96px; padding: 6px 5px 8px; display: grid; align-content: start; gap: 3px; min-width: 0; }
.cal-day.is-weekend { background: var(--sunk); }
.cal-day.is-holiday { background: var(--sunk); }
.hol-note { margin: 6px 0 0; }
/* A day outside the month is quieter than one inside it, and both clear
   4.5:1: `--line-ctl` is a border tone (3:1) and failed as text. */
.cal-day.is-out .cal-num { color: var(--ink-mute); }
.cal-num b { font-weight: 600; }
.cal-num small { display: none; font-size: 12px; }
.cal-num small:first-child { font-weight: 600; color: var(--ink-soft); }
.cal-day.is-today .cal-num b { display: inline-grid; place-items: center; min-width: 22px; height: 22px; border-radius: 11px; background: var(--fill); color: var(--on-fill); }
/* A chip is the task at the size a cell can hold: the tone the stage carries,
   the title truncated, and a press that opens the task. */
/* Two lines in the small face, the target kept, so a day holds five (the
   user, 2026-10-08: "the text shown is limited due to its size of the
   font"): whose work it is over which piece (2026-10-09: "you dont need show
   the full card name"), each line cut to the cell. Each line is its own
   block, since a button clamps no lines in Safari. */
.cal-chip, .cal-chip.btn-sm {
  display: grid; align-content: center; gap: 1px;
  width: 100%; min-width: 0; text-align: left; padding: 3px 6px; border: 0; border-radius: 5px;
  background: var(--line-soft); color: var(--ink-soft); font: inherit; font-size: 11px; font-weight: 500; line-height: 13px;
  overflow: hidden; white-space: normal; cursor: pointer; min-height: var(--ctl-h-sm);
}
.cal-chip-who, .cal-chip-what { display: block; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.cal-chip.is-pub .cal-chip-who { color: inherit; }
/* A task's stage is the dot before its client, as Calendar draws an event
   (2026-10-07); the chip stays the quiet ground. */
.cal-chip:is(.is-ok, .is-warn, .is-danger, .is-off) .cal-chip-who::before {
  content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  margin-right: 5px; vertical-align: 1px; background: var(--dot-off);
}
.cal-chip.is-late { box-shadow: inset 3px 0 0 var(--err); }
/* The publish date is a date of its own kind, so it has a colour of its own,
   used nowhere else: the day a post goes out, beside the day work is owed. */
.cal-chip.is-pub { background: var(--pub-bg); color: var(--pub); }
/* The key is the chips themselves in small, each on its own ground with no
   edge: an outlined square read as a tick box (2026-10-27, WORK-7). */
.cal-key > span { display: inline-flex; align-items: center; white-space: nowrap; padding: 2px 8px; border-radius: 5px; font-weight: 500; }
.cal-key-due { background: var(--line-soft); color: var(--ink-soft); }
.cal-key-pub { background: var(--pub-bg); color: var(--pub); }
/* Under 480px the bar has no room for the key, so the publish chip names
   itself instead. */
@media (max-width: 479px) {
  .cal-chip.is-pub .cal-chip-who::before { content: "Post · "; font-weight: 500; }
}
.is-narrow .cal { grid-template-columns: minmax(0, 1fr); }
.is-narrow .cal-dow, .is-narrow .cal-day.is-out, .is-narrow .cal-day.is-empty { display: none; }
.is-narrow .cal.is-blank { display: none; }
.is-narrow .cal-none { display: block; }
.is-narrow .cal-num small { display: inline; }

/* CAPACITY. The week's recorded hours against each person's capacity: a bar
   a person can read their own week off, and a manager the team's. It is a
   planning figure beside the record of what was pressed, never a measure of
   anybody's attention. */
.capstrip { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px 24px; margin: 0 0 16px; padding: 12px 16px; background: var(--card); border: 1px solid var(--card-line); border-radius: var(--radius); }
.caprow { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 10px; align-items: center; font-size: 12.5px; min-width: 0; }
.caprow.is-over .caprow-fig { color: var(--ink); font-weight: 600; }
.capbar i { display: block; height: 100%; background: var(--ink-soft); border-radius: 3px; }
.caprow.is-over .capbar i { background: var(--warn); }

/* The bell is the Activity record's shape (`.headlink`: the same glyph size,
   the same mute ink, the same 8px fill on hover and while open), never the
   account's circle: the circle is the avatar's, and a ring drawn round the
   bell while its panel was open made it the one control in the bar with an
   outline. */
.notif-btn {
  position: relative; display: grid; place-items: center; width: var(--ctl-h); height: var(--ctl-h);
  border: 0; border-radius: 8px; background: none; padding: 0; color: var(--ink-mute);
  transition: background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.notif-btn svg { width: 15px; height: 15px; stroke-width: 1.7; }
@media (hover: hover) { .notif-btn:hover { color: var(--ink); background: var(--line-soft); } }
.notif-btn[aria-expanded="true"] { color: var(--ink); background: var(--line-soft); }
/* A count of what is unread is a fact, never the action blue: the ink
   surface and its own ink (SHELL-4, 2026-10-22). */
.notif-count {
  position: absolute; top: 4px; right: 3px; min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 8px; background: var(--fill); color: var(--on-fill);
  font-size: 11px; font-weight: 600; line-height: 16px; text-align: center;
}
.kmenu.notif-menu { width: 340px; max-width: calc(100vw - 32px); padding: 8px; right: 0; }
/* On a phone the bell sits two controls in from the right edge, so a 340px
   panel hung off its right side ran past the left edge of the screen and the
   first character of every line was cut. It takes the page's own gutters
   instead, fixed to the viewport under the head. */
@media (max-width: 640px) {
  .kmenu.notif-menu { position: fixed; top: calc(var(--head-h) + 6px); left: 16px; right: 16px; width: auto; max-width: none; }
}
.notif-head .btn { min-width: 0; }
.notif-item { display: grid; gap: 2px; width: 100%; text-align: left; padding: 8px 10px; border: 0; border-radius: 8px; background: none; font: inherit; color: var(--ink); cursor: pointer; min-height: var(--ctl-h); }
@media (hover: hover) { .notif-item:hover { background: var(--sunk); } }
.notif-item b { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.notif-item span { font-size: 12.5px; color: var(--ink-soft); }
.notif-item small { font-size: 12px; color: var(--ink-mute); }
/* A notice leads with the glyph of the section it opens (2026-10-08): the
   rail's own drawing on a tonal tile, the words in the second column. */
.notif-item.has-tile { grid-template-columns: 32px minmax(0, 1fr); column-gap: 12px; align-items: start; }
.notif-item.has-tile > :not(.notif-tile) { grid-column: 2; }
.notif-item > .notif-tile { grid-column: 1; grid-row: 1 / span 3; width: 32px; height: 32px; border-radius: var(--radius-ctl-sm);
  background: var(--tonal); color: var(--ink-soft); display: grid; place-items: center; }
.notif-tile svg { width: 16px; height: 16px; stroke-width: 1.8; }
.notif-item.has-tile > b:empty { display: none; }
/* What was read stays a week under Earlier, quieter than what is new. */
.notif-earlier { margin: 6px 10px 2px; font-size: 12px; font-weight: 600; color: var(--ink-mute); }
.notif-item.is-read b { font-weight: 500; color: var(--ink-soft); }
/* A notice a colleague sent (js/notice.js): its title wraps, its message
   shows three lines and opens whole on a press, as typed. */
.notif-item.is-notice b { white-space: normal; overflow-wrap: anywhere; }
.notif-item.is-notice .notif-msg { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
  white-space: pre-line; overflow-wrap: anywhere; }
/* =========================================================================
   THE COMMAND BAR ON A PHONE: ONE ROW, THE FILTERS BEHIND ONE BUTTON.
   At 390 the bar was four rows and 430px of controls before the first
   record (the search, the view segment, two rows of selects, the count and
   the action), which is the bar doing at a phone width exactly what it does
   at a desk. The two controls used all day keep the bar; the selects, the
   count and the `?` go behind a Filters button and come up in a sheet from
   the floor (`js/cmdbar.js`), which is the shape every list on a phone has.
   Above 640 nothing here draws and the bar is what it was.
   ========================================================================= */
.cmdbar-search, .cmdbar-filters, .cmd-more { display: none; }
/* A declared rare act (`data-rare`) is behind the bar's ⋯ at every width
   (SHELL-5, 2026-10-22): its ⋯ draws at a desk too, and the button itself
   never does; the items a phone alone moves there stay out at a desk. */
.cmdbar-acts .cmd-more.has-rare { display: inline-block; position: relative; }
.cmdbar-acts .cmd-rare { display: none; }
@media (min-width: 641px) { .cmd-more-menu .cmd-phone-only { display: none; } }
.cmdbar-acts .cmd-more.has-rare .cmd-more-btn {
  display: inline-grid; place-items: center; position: relative; padding: 0;
  width: var(--ctl-h-sm); min-width: 0; height: var(--ctl-h-sm);
  border: 1px solid transparent; border-radius: var(--radius-ctl-sm);
  background: var(--tonal); color: var(--ink);
}
.cmdbar-acts .cmd-more.has-rare .cmd-more-btn[aria-expanded="true"] { background: var(--tonal-press); }
.cmdbar-badge {
  position: absolute; top: 4px; right: 4px; min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 8px; background: var(--action); color: var(--on-action);
  font-size: 11px; font-weight: 600; line-height: 16px; text-align: center;
}
.cmdbar-badge[hidden] { display: none; }
.cmdsheet-field .select { width: 100%; }
/* AT A DESK THE FILTERS ARE BEHIND ONE BUTTON TOO (2026-10-01). The bar
   keeps its views (`data-nofilter`: whose work, the workflow, a month); the
   filters wait in a card that hangs from the Filters mark (`#cmdPop`, laid by
   `ADspaceMenu.pop`), so every bar is one shape at any width. */
.cmdbar-filters[hidden] { display: none !important; }
.cmdpop {
  position: fixed; z-index: 50; width: min(300px, calc(100vw - 32px));
  background: var(--card); border: 1px solid var(--line);
  padding: 16px; display: flex; flex-direction: column; gap: 12px;
  transform-origin: top left; animation: mk-menu-in var(--t) var(--ease-out);
}
.cmdpop-title { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; color: var(--ink); }
.cmdpop-body .select { width: 100%; }
@media (min-width: 641px) {
  .cmdbar .cmdbar-filters {
    display: inline-grid; place-items: center; position: relative; padding: 0;
    width: var(--ctl-h-sm); min-width: 0; height: var(--ctl-h-sm); flex: 0 0 auto;
    border: 1px solid transparent; border-radius: var(--radius-ctl);
    background: var(--tonal); color: var(--ink);
  }
  @media (hover: hover) { .cmdbar .cmdbar-filters:hover { background: var(--tonal-hover); } }
  .cmdbar .cmdbar-filters[aria-expanded="true"] { background: var(--tonal-press); }
  .cmdbar .cmdbar-filters svg { width: 15px; height: 15px; }
}
/* Drawn only while the search is open, and only on a phone: with no rule of
   its own at a desk it rendered as a bare button under the search box. */
.cmdbar-find-clear {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: 30px; height: 30px; display: none; place-items: center;
  border: 0; border-radius: 8px; background: none; color: var(--ink-mute); padding: 0;
}
.cmdbar-find-clear svg { width: 15px; height: 15px; stroke-width: 1.8; }
/* The padlock beside Performance: shut and mute while the code is needed,
   open and ink while the reviews are unlocked. */
/* A tab strip and a control of the pane beside it: the strip at its own
   width, the control at the row's right end, outside the track, so it never
   reads as another tab (the user, 2026-09-27, of the Performance padlock).
   The row owns the strip's step to the pane, and leaves when both are gone. */
.tabline { display: flex; align-items: center; gap: 12px; margin: 0 0 12px; min-width: 0; }
.tabline:not(:has(> :not([hidden]))) { display: none; }
.perf-tools[hidden] { display: none; }
.perf-views > .perf-tools.is-in-views { order: 2; margin-left: auto; }
.perf-lockicon.is-open { color: var(--ink); }
.perf-lockicon[hidden] { display: none; }
.perf-lockicon svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
@media (hover: hover) { .perf-lockicon.is-open:hover { background: var(--tonal); } }
/* THE VIEW SEGMENT STANDS STILL (2026-09-24). My Work's segment sat after a
   420px search box that the report and workload views take away, so the
   segment jumped left and right as the view changed under it (reported by
   the user with three screenshots). At a desk the segment leads the bar and
   the search is the phone's mark that grows into the field: whatever a view
   adds or removes after it, the segment is at the same x. Every other
   console bar takes the same mark (2026-09-25, the user: "other pages still
   have a huge large search bar"), so a search is one shape everywhere. */
.cmdbar-search[hidden] { display: none !important; }
@media (min-width: 641px) {
  .cmdbar .cmdbar-search {
    display: inline-grid; place-items: center; position: relative; padding: 0;
    width: var(--ctl-h-sm); min-width: 0; height: var(--ctl-h-sm); flex: 0 0 auto;
    border: 1px solid transparent; border-radius: var(--radius-ctl);
    background: var(--tonal); color: var(--ink);
    transition: max-width var(--t-slow) var(--ease), width var(--t-slow) var(--ease),
                opacity var(--t) var(--ease-out), margin var(--t-slow) var(--ease);
  }
  @media (hover: hover) { .cmdbar .cmdbar-search:hover { background: var(--tonal-hover); } }
  .cmdbar .cmdbar-search.is-set { border-color: var(--ink-mute); }
  /* Collapsed, the field costs nothing: no width and none of the bar's gap. */
  .cmdbar .cmdbar-find {
    flex: 0 0 auto; width: 0; max-width: 0; min-width: 0; margin-left: -10px;
    opacity: 0; overflow: hidden;
    transition: width var(--t-slow) var(--ease), max-width var(--t-slow) var(--ease),
                margin var(--t-slow) var(--ease), opacity var(--t) var(--ease-out);
  }
  .cmdbar.is-searching .cmdbar-search {
    width: 0; max-width: 0; margin-left: -10px; opacity: 0; border-width: 0; overflow: hidden;
  }
  }
@media (max-width: 640px) {
  /* While it is open the field is the row; everything else collapses the
     same way and comes back when the search closes. */
  .cmdbar.is-searching .cmdbar-search,
  .cmdbar.is-searching .cmdbar-filters,
  .cmdbar.is-searching .cmdbar-end {
    flex: 0 0 0; max-width: 0; min-width: 0; width: 0; opacity: 0;
    padding-left: 0; padding-right: 0; border-width: 0; overflow: hidden;
    transition: max-width var(--t-slow) var(--ease), width var(--t-slow) var(--ease),
                opacity var(--t) var(--ease-out), padding var(--t-slow) var(--ease);
  }
  .cmdbar > .select { display: none; }
  /* Two icon controls in the bar, one shape: the card ground and the control
     edge every select has, so they read as controls of the bar and not as
     row marks. */
  .cmdbar-search, .cmdbar-filters, .cmd-more-btn {
    display: inline-grid; place-items: center; position: relative; padding: 0;
    width: var(--ctl-h-sm); min-width: 0; height: var(--ctl-h-sm);
    border: 1px solid transparent; border-radius: var(--radius-ctl);
    background: var(--tonal); color: var(--ink);
  }
  .cmdbar-filters[aria-expanded="true"], .cmd-more-btn[aria-expanded="true"] { background: var(--tonal-press); color: var(--ink); }
  /* A search that is holding a value says so while it is shut, the way the
     Filters mark does: the list is filtered and the reason has to be on the
     screen somewhere. */
  .cmdbar-search.is-set { border-color: var(--ink-mute); color: var(--ink); }
  /* The primary keeps its fill and gives up its word; the word is still its
     name. A second action is behind the ⋯ beside it. */
  .cmdbar-acts .cmd-primary { width: var(--ctl-h-sm); height: var(--ctl-h-sm); min-width: 0; padding: 0; }
  .cmdbar-acts .cmd-secondary { display: none; }
  .cmdbar-views { flex: 1 1 100%; order: 5; }
  .cmdbar .btn { flex: 0 0 auto; }
}

/* =========================================================================
   NO FIELD UNDER 16px WHERE A FINGER USES IT
   =========================================================================
   iOS zooms the page the moment a field under 16px takes focus, and it does
   not zoom back out: the reader is left on a page a third too wide, hunting
   for the control they were about to use, and every tap after that lands
   somewhere they did not aim. `--field-text` has said 16px under a coarse
   pointer for months, and six rules stated their own size past it — the small
   input and select, the state select, the note box, the link row, the draft
   box — so every one of those was a zoom on a phone and nothing measured it.

   It sits at the foot of the file on purpose. Those rules are further down,
   so a block earlier in the file loses to them whatever it says, and the
   alternative is an override on each one: six unexplained hacks rather than
   one stated rule at the end.

   Deliberately not `user-scalable=no` or `maximum-scale=1`, which stop the
   zoom by taking pinch zoom away from everybody: that fails WCAG 1.4.4 and
   the 200% browser zoom pass the matrix runs at 1280 and 1440. A field at the
   size a phone reads is the fix; refusing the reader their own zoom is not.

   The `:root` in front is what makes it land. Two of the rules it has to
   beat are two-class selectors written for their own reasons —
   `.select-sm.state-select` and `.input.readfield` — so a bare class here
   loses to them wherever it matters most. One stated device in one place,
   rather than a `!important` or a copy of each selector with a size bolted
   on.

   **A file field is exempt with the readonly ones**, for the same reason:
   pressing it opens the system file picker and raises no keyboard, so it can
   never zoom the page. It was listed above as a rule to beat and that was
   wrong. The cost was visible: the field's own `no files selected` came out
   at 16px beside a 12.5px `Choose Files` inside it, so the words the browser
   writes shouted over the button somebody actually presses. */
@media (pointer: coarse) {
  :root .input:not([readonly]):not([disabled]):not([type="file"]),
  :root .select:not([disabled]), :root .textarea:not([readonly]):not([disabled]),
  :root .input-sm:not([readonly]):not([disabled]):not([type="file"]),
  :root .select-sm:not([disabled]), :root .state-select:not([disabled]),
  :root input:not([readonly]):not([disabled]):not([type="file"]),
  :root select:not([disabled]), :root textarea:not([readonly]):not([disabled]) { font-size: max(15px, var(--field-text)); }
  /* A small control is small on a phone too: the desk's type a step up, not
     the field floor, so a status select no longer outweighs the name it
     sits beside. */
  :root .select-sm:not([disabled]), :root .input-sm:not([readonly]):not([disabled]) { font-size: 14px; }
  :root .state-select:not([disabled]) { font-size: 13.5px; }
}
/* **A field nobody can type into cannot zoom the page**, so the floor above is
   not its floor. The rule is about the keyboard: iOS zooms when a field takes
   focus *for typing*, and a `readonly` field never does. Applied to one, it
   did real harm — the client's review link is a token to read and copy, set in
   the mono face at 12.5px, and 16px mono made it the largest thing on the
   card, wider and heavier than the client's own name above it and than the two
   buttons beside it. Reported from a phone on 2026-09-21. `.readfield` is the
   shape this portal gives a value that is only read; its size is the size the
   thing it holds is set in. */

/* The workflow as thin bars, one a step: done quiet, current in the action
   colour, the rest a hairline. Not buttons, and named for a screen reader. */
.tsteps { margin: 0 0 14px; }
.tsteps[hidden] { display: none; }
.tsteps-bar { list-style: none; margin: 0; padding: 0; display: flex; gap: 3px; }
.tsteps-bar li { flex: 1 1 0; height: 4px; border-radius: 2px; background: var(--line); }
.tsteps-bar li.is-done { background: var(--line-ctl); }
.tsteps-bar li.is-now { background: var(--fill); }
.tsteps-word { margin: 8px 0 0; font-size: 12.5px; color: var(--ink-mute); }
.tsteps-word b { color: var(--ink); font-weight: 600; }
/* A blocker is the warn colour and says so in words; the list under it is
   each thing that is missing, and the button is the first of them. */
.tnext-list { margin: 8px 0 0; padding-left: 18px; font-size: 13.5px; color: var(--ink); }
.tnext-list[hidden] { display: none; }
.tnext-late { margin: 8px 0 0; font-size: 13px; font-weight: 600; color: var(--err); }
.tnext-late[hidden] { display: none; }

/* The rail's facts: a label beside its value, one a line, because a rail is
   read down. The value can carry its own control (Change beside the owner,
   the pen on a date) and its own small print under it. */
.tfacts { display: grid; gap: 8px; margin: 0; }
.tfacts:empty { display: none; }
.tfacts > div { display: grid; grid-template-columns: 112px minmax(0, 1fr); gap: 12px; align-items: baseline; }
/* A detail changed where it is read keeps the in-place standard (js/ask.js,
   2026-10-04): the value in its own words with its chevron, no box line,
   pulled back so it starts where the other values do; the tonal ground only
   while pointed at or open (the user, 2026-10-08: "the design ui for
   changes or edits are different from what we last discussed"). */
.select.tdetail {
  width: auto; max-width: 100%; border-color: transparent; background-color: transparent;
  margin-left: -9px; padding-left: 8px; font-size: 13px; color: var(--ink);
}
@media (hover: hover) { .select.tdetail:hover { background-color: var(--tonal); } }
.select.tdetail:focus-visible, .select.tdetail:focus { background-color: var(--tonal); }
.tfacts dt { font-size: 13px; color: var(--ink-mute); }
/* A review's facts sit in a wide card, so the label track is measured
   against the longest label a review prints ("Budget pacing variance") and
   never wraps it; 112px folded three of them onto two lines (the user,
   2026-09-26). A narrow card keeps the rail's own track. */
.perf-facts > div { grid-template-columns: 176px minmax(0, 1fr); gap: 16px; }
.perf-facts dt { white-space: nowrap; }
.tfacts dd {
  margin: 0; min-width: 0; font-size: 13.5px; color: var(--ink); overflow-wrap: anywhere;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px;
}
.tfacts .linkbtn {
  min-height: var(--ctl-h); margin-block: calc((20px - var(--ctl-h)) / 2); padding: 0 6px;
  font-size: 13px; color: var(--ink-soft); cursor: pointer;
}
.tfacts .linkbtn[hidden], .towner[hidden], .towner-pick[hidden] { display: none; }
.tdate svg { width: 13px; height: 13px; color: var(--ink-mute); flex: 0 0 auto; }
/* On the card the pen ends every date row on one right edge, and a detail's
   chevron on the same edge (the user, 2026-10-11: "the pen is here and
   there"): the date takes its row, its words at the left. */
.tfacts dd > .tdate { flex: 1 1 auto; justify-content: space-between; margin-right: -8px; }
.tdate.is-over, .tdate-read.is-over { color: var(--err); font-weight: 600; }
.tdate-pick { width: 100%; max-width: 190px; }
.tmore[hidden] { display: none; }
.tmore > summary {
  display: inline-flex; align-items: center; gap: 6px; min-height: var(--ctl-h);
  margin-left: -8px; padding: 0 8px; border-radius: var(--radius-ctl);
  font-size: 13px; color: var(--ink-soft); cursor: pointer; list-style: none;
}
.tmore > summary::-webkit-details-marker { display: none; }
.tmore > summary::after {
  content: ""; width: 6px; height: 6px; margin-left: 2px;
  border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
  transform: rotate(45deg) translateY(-2px); transition: transform var(--t) var(--ease);
}
.tmore[open] > summary::after { transform: rotate(-135deg) translateY(-1px); }
@media (hover: hover) { .tmore > summary:hover { background: var(--line-soft); color: var(--ink); } }
.givelist { margin: 0; padding-left: 16px; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

@media (max-width: 640px) {
  .tfacts > div { grid-template-columns: 104px minmax(0, 1fr); }
}

.tcheck {
  position: relative; width: var(--ctl-h); height: var(--ctl-h); margin: calc((22px - var(--ctl-h)) / 2) 0;
  border: 0; background: none; cursor: pointer; flex: 0 0 auto;
}
.tcheck::before {
  content: ""; position: absolute; left: 50%; top: 50%; width: 18px; height: 18px;
  margin: -9px 0 0 -9px; border: 1.6px solid var(--line-ctl); border-radius: 50%;
  transition: background var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out);
}
.tcheck::after {
  content: ""; position: absolute; left: 50%; top: 50%; width: 5px; height: 9px;
  margin: -6px 0 0 -3px; border-right: 2px solid var(--on-ok); border-bottom: 2px solid var(--on-ok);
  transform: rotate(45deg) scale(0); transition: transform var(--t-fast) var(--ease-out);
}
.tcheck.is-done::before { background: var(--ok-solid); border-color: var(--ok-solid); }
.tcheck.is-done::after { transform: rotate(45deg) scale(1); }
.tcheck[hidden] { display: none; }
/* A content deliverable finishes through its workflow, so its row carries a
   quiet mark in the circle's place and no control. */
/* The ring stands where the checkbox would, on the title's own line. */
.tring { width: 18px; height: 18px; }
/* The owner and the due date are the row's own controls: the value, at the
   control floor, with the negative margin that keeps a 38px target from
   setting the height of a 20px line. */
.tinline {
  display: inline-flex; align-items: center; max-width: 100%; min-height: var(--ctl-h);
  /* A word as short as Today is still a whole target. */
  min-width: var(--ctl-h);
  margin: calc((20px - var(--ctl-h)) / 2) 0 calc((20px - var(--ctl-h)) / 2) -8px; padding: 0 8px;
  border: 0; border-radius: var(--radius-ctl); background: none; color: inherit;
  font: inherit; font-size: 13px; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (hover: hover) { .tinline:hover { color: var(--ink); } }
.tinline[hidden] { display: none; }
.tinline-pick { width: 100%; min-width: 0; }
.task-due.is-over .tinline, .task-due.is-over > span { color: var(--err); font-weight: 600; }
/* ---------- The task, opened from a list ----------
   The portal's own sheet, docked to the right at a desk so the list it was
   opened from stays on the screen beside it, and the bottom sheet every other
   form is on a phone. It sits one layer under the other sheets (85 against
   90), because the date, the move and the delete sheets open over it. Inside,
   one card a question on the page's own ground: where it stands and what to
   do next, the facts, the brief, the checklist, the files, the talk, the time
   and what happened lately. The cards are what the user asked for on
   2026-09-23; a column of hairlines had read as one long form. */
.sheet.sheet-side { z-index: 85; justify-content: flex-end; align-items: stretch; padding: 0; }
.sheet-side > .sheet-card.taskcard {
  width: min(520px, 100%); max-height: none; height: 100%;
  padding: 0; border-radius: 0; background: var(--page);
  box-shadow: none; border-left: 1px solid var(--line);
  animation: mk-side-in var(--t-slow) var(--ease);
}
@keyframes mk-side-in { from { transform: translateX(28px); opacity: 0; } to { transform: none; opacity: 1; } }
.qhead {
  display: block; margin: 0; padding: 10px 20px 14px;
  background: var(--card); border-bottom: 1px solid var(--line);
}
.stagepick .state-select { width: auto; min-width: var(--state-w); max-width: 220px; }
.qhead-title { margin: 0; font-size: 19px; font-weight: 600; letter-spacing: -.02em; line-height: 25px; overflow-wrap: anywhere; }
.qhead-ctx { margin: 4px 0 0; font-size: 13px; color: var(--ink-soft); }
.qhead-ctx[hidden] { display: none; }
.sheet-side .qbody {
  flex: 1 1 auto; margin: 0; padding: 16px 16px 24px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 16px; -webkit-overflow-scrolling: touch;
}
/* More: the cards a day's work rarely needs, folded under one quiet line. */
.qmore > summary {
  display: flex; align-items: center; gap: 10px; min-height: var(--ctl-h); padding: 0 4px;
  list-style: none; cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ink-soft);
}
.qmore > summary::-webkit-details-marker { display: none; }
.qmore > summary::after {
  content: ""; width: 7px; height: 7px; flex: 0 0 auto; order: 3;
  border-right: 1.6px solid var(--ink-mute); border-bottom: 1.6px solid var(--ink-mute);
  transform: translateY(-2px) rotate(45deg); transition: transform var(--t) var(--ease);
}
.qmore[open] > summary::after { transform: translateY(1px) rotate(-135deg); }
.qmore-sum { margin-left: auto; order: 2; font-size: 12.5px; font-weight: 400; color: var(--ink-mute);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qcard[hidden] { display: none; }
.qcard-title { margin: 0 0 8px; font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
/* A card's heading and its one quiet action share a line; the action's 32px
   target does not set the height of a 20px heading. */
.qcard-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 8px; }
/* No floor on its width: a centred word in a 116px box ended short of the
   card's right margin while "Add comment" reached it, so the quiet actions
   down the sheet had two right edges. */
.qcard-act { min-width: 0; margin-block: calc((20px - var(--ctl-h-sm)) / 2); margin-right: -8px; }
.qcard-act svg { width: 13px; height: 13px; margin-right: 4px; flex: 0 0 auto; }
.qcard-act[hidden] { display: none; }
.qform[hidden] { display: none; }
.qform > .input, .qform > textarea.input, .qform .field > .input, .qform .field > .select { width: 100%; }
.qform .row > .qform-kind { flex: 0 0 132px; }
.qform .row > .qform-kind + .field { flex: 1 1 0; min-width: 0; }
.qform-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
.qform-acts .btn { flex: 0 0 auto; }
.qempty { margin: 0; font-size: 13.5px; color: var(--ink-mute); }
.qitem > :first-child { flex: 1 1 auto; min-width: 0; }
.qitem + .qitem { border-top: 1px solid var(--line-soft); }
.qlinks, .qcomments { list-style: none; margin: 0; padding: 0; }
.qlinks li:first-child, .qcomments li:first-child { padding-top: 2px; }
.qlink-main { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.qlink-main a, .qlink-main span.qlink-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; color: var(--ink); }
.qcomments p { margin: 0; font-size: 13.5px; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
.qcomments small { display: block; margin-top: 2px; font-size: 12px; color: var(--ink-mute); }
.qhead-name .rec-pen {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: var(--ctl-h); height: var(--ctl-h); margin-block: calc((25px - var(--ctl-h)) / 2);
  border: 0; border-radius: var(--radius-ctl); background: none; color: var(--ink-mute); cursor: pointer;
}
.qhead-name .rec-pen svg { width: 15px; height: 15px; }
@media (hover: hover) { .qhead-name .rec-pen:hover { background: var(--line-soft); color: var(--ink); } }
/* The hairline between items is the item row's, so it runs under the ⋯ as
   well; the check row's own rule would stop short of it. A single-line row
   centres its ⋯ on the line, inside the row's own height. */
.qcard .checkrow { padding: 6px 0; }
/* The step: what it is, why, and the one action. A blocker is the warn
   colour and says so in words; the next person rides on the same press. */
.qnext-title { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -.01em; line-height: 1.35; }
.qnext.is-blocked .qnext-title::before { content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 7px; vertical-align: 1px; background: var(--dot-wait); }
.qnext-line { margin: 4px 0 0; font-size: 13.5px; color: var(--ink-soft); overflow-wrap: anywhere; }
.qnext-line:empty, .qnext-line[hidden] { display: none; }
.qhand[hidden] { display: none; }
.qhand-lab[hidden] { display: none; }
.qnext-acts:empty { display: none; }
.qnext-acts .btn { flex: 0 0 auto; }
.qnext .msg:empty { display: none; }
.qrate[hidden] { display: none; }
.qrate-lab { margin: 0 0 4px; font-size: 13px; color: var(--ink-mute); }
.qstar {
  width: var(--ctl-h); height: var(--ctl-h); display: inline-grid; place-items: center;
  border: 0; border-radius: var(--radius-ctl); background: none; color: var(--line-ctl); cursor: pointer; padding: 0;
}
.qstar svg { width: 20px; height: 20px; }
.qstar.is-on { color: var(--ink); }
@media (hover: hover) { .qstar:hover { background: var(--line-soft); } }
.qrate-note { margin: 4px 0 0; font-size: 12.5px; color: var(--ink-mute); }
/* The task's key details, shown, never folded (the user, 2026-10-09): who
   has it, then its dates and its details, each group under a small heading
   on the sheet's 13/600 and a hairline, as a form's sections are. */
.qkey-h { margin: 16px 0 8px; padding-top: 16px; border-top: 1px solid var(--line-soft);
  font-size: 13px; font-weight: 600; letter-spacing: -.005em; color: var(--ink); }
.qkey .msg:empty { display: none; }
/* A report task's report: where it stands and the way to it. */
.qreport-line { margin: 0; font-size: 13.5px; color: var(--ink-soft); }
.qreport .msg:empty { display: none; }
.stephand[hidden] { display: none; }
@media (max-width: 640px) {
  .sheet-side > .sheet-card.taskcard {
    width: 100%; height: auto; max-height: 92vh; border-left: 0;
    border-radius: var(--radius) var(--radius) 0 0;
    animation: mk-sheet-up var(--t-slow) var(--ease);
  }
  @supports (max-height: 92dvh) { .sheet-side > .sheet-card.taskcard { max-height: 92dvh; } }
  .sheet-side .qbody { padding: 12px 12px calc(20px + env(safe-area-inset-bottom, 0px)); gap: 12px; }
  .qnext-acts .btn { flex: 1 1 0; min-width: 0; white-space: nowrap; }
  .qform-acts .btn { flex: 1 1 0; min-width: 0; }
  /* One field over the other. A column row keeps each field at its own
     height: `.row > div`'s 180px basis is a height in a column, and it drew
     the label a field's height above its box and let the row wrap sideways,
     Kind past the card's edge (the user's iPhone, 2026-09-27). */
  .qform .row { flex-direction: column; flex-wrap: nowrap; }
  .qform .row > div, .qform .row > .qform-kind, .qform .row > .qform-kind + .field { flex: 0 0 auto; width: 100%; }
  }

.work-more[hidden] { display: none; }
.work-more-btn {
  display: inline-grid; place-items: center; padding: 0;
  width: var(--ctl-h-sm); min-width: 0; height: var(--ctl-h-sm);
  border: 1px solid transparent; border-radius: var(--radius-ctl);
  background: var(--tonal); color: var(--ink);
}
@media (hover: hover) { .work-more-btn:hover { background: var(--tonal-hover); } }
.work-more-btn[aria-expanded="true"] { background: var(--tonal-press); color: var(--ink); }

/* Several tasks at once. It stays under the console bar while the list
   scrolls, so the count and the two acts are never a screen from the ticks. */
.bulkbar {
  position: sticky; top: var(--head-h); z-index: 20;
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px;
  margin: 0 0 12px; padding: 8px 12px;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.bulkbar[hidden] { display: none; }
/* Inside a card the bar is shaded, never a second outline (2026-10-20, REV-7). */
.bulkbar.is-incard { background: var(--sunk); border-color: transparent; }
/* Red on the tonal button over the dark shade is 4.37:1; the heavier red
   holds 4.5 there (2026-10-21, the Select posts bar's Delete). */
:root[data-theme="dark"] .bulkbar.is-incard { --err-ink: var(--err-hi); }
.bulkbar .tickline { margin: 0; font-size: 13.5px; font-weight: 600; font-variant-numeric: tabular-nums; }
.bulkbar-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-left: auto; }
.bulkbar-acts .btn { flex: 0 0 auto; }
/* `.select-sm` fills its row; in the bar it keeps its own width, or the
   acts wrap and Remove falls under the select. */
.bulkbar-acts .select { flex: 0 0 auto; width: auto; }
.trow-pick { width: 18px; height: 18px; accent-color: var(--accent); margin: 0; }
.task-row.is-picked { background: var(--line-soft); }
/* A control's edge is read against what is outside it, and the chosen row's
   fill takes `--line-ctl` to 2.92:1 in light. The untinted stage select on a
   ticked row takes the mute ink instead, which clears 3:1 on the fill in both
   themes; a tinted one keeps its own colour. */
.task-row.is-picked .state-select { background-color: var(--tonal-hover); }
/* On a phone the way out shares the count's line, and the two acts share
   the next as equal halves: three to a line put "Assign task owner" on two
   lines inside its own button. */
@media (max-width: 640px) {
  .bulkbar-acts { order: 2; width: 100%; margin-left: 0; }
  .bulkbar-acts .btn, .bulkbar-acts .select { flex: 1 1 0; min-width: 0; }
}
.bulkbar.is-narrow .bulkbar-acts .btn { flex: 1 1 0; min-width: max-content; white-space: nowrap; }

/* The kind of task is chosen first, at the top of either sheet. */
/* Switching Task / Content deliverable is one sheet changing its form, not
   one sheet leaving and another arriving: no scrim fade, no lift from below,
   only the card's contents settling in. The swap used to hide one sheet,
   read the database, then animate the other in from nothing, which read as
   a stall (reported 2026-09-24). Three classes, so it outranks the phone's
   own `.sheet-card` lift further down the file. */
.sheet.is-swap { animation: none; }
.sheet.is-swap .sheet-card { animation: mk-swap-in var(--t) var(--ease-out); }
@keyframes mk-swap-in { from { opacity: .55; } }
.kindseg { display: flex; margin: 0 0 16px; }
.is-narrow .due-long { display: none; }
.is-narrow .due-short { display: inline; }

/* ---------- Performance reviews (2026-09-24) ----------
   Team > Performance for management, behind the master code, and My
   performance for the person the record is about. The review itself opens
   in the task drawer's own side sheet and cards (`.taskcard`, `.qcard`), so
   a record read beside a list is one shape across the console. */
#teamTabs[hidden], #teamMembersPane[hidden], #teamPerfPane[hidden], #perfOpen[hidden] { display: none; }
/* A LOCK IS A CARD IN THE MIDDLE OF THE PAGE. Both performance locks (the
   master code on Team, the email code on My performance) were a form pinned
   to the pane's top left corner, which read as a fragment of a page rather
   than as the page's one question (reported by the user on 2026-09-24). One
   component: a mark, a title, a line, the field and the action stacked at
   the card's full width, centred in the pane. */
.lockcard {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  max-width: 400px; margin: 48px auto 0; padding: 32px 28px 28px;
  background: var(--card); border: 1px solid var(--card-line);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.lockcard[hidden] { display: none; }
.lockcard-mark {
  display: grid; place-items: center; width: 48px; height: 48px; margin-bottom: 16px;
  border-radius: 50%; background: var(--line-soft); color: var(--ink-soft);
}
.lockcard-mark svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.lockcard-title { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -.01em; text-wrap: balance; }
.lockcard-line { margin: 6px 0 0; font-size: 13.5px; color: var(--ink-soft); overflow-wrap: anywhere; }
.lockcard-form { display: flex; flex-direction: column; gap: 10px; width: 100%; margin-top: 20px; text-align: left; }
.lockcard-form[hidden] { display: none; }
.lockcard-form .input, .lockcard-form .btn { width: 100%; flex: 0 0 auto; }
.lockcard-form .input[hidden], .lockcard-form .btn[hidden] { display: none; }
.lockcard-code::placeholder { letter-spacing: normal; font-size: 15px; }
.lockcard .msg:empty { display: none; }
/* A header row keeps every table's header pad, whatever its row's own. */
.crm-head.perf-row, .crm-head.link-row { padding: 12px 15px 8px; }
.perf-open, .mine-month {
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  min-width: 0; text-align: left;
}
.perf-open {
  align-self: stretch; margin: -10px 0; padding: 10px 0; min-height: var(--ctl-h);
  border: 0; background: none; font: inherit; color: inherit; cursor: pointer;
}
.perf-open b, .mine-month b { font-size: 14px; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.perf-open small, .mine-month small { font-size: 12px; color: var(--ink-mute); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.perf-state, .perf-grade { min-width: 0; }
.perf-chev svg { width: 16px; height: 16px; }
/* A narrow list (the list measures itself: `data-narrow="780"`, because the
   six desk tracks need about that much) reads each row as two lines: who and
   where it stands, then the score, grade and reward as one line of meta. */
.is-narrow .perf-row, .is-narrow .mine-row {
  grid-template-columns: minmax(0, 1fr) 120px var(--ctl-h);
  grid-template-areas: "who state act" "sum sum act";
  gap: 4px 10px;
}
.is-narrow .mine-row { grid-template-columns: minmax(0, 1fr) 120px 16px; }
.is-narrow .perf-row > .perf-sum, .is-narrow .mine-row > .perf-sum { grid-area: sum; display: block; font-size: 12.5px; color: var(--ink-mute); min-height: 16px; }
.perf-grade-big {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 52px; height: 52px; border-radius: var(--radius-sm);
  font-family: var(--font-head); font-size: 28px; font-weight: 600;
  background: var(--sunk); color: var(--ink);
}
.perf-grade-words { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.perf-grade-words b { font-size: 17px; font-weight: 600; letter-spacing: -.015em; }
.perf-grade-words small { font-size: 13px; color: var(--ink-soft); }
.perf-note { margin: 10px 0 0; font-size: 13px; color: var(--ink-soft); }
.perf-quiet { margin: 0; font-size: 13px; color: var(--ink-mute); }
.perf-cat:first-child { border-top: 0; padding-top: 4px; }
.perf-cat-name { display: flex; flex-direction: column; gap: 3px; min-width: 0; font-size: 13.5px; }
.perf-cat-name small { font-size: 12px; color: var(--ink-mute); }
.perf-cat-val small { font-size: 12px; font-weight: 400; color: var(--ink-mute); }
.perf-num { width: 76px; text-align: right; font-variant-numeric: tabular-nums; }
.perf-pct small { font-size: 12px; color: var(--ink-mute); }
.perf-breach:first-child, .perf-dispute:first-child { border-top: 0; padding-top: 2px; }
.perf-breach-what { margin: 0; font-size: 13.5px; white-space: pre-line; }
.perf-breach small, .perf-dispute small { font-size: 12px; color: var(--ink-mute); }
/* Quiet and on the text's own margin: its label starts where the breach's
   words do, not a button's padding further in. */
.perf-void { justify-self: start; min-width: 0; margin-left: -13px; }
.perf-answer { margin: 4px 0 0; padding: 8px 10px; background: var(--sunk); border-radius: var(--radius-sm); font-size: 13px; }
.perf-answer small { display: block; margin-bottom: 2px; }
.perf-dpick textarea[hidden] { display: none; }
.perf-decide [data-show][hidden] { display: none; }
.rp-acc-row.crm-head, .rp-post-row.crm-head { padding: 12px 15px 8px; }
.rp-acc-row.crm-row, .rp-post-row.crm-row { padding: 10px 15px; min-height: 52px; }
.rp-name { display: flex; flex-direction: column; min-width: 0; }
.rp-name b { font-size: 14px; font-weight: 600; letter-spacing: -.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rp-name small { font-size: 12px; color: var(--ink-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A chip on a name stands beside it, never inside the clipped name; where
   both do not fit it takes the line under the name, whole (2026-10-20, RPT-1). */
.rp-line { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; min-width: 0; }
.rp-line > b { min-width: 0; }
.rp-line > .chip { flex: none; }
.sheet-foot > .rp-adfoot { order: 1; margin-right: auto; display: flex; align-items: center; }
.kmenu.rp-idspop { z-index: 93; width: min(320px, calc(100vw - 32px)); padding: 16px; gap: 8px; }
.rp-idspop-title { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; color: var(--ink); }
.rp-idslist { list-style: none; margin: 0; padding: 0; max-height: 240px; overflow-y: auto; }
.rp-idslist .serial-copy { margin-left: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px; font-weight: 400; color: var(--ink-soft); }
.rp-ver, .rp-date, .rp-figs { font-size: 13px; color: var(--ink-soft); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rp-num .mute { color: var(--ink-mute); }
.rp-post-row.crm-head > .rp-n1, .rp-post-row.crm-head > .rp-n2,
.rp-acc-row.crm-head > :nth-child(2), .rp-acc-row.crm-head > :nth-child(3) { text-align: right; }
.rp-go svg { width: 16px; height: 16px; }
.rp-thumb { width: 34px; height: 42px; border-radius: 6px; background: var(--sunk); overflow: hidden; display: block; }
.rp-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.rp-thumb-lg { width: 64px; height: 80px; flex: 0 0 auto; }
/* No picture leaves its cell empty, never a grey square (2026-10-20, RPT-8). */
.rp-thumb.is-empty { background: none; }
/* A message under the head sits one step under it, never the gap and its
   own margin together. */
.rp-head > .msg { margin: 0; }
.rp-who h3 { margin: 0; font-size: 19px; font-weight: 600; letter-spacing: -.02em; }
.rp-meta { margin: 2px 0 0; font-size: 12.5px; color: var(--ink-mute); }
/* The head's facts: each under its label, as wide as its words at a desk,
   two across in a narrow pane with an odd last one taking the row. */
.facts.rp-facts { margin: 0; gap: 12px 32px; grid-template-columns: repeat(auto-fit, minmax(96px, max-content)); }
/* The Sent fact sets itself: the words keep the label's left edge. */
.rp-facts .rp-sentfact { font-size: 14px; font-weight: 400; color: var(--ink); margin-inline-start: -8px; }
.rp-facts .rp-sentfact svg { width: 14px; height: 14px; flex: 0 0 auto; color: var(--ink-mute); }
/* Mark as sent and Send on WhatsApp: two buttons, 8px apart, wrapping as a
   pair where the cell is narrow. */
.rp-facts .rp-sendacts { display: flex; flex-wrap: wrap; gap: 8px; }
.rp-note { margin: 0; font-size: 13px; color: var(--ink); }
.rp-note.is-warn::before { content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 7px; vertical-align: 1px; background: var(--dot-wait); }
/* Sent to the client? (2026-10-06): one line under the head once a
   published report's PDF is drawn, shaded as a box inside a card is, the
   question then its one press and the way to close it. */
.rp-sentask { display: flex; align-items: center; gap: 8px 12px; margin: 0; padding: 6px 6px 6px 14px;
  background: var(--sunk); border-radius: var(--radius-sm); font-size: 13px; color: var(--ink); }
.rp-sentask-q { flex: 1 1 auto; min-width: 0; }
.rp-sentask > .btn { flex: 0 0 auto; }
.rp-actions > .btn { flex: 0 0 auto; }
.rp-actions .btn svg { width: 15px; height: 15px; }
.rp-aidraft .btn { flex: 0 0 auto; }
.rp-ailang { display: flex; align-items: center; gap: 12px; min-width: 0; }
.rp-sec-head .btn svg { width: 14px; height: 14px; }
.rp-group { margin: 0; font-size: 13px; font-weight: 600; color: var(--ink); }
.rp-group .mute { font-weight: 400; color: var(--ink-mute); margin-left: 4px; }
.rp-paste-sum { margin: 8px 0 0; font-size: 12.5px; color: var(--ink-mute); }
.rp-paste-sum.is-warn { color: var(--ink); }
.rp-paste-sum.is-warn::before { content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 7px; vertical-align: 1px; background: var(--dot-wait); }
.is-narrow .rp-acc-row { grid-template-columns: minmax(0, 1fr) 84px auto; grid-template-areas: "name grow act" "fol fol act" "figs figs act"; gap: 4px 12px; }
.is-narrow .rp-acc-row > .rp-num:nth-child(2) { grid-area: fol; text-align: left; font-size: 12.5px; color: var(--ink-mute); }
.is-narrow .rp-acc-row > :last-child { grid-area: act; }
.is-narrow .rp-post-row { grid-template-columns: 34px minmax(0, 1fr) minmax(72px, max-content) auto; grid-template-areas: "th name n1 act" "th date n2 act"; gap: 2px 12px; }
.is-narrow .rp-post-row > .rp-date { grid-area: date; font-size: 12px; color: var(--ink-mute); }
.is-narrow .rp-post-row > .rp-n2 { grid-area: n2; font-size: 12.5px; color: var(--ink-mute); }
.is-narrow .rp-post-row > :last-child { grid-area: act; }
.is-tight .rp-ticks { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.rp-ad-row.crm-head { padding: 12px 15px 8px; }
.rp-ad-row.crm-row { padding: 10px 15px; min-height: 52px; }
.is-narrow .rp-ad-row { grid-template-columns: 34px minmax(0, 1fr) minmax(96px, max-content) auto; grid-template-areas: "th name spend act" "th res cpr act"; gap: 2px 12px; }
.is-narrow .rp-ad-row > .rp-res { grid-area: res; text-align: left; font-size: 12.5px; color: var(--ink-mute); }
.is-narrow .rp-ad-row > .rp-cpr { grid-area: cpr; font-size: 12.5px; color: var(--ink-mute); }
.is-narrow .rp-ad-row > :last-child { grid-area: act; }
.rp-unit { display: none; }
.is-narrow .rp-unit { display: inline; }
.is-narrow .rp-ad-row > .rp-num, .is-narrow .rp-post-row > .rp-num { white-space: nowrap; }
.rp-ad-row.is-picked { background: var(--line-soft); }
.rp-post-row.is-picked { background: var(--line-soft); }
.rp-adbar { margin: 0; }
.rp-steps::-webkit-scrollbar { display: none; }
/* The step strip is the view strip (the user, 2026-10-05): the tonal track,
   one raised surface sliding under the chosen step (`ADspaceForm.thumb`);
   on a narrow pane it scrolls sideways with the edge that has more faded. */
.rp-step { display: flex; align-items: center; gap: 10px; min-height: var(--ctl-h); padding: 8px 12px; min-width: 0;
  border: 2px solid transparent; background-color: transparent; background-clip: padding-box; border-radius: var(--radius-ctl);
  text-align: left; font: inherit; color: var(--ink-soft); cursor: pointer;
  transition: color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out); }
@media (hover: hover) { .rp-step:not(.is-on):hover { background-color: var(--tonal); color: var(--ink); }
  }
.rp-step.is-on { background-color: var(--seg-on); color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); }
.rp-steps.has-thumb { position: relative; isolation: isolate; }
.rp-steps.has-thumb::before {
  content: ""; position: absolute; left: 0; top: 0; z-index: 0; pointer-events: none;
  width: var(--thumb-w, 0); height: var(--thumb-h, 0);
  transform: translate3d(var(--thumb-x, 0), var(--thumb-y, 0), 0);
  border-radius: calc(var(--radius-ctl) - 2px); background: var(--seg-on); box-shadow: inset 0 0 0 1px var(--line);
  opacity: var(--thumb-o, 0);
  transition: transform var(--t-slow) var(--ease), width var(--t-slow) var(--ease),
              height var(--t-slow) var(--ease), opacity var(--t-fast) var(--ease-out);
}
.rp-steps.has-thumb > .rp-step.is-on { background-color: transparent; box-shadow: none; }
.rp-step-n { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%;
  background: var(--card); color: var(--ink-soft); font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; }
.rp-step.is-on .rp-step-n { background: var(--fill); color: var(--on-fill); }
.rp-step.is-done .rp-step-n { background: var(--ok-solid); color: var(--on-ok); }
.rp-step-n svg { width: 13px; height: 13px; }
.rp-step-t { display: flex; flex-direction: column; min-width: 0; }
.rp-step-t b { font-size: 13px; font-weight: 600; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rp-step-t small { font-size: 12px; color: var(--ink-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rp-steps.is-more-end { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent); }
.rp-steps.is-more-start { -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to left, #000 calc(100% - 32px), transparent); }
.rp-steps.is-more-start.is-more-end { -webkit-mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent); }
/* A form inside a step steps its fields 16px apart, as a sheet does. */
.rp-form :where(.row) { margin-top: 16px; row-gap: 16px; }
.rp-form :where(.row .row) { margin-top: 0; }
.rp-aileft.is-out { color: var(--ink); }
.rp-form .rp-tophead { margin: 24px 0 8px; font-size: 12px; font-weight: 600; letter-spacing: 0; color: var(--ink-mute); }
.rp-form > .rp-aidraft + .msg:empty { display: none; }
.rp-hint { margin: 0; font-size: 12.5px; color: var(--ink-mute); }
.rp-stepfoot .msg { margin: 0; }
.rp-stepfoot > .btn { flex: 0 0 auto; }
.rp-head .msg:empty, .rp-stepfoot .msg:empty, .rp-sec > .msg:empty { display: none; }
/* On a narrow pane the head keeps the record head's shape: the name and
   the controls centred on one line (the state, PDF and the ⋯ at the card's
   right edge), then the meta across the card. The verb leaves the PDF
   button, so the three sit together rather than strung across a row of
   their own (the user, 2026-09-28). */
.is-narrow .rp-head-top { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name ctl" "meta meta"; gap: 4px 12px; }
.is-narrow .rp-who { display: contents; }
.is-narrow .rp-pdf-long { display: none; }
.is-narrow .rp-pdf-short { display: inline; }
.rp-rank .select { width: auto; min-width: 160px; }
.rp-ctl .btn svg { width: 14px; height: 14px; }
.rp-check + .rp-check { border-top: 1px solid var(--line-soft); }
.rp-check-mark { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%;
  box-shadow: inset 0 0 0 1.5px var(--line-ctl); color: var(--on-ok); }
.rp-check.is-done .rp-check-mark { background: var(--ok-solid); box-shadow: none; }
.rp-check.is-missing .rp-check-mark { box-shadow: inset 0 0 0 1.5px var(--line-ctl); }
.rp-check-mark svg { width: 13px; height: 13px; }
.rp-check.is-override .rp-check-mark::after { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--dot-wait); }
.rp-check-t { display: flex; flex-direction: column; min-width: 0; }
.rp-check-t b { font-size: 14px; font-weight: 600; }
.rp-check-t small { font-size: 12.5px; color: var(--ink-mute); }
.rp-audit-title { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
.rp-audit-head + .rp-check { border-top: 1px solid var(--line-soft); }
.rp-finding + .rp-check, .rp-f-more + .rp-check, .rp-metatable + .rp-check { border-top: 1px solid var(--line-soft); }
.is-narrow .rp-part { grid-template-columns: 24px minmax(0, 1fr); }
.rp-f-acts .btn { flex: 0 0 auto; min-width: 0; }
.rp-finding.is-applied > :not(.rp-f-acts) { opacity: .55; }
/* What the reader must know or do on the audit card (2026-10-12, the user:
   the card's words "blend together and don't look like warning or
   instructions"): a shaded note under its part, led by the amber dot, in
   ink at the body's size; never a tinted slab. A note the card's message
   line gives (`.msg.warn`) reads the same. */
.rp-anote, .rp-aicheck > .msg.warn:not(:empty) {
  display: flex; gap: 8px; margin: 0 16px 12px 52px; padding: 10px 12px;
  border-radius: var(--radius-sm); background: var(--sunk);
  font-size: 13px; line-height: 1.5; color: var(--ink); overflow-wrap: anywhere;
}
.rp-anote::before, .rp-aicheck > .msg.warn:not(:empty)::before {
  content: ""; flex: 0 0 8px; width: 8px; height: 8px; margin: 6px 0 0; border-radius: 50%; background: var(--dot-wait);
}
.rp-anote + .rp-metatable, .rp-anote + .rp-check { border-top: 1px solid var(--line-soft); }
.rp-m-act .btn { flex: 0 0 auto; min-width: 0; margin-block: calc((22px - var(--ctl-h-sm)) / 2); }
.is-narrow .rp-metahead { display: none; }
.is-narrow .rp-metarow {
  grid-template-columns: repeat(3, minmax(0, 1fr)); row-gap: 6px; padding: 12px 0;
}
.is-narrow .rp-m-num { display: grid; gap: 2px; text-align: left; white-space: normal; overflow-wrap: anywhere; min-width: 0; }
.is-narrow .rp-m-num::before { content: attr(data-l); font-size: 12px; font-weight: 600; color: var(--ink-mute); }
.is-narrow .rp-m-none { display: none; }
.rp-aicheck > .msg:empty { display: none; }
/* Past its due time is late, not merely not done: red, as late is everywhere. */
.rp-check.is-late .rp-check-mark { box-shadow: inset 0 0 0 1.5px var(--err); }
.rp-vrow .rp-name .nb { white-space: nowrap; }
.rh-row.crm-head { padding: 12px 15px 8px; }
.is-narrow .rh-row { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name ver" "name upd" "name sent"; gap: 4px 12px; }
/* Select (2026-10-06): a tick leads each row; the row is its label. */
.rh-row.is-picking { grid-template-columns: 24px minmax(0, 1fr) 64px 110px 20px; cursor: pointer; }
.rh-row.is-picked { background: var(--line-soft); }
.is-narrow .rh-row.is-picking { grid-template-columns: 24px minmax(0, 1fr) auto; grid-template-areas: "pick name ver" "pick name upd" "pick name sent"; }
/* =========================================================================
   THE PHONE'S OWN EDGES (2026-09-26)
   =========================================================================
   Every page carries `viewport-fit=cover`, so on an iPhone the page runs
   under the home indicator and `env(safe-area-inset-*)` reports it. Without
   it every inset read 0, and the padding the sheets already asked for did
   nothing: a sheet's Save sat on the home bar. Each surface that meets an
   edge of the screen pads by that edge's inset, and nothing else moves: on a
   screen with no inset the values are 0 and the desk is unchanged. */
/* The home bar's inset already holds the clearance it needs, so the foot
   takes whichever is larger, 16px or the inset, rather than both: added
   together they set the two words high in the band on an iPhone. */
.portalfoot-in { padding-bottom: max(16px, env(safe-area-inset-bottom, 0px)); }
.confirmbar-in { padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
.sidebar { padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
@media (max-width: 860px) {
  .console-body { padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); }
  .sidebar { padding-left: max(14px, env(safe-area-inset-left, 0px)); }
}
@media (max-width: 640px) {
  .topbar-inner { padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); }
}

/* An empty date field says what it wants. iOS draws nothing at all in an
   empty `input[type=date]` and Chrome draws `mm/dd/yyyy`, so a field marked
   `data-hint` is wrapped by js/form.js (`.datefield`) and the hint replaces
   the browser's own text, starting where the value would (after the glyph
   under a finger) at the value's size. It leaves the moment the field is
   focused, because from then on the browser's editor is what is typed into.
   The same device as the Schedule's `.sched-field`. */
.datefield { position: relative; display: flex; min-width: 0; }
.datefield > .input { width: 100%; }
.datefield.is-empty::after {
  content: attr(data-hint);
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  font-size: var(--field-text); color: var(--ink-mute); pointer-events: none;
}
@media (pointer: coarse) { .datefield.is-empty::after { left: 38px; } }
.datefield.is-empty > .input::-webkit-datetime-edit { opacity: 0; }
.datefield.is-empty:focus-within > .input::-webkit-datetime-edit { opacity: 1; }
.datefield.is-empty:focus-within::after { content: none; }
/* A field standing aside for another (Month while a custom period is open)
   reads as set aside rather than broken. */
.input[type="month"]:disabled, .input[type="date"]:disabled { opacity: .55; cursor: not-allowed; }

/* Pull to refresh in the installed console (js/refresh.js): one glyph that
   comes down with the finger, turns as it travels, takes the ink once a
   release would reload, and turns on its own while the page is fetched. */
.pullmark {
  position: fixed; left: 50%; top: calc(env(safe-area-inset-top, 0px) + 6px); z-index: 70;
  width: 36px; height: 36px; margin-top: -36px; border-radius: 50%;
  display: grid; place-items: center; pointer-events: none;
  background: var(--card); color: var(--ink-mute); box-shadow: var(--shadow-lift);
  opacity: 0; transform: translate(-50%, 0);
  transition: opacity var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.pullmark svg { width: 18px; height: 18px; }
.pullmark.is-armed, .pullmark.is-busy { color: var(--ink); }
@keyframes pullturn { to { transform: rotate(360deg); } }
/* The Turnstile widget (js/captcha.js) draws nothing unless Cloudflare wants
   a box pressed; when it does, it sits under the button that asked. */
.cfbox { margin-top: 8px; }
.cfbox:empty { display: none; }

/* =========================================================================
   THE WAY OUT ON THE LEFT, THE ACTION AT THE RIGHT EDGE (2026-09-26)
   =========================================================================
   Supersedes "primary first, anchored left" (2026-09-20/21). The user pressed
   Cancel while creating something, because the hand leaving the last field
   moves right and Cancel stood to the right of Create. The convention the
   team already knows from Google, Apple, Notion and Stripe is the reverse:
   the way out on the left, the action at the end of the row, on the corner
   the hand finishes on; on a phone the action is the right half, under a
   right thumb. One rule does it for every action row: the markup keeps the
   primary first, so Tab and Cmd + Enter still reach it first, and the row
   draws itself from the right edge. A count that explains the action
   (`.lpicksum`) follows the pair to the left. `uxaudit`'s `order` rule and
   `tests/geom.js` measure it by x. */
.sheet-foot, .changebox-actions, .zone-actions, .qform-acts, .row.acts {
  flex-direction: row-reverse; justify-content: flex-start;
}
/* A required field's label carries a red asterisk (js/form.js reads the
   field's own `aria-required`, which is what a screen reader announces; the
   mark itself is decoration and is given no spoken text). */
.field-label.is-req::after { content: "*"; content: "*" / ""; margin-left: 3px; color: var(--err); font-weight: 600; }

.reclist { display: grid; gap: 4px; min-width: 0; }
.recday { margin: 8px 0 2px; font-size: 12px; font-weight: 600; color: var(--ink-mute); }
.reclines { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.recline {
  display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 0 10px;
  font-size: 12.5px; line-height: 1.5; min-width: 0;
}
.rl-body { margin: 0; min-width: 0; overflow-wrap: anywhere; color: var(--ink); }
.rl-head { min-width: 0; }
.rl-what.is-danger { color: var(--err); }
/* In the Activity record the day stays in view while its lines scroll. */
#activityList .recday { position: sticky; top: 0; z-index: 2; background: var(--card); padding: 6px 0 8px; margin-bottom: 0; }
.recline.is-open .rl-head { white-space: normal; overflow: visible; }
.recline.is-open .rl-detail { display: block; -webkit-line-clamp: unset; line-clamp: unset; overflow: visible; }
.recline.is-long { cursor: pointer; border-radius: 4px; }
@media (hover: hover) { .recline.is-long:hover .rl-detail { color: var(--ink); } }
.reclist.is-wide .rl-by { display: block; color: var(--ink-mute); text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.reclist.is-wide .rl-body { display: flex; align-items: baseline; gap: 10px; min-width: 0; overflow-wrap: normal; }
.reclist.is-wide .rl-head { flex: 0 1 auto; max-width: 62%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.reclist.is-wide .rl-detail { flex: 1 1 0; margin: 0; display: block; -webkit-line-clamp: unset; line-clamp: unset; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.reclist.is-wide .recline.is-long { border-radius: 0; }

/* ---------- One strip (2026-09-27) ----------
   A record's panes and a route's tabs take the view strip's look, asked for
   by the user ("make the slider all secondary accent style like in activity
   record"): the tonal track at the strip's own width, the chosen tab raised
   in white on the thumb that slides (js/form.js), every word whole, and
   sideways scrolling with the edge that has more beyond it fading. It was an
   underline on a rule across the column: two looks for one control. */
.rectabs, .tabrow {
  display: flex; width: max-content; max-width: 100%; gap: 0; padding: 0; border: 0;
  background: var(--tonal); border-radius: var(--radius-ctl); box-shadow: none;
  overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: none;
}
.rectabs { justify-self: start; }
.tabrow::-webkit-scrollbar { display: none; }
/* A record's tabs are its navigation, so they keep the full control's height
   (44px under a finger) where the view strip's options are small controls. */
.rectabs .tab, .tabrow .tab {
  flex: 0 0 auto; margin: 0; min-height: var(--ctl-h); padding: 0 12px;
  border: 2px solid transparent; background-color: transparent; background-clip: padding-box;
  border-radius: var(--radius-ctl); font-size: 12.5px; font-weight: 500; color: var(--ink-soft);
  white-space: nowrap; transition: color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out);
}
.rectabs .tab.is-on, .tabrow .tab.is-on {
  background-color: var(--seg-on); border-color: transparent; color: var(--ink); font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--line);
}
@media (hover: hover) {
  .rectabs .tab:not(.is-on):hover, .tabrow .tab:not(.is-on):hover { background-color: var(--tonal); color: var(--ink); }
}
.rectabs.is-more-end, .tabrow.is-more-end { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent); }
.rectabs.is-more-start, .tabrow.is-more-start { -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to left, #000 calc(100% - 32px), transparent); }
.rectabs.is-more-start.is-more-end, .tabrow.is-more-start.is-more-end { -webkit-mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent); }
/* On a phone every strip is the column's width (the user, 2026-09-28: "Why
   ur tab doesnt go full width. Such awkward width now on mobile?"): at its
   own width the portal's five tabs stopped 11px short of the cards' edge,
   which reads as a mistake rather than a choice. The tabs share what is
   spare, as the view strip's already do; a strip that does not fit scrolls
   as before, and a desk keeps the strip at its own width. The Performance
   padlock stays at the row's right end, after the strip. */
/* A record's strip sits on the card under it, so it runs that card's width
   at a desk too (the user, 2026-09-28: "since you have the full card below,
   why not just fit full width"); a section's two-tab switch keeps its own
   width at a desk, where two tabs across the page would be two slabs. */
.rectabs { width: 100%; }
.rectabs .tab { flex: 1 0 auto; text-align: center; }
/* A set's stage strip sits inside its Posts card, so it runs the card's
   width as a record's strip does (the user, 2026-10-03). */
.tabrow.poststages { width: 100%; }
@media (max-width: 640px) {
  .tabrow { width: 100%; }
  .tabrow .tab { flex: 1 0 auto; text-align: center; }
  }

/* ---- Charts (`js/chart.js`) ------------------------------------------------
   One card per figure, in the page's own tokens so it follows the theme. The
   marks are ink, and a colour only where the word beside it is a state: green
   for a milestone reached, warn for late or over, red for the grade that
   refuses. The figures fold under every chart as a table. */
.chartcard {
  position: relative; background: var(--card); border: 1px solid var(--card-line);
  border-radius: var(--radius); box-shadow: var(--shadow); padding: 18px 20px 14px; min-width: 0;
}
.is-narrow .chartcard, .is-tight .chartcard { padding: 14px 14px 10px; }
.chart-title { margin: 0 0 12px; font-size: 15px; font-weight: 600; letter-spacing: -.01em; color: var(--ink); }
.chart-empty { margin: 0; font-size: 13px; color: var(--ink-mute); }
.chart-plot { min-width: 0; }
.is-narrow .chartgrid, .is-tight .chartgrid { grid-template-columns: minmax(0, 1fr); }
.cb-label { min-width: 0; color: var(--ink); overflow-wrap: anywhere; }
.cb-label small { margin-left: 4px; font-size: 12px; color: var(--ink-mute); }
.cb-label small.is-warn { color: var(--ink); font-weight: 600; }
.cb-label small.is-err { color: var(--err); font-weight: 600; }
.cb-value.is-warn { color: var(--ink); }
.cb-value.is-err { color: var(--err); }
.cb-fill { display: block; height: 100%; min-width: 2px; border-radius: 4px; background: var(--ink-soft); }
.cb-mark { position: absolute; top: -3px; bottom: -3px; width: 2px; margin-left: -1px; border-radius: 1px; background: var(--ink); }

/* The tones every mark shares. */
.cb-fill.is-ok, .cc-bar.is-ok, .cc-seg.is-ok, .chart-key .is-ok { background: var(--ok); }
.cb-fill.is-warn, .cc-bar.is-warn, .cc-seg.is-warn, .chart-key .is-warn { background: var(--warn); }
.cb-fill.is-err, .cc-bar.is-err, .cc-seg.is-err, .chart-key .is-err { background: var(--err); }
.cb-fill.is-mute, .cc-bar.is-mute, .cc-seg.is-mute, .chart-key .is-mute { background: var(--line-ctl); }

.cc-axis { position: relative; align-self: start; height: 140px; min-width: 24px; font-size: 11.5px; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.cc-axis span { position: absolute; right: 0; transform: translateY(50%); white-space: nowrap; line-height: 1; }
.cc-body { position: relative; display: flex; align-items: stretch; min-width: 0; }
.cc-grid i { position: absolute; left: 0; right: 0; height: 1px; background: var(--line-soft); }
.cc-grid i:nth-child(1) { top: 0; }
.cc-grid i:nth-child(2) { top: 50%; }
.cc-grid i:nth-child(3) { top: 100%; background: var(--line); }
.cc-col { position: relative; flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; cursor: default; }
.cc-plot { position: relative; height: 140px; width: 100%; display: flex; align-items: flex-end; justify-content: center; gap: 3px; }
.cc-bar, .cc-stack { width: min(28px, 36%); }
.cc-seg { display: block; width: 100%; background: var(--ink-soft); }
@media (hover: hover) {
  .cc-col:hover .cc-plot { background: var(--sunk); border-radius: 4px 4px 0 0; }
}
.cc-col.is-tipped .cc-plot { background: var(--sunk); border-radius: 4px 4px 0 0; }
.cl-plot svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.cl-dot { position: absolute; width: 8px; height: 8px; margin: 0 0 -4px -4px; border-radius: 50%; background: var(--card); border: 2px solid var(--ink-soft); }
.cl-dot.is-ok { border-color: var(--ok); }
.cl-dot.is-mute { border-color: var(--line-ctl); }
.cl-hit .cc-plot { background: transparent; }

.cp-ring { flex: 0 0 auto; width: 120px; height: 120px; }
.cp-keys { flex: 1 1 auto; min-width: 0; display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.cp-key > i { flex: 0 0 auto; width: 10px; height: 10px; border-radius: 3px; background: var(--ink); transform: translateY(1px); }
.cp-label { min-width: 0; color: var(--ink); overflow-wrap: anywhere; }
.cp-value small { margin-left: 8px; font-weight: 400; color: var(--ink-mute); font-size: 12px; }
/* Bars on one line (2026-10-12): the label, the bar, the figure; a red
   share at the bar's end where part of it is late. */
/* The rows share one grid: the label track is the longest label's, up to
   two fifths, so a label holds its line on a phone and every bar starts on
   one edge (ux-audit OVW-6, 2026-10-15). */
.chart-bars.is-inline { grid-template-columns: fit-content(40%) minmax(0, 1fr) auto; column-gap: 12px; row-gap: 10px; }
.chart-bars.is-inline .cb-track { display: flex; margin-top: 0; overflow: hidden; }
.chart-bars.is-inline .cb-fill:first-child { border-radius: 4px 0 0 4px; min-width: 2px; }
/* A zero is no bar at all: the 2px floor read as a value (2026-10-27, ANL-4). */
.chart-bars .cb-fill.is-zero, .chart-bars.is-inline .cb-fill.is-zero:first-child { min-width: 0; }
.chart-bars.is-inline .cb-fill:last-child { border-radius: 0 4px 4px 0; }
.chart-bars.is-inline .cb-fill:only-child { border-radius: 4px; }
.chart-bars.is-inline .cb-value { margin-left: 0; text-align: right; font-weight: 400; color: var(--ink-soft); }
.cp-key > i.is-ok { background: var(--dot-ok); border-radius: 50%; } .cp-key > i.is-warn { background: var(--dot-wait); border-radius: 50%; }
.cp-key > i.is-err { background: var(--dot-late); border-radius: 50%; } .cp-key > i.is-off { background: var(--dot-off); border-radius: 50%; }
.ovw-ghost { position: absolute; margin: 0; pointer-events: none; z-index: 0; }
.ovw-ghost.is-out-next { animation: ovw-out-next var(--t-slow) var(--ease) forwards; }
.ovw-ghost.is-out-prev { animation: ovw-out-prev var(--t-slow) var(--ease) forwards; }
.ovw.is-in-next > :not(.ovw-bar) { animation: ovw-in-next var(--t-slow) var(--ease) both; }
.ovw.is-in-prev > :not(.ovw-bar) { animation: ovw-in-prev var(--t-slow) var(--ease) both; }
@keyframes ovw-out-next { to { transform: translateX(-100%); } }
@keyframes ovw-out-prev { to { transform: translateX(100%); } }
@keyframes ovw-in-next { from { transform: translateX(100%); } }
@keyframes ovw-in-prev { from { transform: translateX(-100%); } }
.chart-key { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 12px 0 0; padding: 0; list-style: none; font-size: 12px; color: var(--ink-mute); }
.chart-key li { display: inline-flex; align-items: center; gap: 6px; }
.chart-key i { width: 10px; height: 10px; border-radius: 3px; background: var(--ink-soft); }
.chart-key i.is-mark { width: 2px; height: 12px; border-radius: 1px; background: var(--ink); }

.chart-data > summary { padding-top: 6px; min-height: var(--ctl-h-sm); }
.chart-tbl { width: 100%; border-collapse: collapse; font-size: 13px; font-variant-numeric: tabular-nums; }
.chart-tbl th, .chart-tbl td { padding: 6px 0; border-bottom: 1px solid var(--line-soft); text-align: left; font-weight: 400; color: var(--ink); }
.chart-tbl .is-end { text-align: right; padding-left: 16px; white-space: nowrap; }
.chart-tbl tbody tr:last-child > * { border-bottom: 0; }

/* =========================================================================
   NOTIFICATIONS ON THIS DEVICE (js/push.js)
   The client pages' bell in the bar opens a small named dialog, a menu's
   card (`.kmenu`) set under the bell's own right edge; the console's is an
   item in the account menu, answered on a line under it.
   ========================================================================= */
.chrome-actions { position: relative; }
.pushbtn.is-on { color: var(--ink); }
.kmenu.pushpop {
  top: calc(100% + 8px); right: 0; padding: 16px;
  width: min(320px, calc(100vw - 32px));
  display: grid; gap: 8px;
}
.pushpop:focus { outline: none; }
.pushpop-title { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; color: var(--ink); }
.pushpop-line { margin: 0; font-size: 14px; line-height: 1.5; color: var(--ink-soft); }
.pushpop-acts { display: flex; justify-content: flex-end; }
.pushpop .msg { margin: 0; }
.acct-msg { margin: 0; padding: 4px 12px 8px; font-size: 12.5px; line-height: 1.45; }
.headsearch svg { width: 16px; height: 16px; }
.sheet-card.searchcard { width: min(640px, 100%); max-height: min(72vh, 600px); }
.searchfield { position: relative; margin: 8px 0 12px; }
.searchfield .input { width: 100%; padding-left: 36px; }
.searchfield svg {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; stroke: var(--ink-mute); stroke-width: 1.8; fill: none; pointer-events: none;
}
.sgroup-h { margin: 0 0 4px; padding: 0 12px; font-size: 12px; font-weight: 600; color: var(--ink-mute); }
.sgroup-h svg { width: 14px; height: 14px; flex: 0 0 auto; stroke-width: 1.8; }
/* A plain row: the name, its code in the token face, one mute line. */
.srow {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 12px; row-gap: 2px;
  align-items: baseline; width: 100%; min-height: var(--ctl-h); padding: 8px 12px;
  border: 0; border-radius: var(--radius-sm); background: none; color: var(--ink);
  font: inherit; text-align: left; cursor: pointer;
  transition: background var(--t-fast) var(--ease-out);
}
/* The match is marked by weight, never by colour. */
.srow b { font-weight: 600; color: inherit; }
.srow.is-active { background: var(--line-soft); }
@media (hover: hover) {
  .srow:hover { background: var(--sunk); }
  .srow.is-active:hover { background: var(--line-soft); }
}
@media (max-width: 640px) {
  .sheet-card.searchcard { height: 92vh; max-height: 92vh; }
  @supports (height: 92dvh) { .sheet-card.searchcard { height: 92dvh; max-height: 92dvh; } }
  }
/* ===== end console search ===== */

.crm-head.rw-row { padding: 12px 15px 8px; }
.rw-who { display: flex; flex-direction: column; justify-content: center; gap: 2px; min-width: 0; text-align: left; }
.rw-who b { font-size: 14px; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rw-who small { font-size: 12px; color: var(--ink-mute); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rw-open {
  align-self: stretch; margin: -10px 0; padding: 10px 0; min-height: var(--ctl-h);
  border: 0; background: none; font: inherit; color: inherit; cursor: pointer;
}
.rw-c { min-width: 0; font-size: 13.5px; color: var(--ink-soft); font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rw-c small { color: var(--ink-mute); font-size: 12px; }
.rw-c .rw-why { display: block; margin-top: 2px; }
.rw-row .rw-c:has(.rw-why) { white-space: normal; }
.rw-note { margin: 0 0 12px; }
.rw-dl { margin: 0; }
.rw-figs > .crm-group-head h3 { min-height: var(--ctl-h); display: flex; align-items: center; margin-block: calc((22px - var(--ctl-h)) / 2); }
.rw-total { margin: 16px 0 0; font-size: 13.5px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.rw-total:empty { display: none; }
.rw-hint:empty { display: none; }
/* A narrow list reads each row as two lines: who, and the one figure the
   table is for at the right edge; then the rest as one line of meta. */
.is-narrow .rw-row {
  grid-template-columns: minmax(0, 1fr) 120px;
  grid-template-areas: "who key" "sum sum";
  gap: 4px 10px;
}
.is-narrow .rw-row.has-act {
  grid-template-columns: minmax(0, 1fr) 120px var(--ctl-h);
  grid-template-areas: "who key act" "sum sum act";
}
.is-narrow .rw-row > .rw-sum { grid-area: sum; display: block; font-size: 12.5px; color: var(--ink-mute); min-height: 16px; overflow-wrap: anywhere; }
/* ---- End of performance rewards ---- */

.crm-head.init-row { padding: 12px 15px 8px; }
.init-row.is-off > :not(.team-act) { opacity: .55; }
.init-name { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.init-name b { font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.init-name small { font-size: 12px; color: var(--ink-mute); overflow-wrap: anywhere; }
.init-name .init-detail, .init-name .init-note { color: var(--ink-soft); white-space: pre-line; }
.init-state { min-width: 0; }
.is-narrow .init-row {
  grid-template-columns: minmax(0, 1fr) auto var(--ctl-h);
  grid-template-areas: "who state act";
  gap: 4px 10px;
}
.crm-head.ml-row { padding: 12px 15px 8px; }
.ml-row.is-off > :not(.ml-act) { opacity: .55; }
.is-narrow .ml-row { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "who act"; gap: 4px 10px; }
.is-narrow .refl-line, .perfcard .refl-line { grid-template-columns: minmax(0, 1fr); }
#mineSelf[hidden] { display: none; }
.perf-self-sum { margin: 0 0 8px; }
.refl-acts > .btn { flex: 0 0 auto; }
.ctx-init > span { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ctx-init b { font-weight: 600; overflow-wrap: anywhere; }
.ctx-init small { font-size: 12px; color: var(--ink-mute); }
.ctx-none { margin: 0; color: var(--ink-mute); }
/* =========================================================================
   THE CLIENT PAGES' BAR
   The bell, sign out and the language toggle are one family, read left to
   right bell, sign out, 中文 (the user, 2026-09-28): the same height, the
   same corner, the same tonal ground. A circle, a bare glyph and a bordered
   box side by side read as three kinds of thing.
   On a phone the name the page is for takes two lines, its label over it,
   because one line left it "Signed in as k…".
   ========================================================================= */
.topbar .chrome-actions { gap: 8px; }
.topbar .chrome-actions > .pill,
.topbar .chrome-actions > .langtoggle,
.topbar .chrome-actions > .pushbtn {
  height: var(--ctl-h); min-height: var(--ctl-h); min-width: var(--ctl-h);
  border: 0; border-radius: var(--radius-sm); padding: 0 12px;
  background: var(--tonal); color: var(--ink-soft);
  font-size: 13px; font-weight: 500;
}
.topbar .chrome-actions > .pushbtn { width: var(--ctl-h); padding: 0; }
.topbar .chrome-actions > .pushbtn.is-on { color: var(--ink); }
@media (hover: hover) {
  .topbar .chrome-actions > .pill:hover,
  .topbar .chrome-actions > .langtoggle:hover,
  .topbar .chrome-actions > .pushbtn:hover { background: var(--tonal-hover); color: var(--ink); }
}
@media (max-width: 640px) {
  .topbar #signOutBtn { width: var(--ctl-h); padding: 0; justify-content: center; }
  .topbar .brand-for { display: grid; line-height: 1.25; }
  .topbar .brand-for b { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* =========================================================================
   A POPOVER CARD (js/menu.js `pop`)
   The bell's card on the client pages and the section's purpose in the
   console. At a desk it hangs from its control with a caret pointing at it;
   on a phone it docks at the foot of the screen, inside the gutters and
   above the home bar, and rises from the floor like a sheet, because a card
   hanging from a bar control at the top read as dropped somewhere (the
   user, 2026-09-28).
   ========================================================================= */
.popcard { border-radius: var(--radius); box-shadow: var(--shadow-lift); }
.popcard::before {
  content: ""; position: absolute; top: -7px; left: calc(var(--caret, 50%) - 6px);
  width: 12px; height: 12px; background: var(--raised);
  border: 1px solid var(--line-lift); border-right: 0; border-bottom: 0;
  border-top-left-radius: 3px; transform: rotate(45deg);
}
.popcard.is-up::before { top: auto; bottom: -7px; transform: rotate(225deg); }
.popcard.is-up { transform-origin: bottom center; }
.popcard.is-dock {
  position: fixed; top: auto; right: 16px; left: 16px;
  bottom: max(16px, env(safe-area-inset-bottom, 0px));
  width: auto; padding: 20px; border-radius: var(--radius-lg);
  transform-origin: bottom center; animation: popcard-up var(--t-slow) var(--ease);
}
.popcard.is-dock::before { display: none; }
@keyframes popcard-up { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: none; } }
.popcard-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
/* The close mark gives its padding back so the title keeps the card's top. */
.popcard-x { margin: calc((20px - var(--ctl-h)) / 2) calc((16px - var(--ctl-h)) / 2) calc((20px - var(--ctl-h)) / 2) 0; }
.kmenu.pushpop { gap: 8px; padding: 16px; }
.popcard.is-dock .pushpop-acts { --ctl-h-sm: var(--ctl-h); }
.aboutpop.popcard { padding: 14px 16px 16px; }
.aboutpop.is-dock { padding: 18px 20px 20px; }
/* The name at the body's weight beside its mark (the user, 2026-10-09: the
   bold heading read heavy in a small card). */
.aboutpop-name { display: inline-flex; align-items: center; gap: 8px; min-width: 0;
  font-size: 15px; font-weight: 500; letter-spacing: -.01em; color: var(--ink); }
.aboutpop-mark { width: 18px; height: 18px; flex: 0 0 auto; color: var(--action-ink); stroke-width: 2; }
.chip-vic svg { width: 12px; height: 12px; flex: 0 0 auto; stroke-width: 2; }
.cl-mark.is-vic, .rec-mark.is-vic { box-shadow: 0 0 0 2px var(--card), 0 0 0 3.5px var(--vic); }
.vicpop.popcard { display: grid; gap: 8px; width: min(320px, calc(100vw - 16px)); padding: 14px 16px 16px; }
.vicpop.is-dock { padding: 18px 20px 20px; }
.vicpop-name { display: inline-flex; align-items: center; gap: 8px; min-width: 0;
  font-size: 15px; font-weight: 500; letter-spacing: -.01em; color: var(--ink); }
.vicpop-name svg { width: 18px; height: 18px; flex: 0 0 auto; color: var(--vic); stroke-width: 2; }
.vicpop p { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); }
.vicpop .vicpop-note { background: var(--sunk); border-radius: var(--radius-ctl-sm); padding: 8px 10px; color: var(--ink);
  overflow-wrap: anywhere; }
.vicpop .vicpop-foot { font-size: 12.5px; color: var(--ink-mute); }
.vicpop.is-dock p { font-size: 14px; }

/* =========================================================================
   A FIRST-VISIT GUIDE (js/guide.js, the user, 2026-10-07)
   A card, never a cover: on the floating ground like the ⓘ card, hung from
   the control its step names (laid by ADspaceMenu.pop), docked at the foot
   on a phone. The control is ringed in the focus colour while its step
   shows. Nothing under it is dimmed or locked.
   ========================================================================= */
.guidecard {
  position: fixed; z-index: 50;
  width: min(320px, calc(100vw - 32px));
  display: grid; gap: 8px; padding: 14px 16px 16px;
  background: var(--raised); border: 1px solid var(--line-lift);
  border-radius: var(--radius); box-shadow: var(--shadow-lift);
  transform-origin: top center; animation: mk-menu-in var(--t) var(--ease-out);
}
.guidecard[hidden] { display: none; }
/* A guide inside a sheet (Arrange sections, a new WhatsApp message) stands
   above it, under a question asked over it (95). */
.guidecard.is-insheet { z-index: 92; }
.guide-head { align-items: baseline; }
.guide-title { min-width: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; color: var(--ink); }
.guide-n { flex: 0 0 auto; font-size: 12px; color: var(--ink-mute); font-variant-numeric: tabular-nums; white-space: nowrap; }
.guide-text { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); overflow-wrap: anywhere; }
/* Skip, then Next at the right edge (the markup keeps the primary first). */
.guide-acts { display: flex; flex-direction: row-reverse; justify-content: flex-start; gap: 8px; margin-top: 4px; }
.guide-acts .btn { min-width: 0; }
.guidecard.is-dock { padding: 18px 20px 20px; }
.guidecard.is-dock .guide-text { font-size: 14px; }
.guidecard.is-dock .guide-acts { --ctl-h-sm: var(--ctl-h); }
.guidecard.is-dock .guide-acts .btn { flex: 1 1 0; }
.guide-on { outline: 2px solid var(--action-ring); outline-offset: 3px; }
/* A row runs its table's width, and the table clips what falls outside it:
   a row's ring is drawn inside the row. */
.guide-on:is(.crm-row, .svc-row, .hb-row, .wam-row, .camp-row, .cr-client-row, .qrow) { outline-offset: -2px; }
/* The account menu docked on a phone: the card at the foot of the screen,
   its rows at the finger's height, Sign out last and nearest the thumb. */
.popcard.is-dock.acct-menu { padding: 6px 8px 8px; max-width: none; }
.popcard.is-dock.acct-menu .acct-who { padding: 10px 4px 10px 12px; }
.hinttext::before {
  content: ""; position: absolute; left: -4px; top: calc(50% - 4px);
  width: 8px; height: 8px; background: var(--sunk); border-radius: 1px; transform: rotate(45deg);
}

/* =========================================================================
   THE FRONT DOOR (index.html, body.lp)
   A visitor card in the manner of Apple's visitor centre page (the user,
   2026-09-28): the name and the tagline centred, the website's ways in as a
   ticked band, one photograph edge to edge, the week's hours, the address
   and WhatsApp. Everything that is not the photograph sits on one centred
   column, the header's wordmark and the foot included, so the page has one
   left edge and one right edge at every width ("very uneven spacing": the
   bar kept the portal's gutter while the content took its own). An
   editorial page, so its content takes the larger steps (32 to 56).
   ========================================================================= */
@font-face {
  font-family: 'ADspace Optima';
  src: local('Optima'), local('Optima-Regular'), url('/css/OPTIMA.TTF') format('truetype');
  font-display: swap;
}
body.lp { --lp-col: 980px; --lp-gut: 24px; background: var(--card); }
/* The bar and the foot keep every page's edge (the user, 2026-10-03: the
   front door's mark sat in the column while every other page's sat at the
   left edge); the column holds the page's own content only. */
/* The header's label says what ADspace is, in the body face at the weight
   of a line of text rather than a section's name, on this page alone. */
body.lp .brand-kicker { font-size: 16px; font-weight: 400; letter-spacing: 0; color: var(--ink-soft); }
/* The name in the brand's face, and what we do under it in Slate Regular
   (the user, 2026-09-28), centred on the page. */
/* Even to the ink, not the boxes: the name's leading sits above its capitals
   and the tagline's descenders below it, so equal padding left 62px of air
   over the name and 52 under the tagline (the user saw it). These pads put
   56 of air on both sides at a desk and 41 on a phone. */
.lp-intro { padding: 50px var(--lp-gut) 52px; text-align: center; }
.lp-intro-act { margin: 24px 0 0; }
.lp-title {
  margin: 0; font-family: 'ADspace Optima', Optima, Candara, 'Segoe UI', sans-serif;
  font-size: 48px; font-weight: 400; letter-spacing: -.01em; line-height: 1.1; color: var(--ink);
}
.lp-tagline { margin: 12px 0 0; font-family: var(--font-head); font-size: 19px; font-weight: 400; line-height: 1.4; color: var(--ink-soft); }
.lp-sep { margin: 0 .3em; color: var(--ink-mute); }
/* One photograph, edge to edge and whole, at the 16:9 it was cut to (the
   user, 2026-09-28). Past 1920 it stops growing and stands centred. */
.lp-hero { margin: 0 auto; max-width: 1920px; aspect-ratio: 16 / 9; overflow: hidden; background: var(--line-soft); }
.lp-hero picture, .lp-hero img { display: block; width: 100%; height: 100%; }
.lp-hero img { max-width: none; object-fit: cover; }
/* No drag, no selection and no iPhone Save to Photos over the photograph. */
.lp-hero, .lp-hero img { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; -webkit-user-drag: none; }
/* At a desk the ways in come under the name, as on Apple's page; once they
   read down as a list, the photograph comes under the name and the list
   follows it (the user, 2026-09-28). The markup keeps the desk's order, and
   the photograph holds nothing that takes focus. */
.lp-main { display: flex; flex-direction: column; }
.lp-hero, .lp-duo { width: 100%; }
.lp-band { background: var(--page); padding: 48px var(--lp-gut); }
/* Five ways in across the column: the first starts at its left edge, the
   last ends at its right, and the gaps between them are equal, so the band
   is even on both sides whatever each name's length. */
.lp-ways {
  list-style: none; margin: 0 auto; padding: 0; max-width: var(--lp-col);
  display: grid; grid-template-columns: repeat(5, max-content); justify-content: space-between; gap: 24px;
}
.lp-way { display: grid; gap: 8px; align-content: start; }
.lp-tick { width: 20px; height: 20px; }
/* The ticks in the brand's monochrome, the ink the wordmark is set in
   (the user, 2026-09-28). */
.lp-tick circle { fill: var(--fill); }
.lp-tick path { fill: none; stroke: var(--on-fill); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
/* One hierarchy down the page: headings in the body face at 600, what they
   hold in Slate Regular, figures and links lighter again (the user,
   2026-09-28: "Bold hierarchy is slightly inverted now"). */
.lp-way-name { margin: 0; font-family: var(--font-head); font-size: 15px; font-weight: 400; line-height: 1.35; color: var(--ink); }
/* A link at the control floor, giving its height back so the text keeps its
   line. */
.lp-go {
  display: inline-flex; align-items: center; gap: 4px; min-height: var(--ctl-h);
  margin-block: calc((20px - var(--ctl-h)) / 2); font-size: 13.5px; color: var(--action-ink); text-decoration: none;
  /* Never an underlined word (FRONT-1): the hover is a shade that hugs the
     line, as `.linkbtn`'s does, its 6px either side given back so the
     words keep their edge. */
  position: relative; isolation: isolate; padding-inline: 6px; margin-inline: -6px;
}
.lp-go::before {
  content: ""; position: absolute; z-index: -1; inset: calc(50% - 13px) 0;
  border-radius: 6px; background: transparent; transition: background-color var(--t-fast);
}
@media (hover: hover) { .lp-go:hover::before { background: var(--line-soft); } }
/* Each opens adspacestudios.com or Maps in a new tab, so it ends in the
   leaving mark, never the chevron (FRONT-2). */
.lp-go svg { width: 14px; height: 14px; flex: 0 0 auto; }
/* The hours on the left; the address and WhatsApp on the right, on the
   same column. */
.lp-duo {
  max-width: calc(var(--lp-col) + 2 * var(--lp-gut)); margin: 0 auto; padding: 56px var(--lp-gut);
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 32px 56px; align-items: start;
}
.lp-side { display: grid; gap: 32px; }
.lp-h2 { margin: 0 0 12px; font-size: 17px; font-weight: 600; letter-spacing: -.01em; line-height: 1.3; color: var(--ink); }
.lp-hours { width: 100%; border-collapse: collapse; font-size: 14px; }
.lp-hours th, .lp-hours td { padding: 10px 0; border-top: 1px solid var(--line-soft); text-align: left; vertical-align: top; }
.lp-hours tr:last-child th, .lp-hours tr:last-child td { border-bottom: 1px solid var(--line-soft); }
.lp-hours th { font-family: var(--font-head); font-size: 15px; font-weight: 400; color: var(--ink); width: 34%; }
.lp-hours td { color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.lp-hours td:last-child { text-align: right; white-space: nowrap; }
.lp-hours .is-shut td:last-child { color: var(--ink-mute); }
.lp-hol { display: block; font-size: 13.5px; color: var(--ink-mute); }
.lp-note { margin: 12px 0 0; font-size: 13.5px; color: var(--ink-mute); }
.lp-address { margin: 0 0 4px; font-style: normal; font-size: 14px; line-height: 1.55; color: var(--ink-soft); }
.lp-lead { margin: 0 0 16px; max-width: 60ch; font-size: 14px; line-height: 1.55; color: var(--ink-soft); }
.lp-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; }
/* The ways in read down the band once five across no longer fit, the tick
   beside its words, on a narrower column the hours and the address share. */
@media (max-width: 900px) {
  body.lp { --lp-col: 560px; }
  .lp-ways { grid-template-columns: minmax(0, 1fr); justify-content: stretch; gap: 16px; }
  .lp-way { grid-template-columns: 18px minmax(0, 1fr); gap: 12px; }
  .lp-duo { grid-template-columns: minmax(0, 1fr); }
  .lp-band, .lp-duo { order: 1; }
  .lp-band { padding-block: 32px; }
}
@media (max-width: 640px) {
  /* Apple's side margin on a phone, the bar and the foot on it too: one
     left edge from the wordmark down. The photograph runs edge to edge. */
  .lp-intro { padding: 36px var(--lp-gut) 38px; }
  .lp-title { font-size: 36px; }
  .lp-tagline { margin-top: 8px; font-size: 16px; }
  .lp-band { padding-block: 24px; }
  .lp-tick { width: 18px; height: 18px; margin-top: 1px; }
  .lp-way-name { font-size: 14px; }
  .lp-go { font-size: 13px; }
  /* The days in Slate Regular 14 on a phone, as the names are; the
     figures 14, as the address beside them (FRONT-3). */
  .lp-hours th { font-size: 14px; }
  /* The bar on the content's 24px edge, so a phone reads one left edge
     from the wordmark down (FRONT-4). The foot keeps every page's 16: at
     24 its one line broke in two on a 390 phone. */
  body.lp .topbar-inner { padding-left: max(24px, env(safe-area-inset-left, 0px)); padding-right: max(24px, env(safe-area-inset-right, 0px)); }
  .lp-duo { padding-block: 32px 48px; gap: 32px; }
  .lp-side { gap: 32px; }
  .lp-h2 { font-size: 16px; }
  /* The header keeps its label on a phone: nothing else stands in the bar. */
  body.lp .brand-kicker { display: block; font-size: 14px; }
  body.lp .brand .brand-divider { display: block; }
}

/* =========================================================================
   THE CORNER FOLLOWS THE SIZE AND THE NESTING (the user, 2026-09-28: "rather
   than making all buttons all cards same radius do you considered its
   paddings and the radii curvature"). Measured before: a 38px control's 10px
   corner is 26% of its height, but the 32px controls and the contact chip
   kept the same 10 (31 to 32%, visibly rounder), the bar's icon buttons had
   8 (21%), the rail's rows 9, and a ⋯ menu's rows kept 10 inside a 14px
   menu padded 6, where the rule gives 14 - 6 = 8. The scale by role:
   sheet 18, card 14, control 10 (38 and 44px), small control 8 (32px; 10
   under a finger, where it is 38), chip 5, and a corner inside a padded
   surface is the surface's corner less the inset.
   ========================================================================= */
.btn-sm, .input-sm, .select-sm, .state-select, .select.state-select, .select-sm.state-select, .plink {
  border-radius: var(--radius-ctl-sm);
}
.headlink, .backlink, .console-title, .notif-btn, .navitem, .railrow { border-radius: var(--radius-sm); }
/* Inside a menu: 14 less its 5px padding and 1px border. */
.kmenu > .kmenu-item, .kmenu .kmenu-item { border-radius: calc(var(--radius) - 6px); }
/* The command bar's controls are 32px, so they take the small corner. */
.cmdbar .btn, .cmdbar .select, .cmdbar-find .input, .cmdbar-views, .cmdbar-views .acttab { border-radius: var(--radius-ctl-sm); }
.cmdbar-views.has-thumb::before { border-radius: calc(var(--radius-ctl-sm) - 2px); }
/* A standing fact's link (the verify page, the redirect hosts) is the
   outlined chip, inline in its sentence. */
.routenote .plink { margin-block: -4px; vertical-align: baseline; font-size: 12.5px; padding: 3px 9px; min-height: 0; }
.cr-main { min-width: 0; }
.cr-rail { min-width: 0; }
.cr-linktools .linkbox { flex-wrap: nowrap; }
@media (max-width: 640px) {
  .cr-linktools .linkbox { flex-wrap: wrap; }
  }
.set-row {
  display: flex; flex-direction: column; gap: 2px; width: 100%; text-align: left;
  padding: 12px 15px; border: 0; border-top: 1px solid var(--line-soft); background: none; font: inherit; color: var(--ink);
}
.set-row:first-child { border-top: 0; }
.set-row-top { display: flex; align-items: center; gap: 8px; min-width: 0; }
.set-row-top > b { flex: 1 1 auto; min-width: 0; font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.set-row-sub .cr-changes { padding: 0; font-size: 12.5px; }
.crm-c-sets .cr-changes { display: block; width: fit-content; margin-top: 2px; padding: 0; }
.set-row-sub.is-warn { color: var(--ink); }
.set-row.is-on { background: var(--line-soft); }
@media (hover: hover) { .set-row:not(.is-on):hover { background: var(--sunk); } }
.cr-sets > .empty { padding: 12px 15px; margin: 0; }
/* The re-approval note opens under the post, across the whole row. */
.saved > .asknote { grid-column: 1 / -1; margin-top: 10px; }
.setprog-note { margin: 8px 0 0; }
#setTitle, .saved-top > b { color: var(--ink); }
.zone-out .zone-head { display: flex; align-items: center; gap: 12px; }
/* ---- The Overview (`js/overview.js`) ----------------------------------------
   Each section's heading over its cards, two across on a desk and one on a
   phone (`.chartgrid`). A list card is a chart card's shape: its title with a
   count and View all on one line, then five rows that each open a record,
   name over meta on the left and the figure over its age at the right edge. */
.ovw-cardhead { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }
.ovw-cardhead h3 { margin: 0; min-width: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
/* View all gives its padding back, so the title sits where a chart's does
   and the chevron ends at the card's edge. */
.ovw-cardhead .ovgo { margin: -8px -6px -8px auto; flex: 0 0 auto; }
/* The rest of the list, a press away in the card: a quiet button whose words
   sit on the rows' own edge, giving its padding back to the side and below. */
.ovw-more.btn { min-width: 0; margin: 4px 0 -6px -13px; align-self: flex-start; }
/* The empty and failed lines sit in the card as its content, not as a second
   card inside it. */
.ovw-card .softpanel { background: none; border: 0; border-radius: 0; }
.ovw-card .emptyline { padding: 0; }
.ovw-card .emptyline b { font-weight: 400; color: var(--ink-mute); }
.ovw-card .errline { border-bottom: 0; border-radius: var(--radius-sm); }

.ovw > * { margin: 0; }
.ovw-pages { margin: 0; }
/* Detail's groups under their label, and the way on at a page's foot: a
   forward move at its own width, at the right edge. */
.ovw-page-h { margin: 12px 0 0; }
.ovw-turn.btn { flex: 0 0 auto; }
.ovw-turn .ovgo-chev { width: 15px; height: 15px; margin-left: 4px; }
.ovw-turn .ovgo-chev.is-back { margin: 0 4px 0 0; transform: scaleX(-1); }
/* Sales moves: a figure a line, its word over a mute line, the figure at the
   right edge. */
.ovw-facts { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: 24px; margin: 0; }
.is-narrow .ovw-facts { grid-template-columns: minmax(0, 1fr); }
.ovw-facts > div { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 16px; align-items: baseline; padding: 8px 0; border-top: 1px solid var(--line-soft); }
.ovw-facts > div:nth-child(-n+2) { border-top: 0; padding-top: 0; }
.ovw-facts > .is-wide { grid-column: 1 / -1; }
.ovw-facts dt { display: grid; gap: 2px; font-size: 13px; color: var(--ink); min-width: 0; }
.ovw-facts dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.ovw-when > .select { flex: 0 0 auto; width: auto; min-width: 156px; }
.ovw-glance.is-plain { grid-template-columns: minmax(0, 1fr); }
.is-narrow .ovw-glance { grid-template-columns: minmax(0, 1fr); gap: 16px; }
.ovw-meter { display: grid; justify-items: center; min-width: 0; }
.ovw-meter > .chartcard { width: 100%; padding: 0; background: none; box-shadow: none; }
.ovw-meter .chart-data { display: none; }
.chart-meter { position: relative; width: 170px; height: 170px; margin: 0 auto; }
.cm-ring { width: 100%; height: 100%; }
.cm-mid b { font-size: 26px; font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums; color: var(--ink); line-height: 1.1; }
.cm-mid small { font-size: 12px; color: var(--ink-mute); }
.ovw-groups.is-one, .is-narrow .ovw-groups { grid-template-columns: minmax(0, 1fr); }
.ovw-group { display: grid; gap: 8px; align-content: start; min-width: 0; }
.ovw-group-h { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 13px; font-weight: 600; color: var(--ink-mute); }
.ovw-group-h::after { content: ""; flex: 1; height: 1px; background: var(--line); }
/* The trend stands at the tile's foot, so tiles side by side draw theirs on
   one line however their words wrap (2026-10-27). */
.ovw-kpi { display: grid; grid-template-rows: auto auto 1fr auto; gap: 2px; min-width: 0; }
.ovw-kpi-label .ovw-short { display: none; }
.ovw-kpi-fig.is-money { font-size: 21px; }
.ovw-kpi-fig.is-err { color: var(--err); }
.ovw-kpi-fig .skel { display: block; width: 56px; height: 22px; margin: 4px 0 2px; padding: 0; border-radius: var(--radius-sm); background: var(--line-soft); }
.ovw-kpi-again.btn { min-width: 0; margin: -6px 0 -6px -8px; }
.chart-spark { display: block; width: 100%; height: 32px; overflow: visible; }
.ovw-kpi-bar i { display: block; height: 100%; border-radius: 3px; background: var(--ink-soft); }
/* Three charts on a row where they fit, each taking the row's share. */
.ovw-grid.is-three { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.is-narrow .ovw-grid.is-three { grid-template-columns: minmax(0, 1fr); }
/* Three across, a ring and its legend share a third: a smaller ring, the
   legend's words whole and its figures without their share. */
/* The cards of a row share its height, and each card's Figures stands at
   its foot, so the row's Figures read on one line (the user, 2026-10-12). */
.ovw-grid > .chartcard { display: flex; flex-direction: column; }
/* The chart keeps its usual step above Figures' hairline, wherever the
   hairline stands (the user, 2026-10-12: it sat against the last bar). */
.ovw-grid > .chartcard > :has(+ .chart-data) { margin-bottom: 12px; }
.ovw-grid.is-three .cp-label { overflow-wrap: normal; }
.ovw-att-head > span:last-child, .ovw-att-since { text-align: right; justify-self: end; }
.ovw-att-row {
  width: calc(100% + 16px); min-height: var(--ctl-h); margin: 0 -8px; padding: 10px 8px; border: 0; border-top: 1px solid var(--line-soft);
  border-radius: 0; background: none; color: inherit; font: inherit; font-size: 13px; text-align: left; cursor: pointer;
  transition: background-color var(--t-fast);
}
.ovw-att-head + .ovw-att-row { border-top: 0; }
.ovw-att-row:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--action-ring); border-radius: var(--radius-ctl-sm); }
@media (hover: hover) { .ovw-att-row:hover { background: var(--sunk); } }
.ovw-att-what { display: grid; gap: 2px; min-width: 0; }
.ovw-att-what b { font-weight: 600; overflow-wrap: anywhere; }
.ovw-att-what small { font-size: 12.5px; color: var(--ink-mute); overflow-wrap: anywhere; }
.ovw-att-who { min-width: 0; color: var(--ink-soft); overflow-wrap: anywhere; }
.ovw-att-since b.is-err { font-weight: 600; color: var(--err); }
.ovw-more-chev { width: 15px; height: 15px; transition: transform var(--t) var(--ease); }
.is-narrow .ovw-att-head { display: none; }
.is-narrow .ovw-att-row { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "what since" "who since"; }
.is-narrow .ovw-att-sec { display: none; }
.tplrow-name { display: grid; gap: 2px; min-width: 0; }
.tplrow-name small { font-size: 12.5px; color: var(--ink-mute); overflow-wrap: anywhere; }
.tefmt .tickline > span { display: grid; gap: 1px; }
.tefmt .tickline small { font-size: 12px; color: var(--ink-mute); }
.tefmt-hours { width: 100%; text-align: right; }
.dtrow-name { display: grid; gap: 2px; min-width: 0; }
.dtrow-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
.dtrow-title b { font-weight: 600; overflow-wrap: anywhere; }
#holMsg:empty { display: none; }
.dtrow-name small { font-size: 12.5px; color: var(--ink-mute); overflow-wrap: anywhere; }
.dtfield-name { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; min-width: 0; font-size: 14px; }
.is-tight .dtfield, .sheet-card.is-tight .dtfield { grid-template-columns: 1fr; gap: 6px; }

/* A word or a value that acts inside a line of text (Change, a due date, a
   reference, Open the month) keeps its whole target, 38px at a desk and 44
   under a finger, but its fill hugs the line it reads as: drawn full height,
   the hover stood as a grey slab over the rows above and below (the user,
   2026-09-28: "such huge invisible button"). */
.linkbtn, .tdate, .tdate-src, .tinline, .serial-copy, .due-client { position: relative; isolation: isolate; }
.linkbtn::before, .tdate::before, .tdate-src::before, .tinline::before, .serial-copy::before, .due-client::before {
  content: ""; position: absolute; z-index: -1; inset: calc(50% - 13px) 0;
  border-radius: 6px; background: transparent; transition: background-color var(--t-fast);
}
@media (hover: hover) {
  .linkbtn:hover::before, .tdate:hover::before, .tdate-src:hover::before,
  .tinline:hover::before, .due-client:hover::before { background: var(--line-soft); }
  .serial-copy:hover::before { background: var(--sunk); }
}

/* ---- Content Review: stages and revisions (2026-09-30) --------------------
   The posts by where they stood when the page loaded, as the view strip with
   each stage's count inside its tab; the strip has a row of its own, the
   selects under it. A revised post names the round and what the client asked
   of the round before, in the neutral inset (it is news, not a caution). The
   copy is edited inside Request changes. */
.stagestrip { margin-top: 16px; }
.stagestrip + .filterbar { padding-top: 12px; }
/* A tab's count is a small chip of its own (the user, 2026-10-03: noticed
   at a glance): a soft neutral pill, red only where what it counts needs
   acting on now (Content Review: Changes requested; Overview: an item is
   late). Waiting and done counts are neutral (2026-10-07: amber and green
   are a status's dot, never a count). Every pair holds 4.5:1 on the track
   and on the thumb. */
.tab-n {
  display: inline-block; margin-left: 6px; min-width: 20px; padding: 0 6px;
  border-radius: 5px; background: var(--tonal); color: var(--ink-soft);
  font-size: 12px; font-weight: 600; line-height: 20px; text-align: center;
  font-variant-numeric: tabular-nums;
}
.tab-n.is-err  { color: var(--err);  background: var(--err-bg); }
/* On a phone a strip shares the column's width, so the pill gives back
   its room before any tab's word is cut. */
@media (max-width: 640px) {
  .tab-n { margin-left: 4px; min-width: 18px; padding: 0 4px; }
}
@media (max-width: 400px) {
  .tab-n { margin-left: 3px; min-width: 16px; padding: 0 3px; }
}
.stage-empty {
  margin: 4px 0 24px; padding: 18px 20px; border-radius: var(--radius);
  background: var(--card); color: var(--ink-mute); font-size: 14px;
}
.reask.is-revision { background: var(--sunk); border-color: transparent; }
.reask.is-revision b { color: var(--ink); }
.reask small { font-size: 12px; color: var(--ink-mute); }
/* Who asked and when is the block's head; the client's words sit under it
   on their own, a step apart, so a note that is itself a caption never reads
   as part of the heading. */
.reask.is-revision { gap: 8px; }
.reask.is-revision .reask-head { font-size: 12.5px; font-weight: 600; color: var(--ink-mute); }
.reask.is-revision .reask-note { color: var(--ink); }

/* The console's set page: the posts by stage, a round, what a round answers,
   the client's suggested copy with Accept, and the rounds kept. */
.poststages { margin: 0 0 12px; }
.saved-suggest {
  display: grid; gap: 6px; justify-items: start; margin-top: 6px; width: 100%;
  padding: 10px 12px; border-radius: var(--radius-sm); background: var(--sunk);
}
.saved-suggest-t { margin: 0; font-size: 13px; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
.saved-kept { margin-top: 6px; width: 100%; font-size: 12.5px; }
.saved-kept > summary { cursor: pointer; color: var(--ink-soft); font-weight: 500; min-height: 32px; display: flex; align-items: center; }
.is-narrow .saved-kept-row { grid-template-columns: auto minmax(0, 1fr); }
.saved-file { display: grid; grid-template-columns: minmax(0, 1fr); gap: 5px; width: 100%; margin-top: 4px; }
.saved-file > .input { width: 100%; min-width: 0; }
.saved-file > span { font-size: 12px; font-weight: 600; color: var(--ink-mute); }

/* On a phone the stage strip is the column's (the card's) width, its four
   tabs sharing it, the longest stage in its short word. */
.tab-short { display: none; }
@media (max-width: 640px) {
  .stagestrip { width: 100%; }
  .stagestrip .tab { flex: 1 0 auto; padding: 0 8px; }
  .stagestrip .tab-long { display: none; }
  .stagestrip .tab-short { display: inline; }
}
.is-narrow .poststages { width: 100%; }
/* Request changes is one column, every step 12px apart: the note, the name
   the full width of the box, then Cancel and Send request (the caption is
   edited in place, under the pen beside Copywriting). The button and the field sat on one line on a
   phone, at two widths, because both were inline (the user, 2026-09-30:
   "wtf with this user experience"). */
.changebox.is-open { display: grid; gap: 12px; }
.changebox > * { margin: 0; min-width: 0; }
.changebox .changebox-who { width: 100%; }
/* A field under its label, 8px apart (2026-10-31, RV-3). */
.changebox-f { display: grid; gap: 8px; min-width: 0; }
.changebox-f[hidden] { display: none; }
.changebox-f > * { margin: 0; min-width: 0; }
/* The decision pair and the request's own pair are equal halves (the user,
   2026-10-01: Approve took 1.35 shares and Send request its own width, so
   neither row read as balanced). While Approve asks for the name, Request
   changes still gives its half to the field. */
/* Half each from a half's basis, never from 0: a basis of 0 leaves each its
   own padding on top of its share, and Request changes stood 34px wider
   than Approve (2026-10-20, RVP-2). */
.approve-row > .namebox, .approve-row > .btn-approve, .approve-row > .btn-changes { flex: 1 1 50%; min-width: 0; }
.approve-row.is-asking > .namebox { flex: 1 1 100%; }
/* The draft waiting on the client (#needBox) at a desk: the pair at its
   own width at the card's right edge, Approve last, as an action row
   reads; Open the draft at its own width on every screen (CP-17). */
.cx-needs .client-draft-preview > a.btn { justify-self: start; }
@media (min-width: 641px) {
  .cx-needs .approve-row { flex-direction: row-reverse; justify-content: flex-start; }
  .cx-needs .approve-row > .namebox, .cx-needs .approve-row > .btn-approve, .cx-needs .approve-row > .btn-changes { flex: 0 0 220px; }
  .cx-needs .approve-row.is-asking > .namebox { flex: 0 1 460px; }
}
.changebox-actions > .btn { flex: 1 1 0; min-width: 0; }
/* While a request is open the decision pair steps away, so Cancel and Send
   request are the card's only acts, and the request starts the block. */
.approve.is-requesting .approve-row { display: none; }
.approve.is-requesting .changebox { margin-top: 0; }

/* =========================================================================
   THE HANDBOOK (2026-10-01): the company's internal files, a card per
   category. A row is the file and its line, the version, when it last
   changed, then Open and the ⋯. On a narrow list it is the file over its
   line with Open and the ⋯ at the right edge.
   ========================================================================= */
.hb-row, .crm-head.hb-row {
  display: grid; align-items: center; gap: 0 12px;
  grid-template-columns: minmax(0, 1fr) 72px 110px 164px;
  padding: 10px 15px; border-bottom: 1px solid var(--line-soft);
}
.hb-row.is-off > :not(.hb-act) { opacity: .55; }
.hb-name b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hb-name small { font-size: 12.5px; color: var(--ink-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.is-narrow .hb-row {
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "name name act" "ver when act";
  gap: 2px 10px; padding: 12px 15px;
}
.hb-verrow .hb-note { white-space: normal; overflow-wrap: anywhere; color: var(--ink-soft); }

/* ---- A long list is searched (js/form.js §8) ---------------------------------
   A select of ten options or more opens this finder in place of the browser's
   list. At a desk it hangs under its field like a ⋯ menu (the field's own
   label stands above it, so it carries no head); on a phone it is a sheet of
   the screen's height with its name and close mark, the field at the top. */
.picker {
  /* Over the question sheet too (#askSheet, 95): a long list asked inside
     a question opened under its scrim. */
  position: fixed; z-index: 96; display: flex; flex-direction: column;
  background: var(--raised); border: 1px solid var(--line-lift);
  border-radius: var(--radius); box-shadow: var(--shadow-lift);
  padding: 6px; overflow: hidden;
  transform-origin: top center; animation: mk-menu-in var(--t) var(--ease-out);
}
.picker[hidden] { display: none; }
.picker-head { display: none; }
.picker-field { position: relative; flex: 0 0 auto; margin-bottom: 4px; }
.picker-field .input { width: 100%; padding-left: 36px; }
.picker-field svg {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; stroke: var(--ink-mute); stroke-width: 1.8; fill: none; pointer-events: none;
}
.picker-list { list-style: none; margin: 0; padding: 0; overflow-y: auto; overscroll-behavior: contain; min-height: 0; flex: 1 1 auto; }
.picker-opt {
  display: flex; align-items: center; gap: 12px; min-height: var(--ctl-h); padding: 6px 10px;
  border-radius: calc(var(--radius) - 6px); color: var(--ink); font-size: 14px; line-height: 1.35;
  cursor: pointer; overflow-wrap: anywhere;
}
.picker-opt > span { flex: 1 1 auto; min-width: 0; }
.picker-opt > svg { flex: 0 0 auto; width: 15px; height: 15px; stroke: currentColor; stroke-width: 2; fill: none; }
.picker-opt.is-on { background: var(--line-soft); font-weight: 600; }
.picker-opt.is-act:not(.is-on) { background: var(--sunk); }
.picker-opt.is-off { color: var(--ink-mute); cursor: default; }
.picker-group { padding: 10px 10px 4px; font-size: 12px; font-weight: 600; color: var(--ink-mute); }
.picker-none { padding: 10px; font-size: 13px; color: var(--ink-mute); }
.picker.is-full {
  inset: 0; width: auto; max-height: none; border: 0; border-radius: 0; box-shadow: none;
  padding: calc(12px + env(safe-area-inset-top, 0px)) 16px calc(12px + env(safe-area-inset-bottom, 0px));
  animation: mk-sheet-up var(--t-slow) var(--ease);
}
.picker.is-full .picker-head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.picker.is-full .picker-head h3 { flex: 1 1 auto; min-width: 0; font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
.picker.is-full .picker-field { margin-bottom: 8px; }
.picker.is-full .picker-opt { min-height: 44px; font-size: 15px; }
@media (prefers-reduced-motion: reduce) { .picker, .picker.is-full { animation: none; } }

/* ===== Namecard (js/namecard.js, /card/, the console's My namecard) =====
   The signboard's lockup on a #f5f5f5 card: the tagline set at 0.46 of the
   wordmark's size, so it runs about 1.64 times the wordmark's width, as the
   signboard does. The faces share one grid cell, so the card is as tall as
   its longer face and turns over without a measured height. */
body.ncpage { background: var(--nc-ground); }
.ncmain { min-height: 100vh; min-height: 100dvh; box-sizing: border-box; display: grid; place-items: center;
  padding: 24px 16px calc(24px + env(safe-area-inset-bottom, 0px)); }
.ncmain > * { width: 100%; }
.nc { display: flex; flex-direction: column; align-items: center; gap: 24px; }
.nc-stage { width: min(100%, 326px); perspective: 1600px; }
.nc-flip { display: grid; transform-style: preserve-3d; transition: transform .8s var(--ease); }
.nc.is-back .nc-flip { transform: rotateY(180deg); }
.nc-face { grid-area: 1 / 1; position: relative; min-height: 548px; box-sizing: border-box; border-radius: var(--radius-lg);
  background: var(--nc-card); color: var(--nc-ink); overflow: hidden;
  backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.nc-back { transform: rotateY(180deg); padding: 30px 26px 26px; display: flex; flex-direction: column; }
.nc-lock, .nc-qr { position: absolute; inset: 0; box-sizing: border-box; display: flex; flex-direction: column; align-items: center;
  transition: opacity var(--t-slow) var(--ease-out); }
.nc-lock { display: block; }
/* The lockup centred on the card, both ways. */
.nc-lock-in { position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
  display: flex; flex-direction: column; align-items: center; }
.nc-qr { padding: 34px 28px 30px; opacity: 0; pointer-events: none; }
.nc.is-qr .nc-lock { opacity: 0; pointer-events: none; }
.nc.is-qr .nc-qr { opacity: 1; pointer-events: auto; }
.nc-word { display: block; font-family: 'ADspace Optima', Optima, Candara, serif; font-size: var(--nc-sw, 32px); line-height: 1; color: var(--nc-word); }
.nc-tag { display: block; margin-top: calc(var(--nc-sw, 32px) * .2); font-family: var(--font-head); font-size: calc(var(--nc-sw, 32px) * .46);
  line-height: 1; white-space: nowrap; color: var(--nc-word); }
.nc-word-sm { font-size: 19px; }
.nc-qrbox { margin-top: auto; width: 228px; height: 228px; box-sizing: border-box; padding: 18px; border-radius: 18px; background: var(--nc-tile); }
.nc-qrbox canvas, .nc-qrbox img { width: 100% !important; height: auto !important; }
.nc-qrname { margin-top: 24px; font-family: var(--font-head); font-weight: 400; font-size: 22px; line-height: 1.1; }
.nc-qrlink { margin: 6px 0 auto; font-size: 12.5px; color: var(--nc-word); text-align: center; overflow-wrap: anywhere; }
.nc-who { margin-top: 24px; display: flex; flex-direction: column; gap: 5px; }
.nc-name { font-family: var(--font-head); font-weight: 400; font-size: 30px; line-height: 1.1; letter-spacing: -.005em; }
.nc-post { font-size: 13px; color: var(--nc-word); }
.nc-rows { margin-top: 24px; display: flex; flex-direction: column; border-bottom: 1px solid var(--nc-line); }
.nc-row { min-height: 44px; display: grid; grid-template-columns: 64px minmax(0, 1fr); align-items: center;
  border-top: 1px solid var(--nc-line); color: var(--nc-ink); text-decoration: none; font-size: 14px; font-variant-numeric: tabular-nums; }
.nc-row-long { padding: 12px 0; align-items: baseline; font-size: 14px; line-height: 1.45; }
.nc-label { font-size: 12px; color: var(--nc-word); }
.nc-value { min-width: 0; overflow-wrap: anywhere; }
.nc-row-mail .nc-value { overflow-wrap: normal; word-break: normal; white-space: nowrap; overflow: hidden; }
.nc-row-mail .nc-value.is-wrap { white-space: normal; overflow: visible; overflow-wrap: anywhere; }
.nc-acts { margin-top: auto; padding-top: 16px; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; }
.nc-acts.is-one { grid-template-columns: minmax(0, 1fr); }
/* The portrait back reads in one 24px step, mark to name to rows to acts,
   the acts kept at the card's foot and its spare height above the mark
   (CARD-3). */
.nc:not(.is-land) .nc-back { justify-content: flex-end; }
.nc:not(.is-land) .nc-back > .nc-acts { margin-top: 24px; padding-top: 0; }
.nc-cover-acts { display: grid; grid-template-columns: repeat(2, minmax(0, 220px)); justify-content: center; gap: 8px; margin-top: 16px; }
/* Full-size buttons, one width: equal halves of the column on a phone,
   each taking the column where its words would not hold one line beside
   the other (a 390 phone; COV-2). */
.nc-cover-acts > .btn { min-width: 0; white-space: nowrap; }
@media (max-width: 640px) { .nc-cover-acts { grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr)); } }
.nc-cover-acts[hidden] { display: none; }
/* A 320 phone's half holds Save contact's mark and words with room either
   side only a little closer together. */
@media (max-width: 359px) { .nc-acts .nc-pill, .nc-ctl .nc-pill { gap: 6px; padding: 0 10px; font-size: 13px; } }
.nc-pill { height: 44px; padding: 0 18px; border: 0; border-radius: 22px; background: var(--nc-tile); color: var(--nc-ink);
  display: inline-flex; align-items: center; justify-content: center; gap: 7px; font: 500 var(--ctl-text) var(--font);
  letter-spacing: .01em; white-space: nowrap; text-decoration: none; cursor: pointer; transition: transform var(--t-fast) var(--ease); }
.nc-pill svg { width: 15px; height: 15px; stroke-width: 1.8; flex: 0 0 auto; }
.nc-pill-ink { background: var(--nc-ink); color: var(--nc-card); }
.nc-pill:active { transform: scale(.97); }
.nc-pill:focus-visible, .nc-row:focus-visible { outline: 2px solid var(--action-ring); outline-offset: 2px; }
/* The pair under the card keeps one width on every face (the portrait card's),
   so turning the card never moves the button under the finger. */
/* Two halves whatever the labels say: 1fr's floor is the longer label, so
   at 320 the pair moved as the card turned (2026-10-20, CRD-1). */
.nc-ctl { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 8px; width: min(100%, 326px); }
.nc-ctl .nc-pill { min-width: 0; }
.nc-ctl .nc-pill > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nc-ctl .nc-pill { height: 40px; background: var(--nc-card); }
.nc-ctl .nc-pill[aria-pressed="true"] { background: var(--nc-ink); color: var(--nc-card); }
@media (hover: hover) {
  .nc-pill:hover { filter: brightness(.96); }
  .nc-pill-ink:hover, .nc-ctl .nc-pill[aria-pressed="true"]:hover { filter: brightness(1.25); }
  .nc-row:hover .nc-value { color: var(--nc-word); }
}
@media (pointer: coarse) { .nc-ctl .nc-pill { height: 44px; } }
/* On a desk the card lies the way it is printed, landscape (`is-land`,
   set by js/namecard.js from the window at 760 and over), on its page and
   in the console's sheet alike. */
.nc.is-land .nc-stage { width: min(100%, 720px); perspective: 2200px; }
.nc.is-land .nc-face { min-height: 412px; border-radius: 24px; }
.nc.is-land .nc-word-sm { font-size: 24px; }
.nc.is-land .nc-back { padding: 40px 44px 38px; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
    grid-template-rows: auto 1fr auto; column-gap: 44px; }
.nc.is-land .nc-back > .nc-word-sm { grid-column: 1; grid-row: 1; }
.nc.is-land .nc-who { grid-column: 1; grid-row: 2; align-self: center; margin-top: 0; }
.nc.is-land .nc-name { font-size: 38px; }
.nc.is-land .nc-post { font-size: 14px; }
.nc.is-land .nc-acts { grid-column: 1; grid-row: 3; padding-top: 0; }
.nc.is-land .nc-acts .nc-pill { height: 40px; padding: 0 12px; }
/* Under a finger the landscape card's pair keeps the 44px target too: a
   tablet or a folded-open phone lies the card down at 760 and over. */
@media (pointer: coarse) { .nc.is-land .nc-acts .nc-pill { height: 44px; } }
.nc.is-land .nc-rows { grid-column: 2; grid-row: 1 / 4; align-self: center; margin-top: 0; }
.nc.is-land .nc-row { grid-template-columns: 72px minmax(0, 1fr); }
.nc.is-land .nc-qr { padding: 40px 48px; display: grid; grid-template-columns: minmax(0, 1fr) 260px;
    grid-template-rows: auto 1fr auto auto 1fr; column-gap: 48px; justify-items: start; align-items: start; }
.nc.is-land .nc-qr > .nc-word-sm { grid-column: 1; grid-row: 1; }
.nc.is-land .nc-qrbox { grid-column: 2; grid-row: 1 / 6; align-self: center; margin: 0; width: 260px; height: 260px; padding: 20px; }
.nc.is-land .nc-qrname { grid-column: 1; grid-row: 3; margin: 0; font-size: 34px; }
.nc.is-land .nc-qrlink { grid-column: 1; grid-row: 4; margin: 8px 0 0; text-align: left; font-size: 14px; }
@media (prefers-reduced-motion: reduce) { .nc-flip, .nc-lock, .nc-qr { transition: none; } }
.ncsheet-acts .btn { flex: 0 0 auto; min-width: 0; }
.ncsheet-acts .btn svg { width: 15px; height: 15px; }
@media (max-width: 640px) { .ncsheet-acts .btn { flex: 1 1 0; } }

/* =========================================================================
   UPGRADE MODE (js/maintenance.js, 2026-10-03)
   The cover lies over the page under a client page's bar (the 中文 switch
   stays in reach); in the console, which has no such bar, it is the whole
   screen. An admin working on reads one warn line over the section.
   ========================================================================= */
.maint-cover {
  position: fixed; inset: 0; z-index: 39;
  min-height: 0; margin: 0;
  padding: calc(var(--head-h) + 24px) 16px calc(24px + env(safe-area-inset-bottom, 0px));
  background: var(--card); overflow-y: auto;
}
body:not(:has(> .topbar)) .maint-cover { z-index: 60; padding-top: 24px; }
.maint-cover:focus { outline: none; }
body.is-maint { overflow: hidden; }
.maint-cover .maint-back { margin-top: 8px; }
.maint-cover .maint-note { margin-top: 8px; overflow-wrap: anywhere; }
.maint-cover .maint-out { margin-top: 20px; }
.maint-cover .maint-chat { margin-top: 20px; }
/* A neutral band led by its amber dot (2026-10-07). */
.upgradebar > :first-child::before, .annbar-text.is-important::before {
  content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 8px; vertical-align: 1px; background: var(--dot-wait);
}
.upgradebar.is-err > :first-child::before { background: var(--dot-late); }
.upgradebar.is-err { background: var(--err-bg); color: var(--err); padding: 8px 14px; }
.upgradebar .btn { flex: 0 0 auto; }
/* ---- Announcements (js/announce.js, 2026-10-07) ------------------------------
   One bar under the top bar: the team's in the console, laid like the
   upgrade line; the clients' across a client page under its bar, the words
   on the mark's 24px edge. Info is the tonal ground in ink; Important is the
   same ground led by an amber dot. Several live lines share the one bar: a
   track slides between them (`--t-slow`, nothing under reduced motion) and
   the count reads 1/3 between its arrows, never a second bar (the user,
   2026-10-07). The bar is as tall as its longest line, so moving along
   never moves the page. Its link, the count and its close sit at the right
   edge. */
.annbar {
  display: flex; align-items: center; gap: 12px;
  padding: 4px 4px 4px 14px; background: var(--tonal); color: var(--ink);
  font-size: 13px; line-height: 1.45;
}
.annbar[hidden] { display: none; }
.annbar-view { flex: 1 1 auto; min-width: 0; overflow: hidden; }
.annbar-track { display: flex; transition: transform var(--t-slow) var(--ease); }
.annbar-text { margin: 0; flex: 0 0 100%; min-width: 0; padding: 4px 0; overflow-wrap: anywhere; }
.annbar-text.is-important { font-weight: 600; }
.annbar-acts { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; }
.annbar-acts .btn { flex: 0 0 auto; }
.annbar-pager { display: inline-flex; align-items: center; }
.annbar-n { min-width: 30px; text-align: center; font-size: 12.5px; font-weight: 600; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
/* On a phone several lines take the bar's whole width, and the link, the
   count and the close move to a row of their own under them. */
@media (max-width: 640px) {
  .annbar.is-many { flex-wrap: wrap; gap: 0 12px; }
  .annbar.is-many .annbar-view { flex-basis: 100%; }
  .annbar.is-many .annbar-acts { margin-left: auto; }
}
.console .annbar { margin: 16px 28px 0; border-radius: var(--radius-sm); }
/* The device's clock not on GMT+8 (js/chrome.js, 2026-10-12): a coloured
   line led by the amber dot, laid as the announcements' bar is (the user
   asked for colour, so it reads apart from an announcement). */
.tzbar {
  display: flex; align-items: center; gap: 12px;
  padding: 4px 4px 4px 14px; background: var(--warn-bg); color: var(--ink);
  border-bottom: 1px solid var(--warn-line); font-size: 13px; line-height: 1.45;
}
.tzbar-text { margin: 0; flex: 1 1 auto; min-width: 0; padding: 4px 0; overflow-wrap: anywhere; }
.tzbar-text::before {
  content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 8px; vertical-align: 1px; background: var(--dot-wait);
}
.tzbar-x { flex: 0 0 auto; }
.topbar + .tzbar { padding-left: 24px; padding-right: 12px; }
.console .tzbar { margin: 16px 28px 0; border: 1px solid var(--warn-line); border-radius: var(--radius-sm); }
@media (max-width: 640px) {
  .topbar + .tzbar { padding-left: 16px; padding-right: 8px; }
  .console .tzbar { margin: 12px 16px 0; }
}
.topbar + .annbar, .demobar + .annbar { padding-left: 24px; padding-right: 12px; }
/* The demo on a client page (js/demo.js): the announcement's band led by
   the amber dot, Exit demo at its right edge. */
/* A colleague's preview of a prospect's quotation (js/quote.js,
   2026-10-30) is laid the same way, its refusal at the right edge. */
.annbar.demobar .annbar-text, .annbar.previewbar .annbar-text { flex: 1 1 auto; }
.annbar.previewbar .msg { margin: 0; font-size: 12.5px; }
@media (max-width: 640px) {
  .annbar.previewbar { flex-wrap: wrap; row-gap: 0; }
  .annbar.previewbar .annbar-text { flex: 1 1 0; }
  .annbar.previewbar .msg:not(:empty) { order: 1; flex-basis: 100%; padding: 0 0 6px 16px; }
}
.annbar.demobar .annbar-text::before, .annbar.previewbar .annbar-text::before {
  content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 8px; vertical-align: 1px; background: var(--dot-wait);
}
@media (max-width: 640px) {
  .console .annbar { margin: 12px 16px 0; }
  .topbar + .annbar, .demobar + .annbar { padding-left: 16px; padding-right: 4px; }
}
/* The console's list: a section an audience, every announcement a row, those
   live or set for later first; a stopped or ended one's words are faded, its
   Restore and Delete at full strength. */
.ann-rows { display: grid; gap: 8px; }
.ann-row.is-off > .ann-what { opacity: .62; }
.ann-sec:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.ann-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.ann-head .fsec-h { margin: 0; }
.ann-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 12px; align-items: start;
  padding: 12px 14px; background: var(--sunk); border-radius: var(--radius-sm); }
.ann-what { min-width: 0; }
.ann-text, .ann-zh { margin: 0; overflow-wrap: anywhere; }
.ann-zh { color: var(--ink-soft); margin-top: 4px; }
.ann-meta { margin: 4px 0 0; font-size: 12.5px; color: var(--ink-mute); }
.ann-ctl { display: flex; gap: 6px; align-items: center; justify-content: flex-end; }
.ann-acts { grid-column: 1 / -1; display: flex; gap: 8px; justify-content: flex-end; align-items: center; }
.ann-acts .btn { flex: 0 0 auto; }
.ann-none { margin: 0; color: var(--ink-mute); }
.pairnote .btn { flex: 0 0 auto; }
.pairtile-pic img, .pairtile-pic video { display: block; width: 100%; height: 100%; object-fit: cover; background: #000; }
.pairlink svg { width: 15px; height: 15px; }
.sheet-card.is-narrow .paircard, .is-narrow .paircard { grid-template-columns: minmax(0, 1fr); }
.aiu-row:first-child { border-top: 0; padding-top: 0; }
.aiu-name b { font-weight: 600; }
.aiu-cap.is-sum { margin-right: 0; font-weight: 600; color: var(--ink); }
.aiu-cap.is-own { color: var(--ink); font-weight: 600; }
.aiu-cap.is-stopped { color: var(--ink); font-weight: 600; }
.aiu-cap svg { width: 14px; height: 14px; }
.aiu-bar > i { display: block; height: 100%; width: var(--p, 0%); background: var(--fill); border-radius: inherit; }
.aiu-bar.is-full > i { background: var(--warn-solid); }
.input.aiu-num {
  width: 72px; min-width: 0; text-align: right; font-variant-numeric: tabular-nums;
  border-color: transparent; background-color: var(--tonal); -moz-appearance: textfield;
}
/* ONE WEIGHT FOR EVERY CHOICE (the user, 2026-10-04: "not one odd weird 500
   weight"). Every tab, view and segment option reads at the body's 400,
   chosen or not: the raised surface and the ink say which is chosen, so the
   words never change weight under the thumb. A count beside a tab keeps its
   own pill. */
.tab, .tab.is-on, .rectabs .tab, .rectabs .tab.is-on, .tabrow .tab, .tabrow .tab.is-on,
.cmdbar-views .acttab, .cmdbar-views .acttab.is-on, .acttab, .acttab.is-on,
.seg-opt, .seg-opt.is-on, .navitem.is-on, .railfoot.is-on, .picker-opt.is-on { font-weight: 400; }

.widelogo img { max-width: 100%; max-height: 100%; object-fit: contain; }
.widelogo.is-read { display: inline-flex; height: 48px; margin: 0; padding: 8px 12px; max-width: 100%; }
.wl-acts > .btn { flex: 0 0 auto; }
.wl-brands [data-m="list"] { display: grid; }
.wl-brand + .wl-brand { border-top: 1px solid var(--line-soft); }
.readgroup-head .fsec-h { margin: 0; }
.readgroup-head .btn { flex: 0 0 auto; }
.ba .msg.capmsg { margin: 0 0 12px; }
.ba-sec > .fsec-h { margin: 0 0 12px; }
/* The five answers across the card at a desk, two across in a narrow pane
   with the fifth taking the row; the note under them, the card's width. */
.facts.health-facts { grid-template-columns: repeat(5, minmax(0, 1fr)); margin: 0; }
.is-narrow .health-cols { grid-template-columns: minmax(0, 1fr); }
.health-words > :last-child { margin-bottom: 0; }
.health-agree .btn { flex: 0 0 auto; }
.health-fig.is-low::before {
  content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  margin-right: 4px; vertical-align: 1px; background: var(--dot-wait);
}
.health-trow + .health-trow { border-top: 1px solid var(--line-soft); }
.is-narrow .health-trow { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 6px 8px; }
.is-narrow .health-when { grid-column: 1 / -1; font-weight: 600; }
.health-talk + .health-talk { border-top: 1px solid var(--line-soft); }
.health-talk-who b { font-size: 14px; font-weight: 600; }
.health-talk-who small { font-size: 12px; color: var(--ink-mute); }
/* The check-in sheet: the chosen answer in Health's pink, its word under it. */
.health-scale .seg-opt.is-on { color: var(--health); }
.health-who b { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.health-who small { font-size: 12px; color: var(--ink-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.is-narrow .health-prow { grid-template-columns: minmax(0, 1fr) 120px 16px; grid-template-areas: "who at chev"; gap: 4px 10px; }
/* ---------- The client pages refresh (2026-10-07) ----------
   One icon set (`js/icons.js`, `.ic`: 15px outline glyphs in the ink of
   their line), where the work stands, what the reader owes first, and
   results led by one figure. Light only, as every client page. */
.ic { width: 15px; height: 15px; flex: 0 0 auto; vertical-align: -2px; }
.cx-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--dot-off); flex: 0 0 auto; }
.cx-dot.is-ok { background: var(--dot-ok); }
.cx-dot.is-wait { background: var(--dot-wait); }
.cx-dot.is-late { background: var(--dot-late); }
.cx-eyebrow { display: flex; align-items: center; gap: 7px; margin: 0 0 12px; font-size: 13px; font-weight: 600; color: var(--ink); }
.cx-quiet { font-size: 13px; font-weight: 400; color: var(--ink-mute); }

/* Where the campaign stands, under the name card's hairline. */
.cx-standing { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--line-soft); }
.cx-standing-line { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px; margin: 0 0 10px; }
.cx-standing-line b { font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
.cx-standing-line span { font-size: 13.5px; color: var(--ink-mute); }
.cx-track { display: flex; gap: 4px; height: 6px; }
.cx-track i { flex: 1 1 0; border-radius: 4px; background: var(--line-soft); }
.cx-track i.is-ok { background: var(--dot-ok); }
.cx-track i.is-wait { background: var(--dot-wait); }
.cx-track i.is-late { background: var(--dot-late); }
.cx-track i.is-off { background: var(--dot-off); }
.cx-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 10px 0 0; font-size: 13px; color: var(--ink-soft); }
.cx-legend span { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }

/* What the client owes, first: the draft's card with its own heading. */
.cx-needs { margin: 0 0 12px; }
.cx-needs > .booking { margin-bottom: 12px; }
.cx-needs > .booking:last-child { margin-bottom: 0; }

/* A creator's row: the number, the name and state, the six named steps
   (.cx-journey, below), the facts with their glyphs. */
.cx-facts dt { display: flex; align-items: center; gap: 6px; }
.cx-facts dt .ic { color: var(--ink-soft); }

/* Results led by one figure. */
#clientRollup .rollup-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.cx-hero { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px; margin: 12px 0 0; }
.cx-hero b { font-size: 32px; font-weight: 600; letter-spacing: -.03em; line-height: 1.1; font-variant-numeric: tabular-nums; }
.cx-hero span { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; color: var(--ink-mute); }
.cx-figs {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 14px 24px;
  margin: 14px 0 0; padding-top: 14px; border-top: 1px solid var(--line-soft);
}
.cx-figs div { min-width: 0; }
.cx-figs dt { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--ink-mute); white-space: nowrap; }
.cx-figs dd { margin: 3px 0 0; font-size: 17px; font-weight: 600; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
.cx-post { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--line-soft); }
.cx-post .cx-eyebrow { margin-bottom: 10px; }
.cx-post-row { display: grid; grid-template-columns: 60px minmax(0, 1fr); gap: 12px; align-items: center; margin-bottom: 12px; }
.cx-post-row.is-bare { grid-template-columns: minmax(0, 1fr); }
.cx-cover { display: block; aspect-ratio: 3 / 4; border-radius: 10px; overflow: hidden; background: var(--line-soft); }
.cx-cover img, .cx-cover video { width: 100%; height: 100%; object-fit: cover; display: block; }
.cx-post-who { display: grid; gap: 3px; min-width: 0; }
.cx-post-who b { font-size: 15px; font-weight: 600; }
.cx-plat { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--ink-soft); }
.cx-mini { display: flex; gap: 14px; font-size: 13px; color: var(--ink-soft); }
.cx-mini > span { display: inline-flex; align-items: center; gap: 5px; font-variant-numeric: tabular-nums; }
/* Each live post on a booking's card reads as the Top post does
   (2026-10-10), one under another with a hairline between. */
.cx-posts { display: grid; gap: 12px; margin-top: 12px; }
.cx-post-item + .cx-post-item { padding-top: 12px; border-top: 1px solid var(--line-soft); }
.cx-measured { font-size: 12px; color: var(--ink-mute); }
/* At a desk the post's button stands at the row's right edge, beside what
   it opens; on a phone it runs the card's width under it. */
@media (min-width: 641px) {
  .cx-post-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 16px; align-items: center; }
  .cx-post-item .cx-post-row { margin-bottom: 0; }
}
.cx-postbtn { gap: 7px; }
.cx-postbtn svg { width: 13px; height: 13px; opacity: .7; }
@media (max-width: 640px) {
  .cx-postbtn { display: flex; width: 100%; justify-content: center; }
  .cx-figs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* A booking as a journey, each step named (the client's page and the
   creator's own); on the creator's page the job owed first, a large place
   to add files, and the act beside the fact. */
/* The steps as Apple tracks an order: a hairline a step, each named under
   it in small type (the user, 2026-10-08: "Do you think Apple would put
   such huge stage status"; and before it, bars with no names could not be
   read). Done steps in the mute ink, the one in hand amber with its name
   in ink, what follows on the soft line. */
.cx-journey { list-style: none; margin: 12px 0 0; padding: 0; display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 4px; }
.cx-journey li { display: grid; gap: 6px; min-width: 0; font-size: 12px; line-height: 1.3; color: var(--ink-mute); }
.cx-journey li::before { content: ""; height: 3px; border-radius: 2px; background: var(--line-soft); }
.cx-journey li.is-done::before { background: var(--ink-mute); }
.cx-journey li.is-now::before { background: var(--dot-wait); }
.cx-journey li.is-now { color: var(--ink); font-weight: 600; }
/* A step's name is two words at most (Shoot visit, Client review), never
   cut: it breaks at its space where its column is narrow (2026-10-15, the
   user: a status is never shortened into a word that changes its meaning). */
.cx-journey span { overflow-wrap: normal; word-break: keep-all; hyphens: none; }
@media (max-width: 400px) { .cx-journey li { font-size: 11px; } .cx-journey { gap: 4px; } }
.is-tight .cx-journey li { font-size: 11px; }
.is-tight .cx-journey { gap: 4px; }
.cx-nextup { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--line-soft); }
.cx-nextup .cx-eyebrow { margin-bottom: 4px; font-size: 12px; }
.cx-quietlabel { font-weight: 400; color: var(--ink-mute); }
.cx-nextup-title { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; line-height: 1.3; }
.cx-nextup .due-countdown { display: inline-flex; align-items: center; gap: 6px; margin-top: 10px; }
.cx-nextup .booking-deliver { border-top: 0; padding-top: 0; }
.cx-nextup .booking-deliver > .cx-drop:first-child { margin-top: 0; }
.cx-drop {
  position: relative; display: grid; justify-items: center; gap: 4px; margin-top: 12px; padding: 22px 12px;
  border-radius: var(--radius-sm); background: var(--tonal); text-align: center; cursor: pointer;
}
.cx-drop:focus-within { box-shadow: 0 0 0 3px var(--action-ring); }
@media (hover: hover) { .cx-drop:hover { background: var(--tonal-hover); } }
.cx-drop .ic { width: 22px; height: 22px; color: var(--action-ink); margin-bottom: 4px; }
.cx-drop b { font-size: 14px; font-weight: 600; color: var(--ink); }
.cx-drop span { font-size: 13px; color: var(--ink-mute); }
.cx-drop-in { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
.cx-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.cx-chips .plink { gap: 6px; }
/* On a phone a creator's two contact chips (WhatsApp, Call) share their
   row as equal halves, as every reach line does (CR-8). */
@media (max-width: 640px) { .cwork .cx-chips:has(> .plink + .plink) > .plink { flex: 1 1 0; justify-content: center; } }
/* A fact that carries its acts takes the row, so its chips sit side by side. */
.cx-facts > div:has(.cx-chips),
.booking-facts.cx-facts:has(> :nth-child(5)) > div:has(.cx-chips) { grid-column: 1 / -1; }
/* On a phone the shoot (a day and its time) takes its row, and the last
   plain fact left alone before the chip facts takes its row too
   (2026-10-20, CRE-1: Shoot's time wrapped in half a phone). */
@media (max-width: 640px) {
  .cx-facts > div:first-child { grid-column: 1 / -1; }
  .cx-facts > div:not(:first-child):not(:has(.cx-chips)):nth-child(even):has(+ div .cx-chips),
  .cx-facts > div:not(:first-child):not(:has(.cx-chips)):nth-child(even):last-child { grid-column: 1 / -1; }
}
.cx-nextup + .cx-facts { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--line-soft); }
/* A booking card in three parts, never run together on a phone (the user,
   2026-10-09: "looks very compacted in mobile"): who and where it stands,
   the steps, then what is next and the facts under one hairline, 16px
   either side; the facts' rows 16px apart and each value 4px under its
   label. */
.cx-journey + .cx-status,
.cx-journey + .cx-facts { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--line-soft); }
.cx-status ~ .cx-facts { margin-top: 16px; }
.cx-facts { row-gap: 16px; }
.cx-facts dd { margin-top: 4px; }

/* The client portal: the next meeting as a calendar leaf, each confirmed
   line's term, and a glyph a row on the overview. */
.cx-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.cx-meet { display: grid; grid-template-columns: 58px minmax(0, 1fr); gap: 14px; align-items: center; }
.cx-meet-who { min-width: 0; display: grid; gap: 2px; }
.cx-leaf {
  width: 58px; border-radius: 12px; overflow: hidden; background: var(--card);
  box-shadow: inset 0 0 0 1px var(--line); display: grid; justify-items: center; padding-bottom: 6px;
}
.cx-leaf span { font-size: 11.5px; font-weight: 600; color: var(--ink-soft); }
.cx-leaf span:first-child { width: 100%; text-align: center; background: var(--tonal); color: var(--ink); padding: 3px 0 2px; }
.cx-leaf b { font-size: 24px; font-weight: 600; line-height: 1.15; letter-spacing: -.02em; margin-top: 2px; }
.cx-chan { display: inline-flex; align-items: center; gap: 6px; }
.cx-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); }
.cx-pair .btn { gap: 6px; justify-content: center; }
.cx-pair .btn svg { width: 14px; height: 14px; }
.cx-terms { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line-soft); display: grid; gap: 12px; }
.cx-term-bar { height: 6px; border-radius: 3px; background: var(--line-soft); overflow: hidden; margin: 8px 0 0; }
.cx-term-bar i { display: block; height: 100%; border-radius: 3px; background: var(--fill); }
.svc-row.cp-eng-row, .is-narrow .svc-row.cp-eng-row {
  grid-template-columns: 40px minmax(0, 1fr) auto; grid-template-areas: "tile name open"; align-items: center;
}
.svc-row.cp-eng-row > .cx-tile { grid-area: tile; }
.svc-row.cp-eng-row > .svc-name { grid-area: name; }
.svc-row.cp-eng-row > .cp-open { grid-area: open; }
.cx-tile { width: 40px; height: 40px; border-radius: 11px; background: var(--tonal); display: grid; place-items: center; color: var(--ink); }
.cx-tile .ic { width: 18px; height: 18px; }
.cp-reach .plink { gap: 6px; }

.wfy-row:first-child { border-top: 0; }
.wfy-tile svg { width: 16px; height: 16px; stroke-width: 1.8; }
.wfy-what b { font-weight: 600; overflow-wrap: anywhere; }
.wfy-what small { font-size: 12.5px; color: var(--ink-mute); overflow-wrap: anywhere; }
.wfy-what:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--action-ring); border-radius: var(--radius-ctl-sm); }
.wfy-acts .btn { min-width: 0; }
.wfy-age.is-err { color: var(--err); font-weight: 600; }
/* ===== The phone tab bar (2026-10-09; the user: "a tab bar below on
   mobile / pwa", and the foldable iPhone open as well as shut). On a touch
   screen narrower than a desk, and on any window at 640 and under,
   `:root.has-tabbar` (js/admin.js, from one media query) puts the sections
   at the thumb: the first four this person may open, in the rail's order,
   and More, which opens the rest above the bar. The chosen tab is the fill
   behind its glyph, as the rail's chosen row is a fill: never blue (the
   action's), never a weight. Everything that pops up rises above the bar
   as a card (a docked card, the More panel, the confirm bar, and every sheet
   from 2026-10-11, the bar left in sight under its scrim). `--tabbar-space` is
   what every surface at the foot keeps clear: the bar and the home bar
   under it, 0 where there is no bar. ===== */
:root { --tabbar-h: 60px; --tabbar-lift: 10px; --tabbar-space: 0px; }
/* Floating (the user, 2026-10-11, after Instagram's bar): a glass pill held
   16px from each side and lifted off the home bar; `--tabbar-space` keeps
   the pill and its lift clear. */
/* It sits low, as Instagram's does (the user, 2026-10-11: "can go abit
   down"): into the home bar's inset, 14px above the phone's own edge less
   than the inset, never closer than the lift where there is no inset. */
:root.has-tabbar {
  --tabbar-bottom: max(var(--tabbar-lift), calc(env(safe-area-inset-bottom, 0px) - 14px));
  --tabbar-space: calc(var(--tabbar-h) + var(--tabbar-bottom) + var(--tabbar-lift));
}
.tabbar { display: none; }
:root.has-tabbar .tabbar:not([hidden]) {
  position: fixed; z-index: 45;
  left: max(16px, env(safe-area-inset-left, 0px)); right: max(16px, env(safe-area-inset-right, 0px));
  bottom: var(--tabbar-bottom);
  max-width: calc(var(--tabs, 5) * 120px + 10px); margin: 0 auto;
  display: grid; grid-template-columns: repeat(var(--tabs, 5), minmax(0, 1fr));
  box-sizing: border-box; height: var(--tb-h, var(--tabbar-h)); padding: 4px;
  border: 1px solid var(--line-lift); border-radius: calc(var(--tb-h, var(--tabbar-h)) / 2);
  transition: height var(--t-slow) var(--ease), max-width var(--t-slow) var(--ease);
  background: var(--chrome-solid);
  background: color-mix(in srgb, var(--card) 86%, transparent);
  -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px);
  box-shadow: var(--shadow-lift);
  touch-action: none; -webkit-user-select: none; user-select: none;
}
/* The keyboard is up: the bar steps away rather than ride on top of it. */
:root.has-tabbar body.is-typing .tabbar { display: none; }
.tabbar-tab svg { width: 20px; height: 20px; padding: 3px; stroke-width: 1.7; box-sizing: content-box; }
/* More: the rail as a card risen above the bar, holding the sections the bar
   does not (each offered once on the screen) and the Activity record. It
   rises from behind the bar and is taken out of reach while shut. */
:root.has-tabbar .sidebar {
  position: fixed; top: auto; left: max(16px, env(safe-area-inset-left, 0px)); right: max(16px, env(safe-area-inset-right, 0px));
  bottom: var(--tabbar-space); z-index: 44;
  width: auto; height: auto; flex-basis: auto;
  max-height: calc(100vh - var(--tabbar-space) - var(--head-h) - 16px);
  padding: 8px 12px;
  background: var(--raised); border: 1px solid var(--line-lift);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lift);
  transform: translateY(calc(100% + var(--tabbar-space))); visibility: hidden;
  transition: transform var(--t-slow) var(--ease), visibility 0s linear var(--t-slow);
}
@supports (height: 100dvh) {
  :root.has-tabbar .sidebar { max-height: calc(100dvh - var(--tabbar-space) - var(--head-h) - 16px); }
}
:root.has-tabbar .sidebar.is-open {
  transform: none; visibility: visible; box-shadow: var(--shadow-lift);
  transition: transform var(--t-slow) var(--ease);
}
:root.has-tabbar .sidebar-brand,
:root.has-tabbar .navitem.is-tabbed,
:root.has-tabbar .navgroup.is-tabbed { display: none; }
:root.has-tabbar .sidebar { padding-bottom: 4px; }
:root.has-tabbar .scrim {
  position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 43;
  background: var(--scrim); border: 0; padding: 0;
}
/* What sits at the foot keeps clear of the bar. */
:root.has-tabbar .console-body { padding-bottom: calc(32px + var(--tabbar-space)); }
:root.has-tabbar .confirmbar { bottom: var(--tabbar-space); }
:root.has-tabbar .confirmbar-in { padding-bottom: 12px; }
:root.has-tabbar .popcard.is-dock { bottom: calc(12px + var(--sheet-floor)); }
/* Every panel is a card above the bar (the user, 2026-10-11: "every panel
   opens a card above the bar on mobile"), as More is: a sheet, a question,
   search, the task, the finder. Inside the gutters with the large corner,
   it stands on the bar's own floor, so the bar stays in sight under the
   scrim, dimmed and out of reach (a press there is a press on the scrim:
   a form is never left by a tab under a thumb). While the keyboard is up
   the bar steps away and the card takes its room. */
:root.has-tabbar {
  --sheet-floor: var(--tabbar-space);
  --sheet-top: max(16px, calc(env(safe-area-inset-top, 0px) + 8px));
}
:root.has-tabbar body.is-typing { --sheet-floor: calc(8px + env(safe-area-inset-bottom, 0px)); }
:root.has-tabbar .sheet { padding-bottom: var(--sheet-floor); }
@keyframes mk-sheet-float { from { transform: translateY(calc(100% + var(--sheet-floor))); } }
:root.has-tabbar .picker.is-full {
  inset: var(--sheet-top) max(16px, env(safe-area-inset-right, 0px)) var(--sheet-floor) max(16px, env(safe-area-inset-left, 0px));
  padding: 12px 16px; border: 1px solid var(--line-lift);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lift);
  animation: mk-sheet-float var(--t-slow) var(--ease);
}
@media (max-width: 640px) {
  :root.has-tabbar .sheet,
  :root.has-tabbar .sheet.sheet-side,
  :root.has-tabbar .sheet.searchsheet {
    align-items: flex-end; justify-content: center;
    padding: var(--sheet-top) max(16px, env(safe-area-inset-right, 0px)) var(--sheet-floor) max(16px, env(safe-area-inset-left, 0px));
  }
  :root.has-tabbar .sheet-card,
  :root.has-tabbar .sheet-side > .sheet-card.taskcard {
    max-height: calc(100vh - var(--sheet-top) - var(--sheet-floor));
    border-radius: var(--radius-lg); padding-bottom: 8px;
    animation: mk-sheet-float var(--t-slow) var(--ease);
  }
  @supports (height: 100dvh) {
    :root.has-tabbar .sheet-card,
    :root.has-tabbar .sheet-side > .sheet-card.taskcard { max-height: calc(100dvh - var(--sheet-top) - var(--sheet-floor)); }
    }
}
/* Upright at the right edge (the user, 2026-10-27, on the iPhone Duo's
   front screen: "it should be at the right as portrait tabs"). Where a
   phone's page stands narrower than its screen, Safari keeps its own
   controls in a column at the right, and the bar takes that column's line:
   the same tabs one over another, its foot on the bar's usual floor. The
   page keeps the bar's column clear (`--tabbar-room`) in place of the foot,
   More and every panel open beside it, and it is never drawn in. Its tabs
   are their glyphs alone, each still named for a screen reader (the user,
   the same day: "no text needed … just the glyphs icons"). */
:root.has-tabbar.tabbar-side {
  --tabbar-w: 56px;
  --tabbar-right: max(8px, env(safe-area-inset-right, 0px));
  --tabbar-room: calc(var(--tabbar-w) + var(--tabbar-right) + 8px);
  --tabbar-space: env(safe-area-inset-bottom, 0px);
  --sheet-floor: var(--tabbar-bottom);
  --tb-h: var(--tabbar-w);
}
:root.has-tabbar.tabbar-side .tabbar:not([hidden]) {
  left: auto; right: var(--tabbar-right); bottom: var(--tabbar-bottom);
  width: var(--tabbar-w); max-width: none; height: auto; margin: 0;
  max-height: calc(100vh - var(--head-h) - var(--tabbar-bottom) - 16px); overflow-y: auto;
  grid-template-columns: minmax(0, 1fr); grid-template-rows: repeat(var(--tabs, 5), 52px);
  padding: 3px; border-radius: calc(var(--tabbar-w) / 2);
}
:root.has-tabbar.tabbar-side .tabbar-word {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
:root.has-tabbar.tabbar-side .tabbar-dot { top: 9px; }
:root.has-tabbar.tabbar-side .tabbar-thumb {
  top: 0; left: 3px; width: calc(100% - 6px); height: var(--tw, 0px);
  transform: translateY(var(--tx, 0px));
}
:root.has-tabbar.tabbar-side .console-body { padding-right: var(--tabbar-room); }
:root.has-tabbar.tabbar-side .confirmbar { margin-right: 0; }
:root.has-tabbar.tabbar-side .sidebar {
  right: var(--tabbar-room); bottom: var(--tabbar-bottom);
  max-height: calc(100vh - var(--tabbar-bottom) - var(--head-h) - 16px);
  transform: translateX(calc(100% + var(--tabbar-room)));
}
:root.has-tabbar.tabbar-side .sidebar.is-open { transform: none; }
:root.has-tabbar.tabbar-side .popcard.is-dock { right: var(--tabbar-room); }
:root.has-tabbar.tabbar-side .picker.is-full { right: var(--tabbar-room); }
@media (max-width: 640px) {
  :root.has-tabbar.tabbar-side .sheet,
  :root.has-tabbar.tabbar-side .sheet.sheet-side,
  :root.has-tabbar.tabbar-side .sheet.searchsheet { padding-right: var(--tabbar-room); }
}
.crm-logcall.is-here { display: none; }
/* =========================================================================
   VIDEO SCRIPTS (2026-10-09, js/scripts.js): a card a client, a row a video
   (V1, V2… of its shoot): the video over its type, the shoot, the state. A
   video is a record: the facts, the script as the client reads it, and on
   the day each scene's clip number (VC#) and Shot tick; the shoot's other
   videos in the rail.
   ========================================================================= */
.vs-c-name b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vs-c-name small { font-size: 12.5px; color: var(--ink-mute); }
.vs-head .msg { margin: 0; }
.vs-head .msg:empty { display: none; }
.facts.vs-facts { margin: 0; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.is-narrow .facts.vs-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.vs-block { display: grid; gap: 12px; min-width: 0; }
.vs-block > .fsec-h { margin: 0; }
/* What is said and seen reads at 14px, the facts' size, under the 13/600
   headings, on the client's page as in the console (2026-10-31, VS-1). */
.vs-prose { margin: 0; white-space: pre-line; overflow-wrap: anywhere; font-size: 14px; }
.vs-ref { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; }
.vs-ref .plink { max-width: 100%; min-width: 0; }
.vs-ref-url { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* A scene: its number, what is seen (and on a detailed script what is
   said), then the clip number and the tick. */
.vs-scene, .crm-head.vs-scene {
  display: grid; align-items: start; gap: 0 12px;
  grid-template-columns: 32px minmax(0, 1fr) 104px 48px;
  padding: 10px 15px; border-bottom: 1px solid var(--line-soft);
}
.vs-scenes.is-two .vs-scene { grid-template-columns: 32px minmax(0, 1fr) minmax(0, 1fr) 104px 48px; }
.vs-scene:last-child { border-bottom: 0; }
.vs-n { font-variant-numeric: tabular-nums; color: var(--ink-mute); font-weight: 600; font-size: 14px; }
.vs-text { white-space: pre-line; overflow-wrap: anywhere; min-width: 0; font-size: 14px; }
.vs-vc .input { width: 100%; }
.vs-shot input { width: 20px; height: 20px; margin: 6px 0 0; accent-color: var(--accent); }
.vs-empty { margin: 0; padding: 12px 15px; color: var(--ink-mute); }
.is-narrow .crm-head.vs-scene { display: none; }
.is-narrow .vs-scene, .is-narrow .vs-scenes.is-two .vs-scene {
  grid-template-columns: 24px minmax(0, 1fr) auto auto;
  grid-template-areas: "n text text text" "n line line line" "n . vc shot";
  gap: 6px 10px; padding: 12px 15px;
}
.is-narrow .vs-scene > .vs-n { grid-area: n; }
.is-narrow .vs-scene > .vs-vis { grid-area: text; }
.is-narrow .vs-scene > .vs-line { grid-area: line; color: var(--ink-soft); }
.is-narrow .vs-lbl { display: inline; font-size: 12px; font-weight: 600; color: var(--ink-mute); white-space: nowrap; }
/* The client reads the script alone: the scene and, on a detailed script,
   its words, and no clip number or tick (the user, 2026-10-09). */
.vs-scenes.is-read .vs-scene { grid-template-columns: 32px minmax(0, 1fr); }
/* The last scene gives its padding back, so the list ends as far above the
   next block as the hook does (2026-10-20, VSP-1). */
.vs-scenes.is-read .vs-scene:last-child { padding-bottom: 0; }
.vs-scenes.is-read.is-two .vs-scene { grid-template-columns: 32px minmax(0, 1fr) minmax(0, 1fr); }
.is-narrow .vs-scenes.is-read .vs-scene, .is-narrow .vs-scenes.is-read.is-two .vs-scene {
  grid-template-columns: 24px minmax(0, 1fr); grid-template-areas: "n text" "n line";
}
.vs-monthhead h3 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
.vs-monthhead .btn { flex: 0 0 auto; }
.vs-pos { font-size: 13px; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.vs-navbtns .btn { flex: 0 0 auto; min-width: 96px; }
.vs-rec > .panel { margin: 0; }
.vs-factcard > .msg { margin: 0; }
.vs-factcard > .msg:empty { display: none; }
/* A script's card head: its code and title over its type, a hairline
   under it; the console's record and the client's page alike (moved from
   console.css, 2026-10-31, VS-2: the client's head stood bare on the
   ground). */
.vs-cardhead {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px;
  padding-bottom: 16px; border-bottom: 1px solid var(--line-soft);
}
.vs-cardwho { min-width: 0; flex: 1 1 240px; }
.vs-cardwho h3 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; overflow-wrap: anywhere; }
.vs-cardwho p { margin: 2px 0 0; font-size: 12.5px; color: var(--ink-mute); }
.facts.vs-facts .vs-ref { grid-column: 1 / -1; display: block; }
.facts.vs-facts .vs-ref dd { min-width: 0; }

.vs-edit > .rec-head { margin: 0; }
.vs-edit .vs-editform { margin: 0; }
.vs-editform :where(.row) { margin-top: 16px; row-gap: 16px; }
.vs-editfoot .msg { margin: 0; }
.vs-editfoot .msg:empty { display: none; }
/* The client link for Video Scripts (/script/?k=): a tab a content month,
   then each video: its code and type, its facts in one card and its script
   in the next, each scene's VC# and Shot recorded on site. */
.shell-script { max-width: 960px; }
.shell-script > .rec-head { margin-top: 16px; }
.shell-script > .rec-head + .stagestrip { margin-top: 0; }
#vsContent { display: grid; gap: 12px; margin-top: 12px; }
/* A month is a list, a row a script (the user, 2026-10-09: "if i have 30
   videos = one whole long page"): the code and title over the type and the
   shoot, where the crew stands at the right, a chevron. One script opens on
   its own under a line holding the way back to the list, where it stands in
   the month, and Previous and Next. */
.vs-index { padding: 0; overflow: hidden; }
.vs-pick {
  display: grid; grid-template-columns: minmax(0, 1fr) auto 16px; align-items: center; gap: 4px 12px;
  width: 100%; padding: 14px 20px; border: 0; border-bottom: 1px solid var(--line-soft);
  background: none; color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.vs-pick:last-child { border-bottom: 0; }
@media (hover: hover) { .vs-pick:hover { background: var(--sunk); } }
.vs-pick-name { display: grid; gap: 2px; min-width: 0; }
.vs-pick-name b { font-weight: 600; overflow-wrap: anywhere; }
.vs-pick-name small { font-size: 13px; color: var(--ink-mute); }
.vs-pick-state { font-size: 13px; color: var(--ink-soft); white-space: nowrap; }
.vs-pick > svg { width: 16px; height: 16px; color: var(--ink-mute); }
.vs-open { display: grid; gap: 12px; min-width: 0; }
.vs-openbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; }
.vs-openbar .backlink { margin: 0; }
.vs-steps { display: flex; align-items: center; gap: 8px; }
.vs-pos { margin-right: 4px; font-size: 13px; color: var(--ink-mute); white-space: nowrap; }
.vs-video { display: grid; gap: 12px; min-width: 0; }
.vs-video > .panel { margin: 0; }
/* The panel's own padding at every width (2026-10-31, VS-4: 14/16 on a
   phone where every client panel pads 15/16). */
.vs-card { display: grid; gap: 24px; }
@media (max-width: 640px) {
  .vs-pick { padding: 12px 14px; }
  .vs-steps { flex: 1 1 100%; }
  .vs-steps > .btn { flex: 1 1 0; min-width: 0; }
  .vs-card { gap: 16px; }
  .facts.vs-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.wam-head b { font-variant-numeric: tabular-nums; white-space: nowrap; }
.wam-to b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wam-to small, .wam-tpl small, .wam-state small {
  font-size: 12.5px; color: var(--ink-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wat-name b { font-weight: 600; }
.wat-name small { font-size: 12.5px; color: var(--ink-mute); overflow-wrap: anywhere; }
.wat-act .btn { flex: 0 0 auto; }
.wa-preview p { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.wa-preview .wa-head { font-weight: 600; }
.wa-btns li { display: grid; gap: 2px; text-align: center; font-size: 13.5px; color: var(--ink-soft); }
.wa-btns small { font-size: 12px; color: var(--ink-mute); overflow-wrap: anywhere; }
.ro-row:first-child { border-top: 0; }
.ro-row.is-drag { background: var(--sunk); border-radius: var(--radius-sm); }
.qt-amt small, .qt-state small { font-size: 12px; color: var(--ink-mute); font-weight: 400; }
.qt-ovrow:first-child { border-top: 0; }
.qt-ovend b { font-weight: 600; }

.qb-name small { display: block; }
.qb-tot > div { display: flex; justify-content: space-between; gap: 16px; }
.qb-tot dt { color: var(--ink-soft); }
.qb-tot dd { margin: 0; }
.qb-tot > .is-total { border-top: 1px solid var(--line); padding-top: 8px; font-weight: 600; color: var(--ink); }
.qb-tot > .is-total dt { color: var(--ink); }
.qb-plan.is-on { background: var(--line-soft); }
.qb-plantext b { font-size: 14px; font-weight: 600; }
.qb-plantext small { font-size: 12.5px; color: var(--ink-mute); }
.qb-three > [hidden] { display: none; }
.qb-stepval::-webkit-outer-spin-button, .qb-stepval::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* ---------- The quotation website (/quote/, js/quote.js, 2026-10-14) ----------
   As the approved canvas draws it: one 560px column of steps, each a step
   count over a track, the title, its choices as shaded cards ringed in ink
   once chosen, and Back / Continue at the foot; the quotation itself on the
   record's proportion at a desk, the billing details beside it, and on a
   phone under it once Proceed is pressed. */
.shell.qs { max-width: 1180px; padding-top: 24px; }
/* One control size on the page, the portal's own (the user, 2026-10-27):
   every field, select, segment, button, language card and stepper stands
   at the size token (--ctl-h, 34 at a desk, 36 under a finger) with its
   words at the control text (--ctl-text, --field-text), never a page's own
   figure. A stepper's − and + are the small control inside it. */
.qs-chip .qs-text, .qs-pick:not(:has(small)) .qs-text, .qs-stepper > span:first-child { font-size: var(--ctl-text); }
.qs-step, .qs-foot { max-width: 560px; margin: 0 auto; }
.qs-step { display: grid; gap: 16px; }
.qs-progress { display: grid; gap: 6px; font-size: 13px; color: var(--ink-mute); }
.qs-track { height: 4px; border-radius: 2px; background: var(--line-soft); overflow: hidden; }
.qs-track > i { display: block; height: 4px; border-radius: 2px; background: var(--fill); transition: width var(--t-slow) var(--ease); }
/* The page's type on the golden ratio from its 16px body (the user,
   2026-10-27): the title 26 (16 × 1.618), a card's name 16 over its line
   13 (16 ÷ √1.618), the notes 13; the title's own line 8 under it, the
   next block 24. */
/* The title is its lead times φ, one figure setting both (`--qs-lead`): the
   quotation's opening page 16 over 26; a details form's lead is a fact read
   beside 14 and 15px fields, so the form takes 14 over 22.65 (2026-10-31,
   DET-5, SUP-4). */
.qs { --qs-lead: 16px; }
.qs.qs-formpage { --qs-lead: 14px; }
.qs-title { margin: 0; font-family: var(--font-head); font-size: calc(var(--qs-lead, 16px) * 1.618); font-weight: 600; letter-spacing: -.02em; line-height: 1.2; }
.qs-lead { margin: 0; font-size: var(--qs-lead, 16px); color: var(--ink-soft); }
.qs-top { display: grid; gap: 8px; }
.qs-lead .qs-part { white-space: nowrap; }
/* A page's head (its title over its lead) stands 24 above what follows, the
   opening page and a details form alike (DET-4, SUP-3). */
.qs-intro, .qs-step:has(> .qs-top) { gap: 24px; }
.qs-steps { margin: 0; padding: 0; list-style: none; counter-reset: qs-step; display: grid; gap: 12px; }
.qs-steps li { counter-increment: qs-step; display: grid; grid-template-columns: 28px minmax(0, 1fr); column-gap: 12px; align-items: baseline; }
.qs-steps li::before {
  content: counter(qs-step); align-self: start; display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 50%; background: var(--sunk); font-size: 13px; font-weight: 600; color: var(--ink);
}
.qs-list { display: grid; gap: 8px; }
/* A step's categories: each a heading over its items 8px apart, the next set off by a hairline and a 24px step. */
.qs-secs { display: grid; gap: 24px; }
.qs-sec { display: grid; gap: 8px; min-width: 0; }
/* The plan's own questions (#qsExtraBox) are redrawn alone, yet stand in the
   step's one grid: the box gives up its own box so each category keeps the
   24px step and the hairline (2026-10-28: the hairline stood under the last
   option with no step, and none stood above the first). */
.qs-xbox { display: contents; }
.qs-sec + .qs-sec, .qs-sec + .qs-xbox > .qs-sec:first-child { border-top: 1px solid var(--line-soft); padding-top: 24px; }
.qs-sec-h { display: block; margin: 0 0 4px; font-size: 16px; font-weight: 600; letter-spacing: -.01em; line-height: 1.3; color: var(--ink); }
.qs-sec > .qs-note, .qs-sec > .field { margin: 0; }
.qs-note { margin: 0; font-size: 13px; color: var(--ink-mute); }
.qs-hint { font-size: 13px; color: var(--ink-mute); }
.qs-pick, .qs-chip {
  display: grid; grid-template-columns: minmax(0, 1fr) 22px; column-gap: 12px; align-items: start; padding: 14px; border-radius: var(--radius);
  background: var(--sunk); cursor: pointer; transition: box-shadow var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.qs-pick.is-on, .qs-chip.is-on { background: var(--card); box-shadow: inset 0 0 0 1.5px var(--ink); }
.qs-pick:has(input:focus-visible), .qs-chip:has(input:focus-visible) { box-shadow: inset 0 0 0 1.5px var(--ink), 0 0 0 3px var(--action-ring); }
.qs-text { grid-column: 1; grid-row: 1; min-width: 0; }
.qs-consent { display: grid; grid-template-columns: 22px minmax(0, 1fr); column-gap: 12px; align-items: start; font-size: 15px; cursor: pointer; }
.qs-consent > input, .qs-consent > .qs-mark { grid-column: 1; grid-row: 1; margin: 1px 0 0; }
.qs-consent > input { width: 22px; height: 22px; position: relative; z-index: 1; opacity: 0; cursor: pointer; }
.qs-consent > .qs-text { grid-column: 2; }
.qs-consent:has(input:focus-visible) .qs-mark { box-shadow: inset 0 0 0 1.5px var(--line-ctl), 0 0 0 3px var(--action-ring); }
.qs-pick > input, .qs-chip > input, .qs-mark { grid-column: 2; grid-row: 1; width: 22px; height: 22px; margin: 0; }
.qs-pick > input, .qs-chip > input { position: relative; z-index: 1; opacity: 0; cursor: pointer; }
.qs-mark {
  display: inline-flex; align-items: center; justify-content: center; border-radius: 50%;
  box-shadow: inset 0 0 0 1.5px var(--line-ctl); color: var(--on-fill);
  transition: background var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.qs-mark svg { opacity: 0; transition: opacity var(--t-fast) var(--ease); }
/* Several may be chosen: a rounded square, filled in ink with its tick. One of a set: a ring with an ink dot. */
input[type=checkbox] ~ .qs-mark { border-radius: 6px; }
input[type=checkbox]:checked ~ .qs-mark { background: var(--fill); box-shadow: none; }
input[type=checkbox]:checked ~ .qs-mark svg { opacity: 1; }
input[type=radio] ~ .qs-mark svg { display: none; }
input[type=radio] ~ .qs-mark::after { content: ""; width: 10px; height: 10px; border-radius: 50%; background: var(--fill); transform: scale(0); transition: transform var(--t-fast) var(--ease); }
input[type=radio]:checked ~ .qs-mark { box-shadow: inset 0 0 0 1.5px var(--ink); }
input[type=radio]:checked ~ .qs-mark::after { transform: scale(1); }
.qs-pick b { display: block; font-weight: 600; }
.qs-pick small { display: block; margin-top: 2px; font-size: 13px; color: var(--ink-soft); white-space: pre-line; }
.qs-chips { display: grid; gap: 8px; }
.qs-two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.qs-three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* A choice of one line (a chip, or a card with no line under its name) is a control: the token's height. */
.qs-chip, .qs-pick:not(:has(small)) { align-items: center; min-height: var(--ctl-h); padding: 0 12px 0 14px; border-radius: var(--radius-sm); }
.qs-stepper > span:first-child small { display: block; font-size: 13px; color: var(--ink-mute); }
.qs-stepper {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: var(--ctl-h);
  padding: 0 4px 0 14px; border-radius: var(--radius-sm); background: var(--sunk);
}
.qs-count { display: flex; align-items: center; gap: 4px; }
.qs-count b { min-width: 18px; text-align: center; font-weight: 600; font-variant-numeric: tabular-nums; }
.qs-pm { min-width: var(--ctl-h-sm); width: var(--ctl-h-sm); padding: 0; font-size: 18px; }
.qs-form { display: grid; gap: 16px; }
/* Fields in a form's section stand the form's 16 apart, so a label sits with its own field
   (the 8 of .qs-sec is for choice cards). */
.qs-form .qs-sec { gap: 16px; }
.qs-form .field { display: grid; gap: 4px; }
/* On the scale (DET-8, SUP-8): a label 8 over its field (its 4 and the
   field's gap), a section's heading 16 over its first field (the gap alone). */
.qs-form .field-label { margin-bottom: 4px; }
.qs-form .qs-sec-h { margin-bottom: 0; }
/* A tick's refusal stands 4 under it, in line with its words (DET-3, SUP-2,
   QTE-5): the form's 16 given back to 4, the tick's track and its gap in. */
.qs-form .qs-consent + .msg { margin-top: -12px; padding-left: 34px; }
/* A select with nothing chosen reads its prompt (Optional, Choose a term) in
   the mute ink, as a field's placeholder does; the list keeps its ink (DET-9). */
.qs-form .select:has(> option[value=""]:checked) { color: var(--ink-mute); }
.qs-form .select option { color: var(--ink); }
.qs-form .field .msg { margin: 0; }
.qs-form .field .msg:empty { display: none; }
.qs-form .input, .qs .field .select { width: 100%; }
.qs-caps { text-transform: uppercase; }
.qs-consent { font-size: 14px; color: var(--ink-soft); align-items: flex-start; }
.qs-step > .msg { margin: 0; }
.qs-step .msg:empty, .qs-cf:empty { display: none; }
.qs-foot { display: flex; gap: 8px; padding: 20px 0 8px; }
.qs-foot > .btn { flex: 1 1 0; }
/* A forward move is a button at its own width: a lone Submit stands at the
   column's right edge at a desk, full width on a phone (SPF-4, DTL-3). */
@media (min-width: 641px) { .qs-foot > .btn:only-child { flex: 0 0 auto; margin-left: auto; } }

.qs-quote { display: grid; gap: 24px; }
/* A quotation is read as a document of figures, not page copy: its lines,
   totals and the Accept card take the console's 14px reading size beside
   14px controls and 13px notes; 16px body made them read oversized on a
   phone (the user, 2026-10-31). */
.qs-quote { font-size: 14px; }
.qs-main { display: grid; gap: 12px; align-content: start; min-width: 0; }
.qs-change { justify-self: start; }
.qs-head { display: grid; gap: 4px; }
.qs-meta { font-size: 13px; color: var(--ink-mute); }
.qs-status .chip-state { font-size: 13px; }
.qs-table { display: grid; }
.qs-row {
  display: grid; grid-template-columns: minmax(0, 1fr) 64px 110px 130px; gap: 0 12px; padding: 10px 0;
  border-top: 1px solid var(--line-soft);
}
.qs-headrow { border-top: 0; padding-top: 0; font-size: 12px; font-weight: 600; color: var(--ink-mute); }
.qs-what { overflow-wrap: anywhere; }
.qs-what small { display: block; font-size: 12.5px; color: var(--ink-mute); }
.qs-calc { display: none; font-size: 12.5px; color: var(--ink-mute); }
.qs-num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.qs-mute { color: var(--ink-mute); }
/* A line quoted on request has no quantity or rate: its words take the figure tracks, ending on the column's right edge. */
.qs-onreq { grid-column: 2 / -1; }
.qs-tot { margin: 0; display: grid; justify-self: end; width: min(420px, 100%); border-radius: var(--radius-sm); overflow: hidden; background: var(--sunk); }
.qs-tot > div { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 8px 16px; }
.qs-tot dt { color: var(--ink-soft); }
.qs-tot dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
.qs-tot > .is-total { border-top: 1px solid var(--line); font-weight: 600; }
.qs-tot > .is-total dt { color: var(--ink); }
.qs-acts { display: flex; gap: 8px; }
.qs-acts > .btn { flex: 1 1 0; }
.qs-notes { display: grid; gap: 2px; font-size: 13px; color: var(--ink-mute); }
/* The review page's way to chat stands under its note at its own width at a
   desk and runs the column on a phone, as a client form's lone act does. */
.qs-review-act { justify-self: start; }
@media (max-width: 640px) { .qs-review-act { justify-self: stretch; display: grid; } }
/* Beside the quotation (under it on a phone): a proposal to ask for, else
   the colleague who took the lead (2026-10-26). */
.qs-side { display: grid; gap: 12px; align-content: start; align-self: start; background: var(--sunk); border-radius: var(--radius-lg); padding: 22px; }
.qs-side h2 { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
.qs-side p { margin: 0; color: var(--ink-soft); }
.qs-side .btn { width: 100%; }
.qs-side .msg:empty { display: none; }
.qs-ask { margin: 0; padding: 0; border: 0; min-width: 0; display: grid; gap: 8px; }
.qs-ask legend { padding: 0; margin: 0 0 8px; font-size: 15px; font-weight: 600; color: var(--ink); }
.qs-side .qs-pick { background: var(--card); }
.qs-side-rep { display: grid; gap: 12px; border-top: 1px solid var(--line); padding-top: 16px; margin-top: 4px; }
.qs-asked { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 12px; align-items: start; }
.qs-asked > span:last-child { display: grid; gap: 2px; color: var(--ink-soft); }
.qs-asked b { color: var(--ink); font-weight: 600; }
.qs-asked-tick { width: 24px; height: 24px; border-radius: 50%; background: var(--ok-solid); color: var(--on-ok); display: inline-flex; align-items: center; justify-content: center; }
.qs-rep { display: flex; align-items: center; gap: 12px; }
.qs-rep-mark { flex: 0 0 auto; width: 40px; height: 40px; border-radius: 50%; background: var(--tonal); display: inline-flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; }
.qs-rep-who { display: grid; min-width: 0; }
.qs-rep-who small { font-size: 14px; color: var(--ink-mute); }
/* Accepting the quotation in the side card (2026-10-31): the three steps
   as a short numbered list (the one in hand in ink, those done the green
   tick with what was given under them), each step's fields as the details
   step's, Back and the forward step as equal halves, the signature pad, the
   code's six boxes and, once signed, who signed for which company and when. */
.qs-ssteps { gap: 0; }
.qs-ssteps li { grid-template-columns: 22px minmax(0, 1fr); column-gap: 10px; align-items: center; padding: 8px 0; border-top: 1px solid var(--line); font-size: 14px; color: var(--ink-soft); }
.qs-ssteps li:first-child { border-top: 0; padding-top: 0; }
.qs-ssteps li:last-child { padding-bottom: 0; }
.qs-ssteps li::before { align-self: center; width: 22px; height: 22px; font-size: 12px; background: var(--card); }
.qs-ssteps li.is-now { color: var(--ink); font-weight: 600; }
.qs-ssteps li.is-now::before { background: var(--fill); color: var(--on-fill); }
.qs-ssteps li.is-done::before { content: "\2713"; background: var(--ok-solid); color: var(--on-ok); }
.qs-ssteps li > span { display: grid; min-width: 0; }
.qs-ssteps small { font-size: 13px; font-weight: 400; color: var(--ink-mute); overflow-wrap: anywhere; }
.qs-side h2[tabindex] { outline: none; }
.qs-signform .textarea { width: 100%; }
.qs-sform-h { margin: 8px 0 0; padding-top: 16px; border-top: 1px solid var(--line); }
.qs-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.qs-sfoot { display: flex; gap: 8px; padding-top: 4px; }
.qs-sfoot > .btn { flex: 1 1 50%; min-width: 0; }
.qs-side .btn-quiet.btn-sm { width: auto; }
.qs-side .qs-tot { width: 100%; background: var(--card); }
.qs-sig-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: var(--ctl-h-sm); margin-bottom: 4px; }
.qs-sig-head .field-label { margin: 0; }
.qs-sig-acts { display: flex; gap: 4px; margin-right: -8px; }
.qs-pad { position: relative; aspect-ratio: 3 / 1; border-radius: var(--radius-sm); background: var(--field); box-shadow: inset 0 0 0 1px var(--field-edge, transparent); touch-action: none; cursor: crosshair; overflow: hidden; }
.qs-pad[aria-invalid="true"] { box-shadow: inset 0 0 0 1.5px var(--err); }
.qs-pad svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.qs-pad path { fill: none; stroke: var(--ink); stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.qs-pad-line { position: absolute; left: 16px; right: 16px; bottom: 24%; border-top: 1px solid var(--line-ctl); }
.qs-pad-hint { position: absolute; left: 16px; bottom: calc(24% + 6px); font-size: 13px; color: var(--ink-mute); pointer-events: none; }
.qs-terms { justify-self: start; margin: -8px 0 0 -8px; }
.qs-otp { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 6px; }
.qs-otp .input { width: 100%; min-width: 0; padding: 0; text-align: center; font-size: 20px; font-weight: 500; font-variant-numeric: tabular-nums; }
.qs-resend { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 13px; color: var(--ink-mute); }
.qs-resend .btn { margin-right: -8px; }
.qs-sfacts { margin: 0; display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 16px; font-size: 14px; }
.qs-sfacts dt { color: var(--ink-mute); }
.qs-sfacts dd { margin: 0; overflow-wrap: anywhere; }
@media (min-width: 900px) {
  .qs-quote { grid-template-columns: minmax(0, 1.618fr) minmax(0, 1fr); gap: 32px; }
  .qs-quote.is-single { grid-template-columns: minmax(0, 1fr); max-width: 560px; }
}
@media (max-width: 640px) {
  .shell.qs { padding-top: 16px; }
  .qs-headrow, .qs-q, .qs-r { display: none; }
  .qs-row { grid-template-columns: minmax(0, 1fr) auto; }
  .qs-calc { display: block; }
  .qs-tot { width: 100%; }
}
.qs-done { justify-items: start; padding-top: 16px; }
.qs-done p { margin: 0; }
.qs-tick { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; border-radius: 50%; background: var(--ok-solid); color: var(--on-ok); }
body.qs-page { background: var(--card); }
.qs-change { display: inline-flex; align-items: center; gap: 6px; margin-left: -8px; }
@media (min-width: 900px) { .qs-acts > .btn { flex: 0 0 auto; } }

/* A finished booking on the creator's page (2026-10-14): its name and state,
   the rest under one fold, shut. */
.booking-fold { margin-top: 12px; border-top: 1px solid var(--line-soft); }
.booking-fold > summary {
  list-style: none; cursor: pointer; display: flex; align-items: center; justify-content: space-between;
  min-height: var(--ctl-h); padding-top: 4px; font-size: 14px; font-weight: 500; color: var(--ink-soft);
}
.booking-fold > summary::-webkit-details-marker { display: none; }
.booking-fold > summary::after {
  content: ''; width: 8px; height: 8px; border-right: 1.8px solid currentColor; border-bottom: 1.8px solid currentColor;
  transform: rotate(45deg); margin-right: 4px; transition: transform var(--t) var(--ease);
}
.booking-fold[open] > summary::after { transform: rotate(-135deg); }
@media (hover: hover) { .booking-fold > summary:hover { color: var(--ink); } }
.booking-fold > summary:focus-visible { outline: 2px solid var(--action-ring); outline-offset: 2px; border-radius: var(--radius-ctl-sm); }
.booking-fold-body { padding-top: 4px; }
.booking.is-done .booking-rate { margin-top: 12px; }



/* A supplier's details form (2026-10-18, js/supplierform.js): the
   authorisation the Jotform form carried, numbered a) to f) as it was. */
.sf-auth { margin: 0; padding-left: 20px; list-style: lower-alpha; gap: 4px; color: var(--ink-soft); }

/* Buttons sharing a row share one width under a finger wider than a phone
   too (the iPhone Duo pass, 2026-10-18: a foldable opened like a book, a
   tablet upright). The phone's own rules set it at 560 and under; above
   that a touch screen kept each button at its words' width, so Save stood
   92px beside Release to client's 148. Two buttons or more each take 220px,
   one width without becoming slabs, and a row too narrow for them wraps
   rather than squeezing a label onto two lines (a 626 card did); a lone
   button, and a quiet one, keep their own width. */
@media (pointer: coarse) and (min-width: 561px) {
  :is(.kactions, .viewhead, .crm-engage-actions):has(> .btn:not(.btn-quiet):not([hidden]) ~ .btn:not(.btn-quiet):not([hidden])) > .btn:not(.btn-quiet) {
    flex: 0 1 220px; white-space: nowrap;
  }
}

/* Squircle corners (the user, 2026-10-24: "Yes, where supported"): where the
   browser draws `corner-shape` (Chrome), the bounded surfaces and the
   controls take Apple's continuous corner, the same radius tokens; a
   browser without it (Safari today) keeps the round corner. Pills, discs,
   dots and rings stay round. */
@supports (corner-shape: squircle) {
  .panel, .softpanel, .stg-card, .stg-nav, .ovcard, .chartcard, .ovw-kpi, .kcard, .rec-head,
  .sheet-card, .askcard, .kmenu, .popcard, .picker, .guidecard, .cover-panel, .cp-card, .qs-pick, .qs-side,
  .btn, .input, .select, textarea, .acttab, .rectabs, .tabrow, .cmdbar-views, .tab, .undobar, .msg.warn {
    corner-shape: squircle;
  }
}
