/* ==========================================================================
   SFC Workspace — views
   Meetings list · calendar month and week · forms · modal · drawer · profile.
   No colour literals: every value is a token from tokens.css (rule 7).
   ========================================================================== */

/* ---------- 1. Each screen's own opening (§3.28) ---------------------------

   There is no page head. Nine screens used to open with the same 130px block
   — 28px title, grey subtitle, sadu rule, one or two pill buttons — and the
   fresh reviewer's sharpest finding was that the phase changed the grid on
   four screens and left that block on nine, so the top fifth of nine
   screenshots stayed pixel-similar. The block named the screen the sidebar
   was already naming, and its subtitle was read once and never again.

   What replaced it is deliberately NOT one thing:

     My Day    nothing — the display answer is the heading
     Board     a slim action row belonging to the board
     Table     a real <caption> on the register
     Timeline  the chart's own corner, previously empty
     Calendar  folded into the month navigator, which was a second head
     Meetings  folded into the filter row
     Minutes   nothing — the filter row is the whole of the chrome
     Projects  the "new" control is the last BAND in the stack
     Archive   folded into the tab strip
     Deleted   nothing
     Profile   nothing

   Two rules held while doing it: the sadu moved rather than went (it is the
   sidebar's edge now, see shell.css §3.28), and no filter row was removed —
   they changed shape, they did not vanish.
   -------------------------------------------------------------------------- */

/* `.board-actions` is gone: the two buttons live in the shared header's acts
   rail now, which is the same rail Calendar uses. One less bespoke row. */

/* Table: a <caption> is the element a table already has for naming itself, and
   it is the header pattern the other screens were given — so it now carries
   `.page-head` and the shared row and acts rail rather than three classes of
   its own. `.reg-cap` keeps only what is specific to being a caption.

   The <caption> keeps display: table-caption. Setting `display: flex` on it
   takes it OUT of table-caption layout, so caption-side stops applying and it
   rendered below the sticky header instead of above it. `.page-head` sets no
   display for exactly that reason; the flex is on an inner wrapper. */
.reg-cap {
  caption-side: top;
  text-align: start;
}
/* `.reg-count` is `.page-head-count` now — it was the pattern, so it moved to
   where the pattern lives rather than being copied to a second screen. */


/* ---------- Task filter bar (§9.1) ---------------------------------------

   IT SCROLLS, IT DOES NOT WRAP. The 8.6 review's verdict on the Meetings row
   was that it "wraps and reads as rubble" — six controls reflowing into two
   or three ragged lines whose shape changes with the viewport, so the row has
   no silhouette of its own and the eye re-parses it at every width.

   One line, always, with overflow-x on the row itself. A filter bar is a
   TOOL RAIL: the controls have a fixed order worth learning, and scrolling
   preserves that order where wrapping destroys it. The scroll container is
   the row, never the page (§3.24 — the page body must never scroll
   sideways).

   It sits inside the register's <caption>, which is Table's opening object
   (§3.28), so it carries no border and no fill: a second bordered box above
   the table is the "grid of rounded rectangles" 8.6 was called to remove. */
.filter-bar {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-block-start: var(--sp-3);
  padding-block-end: var(--sp-1);
  overflow-x: auto;
  /* The controls must keep their own width inside a scroller — flex children
     shrink to fit by default, which is how a rail becomes rubble without
     wrapping at all. */
  scrollbar-width: thin;
}
.filter-bar > * { flex: 0 0 auto; }
.filter-bar .select { min-height: 34px; padding-block: var(--sp-1); font-size: var(--fs-200); }
.filter-bar .btn { margin-inline-start: var(--sp-2); }

/* ---------- The Board's strip --------------------------------------------

   Not a filter bar. The Board's opening object is its columns (§3.24) and a
   row of selects above them is the collision that phase forbade. This says
   only WHAT IS APPLIED and offers the way out, in one line — because a filter
   applied with nothing on screen saying so is invisible state (§15.3a).

   Tinted, so it reads as a condition on the screen rather than a control of
   it — and therefore it needs a real edge, because --tint-accent on the page
   ground measures 1.01:1 and a fill cannot say where it ends (§3.23).

   A FULL-BLEED BAND, for the same reason `.md-unclosed` is one. Board has a
   shell now, and a rounded tinted box inside it runs its own border 24px
   inside the shell's — two near-identical lines in parallel, which is the
   box-against-box the shell exists to remove. Its sides are bounded by the
   shell; the rules above and below are its own boundary. Its top rule also
   takes over from the header's, so the two do not stack. */
.filter-strip {
  margin-block: 0 var(--sp-4);
  background: var(--tint-accent);
  border-block: 1px solid var(--edge-accent);
}
/* The band below carries the division, exactly as on My Day. */
.page-head:has(+ .filter-strip) { border-block-end: 0; }
.filter-strip .fs-text { font-weight: 700; min-width: 0; overflow-wrap: anywhere; }
.filter-strip .btn { margin-inline-start: auto; flex: 0 0 auto; }

/* My Day's staff-only count of meetings whose time has passed and are still
   Scheduled (§7.6). Tinted, so it reads as a CONDITION rather than a control.

   A FULL-BLEED BAND, NOT A ROUNDED TINTED CARD, and the app had already made
   this decision once: `.blocked-strip` on Board carries the same note, in the
   same words, for the same kind of notice. Inside the shell a rounded box ran
   its own border 24px inside the shell's, two near-identical lines in
   parallel - box-against-box, one step short of the failure the shell exists
   to avoid. Bleeding it to the shell's inner edge removes the parallel run
   entirely: the sides are bounded by the shell itself, and the rules above
   and below are the block's own boundary (§3.23).

   It also moved OUT of the work column, where it never belonged. It is a
   page-level condition, not a property of the right-hand margin note - and a
   grid child cannot bleed to the page edge in any case, only to its column.

   Its TOP rule is now the opening's separator, which is why `.md-open` drops
   its own border when this follows: two lines a few pixels apart, one padded
   and one full-bleed, is the thing being fixed rather than a smaller version
   of it.

   THE WIDTH IS EXPLICIT BECAUSE THIS IS A BUTTON, NOT A DIV. `.blocked-strip`
   is a div, so `width: auto` fills its container and the negative margins do
   the rest. A BUTTON's auto width is FIT-CONTENT - so the same rule rendered
   this at 207px in a 1382px shell, a short box pulled flush against the left
   edge by its own negative margin. The CSS read correctly and the screen was
   wrong, which is the difference only a render can report.

   THE EDGE IS `--ink-warn`, NOT `--edge-warn`, AND THAT IS §3.7 NOT TASTE.
   This is a button: its edge must clear 3:1 on every surface it sits on. The
   ramp edge measured 1.236:1 against the page and 1.218:1 against its own
   fill, and the fill is 1.015:1 - so nothing on the control said the control
   was there. The ink clears 4.89 / 4.82 in light and 10.92 / 8.48 in dark.
   A ramp edge is right for a block you cannot press; this is not one. */
.md-unclosed {
  margin-block: 0;
  background: var(--tint-warn);
  border-block: 1px solid var(--ink-warn);
  font-weight: 700;
  cursor: pointer;
}
.md-unclosed .tnum { margin-inline-start: auto; font-weight: 800; }

/* The Mark-held tick list. Plain rows, not pills — the same reasoning as
   .check-line: a list of people is read, not scanned for which are on. */
.attend-list { list-style: none; margin: var(--sp-3) 0; padding: 0; }
.attend-list li + li { margin-block-start: var(--sp-1); }

/* The drawer's management row (§7.6). Rare actions and the destructive one,
   moved out of the footer because a footer is where the FLOW lives — what you
   opened the drawer to do. Separated by a rule rather than a fill: these are
   not a callout, they are the quiet end of the page. */
/* A meeting on the Calendar that the viewer is not in (§7.5, sql/24). Title
   and time only, and NOT a control — rendered as a span, so there is nothing to
   press and nothing to be refused by. Quieter than a real event and without the
   project dot or kind colour, because it carries neither. */
.cal-event.is-partial,
.wk-event.is-partial {
  display: flex; align-items: center; gap: var(--sp-1);
  background: var(--surface-2);
  border: 1px dashed var(--line);
  color: var(--text-3);
  cursor: default;
}
.cal-event.is-partial .ev-title,
.wk-event.is-partial .ev-title { font-weight: 600; }

.drawer-manage {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  margin-block-start: var(--sp-5);
  padding-block-start: var(--sp-4);
  border-block-start: 1px solid var(--line-soft);
}

/* Deleted's page-level notice. `.page-band` carries the shape; the tone is
   this screen's, the same way every other band supplies its own. The values
   are `.callout.warn`'s, so the message reads identically whether it is a note
   inside a dialog or a band across a page. */
.dl-notice {
  background: var(--tint-accent);
  border-block: 1px solid var(--edge-accent);
  color: var(--ink-accent);
  margin-block: 0 var(--sp-4);
}

/* Deleted: a heading for each short ledger, no card around it. */
.reg-head {
  display: flex; align-items: center; gap: var(--sp-2);
  margin: var(--sp-6) 0 var(--sp-2);
  font-size: var(--fs-500); font-weight: 800;
}
.reg-head svg { width: 16px; height: 16px; color: var(--text-3); }
.register + .register .reg-head { margin-block-start: var(--sp-8); }

/* ---------- 2. Toolbar ----------------------------------------------------
   Filters wrap onto as many rows as they need. A horizontally scrolling
   filter bar hides controls people then never find. */

/* A filter row that also opens its screen (§3.28): the count leads, the
   action is pushed to the far end so it never sits between two filters. */
.tb-count { font-weight: 700; }
.tb-end { margin-inline-start: auto; }
.tb-acts { display: flex; gap: var(--sp-2); }

.toolbar {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin-bottom: var(--sp-4);
}

.select {
  min-height: 38px;
  padding: var(--sp-2) var(--sp-3);
  padding-inline-end: var(--sp-8);
  /* background-COLOR, not the shorthand: `background:` resets
     background-image, which is where the caret lives (shell.css). */
  background-color: var(--input);
  border: 1px solid var(--control-line);
  border-radius: var(--r-sm);
  font-size: var(--fs-300);
  font-weight: 600;
  color: var(--text);
}
.select:focus-visible { outline: none; border-color: var(--focus); box-shadow: 0 0 0 3px var(--focus-glow); }

.date-filter {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-200);
  font-weight: 700;
  color: var(--text-2);
}
.date-filter .select { font-weight: 600; }

.seg {
  display: inline-flex; gap: 2px;
  padding: 3px;
  background: var(--surface-2);
  border: 1px solid var(--control-line);
  border-radius: var(--r-sm);
}
.seg button {
  min-height: 32px;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-xs);
  font-size: var(--fs-300);
  font-weight: 700;
  color: var(--text-2);
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.seg button:hover { color: var(--text); }
/* White-on-near-white was 1.05:1 against the unselected segments, at the
   same 700 weight — measured, the only thing telling them apart was a
   shadow. Filled primary makes it 6.6:1 on fill and 7.0:1 on ink. */
.seg button.is-on {
  background: var(--primary);
  color: var(--on-primary);
  box-shadow: var(--shadow-sm);
}
.seg button.is-on:hover { background: var(--primary-strong); color: var(--on-primary); }

/* ---------- 3. Meetings list ---------------------------------------------
   A date gutter, a title block, and a chip rail. Proximity does the grouping;
   there is no card per row, because forty nested cards is a wall. */

/* ==========================================================================
   MEETINGS — a sparse gutter against a dense body (§3.16)

   Every row used to carry its own date stamp, all forty of them the same
   height and evenly spaced, inside one bordered panel. That is a table, and
   it read as one.

   Now the date is printed ONCE per day, in a gutter, and the rows beneath it
   are dense. Meetings do not arrive evenly — three on a Tuesday, none on the
   Wednesday — and the ragged gutter is that fact made visible. The panel is
   gone too: whitespace and the gutter do the grouping a border was doing.

   Blurred: a narrow ragged column of numerals with clusters of text beside
   them. Not the board's vertical channels, not the table's uniform rules.
   ========================================================================== */

.mt-days { list-style: none; margin: 0; padding: 0; }

.mt-day {
  display: grid;
  grid-template-columns: 68px 1fr;
  gap: var(--sp-4);
  padding-block: var(--sp-3);
}
/* The rule sits between DAYS, not between rows — the grouping is the point. */
.mt-day + .mt-day { border-block-start: 1px solid var(--line); }

.mt-date {
  position: sticky; inset-block-start: calc(var(--topbar-h) + var(--sp-3));
  align-self: start;
  display: flex; flex-direction: column; align-items: flex-end;
  text-align: end;
  line-height: 1;
}
.mt-date .d {
  font-size: var(--fs-display); font-weight: 800;
  line-height: .92;
  color: var(--text-2);
}
.mt-date .w { margin-block-start: 4px; font-size: var(--fs-200); font-weight: 700; color: var(--text-3); }
.mt-date .m { margin-block-start: 2px; font-size: var(--fs-100); font-weight: 700; color: var(--text-3); }

/* Today is the only day that gets the primary. One mark on the screen, not a
   highlight competing with every chip in the body. */
.mt-day.is-today .mt-date .d { color: var(--primary); }
.mt-day.is-today .mt-date .w { color: var(--primary); }

.mt-of-day { list-style: none; margin: 0; padding: 0; min-width: 0; }
/* A gap, not a rule. The rows have their own ground now, so a hairline
   between them would draw the same boundary twice. */
.mt-of-day > li + li { margin-block-start: var(--sp-2); }

/* EACH ROW IS AN OBJECT (§3.22).

   Stripping the panel took one border too many: the rows became bare text on
   the page ground, reading as a paragraph rather than as twenty things you
   can open. Nothing said "this is one meeting", and nothing said "this is
   pressable" until the pointer was already over it.

   A surface fixes both, and it needs no border to do it. White on the cool
   ground is the app's existing language for a solid object — the same move
   the board card makes — so a row reads as one thing without the
   bordered-panel-per-meeting look returning. No shadow at rest: twenty
   shadows down a page is a different kind of noise.

   Three states, each on its OWN channel so two can be true at once — a row
   whose drawer is open and which you are also pointing at must not have to
   choose which of the two it shows:

     rest      white, flat                          fill
     hover     tints a shade and lifts              fill + elevation
     is-open   its drawer is on screen              tint + a bar on the edge

   The bar is a ::before rather than an inset box-shadow, and deliberately:
   box-shadow is one property, so the hover lift and the selected edge would
   evict each other by source order, and contrast.js reads a pseudo-element's
   background as an indicator but cannot see inside a shadow list. A mark the
   checker cannot measure is a mark that will quietly go grey one day.
   ========================================================================== */
.meeting-row {
  position: relative;
  display: flex; align-items: baseline; gap: var(--sp-3);
  width: 100%;
  /* Tight. A day with four meetings should read as a busy day, and 72px
     rows made four look like a page. */
  padding: var(--sp-3);
  border-radius: var(--r-sm);
  background: var(--surface);
  text-align: start;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.meeting-row::before {
  content: '';
  position: absolute;
  inset-block: var(--sp-2);
  inset-inline-start: 0;
  width: 3px;
  /* A pill cap on a 3px bar is a semicircle. --r-pill rather than a literal
     3px: the same shape, from the documented scale. */
  border-radius: 0 var(--r-pill) var(--r-pill) 0;
  background: transparent;
  transition: background var(--dur-fast) var(--ease);
}
html[dir="rtl"] .meeting-row::before { border-radius: var(--r-pill) 0 0 var(--r-pill); }
.meeting-row:hover { background: var(--row-hover); box-shadow: var(--shadow); }
.meeting-row:active { transform: scale(.995); transition-duration: var(--dur-press); }
.meeting-row.is-open { background: var(--primary-soft); }
.meeting-row.is-open::before { background: var(--primary); }

/* The time leads inside a day; the gutter has already said which day. */
.meeting-row .at {
  flex: none; width: 52px;
  font-size: var(--fs-300); font-weight: 800;
  color: var(--text-2);
}

.meeting-row .body { flex: 1; min-width: 0; }
.meeting-row .row-title {
  display: block;
  font-size: var(--fs-400);
  font-weight: 700;
  line-height: var(--lh-tight);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.meeting-row .row-meta {
  display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3);
  margin-top: var(--sp-1);
  font-size: var(--fs-200);
  color: var(--text-2);
}
.meeting-row .row-meta > span { display: inline-flex; align-items: center; gap: var(--sp-1); }
.meeting-row .row-meta svg { width: 13px; height: 13px; opacity: .8; }

.meeting-row .row-chips {
  flex: none;
  display: flex; flex-wrap: wrap; gap: var(--sp-1);
  justify-content: flex-end;
  max-width: 45%;
}

/* A project reads as a colour dot plus its name, never colour alone. */
.project-dot { display: inline-flex; align-items: center; gap: var(--sp-1); }
.project-dot::before {
  content: "";
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--dot, var(--primary));
  flex: none;
}

/* ---------- 4. Calendar — shared nav -------------------------------------- */

.cal-nav {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-bottom: var(--sp-3);
}
.cal-label {
  margin: 0;
  min-width: 190px;
  text-align: center;
  font-size: var(--fs-700);
  font-weight: 800;
}
/* The trailing rail is `.page-head-acts` now and carries the only auto margin
   in this row. `.cal-nav .btn { margin-inline-start: auto }` used to put a
   second one on the Today button; with two, the free space split between them
   and Today floated away from the group it belongs to. */

/* ---------- 5. Calendar — month ------------------------------------------- */

/* THE LATTICE TAKES WEEK'S FRAME — §3.24 REVERSED, DELIBERATELY, BY THE OWNER.

   The note below is the ORIGINAL decision and it is kept because its reasoning
   is still sound: forty-two outlined cells is a spreadsheet, and a week is
   continuous, so ruling between Tuesday and Wednesday asserts a boundary that
   is not there. None of that was found to be wrong.

   WHAT OVERRULED IT WAS CONSISTENCY, decided after both views were seen side
   by side. Month and Week are two views of the same data one tab apart, and
   they were two different visual languages: Week a framed, ruled, filled grid;
   Month a page of dates with nothing holding them. Week rules between its days
   and reads as time perfectly well, which is the counter-example to the second
   argument. So Month comes to Week rather than the reverse, and every value
   below is Week's own — `.wk-grid`'s frame, `.wk-head`'s band, `.wk-col`'s
   rule — with nothing invented for this screen.

   TWO CONSEQUENCES, both inherited rather than introduced. The trailing column
   draws its rule against the frame, so that edge reads 2px — exactly as it
   does in Week, and left matching rather than fixed on one screen only. And
   MONTH NO LONGER TAKES A PAGE SHELL: the grid is its own frame now, so a
   shell around it would be the box-in-box the shell exists to prevent. Week
   already worked that way; Month now does too.

   ---- the original note, kept ----

   It was drawn as a table of boxes: `gap: 1px` over a --line background paints
   a full grid of rules, and a border and radius fenced the whole thing. Forty-
   two outlined cells is a spreadsheet, and the brief's note on this screen is
   exactly that — it is a grid already, its job is to stop looking like a table
   of boxes and start reading as time.

   So the cage goes. Weeks are separated by a hairline and days are separated
   by nothing: the column headings name the days and the events line up under
   them, which is all the vertical structure a month needs. The events become
   the only ink on the page.

   No vertical rules is also the honest reading — a week is continuous, and
   ruling between Tuesday and Wednesday says a boundary that is not there. */
.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 0;
  /* `.wk-grid`'s four properties, unchanged. */
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
}
/* `.wk-head`'s band. The type stays month's own — these are day NAMES with no
   numeral under them, so the uppercase tracking is doing a job Week's
   `<strong>` does instead. The band, the rule and the ink are Week's. */
.cal-dow {
  padding: var(--sp-2) var(--sp-1);
  text-align: center;
  font-size: var(--fs-100);
  font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  background: var(--surface-2);
  color: var(--text-2);
  border-inline-end: 1px solid var(--line);
}
.cal-day {
  min-height: 116px;
  padding: var(--sp-2) var(--sp-1) var(--sp-3);
  /* --line, not --line-soft: the verticals below are --line because `.wk-col`
     is, and a grid ruled in two weights reads as an accident. */
  border-block-start: 1px solid var(--line);
  border-inline-end: 1px solid var(--line);   /* `.wk-col` */
}
/* Week one is separated from the day names by the rule below, not by its own.
   Children 1–7 are the headings, so 8–14 is always the first week however the
   month falls. */
.cal-grid > .cal-day:nth-child(n+8):nth-child(-n+14) { border-block-start: 0; }
.cal-dow:nth-child(-n+7) { border-block-end: 1px solid var(--line); }

/* Days outside the month are context, not content. */
.cal-day.is-out { opacity: .45; }

/* The numeral is a watermark: bigger than it was so the eye can find a date
   without reading, and quiet enough that the events outrank it. */
.daynum {
  display: grid; place-items: center;
  width: 26px; height: 26px;
  border-radius: 50%;
  font-size: var(--fs-400);
  font-weight: 700;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
/* TODAY IS A CELL, and the spec now says so (§3.24 was wrong).

   The decision record described "a tinted column, not a boxed cell". The CSS
   tinted a cell, and the CSS was right: a column of this grid is a WEEKDAY,
   so tinting one would mark every Sunday rather than today. The wording was
   sloppy shorthand for "no frame", and it has been corrected rather than
   implemented.

   What makes it read without a frame: the tint runs the full cell including
   its gutters, and the numeral is filled. */
.cal-day.is-today { background: var(--primary-soft); }
.cal-day.is-today .daynum { background: var(--primary); color: var(--on-primary); }

/* The project shows as a dot, matching .project-dot in the list and the
   drawer — one way to indicate a project everywhere, and no accent stripe. */
.cal-event {
  display: flex; align-items: center; gap: var(--sp-1);
  width: 100%;
  margin-top: 3px;
  padding: 3px var(--sp-1);
  border-radius: var(--r-xs);
  text-align: start;
  font-size: var(--fs-100);
  font-weight: 700;
  overflow: hidden;
  background: var(--tint-info);
  color: var(--ink-info);
}
.cal-event[style*="--dot"]::before {
  content: "";
  flex: none;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--dot);
}
.cal-event .ev-time { flex: none; opacity: .85; font-variant-numeric: tabular-nums; }
.cal-event .ev-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-event.is-cancelled, .cal-event.is-held { opacity: .6; }
.cal-event.is-cancelled .ev-title { text-decoration: line-through; }

/* Kind carries a colour, and the chip in the drawer carries the glyph. */
/* The week view's event drew a self-toning edge and the month view's did not,
   which is the same object bordered in one place and bleeding in the other.
   `currentColor` is the tone's own ink, so one rule covers all four kinds in
   both themes without a token per variant. */
.cal-event { border: 1px solid color-mix(in srgb, currentColor 22%, transparent); }
.cal-event.k-external, .wk-event.k-external { background: var(--tint-accent); color: var(--ink-accent); }
.cal-event.k-online,   .wk-event.k-online   { background: var(--tint-info);   color: var(--ink-info); }
.cal-event.k-visit,    .wk-event.k-visit    { background: var(--tint-warn);   color: var(--ink-warn); }

.cal-more {
  width: 100%;
  margin-top: 3px;
  padding: 2px var(--sp-1);
  border-radius: var(--r-xs);
  text-align: start;
  font-size: var(--fs-100);
  font-weight: 700;
  color: var(--text-2);
}
.cal-more:hover { background: var(--surface-2); color: var(--text); }

/* ---------- 6. Calendar — week -------------------------------------------- */

.wk-wrap { overflow-x: auto; border-radius: var(--r); }
.wk-grid {
  display: grid;
  grid-template-columns: 58px repeat(7, minmax(96px, 1fr));
  min-width: 760px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
}
.wk-head {
  padding: var(--sp-2) var(--sp-1);
  text-align: center;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-100);
  font-weight: 800;
  color: var(--text-2);
}
.wk-head strong {
  display: block;
  font-size: var(--fs-500);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.wk-head.is-today { background: var(--primary-soft); color: var(--primary); }
.wk-head.is-today strong { color: var(--primary); }

.wk-gutter { background: var(--surface-2); border-inline-end: 1px solid var(--line); }
.wk-hour {
  height: 46px;
  padding: 2px var(--sp-1);
  text-align: end;
  font-size: var(--fs-100);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  border-bottom: 1px dashed var(--line);
}
.wk-col { position: relative; border-inline-end: 1px solid var(--line); }
.wk-slot { height: 46px; border-bottom: 1px dashed var(--line); }

.wk-event {
  position: absolute;
  padding: 2px var(--sp-1);
  border-radius: var(--r-xs);
  text-align: start;
  overflow: hidden;
  font-size: var(--fs-100);
  font-weight: 700;
  background: var(--tint-info);
  color: var(--ink-info);
  border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
}
.wk-event .ev-title {
  display: flex; align-items: center; gap: var(--sp-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wk-event[style*="--dot"] .ev-title::before {
  content: "";
  flex: none;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--dot);
}
.wk-event .ev-time { display: block; font-weight: 600; opacity: .85; font-variant-numeric: tabular-nums; }

/* ---------- 7. Modal ------------------------------------------------------ */

.modal {
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center;
  padding: var(--sp-4);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.modal-box {
  position: relative;
  width: min(560px, 100%);
  max-height: min(92dvh, 900px);
  display: flex; flex-direction: column;
  background: var(--glass-solid);
  border: 1px solid var(--glass-line);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg);
  animation: sfc-modal var(--dur) var(--ease-out) both;
  overflow: hidden;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .modal-box {
    background: var(--glass-panel);
    -webkit-backdrop-filter: blur(var(--blur-strong)) saturate(160%);
    backdrop-filter: blur(var(--blur-strong)) saturate(160%);
  }
}
.modal-box.is-wide { width: min(760px, 100%); }
@keyframes sfc-modal {
  from { opacity: 0; transform: translateY(12px) scale(.99); }
  to   { opacity: 1; transform: none; }
}
/* Modals, confirms and the series-scope dialog all live in #modal-host and
   are removed by clearing its innerHTML, so the exit is an animation and the
   removal waits for it. `.modal.is-closing` kills pointer events for those
   140ms — a click landing on a dialog that is already leaving would either
   do nothing or, worse, hit whatever is underneath. */
.modal.is-closing { pointer-events: none; }
.modal-box.is-closing { animation: sfc-modal-out var(--dur-exit) var(--ease-in) both; }
@keyframes sfc-modal-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(8px) scale(.99); }
}
.modal-box > header {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--line-soft);
}
.modal-box > header h3 { margin: 0; flex: 1; font-size: var(--fs-700); font-weight: 800; }
.modal-body {
  flex: 1; padding: var(--sp-4);
  overflow-y: auto; overscroll-behavior: contain;
  scrollbar-gutter: stable;
}
.modal-box > footer {
  display: flex; gap: var(--sp-2); justify-content: flex-end; flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border-top: 1px solid var(--line);
  border-radius: 0 0 var(--r-lg) var(--r-lg);
}

/* ---------- 8. Forms ------------------------------------------------------ */

.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3) var(--sp-4); }
.fg { min-width: 0; }
.fg.full { grid-column: 1 / -1; }
.fg > label {
  display: block;
  margin-bottom: var(--sp-1);
  font-size: var(--fs-200);
  font-weight: 700;
  color: var(--text-2);
}
.fg .req { color: var(--ink-accent); }
/* THE ROOM INPUTS ARE FIELDS TOO, and they were not styled at all — no class
   of ours, so a 2px rgb(118,118,118) UA border, radius 0, and a fill the USER
   AGENT chose. That is what made them unreadable whenever the OS theme and the
   app theme disagreed. THE EMPTY BOX WAS NEVER CORRECT EITHER: it is the same
   unstyled control, with no text in it to be unreadable. */
.fg input, .fg select, .fg textarea,
.st-room-name-input, .st-room-add input {
  width: 100%;
  min-height: 40px;
  padding: var(--sp-2) var(--sp-3);
  /* background-COLOR — the shorthand would wipe the select caret. */
  background-color: var(--input);
  border: 1px solid var(--control-line);
  border-radius: var(--r-sm);
  font-size: var(--fs-600);
  color: var(--text);
  transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
.fg select { padding-inline-end: var(--sp-8); }
.fg textarea { min-height: 76px; line-height: var(--lh-body); resize: vertical; }
.fg input::placeholder, .fg textarea::placeholder,
.st-room-add input::placeholder { color: var(--text-3); opacity: 1; }
.fg input:focus-visible, .fg select:focus-visible, .fg textarea:focus-visible,
.st-room-name-input:focus-visible, .st-room-add input:focus-visible {
  outline: none; border-color: var(--focus); box-shadow: 0 0 0 3px var(--focus-glow);
}
.fg input:disabled, .fg select:disabled,
.st-room-name-input:disabled {
  background: var(--surface-2);
  color: var(--text-2);
  cursor: not-allowed;
}
.fg input[aria-invalid="true"] { border-color: var(--ink-danger); }
.hint { margin: var(--sp-1) 0 0; font-size: var(--fs-100); color: var(--text-3); }

.field-error {
  display: flex; align-items: center; gap: var(--sp-1);
  margin-top: var(--sp-1);
  font-size: var(--fs-200);
  font-weight: 700;
  color: var(--ink-danger);
}
.field-error svg { width: 14px; height: 14px; flex: none; }

.repeat-row { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.repeat-row input { flex: 1; min-width: 0; }
.repeat-row .handle {
  flex: none; width: 4px; height: 22px;
  border-radius: var(--r-pill);
  background: var(--line);
}
.btn-add { margin-top: var(--sp-1); }

/* A CHECKBOX INSIDE A FORM IS NOT THE SAME CONTROL AS `.check`.
   `.check` is a PILL — surface fill, --control-line edge, --r-pill, weight
   600 — because Settings uses it as the readout of a permission, scanned in a
   grid for which ones are on. Dropped into a form field stack it inherits the
   exact geometry of the `.fg select` above it and reads as a second dropdown,
   with the label crushed against the border.
   Here the native box does the talking and the row is plain: no fill, no
   edge, no pill. It sits with the field it qualifies rather than competing
   with it, and 18px is the smallest box that still looks pressable beside a
   40px select. */
.check-line {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  margin-top: var(--sp-2);
  font-size: var(--fs-300);
  color: var(--text);
  cursor: pointer;
}
.check-line input {
  width: 18px; height: 18px; margin: 0;
  /* Nudged down to sit on the first line's baseline rather than its box top,
     which is visible the moment the label wraps to two lines. */
  margin-block-start: 1px;
  flex: 0 0 auto;
  accent-color: var(--primary);
  cursor: pointer;
}
/* FIFTH CASCADE TRAP OF THIS BUILD. `.check-line` is a <label>, and inside a
   form field it is a DIRECT CHILD of `.fg` — where `.fg > label` sets
   `display: block` at higher specificity (0,1,1 against 0,1,0). The flex row
   lost, the tick stacked above its own label, and nothing errored. Same family
   as `.burger` vs `.icon-btn` and the duplicate `.side` rule: a rule that
   appears to apply and does not. */
.fg > label.check-line { display: flex; align-items: flex-start; margin-bottom: 0; }

.check-line:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: var(--r-xs); }

.check-grid { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.check {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: 40px;
  padding: var(--sp-1) var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--control-line);
  border-radius: var(--r-pill);
  font-size: var(--fs-300);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.check input { width: 16px; height: 16px; margin: 0; accent-color: var(--primary); }
/* MEASURED, and it failed exactly the way .seg did before it (see the note
   above that rule). The tinted version — --primary-soft fill, --primary ink —
   read at 1.2:1 on fill and 2.51:1 on ink against an unselected pill in light,
   1.13–1.61 and 2.33 in dark, on EVERY surface, with no painted indicator to
   carry the difference. Below 3:1 on all three channels is a selected state
   you find by accident.

   It went unmeasured for eight phases because a `.check` was a form option,
   where the native box does the talking. Settings made it the readout of a
   PERMISSION — a deputy card is scanned for which toggles are on — so the
   contrast gallery gained the pair (§3.7) and the pair failed on first run.

   Filled primary, as .seg: 6.6:1 on fill, and --on-primary is sized to clear
   4.5:1 as a label. The accent-color has to flip with it — a --primary box on
   a --primary fill is an invisible tick. */
.check.is-on {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--on-primary);
}
.check.is-on input { accent-color: var(--on-primary); }
.check.is-on:hover { background: var(--primary-strong); border-color: var(--primary-strong); }
.check:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ---------- 9. Callouts --------------------------------------------------- */

.callout {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  padding: var(--sp-3);
  border-radius: var(--r-sm);
  font-size: var(--fs-300);
  line-height: var(--lh-snug);
}
.callout svg { width: 17px; height: 17px; flex: none; margin-top: 1px; }
.callout.warn   { background: var(--tint-accent); border: 1px solid var(--edge-accent); color: var(--ink-accent); }
.callout.danger { background: var(--tint-danger);    border: 1px solid var(--edge-danger);    color: var(--ink-danger); }
.callout.info   { background: var(--tint-info);   border: 1px solid var(--edge-info);   color: var(--ink-info); }
.callout p { margin: 0 0 var(--sp-1); }
.callout p:last-child { margin-bottom: 0; }

.conflict-list { margin: var(--sp-1) 0; padding-inline-start: var(--sp-4); }
.conflict-list li { margin-bottom: 2px; }
.dialog-lede { margin: 0; font-size: var(--fs-400); line-height: var(--lh-body); }
.dialog-note { margin: var(--sp-1) 0 0; font-size: var(--fs-200); opacity: .85; }

/* The edge is --ink-danger, not --edge-danger. The chip ramp's edge token is
   sized to sit quietly around a small tinted pill; used on a transparent
   button it measured 1.11:1 against a board column, so the one destructive
   control in the app was also the faintest thing on screen. Matching the
   label colour puts it at 5.3:1 or better on every surface. */
.btn.danger {
  background: transparent;
  border-color: var(--ink-danger);
  color: var(--ink-danger);
}
.btn.danger:hover { background: var(--tint-danger); border-color: var(--ink-danger); }
/* Icon-only, and deliberately edgeless. An icon button's affordance IS the
   glyph, so the ratio that matters is the glyph's own — measured 4.58:1 light
   and 8.63:1 dark, both resting. Boxing every drawer close and every row
   delete would add forty small rectangles to buy nothing. Recorded as an
   exemption in the checker, not an oversight. */
.icon-btn.ghost { background: transparent; border-color: transparent; }
.icon-btn.ghost:hover { background: var(--surface-2); border-color: var(--control-line); }

/* ---------- 10. Drawer ----------------------------------------------------
    Side panel on desktop, bottom sheet on mobile — same DOM, restyled (§8.3). */

.drawer {
  position: fixed; inset-block: 0; inset-inline-end: 0;
  z-index: 51;
  width: min(560px, 100%);
  display: flex; flex-direction: column;
  background: var(--glass-solid);
  border-inline-start: 1px solid var(--glass-line);
  box-shadow: var(--shadow-lg);
  transform: translateX(100%);
  /* visibility is transitioned with a DELAY equal to the slide, not switched
     instantly — that one line is the whole reason the drawer used to vanish
     mid-exit while its transform was still animating something invisible. */
  transition: transform var(--dur-exit-slow) var(--ease-in),
              visibility 0s linear var(--dur-exit-slow);
  visibility: hidden;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .drawer {
    background: var(--glass-panel);
    -webkit-backdrop-filter: blur(var(--blur-strong)) saturate(160%);
    backdrop-filter: blur(var(--blur-strong)) saturate(160%);
  }
}
html[dir="rtl"] .drawer { transform: translateX(-100%); }

/* `html .drawer.is-on`, not `.drawer.is-on`.
   The direction rule above is (0,2,1); a bare `.drawer.is-on` is (0,2,0) and
   LOSES to it, so in Arabic — the default language — the drawer stayed parked
   off-screen while the scrim and the class both said it was open. Matching the
   specificity and coming later in the source is what makes `is-on` win, in
   both directions, without reaching for !important. */
html .drawer.is-on {
  transform: none; visibility: visible;
  transition: transform var(--dur-slow) var(--ease-out), visibility 0s;
}
/* Still on screen for the length of the slide, but no longer a target — a
   click landing on a Save button in a drawer that is leaving is worse than
   a click that goes nowhere. */
.drawer:not(.is-on) { pointer-events: none; }

#drawer-scrim { z-index: 50; }

.drawer-head {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-5);
  border-bottom: 1px solid var(--line-soft);
}
.drawer-title { flex: 1; min-width: 0; }
.drawer-head h2 {
  margin: 0;
  font-size: var(--fs-700);
  font-weight: 800;
  line-height: var(--lh-tight);
  text-wrap: balance;
}
.drawer-sub { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-top: var(--sp-2); }
.drawer-body {
  flex: 1; padding: var(--sp-5);
  overflow-y: auto; overscroll-behavior: contain;
  scrollbar-gutter: stable;      /* content length varies per task — see shell.css */
}
.drawer-foot {
  display: flex; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-4) var(--sp-5);
  border-top: 1px solid var(--line-soft);
  background: var(--surface-2);
}
/* FOUR LABELLED ACTIONS IN 560px, IN ARABIC. Moving two actions into the body
   took the peak from five to four and it still wrapped, with Edit meeting
   alone on a second row — because the count was never the constraint, the
   WIDTH was. Two changes, both structural rather than a guess at a number:

   The icon goes. In a footer of labelled buttons an icon is decoration — the
   label already says it — and four of them cost about 110px of a row that has
   roughly 480px to spend. Icons stay everywhere else, including the body's
   management row, where they sit beside quieter ghost labels.

   And the buttons share what is left instead of each demanding its natural
   width: `flex: 1 1 auto` with `min-width: 0` lets four long Arabic labels
   compress together rather than one of them being evicted to its own line. */
.drawer-foot .btn { flex: 1 1 auto; min-width: 0; justify-content: center; }
.drawer-foot .btn svg { display: none; }
/* A phone is too narrow for four across at any icon setting; there the wrap is
   correct and each button takes a full row rather than leaving an orphan. */
@media (max-width: 480px) {
  .drawer-foot .btn { flex: 1 1 100%; }
}

.drawer-section + .drawer-section { margin-top: var(--sp-5); }
.drawer-section > h4 {
  display: flex; align-items: center; gap: var(--sp-2);
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-100);
  font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-2);
}
.drawer-section > h4::after { content: ""; flex: 1; height: 1px; background: var(--line); }

.kv { display: grid; grid-template-columns: minmax(88px, auto) 1fr; gap: var(--sp-2) var(--sp-3); margin: 0; }
.kv dt { font-size: var(--fs-300); font-weight: 600; color: var(--text-2); }
.kv dd { margin: 0; font-size: var(--fs-300); font-weight: 600; }

.note {
  margin: var(--sp-3) 0 0;
  padding: var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-size: var(--fs-300);
  white-space: pre-wrap;
}
.muted { margin: 0; font-size: var(--fs-300); color: var(--text-3); }

.agenda-list { margin: 0; padding-inline-start: var(--sp-5); }
.agenda-list li { margin-bottom: var(--sp-2); font-size: var(--fs-300); }

.people { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.person {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font-size: var(--fs-300);
  font-weight: 600;
}
.person small { color: var(--text-3); font-weight: 600; }

/* ---------- 11. My profile ------------------------------------------------ */

.profile-grid { display: grid; grid-template-columns: 1.2fr 1fr; gap: var(--sp-4); align-items: start; }
.profile-grid .panel-body > .btn { margin-top: var(--sp-4); }

.team-list { list-style: none; margin: 0; padding: 0; }
.team-row {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) 0;
}
.team-row + .team-row { border-top: 1px solid var(--line-soft); }
.avatar-sm {
  flex: none;
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--tint-accent);
  color: var(--ink-accent);
  font-size: var(--fs-100);
  font-weight: 800;
}
.team-name { flex: 1; min-width: 0; font-size: var(--fs-300); font-weight: 700; }
.team-name small { display: block; font-size: var(--fs-100); font-weight: 600; color: var(--text-3); }

.empty-extra { margin-top: var(--sp-3); }

/* ==========================================================================
   PHASE 3 — Board, Table, Timeline, task drawer
   ========================================================================== */

/* ---------- 13. Blocked strip (§7.3) -------------------------------------
   Above the board, not a column. Blocked work is an exception that needs a
   decision, so it reads as an alert band rather than a place tasks live. */

/* ==========================================================================
   THE BLOCKED BAND

   A full-bleed band across the top of the board, not a rounded tinted card
   floating above it. §7.3 puts blocked tasks above the board rather than in
   it, and a band is what "above" should look like: it spans the same width
   the channels span and shares their edges, so it reads as the first stratum
   of one object instead of a second object sitting on top.

   Earlier revisions fought the opposite problem — a full-width sheet of tint
   behind one card. The fix then was to shrink the band to its contents, which
   solved the emptiness and reintroduced the floating box. Full width is right;
   what was wrong was the tint filling it. Now the band carries a rule and a
   label, and only the cards are filled.
   ========================================================================== */
.blocked-strip {
  /* --shell-pad, not a hardcoded sp-6. The literal was correct only while
     `.content`'s own padding was the thing being negated; Board has a shell
     now and the inset it must cancel is the shell's. */
  margin-inline: calc(var(--shell-pad) * -1);
  padding: 0 var(--shell-pad) var(--sp-4);
  display: flex; align-items: flex-start; gap: var(--sp-4);
  flex-wrap: nowrap;
  border-block-end: 1px solid var(--edge-danger);
  background: linear-gradient(to bottom, transparent, var(--tint-danger));
}
.bs-head {
  display: flex; align-items: baseline; gap: var(--sp-2);
  padding: var(--sp-3) 0;
  color: var(--ink-danger);
  flex: none; width: 168px;
}
.bs-head h3 { margin: 0; flex: 1; font-size: var(--fs-300); font-weight: 800; }
.bs-head svg { width: 16px; height: 16px; align-self: center; }
.bs-head .col-count { color: var(--ink-danger); font-size: var(--fs-800); }
/* A grid, not wrapping flex: with one blocked task the flex version left a
   full-width band of empty tint, which read as a rendering fault rather than
   as an alert with one item in it. */
/* auto-FIT, not auto-fill: fill keeps the empty tracks, so a single blocked
   task sat in a row of empty columns. fit collapses them. */
.bs-body {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(232px, 300px));
  justify-content: start;
  flex: 1; min-width: 0;
  gap: var(--sp-3);
  padding: var(--sp-3) 0;
}
.bs-body .task-card { width: 100%; margin-bottom: 0; }

/* ---------- 14. Board ----------------------------------------------------- */

/* ==========================================================================
   THE BOARD IS COLUMNS (§3.13)

   It was four lists that happened to sit side by side: each a rounded,
   tinted, hairlined region floating on the ground with a gap between them.
   Four rounded rectangles on a page of rounded rectangles.

   Now the content area is DIVIDED. Four channels run the full height of the
   viewport, separated by a hairline and nothing else — no gaps, no radii, no
   region fill. The screen reads as one ruled sheet cut into four, and the
   card becomes the only raised object anywhere on it, which is what a card is
   supposed to mean.

   Blurred, this is four tall bands with small solids stacked at the top of
   each. No other screen in the app divides vertically.
   ========================================================================== */

.board {
  display: grid;
  grid-template-columns: repeat(4, minmax(232px, 1fr));
  gap: 0;
  align-items: stretch;
  /* Out to the edges of the shell's padding, and down to the bottom of the
     viewport: a channel that stops short of the fold is a panel again.

     --shell-pad, NOT A HARDCODED sp-6, and this was missed in the very commit
     that fixed the identical literal on `.blocked-strip`. Measured at 680px,
     where the shell's padding drops to sp-4: the board rendered 9..661 inside
     a shell of 16..654 — EIGHT PIXELS PAST ITS OWN FRAME on both sides. The
     one-of-two-screens species inside a single commit. */
  margin-inline: calc(var(--shell-pad) * -1);
  min-height: calc(100dvh - var(--topbar-h) - 210px);
  /* NO RULE OF ITS OWN. Something above the board always draws one now — the
     header's, or the filter strip's lower rule, or the blocked strip's — and
     two 1px rules with nothing between them is a 2px line nobody chose. */
}
.board-col {
  min-width: 0;
  display: flex; flex-direction: column;
  /* The divider between channels. Logical, so RTL needs no special case. */
  border-inline-start: 1px solid var(--line);
}
.board-col:first-child { border-inline-start: 0; }

/* Flush with the top of its own channel and stuck there while the column
   scrolls — the head, the count and the channel are one object. */
.col-head {
  position: sticky; inset-block-start: 0; z-index: 2;
  display: flex; align-items: baseline; gap: var(--sp-2);
  /* --shell-pad on the inline axis, so a column's contents start on the same
     line the header's actions do. They did not: the header insets by the
     shell's padding (24) and the columns by their own sp-4 (16), so the
     buttons above began 8px inside where the column headings began below.
     THE RULE AND THE BOARD ALWAYS AGREED — both span 25..1405 — and it was
     the CONTENT that disagreed about where the page starts. Measured before:
     shell content 49..1381, first column heading 1076..1389. */
  padding: var(--sp-4) var(--shell-pad) var(--sp-3);
  background: var(--bg);
  border-block-end: 1px solid var(--line-soft);
}
.col-head h3 {
  margin: 0; flex: 1;
  font-size: var(--fs-100); font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-2);
}
/* A NUMERAL, not a pill. The count is the second most useful thing on the
   board and it was wearing the same grey lozenge as every other badge in the
   app; at display weight it can be read from across a desk. */
.col-count {
  font-size: var(--fs-800); font-weight: 800;
  line-height: 1;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.board-col:has(.task-card) .col-count { color: var(--text); }

.col-body {
  flex: 1;
  padding: var(--sp-3) var(--shell-pad) var(--sp-6);
  display: flex; flex-direction: column; gap: var(--sp-3);
  transition: background var(--dur-fast) var(--ease);
}
/* The drop target is the whole channel now, so the feedback is the whole
   channel — a tint, with no inset ring to reintroduce the rounded box. */
.col-body.is-over { background: var(--primary-soft); }
/* ONE EMPTY STATE FOR EVERY COLUMN (was two, side by side).

   Done had an icon, a heading and two lines; the other three had the bare
   words "Nothing here" — the same screen, the same row, a metre apart. That is
   the one-of-two-screens defect INSIDE one screen, and the fourth time this
   phase.

   NO BOX, and that is not a compromise: step 22a took the panel off Board's
   filtered-empty state and it stays off. An icon and a heading are still a
   bare message; what a box would add is a fourth kind of object in a channel
   that already has three.

   Start-aligned, not centred: a centred line in a narrow channel reads as
   something placed there rather than as the absence of rows. */
.col-empty {
  margin: 0;
  padding: var(--sp-4) 0;
  color: var(--text-2);
}
.col-empty svg { width: 24px; height: 24px; color: var(--text-3); margin-block-end: var(--sp-2); }
.col-empty h4 {
  margin: 0 0 var(--sp-1);
  font-size: var(--fs-200);
  font-weight: 700;
  color: var(--text);
}
.col-empty p { margin: 0; font-size: var(--fs-100); line-height: var(--lh-snug); color: var(--text-3); }

/* `.col-empty-done` is gone: Done's empty state was the good one, so the other
   three came UP to it rather than it coming down to them (§8.2). Its words are
   unchanged — they were written deliberately and they teach. */

/* ---------- 15. Task card ------------------------------------------------- */

.task-card {
  position: relative;
  /* So the time bar sits inside the card's own radius instead of squaring off
     its bottom corners. */
  overflow: hidden;
  display: block;
  width: 100%;
  margin-bottom: 0;
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow-sm);
  text-align: start;
  cursor: pointer;
  transition: box-shadow var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.task-card:hover { box-shadow: var(--shadow); border-color: var(--control-line); }
.task-card:active { transform: scale(.995); transition-duration: var(--dur-press); }
.task-card[draggable="true"] { cursor: grab; }
.task-card.is-dragging { opacity: .45; cursor: grabbing; box-shadow: var(--shadow-lg); transform: scale(1.02); }
/* ONE EDGE, ONE MEANING. Lateness used to colour the whole border, which put
   it in a fight with the project stripe on the same pixels — on a board where
   half the cards are late, every stripe read as "late" and none read as
   "which project". Lateness keeps the tinted ground and the red date, both of
   which it already had; the edge belongs to the project. */
.task-card.is-late { background: color-mix(in srgb, var(--danger) 4%, var(--surface)); }

/* The project, as a colour on the card's own edge rather than a named row
   underneath it. The name was the longest string on the card and the least
   scanned; five projects are told apart by hue at a glance, and the name is
   still on the table row, in the drawer, and in this card's tooltip. */
.task-card.has-project {
  border-inline-start-width: 3px;
  border-inline-start-color: var(--dot);
}

.tc-title {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-400);
  font-weight: 700;
  line-height: var(--lh-tight);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.tc-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  font-size: var(--fs-100);
  color: var(--text-2);
}
.tc-foot .who { display: inline-flex; align-items: center; gap: var(--sp-1); min-width: 0; }
.tc-foot .who > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tc-due { display: inline-flex; align-items: center; gap: 3px; flex: none; }
.tc-due svg { width: 12px; height: 12px; }
.tc-due.is-late { color: var(--ink-danger); font-weight: 700; }

/* ==========================================================================
   THE TIME BAR

   A substrate, not an element: full-bleed along the base of the card, two
   pixels, no label. Neutral while the window is running, because the card
   already carries a project colour on its edge and a third hue would make the
   card a chart. Past due it takes the danger ink the date is already using —
   one statement in one colour, not two signals in two.

   scaleX rather than width: the ticker touches every bar on screen, and a
   transform is composited while a width is laid out. transform-origin is
   mirrored for RTL because there is no logical keyword for it.

   No transition. Per tick the change is sub-pixel, so easing buys nothing,
   and on first render a transition would sweep every bar on the board from
   zero — motion with no meaning, which §3.6 rules out.
   ========================================================================== */
/* ==========================================================================
   THE TIME BAR — one appearance, everywhere (§3.15)

   It shipped briefly with three: a card edge on the board, a bare line in a
   table cell, and a full-width grey rule in the drawer that read as a section
   divider. Same element, three looks, so nobody could learn what it meant.

   The track is what fixes it. Without one, a bar at 5% is a stray mark rather
   than "barely started" — there is nothing to say how much is left. So every
   placement draws the same thing: a 3px track in --line, a fill in --text-3,
   and --ink-danger once the window has run out. Height and colour never vary
   by context; only the anchoring does.

   scaleX rather than width: the ticker touches every bar on screen, and a
   transform composites while a width lays out. transform-origin is mirrored
   for RTL because there is no logical keyword for it.

   No transition. Per tick the change is sub-pixel, so easing buys nothing,
   and on first render it would sweep every bar on the view from zero — motion
   with no meaning, which §3.6 rules out.
   ========================================================================== */
.tc-time {
  height: 3px;
  background: var(--line);
  border-radius: var(--r-pill);
  overflow: hidden;
}
.tc-time i {
  display: block; width: 100%; height: 100%;
  background: var(--text-3);
  transform: scaleX(var(--elapsed, 0));
  transform-origin: left center;
}
html[dir="rtl"] .tc-time i { transform-origin: right center; }

/* Overdue changes COLOUR, not size. The bar is already full by then, the date
   beside it is already red and the row is already tinted; growing it too
   would make the bar shout over the date rather than support it. */
.tc-time.is-over i { background: var(--ink-danger); }

/* Anchoring — the only thing that varies. */
.tc-time-base { position: absolute; inset-block-end: 0; inset-inline: 0; border-radius: 0; }
.tc-time-row  { width: 100%; }

/* The drawer's labelled form. Here the mark explains itself: what it measures,
   what it does NOT measure, and the two dates it runs between. */
.tc-window {
  margin-block-start: var(--sp-4);
  padding-block-start: var(--sp-4);
  border-block-start: 1px solid var(--line-soft);
}
.tc-window-head { display: flex; align-items: baseline; gap: var(--sp-2); margin-block-end: var(--sp-2); }
.tc-window-label { font-size: var(--fs-200); font-weight: 700; color: var(--text-2); }
.tc-window-hint  { font-size: var(--fs-100); color: var(--text-3); }
.tc-window.is-absent .tc-window-hint { font-style: italic; }
/* THE RAIL FOR A TASK WITH NO WINDOW. Same height and same footprint as a real
   bar, so the drawer does not change shape between one task and the next — a
   collapsing row read as a rendering fault rather than a state.

   Dashed and UNFILLED on purpose. A solid empty track would read as "0%
   elapsed", which is false: nothing has elapsed because there is no window for
   anything to elapse against. A dashed rail says there is a place for a window
   here and this task has none, which is the true statement. `--line` is
   correct rather than `--control-line`: this cannot be pressed and carries no
   contrast bar (§3.7). */
.tc-rail-absent {
  height: 3px;
  border-block-start: 1px dashed var(--line);
  border-radius: var(--r-pill);
}
.tc-window-ends {
  display: flex; justify-content: space-between;
  margin-block-start: var(--sp-1);
  font-size: var(--fs-100); color: var(--text-3);
}

/* People are the warm thing on a board of blue and grey, so the accent lives
   here — which is also the only place apricot reaches the primary surface.
   Measured: --ink-accent on --tint-accent is 4.77:1 light, 5.9:1 dark. */
.avatar-xs {
  width: 20px; height: 20px;
  display: inline-grid; place-items: center;
  border-radius: 50%;
  background: var(--tint-accent);
  color: var(--ink-accent);
  font-size: var(--fs-050); font-weight: 800;
  flex: none;
}

/* ---------- 16. Table ----------------------------------------------------- */

/* ONE REGISTER, NOT A STACK OF CARDS (§3.24).

   A panel per project meant four rounded containers down the page, each
   re-printing the same four column headers. The register now has no container
   at all: it sits directly on the page ground, the header is printed once and
   sticks under the top bar, and a project is a BAND spanning the full width.

   The silhouette this buys: one unbroken grid from the top of the page to the
   bottom, with occasional heavier labelled breaks. Nothing else in the app is
   a continuous ruled field — Meetings is a ragged gutter with floating rows,
   the board is columns, Minutes is a stack of sheets. */
.register { margin-block-start: var(--sp-2); }

/* Fixed layout so a column lands at the same x in every group. When each
   group was its own <table>, auto layout sized every one to its own content
   and "الحالة" arrived at a different place in each of three panels that read
   as one list. One table now, but the rule still earns its place: it stops a
   long title in one band widening the column for every other band. */
.data-table { width: 100%; font-size: var(--fs-300); }

/* FIXED LAYOUT IS THE REGISTER'S, NOT EVERY TABLE'S — and the `>` is load
   bearing. The four percentage widths below add to 100%, so applied to
   Archive's SIX-column table they left nothing for the last two: the archived
   date was clipped mid-word and the restore button sat off-screen behind a
   horizontal scrollbar. Archive and Deleted wrap their tables in .table-wrap,
   so a direct-child selector reaches the register and misses them, and they
   size their columns to their content as they should. */
.register > .data-table {
  table-layout: fixed;
}
/* THE CARD TREATMENT IS EVERY DATA TABLE'S, NOT THE REGISTER'S ALONE.

   It was written for `.register >` because Table was the screen being worked
   on. Archive and Deleted wrap their tables in `.table-wrap` and would have
   been left as the last ruled ledgers in the app — one-of-two-screens for the
   sixth time this phase, and this time visible in advance. What stays scoped
   to the register is what is genuinely its own: `table-layout: fixed`, the
   four percentage widths, and the --shell-pad cell inset.

   SEPARATE, NOT COLLAPSED, AND THE COLUMN ALIGNMENT SURVIVES IT.
     `table-layout: fixed` and `border-collapse` are independent properties:
   `table-layout: fixed` and `border-collapse` are independent properties: the
   four percentage widths still place every band's columns at the same x.
   Collapsed borders are what make a row impossible to round or separate —
   edges are shared, `border-radius` is ignored on a collapsed cell, and there
   is nowhere to put a gap. Separate borders give both, and `border-spacing`'s
   first value is 0 so nothing opens up horizontally. */
.data-table {
  /* The ONLY border-collapse in this file. It was set to `collapse` in the
     rule above and to `separate` here, two selectors of equal specificity
     thirty lines apart — check-css-dupes named it the moment the card
     treatment stopped being the register's alone. */
  border-collapse: separate;
  border-spacing: 0 var(--sp-2);
}

/* EACH ROW IS AN OBJECT (§3.22) — the same decision `.meeting-row` records,
   arriving here after the register spent a phase as a continuous ruled field.

   THE COST, STATED: Table and Meetings now make a similar picture, and §3.24
   deliberately gave them different ones — a register against a gutter with
   floating rows. That distinction is spent. What buys it is that every other
   screen reads as a stack of objects and this one read as a run of text.

   A ROW IS NOT A <li>, so the values arrive differently: the fill goes on the
   cells (a <tr> paints nothing you can round), and the corners go on the two
   END cells with LOGICAL longhands, so RTL and LTR each round the right pair
   without a direction-specific rule. */
.data-table > tbody > tr:not(.tg-band) > td {
  background: var(--surface);
  border-block-end: 0;
}
.data-table > tbody > tr:not(.tg-band) > td:first-child {
  border-start-start-radius: var(--r-sm);
  border-end-start-radius: var(--r-sm);
}
.data-table > tbody > tr:not(.tg-band) > td:last-child {
  border-start-end-radius: var(--r-sm);
  border-end-end-radius: var(--r-sm);
}
/* Both channels, as everywhere else: the fill carries dark, the lift carries
   light (§3.22). The shadow goes on the ROW — on the cells it would draw a
   seam at every column boundary. */
.data-table > tbody > tr:not(.tg-band):hover > td { background: var(--row-hover); }
.data-table > tbody > tr:not(.tg-band):hover { box-shadow: var(--shadow); }
/* FOUR columns. The time bar has no column of its own.

   STALE FROM HERE DOWN, KEPT BECAUSE THE HISTORY IS THE POINT: this said the
   bar is "a hairline on the row's bottom edge, the same mark it is at the foot
   of a card", which was true when it spanned the row and was never updated
   after §3.28 scoped it to the due-date cell. Two comments in one file
   disagreeing is what raised the question of whether the bar had been wrong
   since the register was built. It has not — see the note beside
   .tc-time-cell, which is the current decision and explains why.

   It had a column briefly, with a header. That was a real substitution and it
   was made for a bad reason — uncertainty about whether an absolutely
   positioned child of a <tr> anchors to the row or escapes to the nearest
   positioned ancestor. Measured rather than assumed: with
   `tr { position: relative }` the probe came back 1116px wide against a
   1116px row and flush with its bottom edge, in Chrome. So the doubt was
   unfounded, the column is gone, and the register is one column denser. */
.register > .data-table th:nth-child(1), .register > .data-table td:nth-child(1) { width: 44%; }
.register > .data-table th:nth-child(2), .register > .data-table td:nth-child(2) { width: 20%; }
.register > .data-table th:nth-child(3), .register > .data-table td:nth-child(3) { width: 19%; }
.register > .data-table th:nth-child(4), .register > .data-table td:nth-child(4) { width: 17%; }

/* THE BAR BELONGS TO THE DATE CELL, NOT THE ROW (§3.15, §3.28).

   It used to span the row edge to edge, with the row as its containing block.
   At card density that reads as a clock; in a register it does not. A
   full-bleed rule under a row is the universal signal for a failed field, and
   on this fixture roughly half the rows carry one — blurred, the whole screen
   read as a form that failed submission. A fresh reviewer called it exactly
   that without knowing what the bar was for.

   Scoping it to the due-date cell keeps ONE canonical bar (the same component,
   the same derivation) and takes away the false signal: it now sits under the
   date it is about, at the width of that column, which is also where the eye
   already is when it wants to know how much time is left. */
.data-table .tc-time-cell { position: relative; }
/* The trailing cell carries the row's rounded corners now, and the bar inside
   it is square-ended and pinned flush — the same notch every other host of a
   `.tc-time-base` had to clip. */
.data-table .tc-time-cell { overflow: hidden; }
.data-table td .cell-title { overflow-wrap: anywhere; }

/* The progress column is gone (§3.15) and the clipped numeral with it: an
   11px glyph was centred inside a 6px `overflow: hidden` track, so every row
   carried an unreadable smear where a percentage should have been. It was
   added to stop a column of identical empty tracks reading as a placeholder —
   the right instinct, applied to a column that should not have existed. The
   time bar replaces it and needs no label at all. */
/* Printed once, and it follows you down. The page is the scroller, so the
   offset is the top bar's height — and the fill has to be opaque --bg or the
   rows show through it as they pass underneath. */
/* Sticky only where the PAGE is the scroller. Archive and Deleted sit inside
   .table-wrap, whose `overflow-x: auto` makes it a scroll container on both
   axes — a --topbar-h offset there resolves against the wrapper and pushes
   the header down over its own first rows, which is exactly what it did to
   the Gantt head before that one was pinned back to 0. */
.register > .data-table > thead th {
  position: sticky;
  inset-block-start: var(--topbar-h);
  z-index: 2;
}
.data-table > thead th {
  padding: var(--sp-2) var(--sp-3);
  text-align: start;
  font-size: var(--fs-100);
  font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-2);
  background: var(--bg);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

/* THE BAND. A row, not a wrapper: it spans all four columns, carries the
   project's name at heading size and its count as a quiet numeral, and the
   space above it is what separates one project from the last.

   NO RULE UNDER IT, AND THE SPACING WAS NEVER THE PROBLEM. Measured: 34.5px
   above the label, 10.8px below — already a 3.2:1 ratio, so it was not
   floating equidistant between two groups. What fenced it off was a
   `--line` BORDER between the label and the rows it labels, HEAVIER than the
   `--line-soft` rules separating those rows from each other. A label divided
   from its own group by the strongest rule in the table reads as belonging to
   neither, whatever the gaps do. Proximity carries it now, which is what the
   comment above already claimed. */
.tg-band > th {
  padding: var(--sp-8) var(--sp-3) var(--sp-2);
  text-align: start;
  background: none;
  font-weight: 800;
}
.data-table > tbody:first-of-type .tg-band > th { padding-block-start: var(--sp-5); }
.tg-name { font-size: var(--fs-500); font-weight: 800; color: var(--text); }
/* THE BAND LINE IS WHAT YOU SCAN, so it stops being the quietest thing on the
   screen. Emphasis, not a box (step 22a stands): a size up to the panel-title
   step, a project dot at half again its size so the colour registers before
   the word does, and the count set against the name rather than trailing it in
   grey. The rows are cards on the page ground now, which leaves the band the
   only thing in the column that is NOT on a card — so weight is the channel
   that separates it, and it is the one channel a box would have taken. */
.tg-band .tg-name { font-size: var(--fs-700); }
.tg-band .tg-name.project-dot::before { width: 12px; height: 12px; }
.tg-band .tg-count { color: var(--text-2); font-size: var(--fs-200); }
.tg-count {
  margin-inline-start: var(--sp-3);
  font-size: var(--fs-100); font-weight: 700;
  letter-spacing: var(--tracking-label);
  color: var(--text-3);
}

.data-table td { padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
/* THE REGISTER ONLY, and the scope is the point: .data-table is Archive's and
   Deleted's table too, and neither has a shell yet — so --shell-pad resolves
   to 0 there and both would have lost their cell padding entirely. Caught by
   measuring rather than by reading, one edit after making the same class of
   change on purpose. */
.register > .data-table > tbody td,
.register > .data-table > thead th { padding-inline: var(--shell-pad); }
/* THE BAND LABEL BEGINS WHERE ITS CARDS BEGIN — no inline padding of its own.
   It had --shell-pad like every cell, which put it 24px inside the leading
   edge of the card below it: measured, label at 1357 against a card edge of
   1381. A label that does not start where its group starts is not attached to
   it, which is the same complaint as the rule removed from under it.
   THE LABEL MOVED, NOT THE CARDS. The cards already agree with the caption
   count and the column header, and they are the line the page is built on;
   the label was the one thing out of step. */
.register > .data-table > tbody > tr.tg-band > th { padding-inline: 0; }
.data-table tbody tr { cursor: pointer; transition: background var(--dur-fast) ease; }
/* The rows rest on the page ground, so hover BRIGHTENS to the surface —
   the opposite move from a meeting row, which rests on the surface already
   (§3.22). Same reasoning, opposite direction. */
.data-table tbody tr:hover { background: var(--surface); }
.data-table tbody tr.is-late .cell-title { color: var(--ink-danger); }
.cell-title { font-weight: 700; }
.cell-sub {
  display: inline-flex; align-items: center; gap: 3px;
  margin-top: 2px;
  font-size: var(--fs-100);
  color: var(--text-3);
}
.cell-sub svg { width: 11px; height: 11px; }
.cell-late { display: block; font-size: var(--fs-100); color: var(--ink-danger); font-weight: 700; }
.data-table .who { display: inline-flex; align-items: center; gap: var(--sp-1); white-space: nowrap; }

/* Card list is the phone rendering of the same rows (§17), one list for the
   whole register with the bands inline. */
.card-list { display: none; list-style: none; margin: 0; padding: 0; }
.cl-band {
  display: flex; align-items: baseline; gap: var(--sp-3);
  /* Same fix as `.tg-band`, and it had the same fence: a --line rule between
     the label and the rows under it. */
  padding: var(--sp-6) var(--sp-1) var(--sp-2);
}
.cl-band .tg-name { font-size: var(--fs-400); }
.card-list > li:first-child { padding-block-start: var(--sp-2); }
.card-row {
  /* POSITIONED, AND IT WAS NOT. This row hosts a `.tc-time-base`, which is
     `position: absolute` — with no positioned ancestor it anchored to the
     nearest one it could find, which is not this card. And `overflow: hidden`
     for the reason `.task-card` carries the same pair: the bar is pinned flush
     with square ends, so a host WITH A RADIUS notches at both bottom corners
     unless it clips. Found while checking Board's card, which has had both
     since it was built. */
  position: relative;
  overflow: hidden;
  display: block; width: 100%;
  padding: var(--sp-3);
  text-align: start;
  border-radius: var(--r-sm);
  transition: background var(--dur-fast) ease;
}
.card-row:hover { background: var(--surface); }
.cl-task + .cl-task { border-top: 1px solid var(--line-soft); }
.cr-title { display: block; font-size: var(--fs-300); font-weight: 700; }
.cr-chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin: var(--sp-2) 0; }
.cr-meta { display: flex; gap: var(--sp-3); font-size: var(--fs-100); color: var(--text-2); }
.cr-meta span { display: inline-flex; align-items: center; gap: 3px; }
.cr-meta svg { width: 12px; height: 12px; }
.card-row.is-late .cr-title { color: var(--ink-danger); }

/* ---------- 17. Timeline (Gantt) ------------------------------------------ */

/* A CHART, NOT A WIDGET (§3.24).

   It was a rounded panel wrapping a Gantt, which is the shape every dashboard
   tile in every product has, and it made the one screen in the app whose
   content runs sideways look like a card that happened to contain a chart.
   The panel is gone: the chart is the page.

   The axis does the organising. Month rules run the full height and the rows
   have no hairlines of their own in the chart area — vertical rules with
   horizontal air is a chart; both directions ruled is a table. The index
   column keeps its hairlines, because a list of forty titles does need them.

   Blurred: a ragged horizontal scatter against vertical rules, with a quiet
   column of text down one side. The only screen where content runs across. */
.gantt-scroll { overflow-x: auto; overflow-y: hidden; }
.gantt { min-width: calc(var(--label-w) + var(--chart-w)); }

.gantt-head {
  display: grid;
  grid-template-columns: var(--label-w) var(--chart-w);
  /* SEEN AND LEFT: this head row runs to the shell's edge while the rows
     under it stop at the chart's end, so the head is wider than the chart it
     heads. Judged not worth a pass — recorded here so it is not rediscovered
     as an oversight.

     Sticky at 0, NOT at --topbar-h. The head lives inside .gantt-scroll, and
     `overflow-x: auto` makes that element a scroll container on both axes —
     so the offset resolves against the scroller, not the viewport, and a
     --topbar-h offset pushed the month labels 60px down over the first three
     rows. Horizontal is the axis this head has to follow; vertical is the
     page's job and the chart is not tall enough to need it. */
  position: sticky; inset-block-start: 0; z-index: 2;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}
.gantt-corner {
  display: flex; align-items: center; justify-content: center;
  padding: var(--sp-1);
  border-inline-end: 1px solid var(--line);
}
.gantt-months { display: flex; }
.gantt-month {
  flex: none;
  padding: var(--sp-2) var(--sp-1);
  text-align: center;
  font-size: var(--fs-100);
  font-weight: 700;
  color: var(--text-2);
  border-inline-end: 1px solid var(--line);
  overflow: hidden; white-space: nowrap;
}

.gantt-body { position: relative; }
.gantt-row {
  display: grid;
  grid-template-columns: var(--label-w) var(--chart-w);
  min-height: 38px;
}
.gantt-row:hover { background: var(--surface); }
/* THE CORNER IS A CELL, NOT A HOLE. Emptying it (the action moved to the page
   header) left a blank rectangle above the index column while the month band
   ran beside it — one head at two heights with nothing between them. A Gantt
   corner is where the index column names itself, so it does that: the same
   type as a month band, in the same head row, and the head reads as one line
   across the whole chart. */
.gantt-corner {
  font-size: var(--fs-100); font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-3);
}

/* The index. Hairlines here and nowhere else: forty titles need tracking, the
   chart beside them does not. */
.gantt-label {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-inline-end: 1px solid var(--line);
  border-block-end: 1px solid var(--line-soft);
  font-size: var(--fs-200);
  font-weight: 600;
  color: var(--text-2);
  overflow: hidden;
}
.gantt-label > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gantt-track { position: relative; }

.gantt-bar {
  /* Already positioned; named here because .tc-time-base anchors to it. */
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  height: 20px;
  display: flex; align-items: center;
  padding: 0 var(--sp-2);
  border-radius: var(--r-xs);
  font-size: var(--fs-100);
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  /* The tints sit 1.06–1.20:1 off the chart behind them, and --shadow-sm is
     the only edge they had — which in dark is a 34%-black blur on a #1E2836
     ground, i.e. nothing. Same self-toning edge as the calendar event. */
  border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
}
.gantt-bar .gb-label { overflow: hidden; text-overflow: ellipsis; }
.gantt-bar.tone-mute    { background: var(--tint-mute);    color: var(--ink-mute); }
.gantt-bar.tone-info    { background: var(--tint-info);    color: var(--ink-info); }
.gantt-bar.tone-warn    { background: var(--tint-warn);    color: var(--ink-warn); }
.gantt-bar.tone-pending { background: var(--tint-pending); color: var(--ink-pending); }
.gantt-bar.tone-ok      { background: var(--tint-ok);      color: var(--ink-ok); }
.gantt-bar.tone-danger  { background: var(--tint-danger);  color: var(--ink-danger); }
/* Late replaces the self-toning edge rather than adding an outline outside
   it. Both at once drew two concentric rings on a 20px bar. */
.gantt-bar.is-late { border-color: var(--ink-danger); }

/* The month boundaries, behind everything. --line rather than --line-soft:
   they are the chart's only axis and a rule you cannot see is not one. */
.gantt-rule {
  position: absolute; inset-block: 0;
  width: 1px;
  background: var(--line);
  pointer-events: none;
}

.gantt-today {
  position: absolute; inset-block: 0;
  width: 2px;
  background: var(--accent);
  z-index: 1;
  pointer-events: none;
}
.gantt-today .gt-label {
  position: absolute; top: 2px; inset-inline-start: 4px;
  font-size: var(--fs-100);
  font-weight: 800;
  color: var(--ink-accent);
  white-space: nowrap;
}

/* Timeline's hidden-task count. A CONDITION NOTICE, so it takes the band that
   already exists rather than staying bare text under a chart — it was the
   fourth thing of this kind and the one that made `.page-band` worth
   extracting. Info, not warning: nothing is wrong, some tasks simply have no
   dates to place. */
.chart-note {
  /* margin-BLOCK, never the `margin` shorthand: the shorthand resets
     margin-inline to 0 and wipes `.page-band`'s negative inline margins, so
     the band rendered 24px out of line and 48px too wide — measured 1..1381
     inside a shell of 25..1405. Only visible once the three bands shared one
     class; while each repeated the margins there was nothing to conflict with. */
  margin-block: var(--sp-4) 0;
  background: var(--tint-info);
  border-block: 1px solid var(--edge-info);
  color: var(--text);
}
.chart-note svg { width: 15px; height: 15px; flex: none; }

/* ---------- 18. Task drawer extras ---------------------------------------- */

.select-sm { min-height: 32px; padding: 2px var(--sp-2); font-size: var(--fs-300); }

.link-btn {
  padding: 0;
  font-size: var(--fs-200);
  font-weight: 700;
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.activity-list { list-style: none; margin: 0; padding: 0; }
.activity-list li {
  display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-2);
  padding: var(--sp-2) 0;
  font-size: var(--fs-200);
  border-bottom: 1px solid var(--line-soft);
}
.activity-list li:last-child { border-bottom: 0; }
.activity-list .who { font-weight: 700; }
.activity-list .verb { color: var(--text-2); }
.activity-list time { margin-inline-start: auto; color: var(--text-3); font-variant-numeric: tabular-nums; }

/* ---------- 19. Comments -------------------------------------------------- */

.comment-list { list-style: none; margin: 0 0 var(--sp-3); padding: 0; }
.comment { display: flex; gap: var(--sp-2); padding: var(--sp-2) 0; }
.comment-body { flex: 1; min-width: 0; }
.comment-head { display: flex; align-items: baseline; gap: var(--sp-2); font-size: var(--fs-200); }
.comment-head time { margin-inline-start: auto; color: var(--text-3); }
.comment-text { display: block; margin-top: 2px; font-size: var(--fs-300); white-space: pre-wrap; word-break: break-word; }

.comment-compose { display: flex; flex-direction: column; gap: var(--sp-2); align-items: flex-end; }
.comment-compose textarea {
  width: 100%;
  min-height: 56px;
  padding: var(--sp-2) var(--sp-3);
  background: var(--input);
  border: 1px solid var(--control-line);
  border-radius: var(--r-sm);
  font-size: var(--fs-600);
  line-height: var(--lh-snug);
  resize: vertical;
}
.comment-compose textarea:focus-visible { outline: none; border-color: var(--focus); box-shadow: 0 0 0 3px var(--focus-glow); }

/* ---------- 20. Attachments ----------------------------------------------- */

.file-list { list-style: none; margin: 0 0 var(--sp-3); padding: 0; }
.file-row {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
}
.file-row + .file-row { margin-top: var(--sp-2); }
.file-icon {
  flex: none;
  width: 32px; height: 32px;
  display: grid; place-items: center;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text-2);
}
/* Minutes' attachment line (§14.1). Deliberately a LIST OF LINES, not the
   drawer's file rows: §3.29 already records that Minutes inlines too much, and
   the point here is that the signed attendance sheet is visible, not that it is
   previewed. Upload and removal live in the meeting drawer. */
.mn-files { list-style: none; margin: 0; padding: 0; }
.mn-files li {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) 0;
  font-size: var(--fs-200);
}
.mn-files li > svg { flex: none; color: var(--text-3); }
.mn-files .tnum { margin-inline-start: auto; color: var(--text-3); font-size: var(--fs-100); }

/* Storage usage (§14.5). The figure is large because it is the one number on
   this screen anyone scans for; the paragraph under it is not decoration —
   it carries the reading that makes the number mean anything. */
.st-storage-figure {
  margin: 0;
  /* --fs-800, the page-heading step. Not --fs-700, which is a panel TITLE and
     would make the figure the same size as the words "Storage" above it; and
     not the display sizes, which §3.25 reserves for a screen's one dominant
     object — this is a side panel, not the subject of the page. */
  font-size: var(--fs-800);
  font-weight: 800;
  line-height: 1.1;
}
.st-storage-of { margin: var(--sp-1) 0 var(--sp-3); color: var(--text-2); }
.st-storage-kv { margin-block-end: var(--sp-3); }

/* The thumbnail fills the icon well exactly, so a row is the same height with
   a preview or a glyph and a list of mixed types does not comb. object-fit is
   load-bearing: a 400px thumb of any aspect ratio has to sit in a 32px square
   without letterboxing or stretching a face. */
.file-thumb {
  width: 100%; height: 100%;
  object-fit: cover;
  border-radius: inherit;
  display: block;
}
.file-body { flex: 1; min-width: 0; }
.file-name { display: block; font-size: var(--fs-300); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The file name is a BUTTON, not a link, because there is no href to give it:
   a signed URL minted at render is dead before most people click it, so the
   URL is minted on the press instead (§14.2). It still has to read as
   something you can open, so it keeps a link's affordance and inherits the
   name's weight rather than a button's chrome. */
.file-name .file-link,
.mn-files .file-link {
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: start;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--control-line);
}
.file-name .file-link:hover,
.file-name .file-link:focus-visible,
.mn-files .file-link:hover,
.mn-files .file-link:focus-visible { text-decoration-color: currentColor; }
.file-meta { display: block; font-size: var(--fs-100); color: var(--text-3); }

/* A row whose bytes are not in the bucket reads as provisional rather than
   broken — one state is still in flight and one is finished with. Opacity
   alone would drop the label below 4.5:1, so the mark is the chip beside it
   (§3.7); this only softens the well. */
.file-row.is-pending .file-icon,
.file-row.is-unfinished .file-icon { background: var(--surface-3); color: var(--text-3); }

.dropzone {
  display: grid; place-items: center; gap: var(--sp-1);
  padding: var(--sp-5) var(--sp-3);
  border: 1px dashed var(--control-line);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text-2);
  text-align: center;
  cursor: pointer;
  transition: border-color var(--dur-fast) ease, background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.dropzone:hover, .dropzone.is-over {
  border-color: var(--primary);
  background: var(--primary-soft);
  color: var(--primary);
}
.dropzone svg { width: 20px; height: 20px; }
.dz-main { font-size: var(--fs-300); font-weight: 700; }
.dz-sub { font-size: var(--fs-100); color: var(--text-3); }

.form-section {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: var(--sp-2);
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-100);
  font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-2);
}
.form-section::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.form-section:first-child { margin-top: 0; }

/* ==========================================================================
   PHASE 4 — minutes and commitments
   ========================================================================== */

/* ---------- 21. Minutes view ---------------------------------------------- */

/* ==========================================================================
   MINUTES IS A DOCUMENT (§3.20)

   It was a stack of cards: a bordered panel per meeting, each holding
   bordered blocks, each block holding a list. Three borders organising
   content that a measure and some whitespace organise better — and the
   result read like a settings screen rather than a record of what was said.

   A minute is prose. So: ONE MEASURE, about 68 characters, held in a wide
   empty field. No panels, no cards, no block borders. Section headings sit
   OUTDENTED in the margin beside the text rather than above it, the way a
   printed record puts its rubrics — which is also what makes the shape
   unmistakable when the text is blurred.

   Blurred: a single narrow column of type floating in white space, with small
   marks in the margin. It is the only screen in the app with large empty
   margins, and the only one where the eye has nowhere to go but down.
   ========================================================================== */

/* "These meetings have no minutes yet" is a call to action, not a record, so
   it sits ABOVE the document rather than inside the measure — but it was
   still a bordered panel wrapping .meeting-row, a class restyled for the
   Meetings gutter. It inherited a layout built for a different container and
   its chips flew to the far edge. Its own quiet list instead. */
/* A CONTAINED CALLOUT, not a wash.

   It had square corners, no border on three sides, and a 3px spine in
   --edge-accent that measured 1.21:1 against the page. So the tint simply
   stopped mid-air and the block read as bleeding into the ground.

   Worth recording why an edge is doing more work here than usual: in light
   the fill is --tint-accent #FCF0E9 on a --bg of #EDF2F9, which is 1.01:1.
   Warm cream on cool paper is plainly visible as a HUE change and invisible
   as a luminance one, so the block's shape cannot come from the fill. The
   boundary is not a refinement of this block, it is the only thing declaring
   where it ends.

   So: a hairline all round in the ramp's own edge and the same --r-lg as the
   record sheets below it. Fill and padding unchanged; this defines the edges,
   it does not raise the volume.

   The 3px inline-start spine is GONE, not kept alongside. It was the block's
   only edge before, so it had a job; once there is a real border it is
   decoration, and the impeccable detector names a thick coloured border on
   one side of a card as the single most recognisable tell of a generated
   interface. A redundant anti-pattern is the easiest kind to delete. */
.awaiting-panel {
  /* Same frame as the document. */
  max-width: min(100%, 1060px);
  margin-inline: auto;
  margin-block-end: var(--sp-10);
  padding: var(--sp-4) var(--sp-5);
  background: var(--tint-accent);
  border: 1px solid var(--edge-accent);
  border-radius: var(--r-lg);
}
.awaiting-panel .panel-head {
  padding: 0 0 var(--sp-2);
  border: 0;
  color: var(--ink-accent);
}
.awaiting-panel .panel-head h3 { font-size: var(--fs-300); font-weight: 800; }
.awaiting-panel .meeting-list { padding: 0; }
/* Inside the accent block the row keeps the tint rather than becoming a white
   slab: it is a line in a callout, not an object on the page ground. */
.awaiting-panel .meeting-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) 0;
  background: none;
  box-shadow: none;
}
.awaiting-panel .row-chips { margin: 0; }
.awaiting-panel .meeting-row::before { content: none; }
.awaiting-panel .meeting-row:hover { background: none; box-shadow: none; text-decoration: underline; }

/* ANCHORED TO THE READING EDGE, not centred. Centred, the column floated
   mid-screen with empty space on both sides and the awaiting block centred
   above it — two axes, and the page read as unfinished rather than composed.
   A document starts where reading starts. `margin-inline: 0 auto` puts all
   the slack on the OUTSIDE edge, so there is one axis: the heading margin,
   then the text, then air. */
.mn-doc {
  /* The measure lives on .mn-record now; this is the frame that holds the
     three columns, centred so leftover width splits rather than pooling. */
  max-width: min(100%, 1060px);
  margin-inline: auto;
}

/* THREE COLUMNS: rubrics, text, particulars.

   A 68ch measure on a 1440px screen left about a third of the width doing
   nothing, which reads as broken rather than composed. The outer field now
   carries each record's particulars — date, project, attendance, commitment
   count, who recorded it — which is both a use for the space and a better
   document: the prose starts directly under the title instead of after a line
   of metadata.

   The whole frame is centred, so whatever slack is left over at a given width
   splits evenly instead of piling up on one side. */
/* A RECORD IS A SHEET. Rule-and-rhythm separated the records and they still
   read as one continuous document rather than as a stack of minutes. Paper is
   the honest metaphor and the app already speaks it: white on the cool
   ground. No border and no shadow — a minute is not a control and does not
   lift — but a real edge, so a reader sees where one ends without measuring
   the gap.
   The rule between records goes with it: the space between two sheets IS the
   separation, and drawing a line there as well would say it twice. */
.mn-record {
  display: grid;
  grid-template-columns: minmax(0, 68ch) minmax(168px, 232px);
  column-gap: var(--sp-8);
  align-items: start;
  background: var(--surface);
  border-radius: var(--r-lg);
  /* 132px is the rubric margin the headings hang in, and it has to be the
     SHEET's padding rather than the document's: hang a heading into a margin
     the paper does not have and it lands on the page ground, outside the
     record it belongs to. */
  padding-block: var(--sp-8) var(--sp-6);
  padding-inline: 132px var(--sp-6);
}
.mn-record > * { grid-column: 1; }
.mn-record > .mn-aside { grid-column: 2; grid-row: 1 / 100; }
.mn-record + .mn-record { margin-block-start: var(--sp-6); }

.mn-record-head { margin-block-end: var(--sp-5); }

/* The particulars. Sticky so they stay beside their record while it scrolls,
   which is what keeps the outer field occupied down the length of a long
   minute rather than only at its head. */
.mn-aside {
  position: sticky;
  inset-block-start: calc(var(--topbar-h) + var(--sp-4));
  padding-inline-start: var(--sp-4);
  border-inline-start: 1px solid var(--line-soft);
  font-size: var(--fs-200);
  color: var(--text-2);
}
.mn-aside-date {
  font-size: var(--fs-700); font-weight: 800;
  line-height: 1;
  color: var(--text);
  margin-block-end: var(--sp-3);
}
.mn-facts { margin: 0; display: grid; gap: var(--sp-2); }
.mn-facts > div { display: grid; gap: 1px; }
.mn-facts dt {
  font-size: var(--fs-100); font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-3);
}
.mn-facts dd { margin: 0; font-size: var(--fs-300); color: var(--text); }
.mn-aside-chip { margin-block-start: var(--sp-3); }
.mn-record-head h3 {
  margin: 0;
  font-size: var(--fs-800); font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}

.mc-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-block-start: var(--sp-5); }

.mn-block { position: relative; margin-block-start: var(--sp-6); }

/* OUTDENTED into the margin. Logical properties, so RTL hangs them on the
   right without a second rule — the thing the Phase 1 stripe got wrong. */
.mn-block > h4 {
  position: absolute;
  inset-inline-start: -132px;
  inset-block-start: 0.35em;
  width: 108px;
  margin: 0;
  text-align: end;
  font-size: var(--fs-100);
  font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-3);
}
/* The prose itself gets the reading measure's leading — and no box. `.note`
   is a bordered card elsewhere in the app (a callout inside a drawer, where
   it IS an aside); in a document the border is the third one organising
   content that whitespace has already organised. */
.mn-block .note {
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  font-size: var(--fs-500);
  line-height: var(--lh-body);
  color: var(--text);
  white-space: pre-wrap;
}
.sub-h {
  margin: var(--sp-4) 0 var(--sp-2);
  font-size: var(--fs-100);
  font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-2);
}

/* ---------- 22. Commitment rows — read (§8.6) -----------------------------
   Four columns on a wide screen, stacked on a narrow one. The status column
   holds a chip and, at most, a link to the task. There is deliberately no
   control here that changes a task: the commitment records what was promised,
   the task is where the work lives. */

/* Numbered, because a commitment is referred to by its number in the room —
   "item three" is how these get discussed at the next meeting. */
.commitment-list { list-style: none; margin: 0; padding: 0; counter-reset: cm; }
.commitment-list > li { counter-increment: cm; }
/* `li.commitment-row`, not `li > .commitment-row`: commitmentReadRow returns
   `<li class="commitment-row">`, so the row IS the list item. The descendant
   form matched nothing and the numbers silently never rendered — the same
   shape of miss the class guard exists for, one level below what it checks. */
.mn-doc .commitment-list > li.commitment-row::before {
  content: counter(cm) ".";
  position: absolute; inset-inline-start: -28px; inset-block-start: var(--sp-3);
  font-size: var(--fs-300); font-weight: 700;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

/* INSIDE THE DOCUMENT the four-column grid is wrong: 68ch cannot hold text,
   owner, date and status side by side, and forcing it wrapped every cell to
   two lines. The commitment reads as a sentence with its particulars
   underneath — which is also how it reads on paper. Everywhere else (the
   minutes editor, the drawer) the grid is still right and is untouched. */
.mn-doc .commitment-row {
  display: block;
  padding-block: var(--sp-3);
}
/* text-align: start resolves against the element's OWN dir, which is why the
   dir="auto" on the span matters as much as the bdi would have: an Arabic
   commitment in an English page aligns to its own start, not the page's. */
.mn-doc .cm-text { display: block; text-align: start; font-size: var(--fs-500); font-weight: 600; line-height: var(--lh-tight); }
.mn-record-head h3 { text-align: start; }
.mn-doc .cm-owner,
.mn-doc .cm-due { display: inline; font-size: var(--fs-200); }
.mn-doc .cm-status { display: inline-flex; justify-content: flex-start; }
/* One meta line under the text: owner · due · status. */
.mn-doc .cm-owner { margin-inline-end: var(--sp-3); }
.mn-doc .cm-due   { margin-inline-end: var(--sp-3); }
.mn-doc .cm-owner, .mn-doc .cm-due, .mn-doc .cm-status { margin-block-start: var(--sp-2); }
.commitment-row {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto auto auto;
  align-items: baseline;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--line-soft);
}

.commitment-list.is-compact .commitment-row { grid-template-columns: 1fr auto auto; padding: var(--sp-2) 0; }
.commitment-row:last-child { border-bottom: 0; }
.cm-text { font-size: var(--fs-300); font-weight: 600; min-width: 0; }
.cm-owner { font-size: var(--fs-200); color: var(--text-2); white-space: nowrap; }
.cm-owner .who { display: inline-flex; align-items: center; gap: var(--sp-1); }
.cm-due { font-size: var(--fs-200); color: var(--text-2); white-space: nowrap; }
.cm-status { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; justify-content: flex-end; }

/* ---------- 23. Commitment rows — edit (§8.4) ----------------------------- */

.commitment-edit {
  padding: var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
.commitment-edit + .commitment-edit { margin-top: var(--sp-2); }
.ce-main { display: flex; align-items: center; gap: var(--sp-2); }
.ce-main .ce-text {
  flex: 1; min-width: 0;
  min-height: 40px;
  padding: var(--sp-2) var(--sp-3);
  background: var(--input);
  border: 1px solid var(--control-line);
  border-radius: var(--r-sm);
  font-size: var(--fs-600);
  font-weight: 600;
}
.ce-main .ce-text:focus-visible { outline: none; border-color: var(--focus); box-shadow: 0 0 0 3px var(--focus-glow); }

.ce-fields {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-2) var(--sp-3);
  margin-top: var(--sp-2);
}
.ce-field { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ce-field > span { font-size: var(--fs-100); font-weight: 700; color: var(--text-2); }
.ce-field .select { min-width: 160px; }
.ce-check { min-height: 38px; }

.ce-note { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin-top: var(--sp-2); }
.ce-note:empty { display: none; }
.ce-hint { font-size: var(--fs-100); color: var(--text-3); }

/* ==========================================================================
   PHASE 5 — projects, archive, deleted, print dialog
   ========================================================================== */

/* ---------- 24. Project bands (§7.8, §3.24) -------------------------------- */

/* STRATA, NOT A WALL OF TILES.

   A masonry of cards, each holding four more cards for its four numbers:
   three levels of container doing what alignment does for free, and five
   projects arriving as a ragged wall of boxes at uneven heights.

   One project is now one full-width band — colour spine, identity on the
   reading side, figures as bare numerals on the other. Brief §1: metrics want
   no container at all. Five bands is a silhouette nothing else in the app has.

   THE BANDS ARE CARDS — §3.24 REVERSED BY THE OWNER, the third time in this
   phase after Calendar and Table.

   The note below is the original and its reasoning is kept: no card, no
   radius, no shadow; the same ledger logic as the register, held apart from it
   by four times the row height and display-scale numerals. What spent that
   argument is that THE REGISTER IS NOT A LEDGER ANY MORE. Table's rows became
   cards, so the screen this one cited for its pattern had abandoned it, and
   Projects was the last place using it.

   `.meeting-row`'s values, not a fifth card: --surface, --r-sm, and a gap
   rather than a hairline between them.

   NO HOVER STATE, and that is not an omission. A band is an <li> holding its
   own buttons — the card itself is not pressable, so a fill that brightens
   under the pointer would promise a click that does nothing. Every other card
   in this app hovers because every other card opens something.

   WHAT KEEPS: the spine, in the project's own colour, which is the one thing
   on this screen carrying data (§7.8); the four figures and their alignment;
   and the dashed slot that makes a new one. */
.pj-bands { list-style: none; margin: var(--sp-2) 0 0; padding: 0; }
.project-band + .project-band { margin-block-start: var(--sp-2); }

.project-band {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--sp-8);
  padding: var(--sp-6) var(--sp-3) var(--sp-6) var(--sp-3);
  padding-inline-start: var(--sp-6);
  background: var(--surface);
  border-radius: var(--r-sm);
}

/* The spine carries the project's own colour, which is data (§7.8) — the one
   place in this screen where a project is identified by hue rather than name.
   A pseudo-element so it spans the band's full height however tall the
   description makes it. */
.project-band::before {
  content: '';
  position: absolute;
  inset-block: var(--sp-4);
  inset-inline-start: 0;
  width: 4px;
  border-radius: 0 var(--r-pill) var(--r-pill) 0;
  background: var(--dot, var(--primary));
}
html[dir="rtl"] .project-band::before { border-radius: var(--r-pill) 0 0 var(--r-pill); }
.project-band.is-dim { opacity: .72; }

/* The "new project" control IS a band — same width, same rhythm, dashed so
   it reads as a slot rather than a record. It stays a SLOT while the records
   became cards: no fill and no radius of its own, because the dashed outline
   on its button is what says "nothing here yet" and a filled card behind it
   would say the opposite. */
.project-band.pb-add {
  display: block;
  padding: 0;
  background: none;
}
.project-band.pb-add::before { content: none; }
.pb-add > button {
  display: flex; align-items: center; justify-content: center; gap: var(--sp-2);
  width: 100%;
  padding: var(--sp-6) var(--sp-3);
  border: 1px dashed var(--control-line);
  border-radius: var(--r-sm);
  margin: var(--sp-3) 0;
  font-size: var(--fs-400); font-weight: 700;
  color: var(--text-2);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.pb-add > button:hover { background: var(--surface); color: var(--primary); border-color: var(--primary); }
.pb-add > button svg { width: 16px; height: 16px; }

.pb-id { min-width: 0; }
.pb-id h3 {
  display: flex; align-items: center; gap: var(--sp-3);
  margin: 0;
  font-size: var(--fs-800); font-weight: 800; line-height: var(--lh-tight);
}
.pb-desc { margin: var(--sp-2) 0 0; font-size: var(--fs-300); color: var(--text-2); max-width: 48ch; }
.pb-next {
  display: flex; align-items: center; gap: var(--sp-2);
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-200);
  color: var(--text-3);
}
.pb-next svg { width: 14px; height: 14px; flex: none; }

/* The figures. Numeral first and large, label under it and small — the
   opposite order from the old stat tile, because the number is the thing you
   came for and the label only says which number it is. */
/* Four fixed-width columns rather than four shrink-to-fit ones. The labels
   are Arabic phrases and the numerals are one glyph wide, so sizing each
   column to its content let the labels overrun each other — "مهام مفتوحة"
   ran straight into "متأخرة". A column wide enough for the numeral and
   narrow enough to make the label wrap is the honest shape: the numeral
   leads, the label explains it underneath in two lines if it needs them. */
.pb-figures {
  display: flex; gap: var(--sp-5);
  flex: none;
  margin: 0;
}
.pb-figures > div { flex: none; width: 84px; text-align: center; }
.pb-figures dd {
  margin: 0;
  font-size: var(--fs-display); font-weight: 800; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.pb-figures dt {
  margin-block-start: var(--sp-1);
  font-size: var(--fs-100); font-weight: 700;
  line-height: var(--lh-snug);
  letter-spacing: var(--tracking-label);
  color: var(--text-3);
}
.pb-figures .is-bad dd { color: var(--ink-danger); }
.pb-figures .is-warn dd { color: var(--ink-accent); }

.pb-actions { display: flex; gap: var(--sp-2); }

.swatches { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.swatch {
  width: 30px; height: 30px;
  border-radius: 50%;
  background: var(--sw);
  border: 2px solid transparent;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent);
}
.swatch.is-on { border-color: var(--focus); box-shadow: 0 0 0 3px var(--focus-glow); }

/* ---------- 25. Archive, deleted, purge ----------------------------------- */

/* Deleted rows are records, not controls: they keep the surface so each one
   still reads as an object, and lose every trace of being pressable. */
.meeting-row.is-static { cursor: default; }
.meeting-row.is-static::before { content: none; }
.meeting-row.is-static:hover { background: var(--surface); box-shadow: none; }

.purge-panel { margin-top: var(--sp-5); border-color: var(--edge-danger); }
.purge-panel .panel-head { background: var(--tint-danger); border-bottom-color: var(--edge-danger); }
.purge-panel .panel-head h3 { color: var(--ink-danger); }
.purge-check { margin-top: var(--sp-4); }
.purge-panel .btn.danger { margin-top: var(--sp-3); }
.purge-panel .btn:disabled { opacity: .45; cursor: not-allowed; }

/* ---------- 26. Print dialog ---------------------------------------------- */

/* §15.2: attachments is disabled and greyed when Type = meetings. The
   `disabled` attribute does the work; this makes the state legible. */
#rp-attach-wrap.is-disabled > label,
#rp-attach-wrap.is-disabled .select { opacity: .5; }
#rp-attach-wrap.is-disabled .hint { opacity: 1; color: var(--ink-accent); font-weight: 600; }
.select:disabled { background-color: var(--surface-2); cursor: not-allowed; }
/* A DISABLED OPTION'S APPEARANCE IS THE PLATFORM'S, NOT OURS. On Windows the
   dropdown list is drawn by the OS and most of this will not land — which is
   why the reason is written into the option's TEXT in report.js and this is
   only the half that helps where it applies. Never the only signal. */
.select option:disabled { color: var(--text-3); }

/* ---------- 12. Narrow ---------------------------------------------------- */

@media (max-width: 900px) {
  /* The one-row blocked band is a DESKTOP composition. Left unconditional it
     pushed the card off the inline-start edge at 390, clipping the title
     mid-word outside the band's own corner — worse than the dead tint it
     replaced. It stacks below the breakpoint.
     This override lives HERE, not in shell.css: the base rule is in this file
     and this file loads last, so an equal-specificity rule over there loses. */
  .blocked-strip { display: block; width: 100%; }
  .bs-head { padding-bottom: 0; }
  .bs-body { grid-template-columns: 1fr; padding: var(--sp-3) var(--sp-4) var(--sp-4); }

  .form-grid { grid-template-columns: 1fr; }
  .profile-grid { grid-template-columns: 1fr; }

  /* A band is a desktop composition: three columns become three rows, and
     the figures scroll rather than shrink to unreadable. */
  .project-band { grid-template-columns: 1fr; gap: var(--sp-4); padding-block: var(--sp-5); }
  .pb-id h3 { font-size: var(--fs-700); }
  /* Two by two, not a wrapped row. Flex-wrap put three on the first line and
     orphaned the fourth on its own, which broke the read the strata exist for
     — four numerals belonging to one project stopped looking like one group.
     A 2×2 grid is even, and even is the point. */
  .pb-figures { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp-4); }
  .pb-figures > div { width: auto; }
  .pb-figures dd { font-size: var(--fs-800); }
  .cal-label { min-width: 0; flex: 1; font-size: var(--fs-500); }
  /* The navigator carries this screen's whole opening now (§3.28) — month,
     chevrons, today, the mode toggle and the action — and that is more than
     348px holds. It wraps onto a second line rather than overflowing. */
  .cal-nav { flex-wrap: wrap; row-gap: var(--sp-2); }
  .page-head-acts { width: 100%; justify-content: flex-end; margin-inline-start: 0; }

  /* Tables become card lists on phones (§17); the month grid keeps its shape
     but sheds the event text, since a 40px cell cannot hold a title. */
  .cal-day { min-height: 62px; }
  .cal-event .ev-title { display: none; }
  .cal-event { justify-content: center; }

  .meeting-row { gap: var(--sp-3); }
  .meeting-row .row-chips { max-width: none; width: 100%; justify-content: flex-start; margin-top: var(--sp-2); }
  .meeting-row { flex-wrap: wrap; }

  /* No margin to hang headings in at 390px: the outdent becomes an inline
     heading and the measure becomes the screen. */
  .mn-doc { max-width: none; }
  /* Neither margin survives 390px: the rubrics come inline and the
     particulars become a row under the title. The sheet keeps a gutter on
     both sides — text against the paper's own edge is not a margin, it is a
     missing one. */
  .mn-record { display: block; padding-inline: var(--sp-4); }
  .mn-aside {
    position: static;
    padding-inline-start: 0; border-inline-start: 0;
    margin-block-end: var(--sp-4);
  }
  .mn-aside-date { font-size: var(--fs-400); }
  .mn-facts { grid-auto-flow: column; grid-auto-columns: max-content; gap: var(--sp-4); overflow-x: auto; }
  .mn-block > h4 {
    position: static; width: auto; text-align: start;
    margin-block-end: var(--sp-2);
  }
  .mn-doc .commitment-list > li.commitment-row::before { position: static; margin-inline-end: var(--sp-2); }

  /* Bottom sheet, same DOM (§8.3, §17). */
  .drawer {
    inset-block: auto 0;
    inset-inline: 0;
    width: 100%;
    max-height: 90dvh;
    border-inline-start: 0;
    border-top: 1px solid var(--line);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    transform: translateY(100%);
    padding-bottom: env(safe-area-inset-bottom);
  }
  /* Both directions slide up here; the is-on rule is repeated because this
     block sits later in the file than the base one. */
  html[dir="rtl"] .drawer { transform: translateY(100%); }
  html .drawer.is-on { transform: none; }

  .modal { padding: 0; place-items: end stretch; }
  .modal-box {
    width: 100%;
    max-height: 92dvh;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    padding-bottom: env(safe-area-inset-bottom);
  }
  .modal-box > footer { border-radius: 0; }
  .modal-box > footer .btn { flex: 1; }

  /* §17: the board scrolls its columns horizontally with snap points, one
     column filling the viewport width. */
  .board {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    gap: var(--sp-3);
    padding-bottom: var(--sp-2);
  }
  .board-col {
    flex: 0 0 min(86vw, 320px);
    scroll-snap-align: start;
  }

  /* §17: tables become card lists on phones. The register has no .table-wrap
     around it any more — it IS the page — so it is named here directly.
     Archive and Deleted still wrap theirs. */
  .table-wrap,
  .register > .data-table { display: none; }
  .card-list { display: block; }

  .gantt { --label-w: 130px; }

  .commitment-row,
  .commitment-list.is-compact .commitment-row {
    grid-template-columns: 1fr;
    justify-items: start;
  }
  .cm-status { justify-content: flex-start; }
  .ce-field .select { min-width: 0; width: 100%; }
  .ce-field { flex: 1 1 100%; }
}

@media print {
  .toolbar, .cal-nav, .page-head-acts, .pb-add, .drawer, .modal { display: none !important; }
}

/* ==========================================================================
   27. My Day — one dominant object (§3.25)

   Every other screen in the app is a list, a grid, a chart or a document, and
   each of those starts with the same page head. This one does not have a page
   head at all: THE ANSWER is the heading. That is the point — the screen
   exists to answer "what now", so the answer is set at 56px and everything
   else on the page is 13px or smaller.

   The size contrast IS the structure. A dashboard of five equal tiles cannot
   answer a question, because five things of equal weight ask the reader to
   choose which one matters. One display line and two quiet columns does not.

   Blurred: a single large block at the top, then a thin vertical line with
   marks down one side and a narrow column of small text beside it. Nothing
   else in the app has one object that dominates.
   ========================================================================== */

/* MY DAY'S MASTHEAD IS THE SHARED HEADER PLUS TYPE, NOT A SECOND CONTAINER.
   It carries the largest type in the app, so it needs more presence than a
   caption - and the way to give it more presence is the display line it
   already has, not a box of its own. What it takes from `.page-head` is the
   full-bleed rule; what it adds is room beneath, because a 34-56px answer
   sitting 16px off a rule reads as cramped where a count does not.

   No top padding: that was standing in for a page edge which did not exist,
   and the shell provides the inset now. Keeping both opened the screen on
   44px of nothing. */
.md-open { padding-block: 0 var(--sp-8); }
/* The band's own top rule takes over as the separator when there is one. */
.md-open:has(+ .md-unclosed) { border-block-end: 0; }
.md-eyebrow {
  display: flex; align-items: baseline; gap: var(--sp-2);
  margin: 0 0 var(--sp-6);
  font-size: var(--fs-100); font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-3);
}
.md-kicker {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-200); font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--primary);
}
.md-open.is-late .md-kicker { color: var(--ink-danger); }

/* The one display line in the app outside the login screen. clamp() rather
   than a breakpoint: the answer is a title someone typed, so its length is
   not ours to plan for, and a 34px floor keeps a long one from collapsing
   into the body scale. */
.md-answer {
  margin: 0;
  max-width: 22ch;
  font-size: clamp(var(--fs-display), 4.4vw, var(--fs-display-2xl));
  font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: -0.015em;
}
.md-particulars {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-4);
  margin: var(--sp-4) 0 var(--sp-6);
  font-size: var(--fs-300);
  color: var(--text-2);
}
.md-particulars > span { display: inline-flex; align-items: center; gap: var(--sp-1); }
.md-particulars svg { width: 14px; height: 14px; }
.md-when { font-size: var(--fs-500); font-weight: 800; color: var(--text); }
.md-late { color: var(--ink-danger); font-weight: 700; }

/* Two columns, and deliberately UNEQUAL — the day is the subject and the work
   is the margin note. Equal halves would be the tile dashboard again in a
   coarser grid. */
.md-body {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: var(--sp-10);
  /* sp-8, not sp-6. `--sp-7` was never a token so this was ZERO from 8.6
     until the token guard found it; sp-6 was the cautious replacement and
     read short, with TODAY sitting almost on the rule. */
  padding-block-start: var(--sp-8);
}
/* SECTION LABELS GET NO BOX, AND THEY EARN THEIR PLACE BY PROXIMITY.
   TODAY and ON YOU label the group under them. A label in its own box, above
   a group of cards, inside a shell, is three borders deep - the failure this
   phase has avoided since step 22a. So: uppercase, tracked, --text-3, and
   TIGHT against what it labels. The gap below is half the gap above; when the
   two were equal the label floated between two groups and belonged to
   neither. */
.md-h {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-100); font-weight: 800;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-3);
}
.md-quiet { margin: 0; font-size: var(--fs-300); color: var(--text-3); }

/* THE SPINE. A continuous line down the hour column with each meeting pinned
   to it — the line is what makes this a day rather than a third list. */
.md-spine { list-style: none; margin: 0; padding: 0; position: relative; }
.md-spine::before {
  content: '';
  position: absolute;
  inset-block: 10px 10px;
  inset-inline-start: 56px;
  width: 1px;
  background: var(--line);
}
.md-slot {
  display: grid;
  grid-template-columns: 56px 1fr;
  align-items: start;
  gap: var(--sp-5);
}
.md-slot + .md-slot { margin-block-start: var(--sp-4); }
.md-hour {
  padding-block-start: var(--sp-2);
  font-size: var(--fs-200); font-weight: 800;
  color: var(--text-2);
  text-align: end;
  white-space: nowrap;
}
/* The pin. Sits ON the spine, so a meeting reads as a point in the day rather
   than a row in a table. */
.md-meeting {
  position: relative;
  display: block; width: 100%;
  padding: var(--sp-3);
  text-align: start;
  border-radius: var(--r-sm);
  background: var(--surface);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.md-meeting::before {
  content: '';
  position: absolute;
  inset-block-start: 14px;
  inset-inline-start: -22px;
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--primary);
  box-shadow: 0 0 0 3px var(--bg);
}
.md-meeting:hover { background: var(--row-hover); box-shadow: var(--shadow); }
.md-slot.is-past { opacity: .55; }
.md-slot.is-past .md-meeting::before { background: var(--text-3); }
.md-meeting-title { display: block; font-size: var(--fs-500); font-weight: 700; }
.md-meeting-meta {
  display: flex; flex-wrap: wrap; gap: var(--sp-3);
  margin-block-start: var(--sp-1);
  font-size: var(--fs-200); color: var(--text-2);
}
.md-meeting-meta > span { display: inline-flex; align-items: center; gap: var(--sp-1); }
.md-meeting-meta svg { width: 12px; height: 12px; }

/* THE WORK COLUMN. Cards, and the note that used to sit here said the
   opposite: "rows, not cards - the brief's 'fewer surfaces' applies hardest
   where the content is secondary." Fewer surfaces was right about PANELS and
   wrong about the things inside them. Stripped to bare text on the page
   ground, these read as a paragraph rather than as things you can open -
   nothing said "this is one task", and nothing said "this is pressable" until
   the pointer was already on it. `.mt-of-day` reached the same conclusion one
   screen earlier and its rows carry the note; this is the same fix, with the
   same values, not a second card look.

   A GAP, NOT A RULE, for the same reason it is a gap on Meetings: the rows
   have their own ground now, so a hairline between them draws the same
   boundary twice. */
.md-tasks { list-style: none; margin: 0; padding: 0; }
.md-task + .md-task { margin-block-start: var(--sp-2); }
/* A HOVER THAT FIRED AND PAINTED NOTHING. This said
   `background: var(--surface)` - white. The rows used to sit on `--bg`, the
   cool paper, so white-on-paper was 1.12:1: faint, but a change. The page
   shell put `--surface` UNDERNEATH them, and white on white is 1.00:1. The
   declaration still applies, still wins, and produces no pixels.

   Same species as the six dead tokens, one step along: those were names that
   resolved to nothing, this is a name that resolves to exactly what is
   already behind it. Neither logs anything.

   AND THE TOKEN SWAP ALONE DOES NOT FIX IT. `--row-hover` on `--surface` is
   1.07:1 - LOWER than the 1.12:1 this row used to have. The reference row
   (.meeting-row) is visible on hover because it moves TWO channels, fill and
   elevation (§3.22), and in light it is the shadow doing nearly all the work
   while in dark it is the fill. Taking one of the pair is how this looked
   fixed and stayed invisible. */
.md-task > button {
  position: relative;
  display: block; width: 100%;
  /* `.meeting-row`'s values exactly, which `.md-meeting` in the column beside
     this one already had: sp-3, r-sm, --surface. White on cool paper is what
     makes a card read as raised (1.125:1 light, 1.150:1 dark) - which is the
     whole reason the shell became an edge rather than a fill. */
  padding: var(--sp-3);
  border-radius: var(--r-sm);
  background: var(--surface);
  text-align: start;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.md-task > button:hover { background: var(--row-hover); box-shadow: var(--shadow); }
.md-task-title { display: block; font-size: var(--fs-300); font-weight: 700; }
.md-task.is-late .md-task-title { color: var(--ink-danger); }
.md-task-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
  margin-block-start: var(--sp-1);
  font-size: var(--fs-100); color: var(--text-3);
}
/* THE BAR IS PART OF THE CARD, NOT LAID ON IT. In flow and inside the card's
   padding on every side, so no rounded corner can cut across it - which is
   what `.tc-time-base` does wherever it is pinned to the foot of a card that
   has a radius (Board, the phone rows, the Gantt). Those hosts come with
   their own screens; here the bar simply sits in the card. */
.md-task .tc-time-row { margin-block-start: var(--sp-3); }

@media (max-width: 900px) {
  /* One column at 390: the day first, the work under it. The answer keeps its
     display scale — it is the whole reason to open this screen on a phone. */
  .md-body { grid-template-columns: 1fr; gap: var(--sp-8); }
  .md-open { padding-block: 0 var(--sp-6); }
  .md-slot { grid-template-columns: 48px 1fr; gap: var(--sp-4); }
  .md-spine::before { inset-inline-start: 48px; }
  .md-meeting::before { inset-inline-start: -20px; }
}

/* ==========================================================================
   28. SETTINGS (§7.12)

   Deliberately NOT a ninth silhouette. Settings is a secondary admin screen —
   the same argument Archive and Deleted make for sharing the register (§3.24):
   a new shape here would be difference for its own sake. It reuses the panel
   language, and its own composition is the asymmetry — a wide roster beside a
   narrow stack of the two admin panels.

   No page head (§3.28): the screen opens on the roster.
   ========================================================================== */

.settings-grid {
  display: grid; grid-template-columns: 1.35fr 1fr;
  gap: var(--sp-4); align-items: start;
}
/* A grid child's min-width is `auto`, i.e. min-CONTENT, so one stubborn
   control widens the whole track and the page scrolls sideways. The matrix
   caught exactly that here: 381 inside 348 at 390, in all four
   theme × language combinations. */
.settings-grid > * { min-width: 0; }
.st-side { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
.st-side > * { min-width: 0; }

/* The panel head carries this screen's primary action, since there is no head
   to hold it (§3.28 — deleting a head must not delete its actions). */
.st-head-act { margin-inline-start: auto; }

/* ---------- Team ---------------------------------------------------------- */

.st-member { gap: var(--sp-2); flex-wrap: wrap; }
.st-member .team-name { flex: 1 1 40%; }
.st-member small { direction: ltr; }
/* Deactivated is a state of the row, not a chip alone: the whole row recedes,
   because a list where inactive and active read alike is a list that gets
   misread under time pressure. The name stays at full ink — it is still a
   person, and the chip beside it carries the meaning. */
.st-member.is-inactive .avatar-sm { opacity: .55; }
.st-member.is-inactive small { opacity: .7; }
.st-last-manager { flex-basis: 100%; margin: 0; }
.st-role-select { flex: none; max-width: 148px; }
.st-row-act { flex: none; }

/* ---------- Deputy permissions ------------------------------------------- */

.st-never { margin-bottom: var(--sp-4); }
.st-never strong { display: block; font-size: var(--fs-300); }

/* One card per admin. --surface-2 against the panel's --surface so a card
   reads as a container of controls, not another panel; the edge is what says
   where it ends, per §3.23 — the fill alone cannot. */
.st-grant-card {
  padding: var(--sp-4);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r);
}
.st-grant-card + .st-grant-card { margin-top: var(--sp-4); }
.st-grant-head { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-3); }

/* Column, not the pill row .check-grid gives by default: these labels are
   sentences, and a wrapped pill row of sentences is rubble. */
.st-grant-list { flex-direction: column; align-items: stretch; gap: var(--sp-1); }
.st-grant-row { justify-content: flex-start; border-radius: var(--r-sm); }
.st-default-on {
  margin-inline-start: auto;
  font-size: var(--fs-050); font-weight: 700;
  color: var(--text-3); text-transform: uppercase; letter-spacing: .04em;
}
/* The pill goes solid primary when on, so a --text-3 grey sits on blue. It
   rides the label's own ink instead — the gallery does not measure this
   sub-label, which is exactly why it needs saying rather than assuming. */
.check.is-on .st-default-on { color: inherit; opacity: .82; }
.st-grant-warn {
  display: flex; align-items: flex-start; gap: var(--sp-1);
  margin: 0 0 var(--sp-1);
  padding-inline-start: var(--sp-3);
  color: var(--ink-accent);
}
.st-grant-warn svg { width: 14px; height: 14px; flex: none; margin-top: 2px; }
/* ONE column, not the form-grid's two. The card sits in the narrow side
   column, and `input[type="date"]` refuses to go below about 194px however
   much `width: 100%` asks — a UA intrinsic width that only `min-width: 0`
   releases. Two of those in a ~177px track is the 1440 overflow the matrix
   reported on every theme and language. Both halves are needed: one column
   for room to breathe, min-width so the control actually takes it. */
.st-grant-meta { margin-top: var(--sp-4); grid-template-columns: 1fr; }
.st-grant-meta input { min-width: 0; }
.st-meta-save { justify-self: start; }
.st-no-admins { margin: 0; }

/* ---------- Org defaults -------------------------------------------------- */

.st-readonly { margin: 0 0 var(--sp-3); color: var(--ink-accent); font-weight: 600; }
.st-rooms-title {
  margin: var(--sp-6) 0 var(--sp-2);
  font-size: var(--fs-300); font-weight: 800;
}
.st-rooms { list-style: none; margin: 0; padding: 0; }
.st-room { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-1) 0; }
.st-room + .st-room { border-top: 1px solid var(--line-soft); }
.st-room-order { display: flex; flex: none; }
/* The shared field rule sets `width: 100%`; in these flex rows the basis is
   what should decide, so it is handed back to flex. */
.st-room-name-input { flex: 1; min-width: 0; width: auto; }
.st-room-active { flex: none; min-height: 34px; }
.st-room-add { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-3); }
.st-room-add input { flex: 1; min-width: 0; width: auto; }

/* ---------- Add-member dialog -------------------------------------------- */

.st-add-hint { margin: 0 0 var(--sp-4); }

@media (max-width: 900px) {
  .settings-grid { grid-template-columns: 1fr; }
  /* The role select and the remove button each take their own line rather
     than crushing the name to two words. */
  .st-member .team-name { flex-basis: 100%; }
  .st-role-select { max-width: none; flex: 1; }

  /* A room row is three fixed controls and a name. At 390 the name was the
     only thing that could give, and it gave down to 130px — technically not
     overflowing, but a field you cannot read the room out of. It takes its
     own line, and the reorder buttons and the active toggle share the next. */
  .st-room { flex-wrap: wrap; row-gap: var(--sp-2); padding-block: var(--sp-2); }
  .st-room-name-input { flex: 1 1 100%; order: -1; }
  .st-room-active { margin-inline-start: auto; }
}
