/* css/table.css — every table: the ten-row wrapper, the element rules, the shared data table and its phone layout.
   One of the files src/index.html links in order; see tokens.css for why. */

/* ==========================================================================
   Tables
   ========================================================================== */
/* position:relative is LOAD-BEARING, not tidiness. `.visually-hidden` is
   position:absolute, and an absolutely positioned descendant is laid out against
   its nearest positioned ancestor — so with a static wrapper the hidden "Actions"
   header in the roster escaped the scroll container entirely and pushed the PAGE
   sideways by 282px on a 390px phone: topbar, notices and every card dragged off
   screen, while the table itself scrolled correctly. Making the wrapper a
   containing block keeps the overflow where overflow-x:auto can catch it. */
/* ==========================================================================
   A TABLE NEVER GROWS PAST TEN ROWS TALL — SITE-WIDE, NOT ONE SCREEN
   ==========================================================================
   Every collection in this product is a table and every table is this wrapper,
   so the standard lands in one place: past ten rows the BODY scrolls inside its
   own box and the page stops getting longer. Before this, a queue of forty rows
   pushed the batches card, the filings card and everything else off the bottom
   of the screen, so a lead admin scrolled past rows she was not reading to
   reach a control she was looking for.

   THIS IS NOT THE 200-ROW RENDER WINDOW AND BOTH APPLY. The window is about how
   many rows exist in the DOM; this is about how much room the ones that exist
   are allowed to take. A 40-row table is one window and four boxes tall.

   THE MAX-HEIGHT IS MEASURED IN JS, in table.js, because ten rows is not a
   constant here: a desktop row is 44px and a phone row is a variable block of
   name, capabilities and a summary line. Measuring the eleventh row's own
   offset is the only answer that is ten rows at BOTH widths. There is no CSS
   fallback height on purpose — a guessed one would be wrong at one width, and
   wrong here means clipping a row in half.
   ========================================================================== */
.table-wrap{overflow:auto;position:relative}

/* THE HEADER STAYS WHILE THE BODY SCROLLS, which is what "the body scrolls
   inside its own box" has to mean — a scrolled table whose column headings have
   gone is a grid of unlabelled values. It needs its own opaque ground or the
   rows read through it, and --field is the card it sits on. Below 640px the
   header is visually-hidden anyway (see the phone rules), so this does nothing
   there and costs nothing. */
.table-wrap thead th{position:sticky;top:0;z-index:2;background:var(--field)}
table{width:100%;border-collapse:collapse;font-size:.9375rem}
caption{text-align:left;margin-bottom:.5rem;color:var(--ink-soft);font-size:.875rem}
th{
  text-align:left;font-family:var(--data);font-size:.6875rem;font-weight:500;
  letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft);
  padding:.5rem .6rem;border-bottom:1px solid var(--rule-strong);white-space:nowrap;
}
td{padding:.65rem .6rem;border-bottom:1px solid var(--rule);vertical-align:top}
tbody tr:last-child td{border-bottom:0}
/* WCAG 2.5.8 Target Size (Minimum) is 24 CSS px and this link measured 23 —
   short by one, on the control every row of every submission table hangs its
   detail view from. A one-pixel miss is the kind that survives review forever
   because nobody can see it; it was found by measuring the rendered box during
   the screenshot run, which is also the only thing that can see it. inline-block
   so the height applies at all: an inline element's height is its line box. */
/* AND IT TRUNCATES VISIBLY, which it did not — CIT-4. The td already carries
   overflow:hidden and text-overflow:ellipsis, and neither reaches INSIDE an
   inline-block child, so an activity title longer than its column was sliced
   mid-word with nothing to say it had been. That is the exact defect CIT-3 fixed
   on .dt-sort and .cell-link and did not fix here, because at the review queue's
   old 29% the title happened to fit; the presence column took four points off
   that width and the defect came out from behind it.
   `max-width:100%` is what gives the ellipsis something to happen at.

   white-space IS DELIBERATELY NOT SET. It inherits: nowrap from .dt-table td on
   a desktop, where the cell is a column and one line with an ellipsis is right;
   normal below 640px, where the row is a block and the title is its heading and
   should wrap rather than be cut. Setting it here would have made the phone's
   heading a single clipped line. */
td .event-name{
  font-weight:600;display:inline-block;min-height:24px;line-height:24px;
  max-width:100%;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;
}
td .sub{display:block;color:var(--ink-soft);font-size:.875rem}

/* A reviewer's own row: visible, inert, dashed. It carries no controls at all —
   the actionable set is filtered in the query, not here. */
tr.own-entry td{background:var(--paper)}
tr.own-entry{outline:1px dashed var(--rule-strong);outline-offset:-1px}

/* A row for somebody who cannot sign in. Softened rather than struck through or
   hidden: she is still on the roster, still has history, and giving access back
   is one of the actions on this very row — a row you have to un-hide to act on
   is a row the filter should have handled instead. */
tr.dt-inactive td{color:var(--ink-soft)}
tr.dt-inactive td:first-child strong{font-weight:400}

.empty{
  padding:var(--gap-lg);text-align:center;color:var(--ink-soft);
  font-family:var(--legible);
}

/* ==========================================================================
   A table on a phone — and what replaced the answer that used to be here
   ==========================================================================
   THE PROBLEM IS UNCHANGED. .table-wrap scrolls a wide table sideways rather
   than letting it push the page, which is right and stays. But a five-column
   table on a 390px phone is 700px wide, so the stamp and the hours are never on
   screen at the same time — and the reader has to remember a status while
   scrolling to find the number it belongs to. Dropping the wrap is worse: every
   cell wraps to three lines, the row rules vanish into the noise, and nothing
   tells you where one row ends and the next begins.

   THE ANSWER USED TO BE `table.stacks`: display:block on everything, each cell
   captioned from a data-label stamped on by stackable(). It worked, and it cost
   the table's semantics — `display:block` on a <table> removes them in several
   screen readers, which is the documented cost of that pattern.

   The shared data table answers it differently and the whole product is on the
   shared data table now, so the stacks rules are deleted rather than kept for a
   caller that no longer exists. See .dt-wide-only / .dt-narrow-only below: the
   columns that do not fit are dropped and a summary line takes their place, the
   element stays a <table> and keeps its semantics, and each fact appears once at
   either width rather than twice.
   ========================================================================== */
/* ==========================================================================
   The shared data table
   ==========================================================================
   Layout for src/ui/table.js, and the shape is docs/mockups/roster-shape.html,
   agreed with Parker. What that mock settles, and what this has to keep:

     * ONE PERSON IS ONE LINE, roughly a line of text tall. A roster of 350 is
       something you scan, not something you scroll through buttons on.
     * ROW ACTIONS LIVE BEHIND ONE "⋯". Every action is still available and
       still bulkable; they are simply not all shouting from all 350 rows.
     * THE BULK BAR IS INVISIBLE UNTIL SOMETHING IS TICKED, and obvious the
       moment one thing is. Ticking rows is how work gets done at this volume.
     * SEARCH IS ALWAYS THERE, FILTERS ARE BEHIND A BUTTON, shut by default.
       Searching is every visit; filtering is a particular question.
     * NOTHING SCROLLS SIDEWAYS AT 1280. table-layout:fixed with per-column
       widths is what guarantees that: a long email is clipped inside its own
       cell rather than widening the table.
     * ON A PHONE A ROW IS A BLOCK — name, email, capabilities, then one muted
       line for the rest — and the bulk bar sticks to the bottom.

   It reuses the existing .btn and .card rules rather than restyling them; a
   table that looked like a different product would be a second design system
   arriving by the back door.
   ========================================================================== */
.dt{position:relative}

/* ---- toolbar: search, count, filters, columns, download ------------------ */
.dt-controls{
  display:flex;gap:15px;align-items:center;flex-wrap:wrap;
  padding:0 0 .7rem;margin-bottom:20px;border-bottom:1px solid var(--rule);
}
/* Capped at 420px — USWDS: a control's width hints at its answer, and no search
   here is a thousand pixels long — CIT-15. The phone rule below still wins. */
.dt-search{flex:0 1 420px;min-width:0}
.dt-search input{width:100%;max-width:420px}
/* The count is the sentence every bulk action is about — "40 of 517" is the
   only way to tell a filter that matched everything from one that matched
   nothing — so it is type beside the box it describes, not a badge. */
.dt-count{
  font-family:var(--data);font-size:.8125rem;color:var(--ink-soft);
  white-space:nowrap;margin:0;
}
/* THE PHONE'S SELECT-ALL, and only the phone's. Above the breakpoint the column
   header carries it and a second copy would be two controls for one thing. */
.dt-selectall{display:none}

/* ---- filters: a band, only when opened ---------------------------------- */
.dt-filters{
  display:flex;flex-wrap:wrap;gap:.6rem .8rem;align-items:end;
  padding:.7rem .8rem;margin-bottom:.7rem;
  background:var(--paper);border:1px solid var(--rule);border-radius:var(--r-sm);
}
.dt-filter{display:flex;flex-direction:column;gap:.15rem}
.dt-filter .label{margin:0}
.dt-filter input,.dt-filter select{min-width:9rem}

/* ---- bulk bar: only when something is selected --------------------------- */
.dt-bulk{
  display:flex;flex-wrap:wrap;gap:.4rem;align-items:center;
  padding:.6rem .8rem;margin-bottom:.7rem;
  background:var(--approved-wash);
  border:1px solid var(--approved);border-radius:var(--r-sm);
}
.dt-bulk-count{margin:0;font-weight:700;white-space:nowrap}
.dt-bulk .actions{display:flex;flex-wrap:wrap;gap:.35rem;margin:0}
.dt-bulk .btn{white-space:nowrap;background:var(--field)}

/* ==========================================================================
   THE TABLE, AND THE FOUR THINGS CIT-3 HAD TO MAKE TRUE OF IT
   ==========================================================================
   What was wrong, at the DEFAULT — which is the case that matters most, because
   nobody has to do anything to hit it:

     * the review queue's CERTIFICATE cell rendered `IMG_9447.jpe`, cut
       mid-word, with no ellipsis and nothing to say anything had been cut;
     * turn every column on and the columns SQUEEZED instead of the table
       growing, so headers landed on top of each other — "CREDITS" and "WAIVER"
       reading as `CRDWASVER` — and the last columns could not be reached,
       because the table never got wider than its box and there was nothing to
       scroll.

   Both live in ui/table.js, so both were every table in the app.

   WHAT MUST HOLD, WITH ALL COLUMNS ON AND WITH NONE:

     1. a cell never runs underneath the actions column. That column is
        RESERVED SPACE — pinned to the right edge, with its own opaque ground —
        rather than something content flows beneath;
     2. content that does not fit is truncated VISIBLY. Silently clipping a
        filename is worse than showing eight characters of it;
     3. every column that is switched on can be scrolled to and read;
     4. a header fits, truncates, or wraps. It never overlaps its neighbour.

   THE MECHANISM BEHIND (2) AND (4) IS ONE FACT, AND IT IS NOT OBVIOUS:
   `text-overflow: ellipsis` DOES NOT REACH AN INLINE-FLEX CHILD. A cell with
   overflow:hidden and text-overflow:ellipsis ellipsises its OWN anonymous text;
   a flex container inside it is a separate formatting context and its text is
   the flex container's, so the cell clips the box and no ellipsis is ever
   drawn. Both `.cell-link` and `.dt-sort` were made inline-flex to reach the
   24px tap target (WCAG 2.5.8), and that is why the certificate name was sliced
   and why two headers could sit on top of each other. Truncating the CELL is
   not enough — each of them has to truncate ITSELF, which is what the two rules
   further down do. The tap targets are kept: line-height carries the height
   that align-items used to.
   ========================================================================== */
/* table-layout:fixed is what makes the screens' percentage widths the actual
   widths: a long email is clipped inside its own cell rather than widening the
   table. `--dt-min` is set per table by ui/table.js from how many columns are on
   — see MIN_COLUMN_PX there — and it is what lets a table with everything
   switched on grow past its box and scroll instead of squeezing. It is 0 in
   effect for the default sets, so nothing about them changes. */
.dt-table{table-layout:fixed;width:100%;min-width:var(--dt-min,0)}
.dt-table td{
  /* 15px cell padding on the 5px rhythm — CIT-15. 44px stays as a floor. */
  height:44px;padding:15px 12px;vertical-align:middle;font-size:.9688rem;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
/* overflow:hidden ON THE HEADER TOO, which it did not have. Without it a header
   longer than its column simply drew over the next one — the table's own
   `white-space:nowrap` guaranteeing that it would. */
.dt-table th{padding:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dt-table td .sub{display:inline;margin-left:.45rem;font-size:.8125rem}
.dt-table tr.dt-picked td{background:var(--approved-wash)}
.dt-table td.dt-pick,.dt-table th.dt-pick{overflow:visible;text-align:left}

/* ---- the actions column is RESERVED SPACE ------------------------------- */
/* Pinned to the right edge in both the header and the body, with its own ground,
   so a cell that overflows scrolls UNDER it rather than through it and the "⋯"
   is in the same place whatever the horizontal scroll is doing. The header cell
   is already sticky to the top (see .table-wrap thead th); sticking in both axes
   at once is one element with two offsets, which is why they share a class now.

   THE SHADOW IS A SIGNAL, NOT DECORATION: it is the only thing that says content
   continues underneath this column. -9px spread keeps it to a hairline at the
   left edge rather than a halo.

   overflow:visible stays. The kebab is a 30px control in a 44px column and has
   nothing to clip; what it must not do is let a NEIGHBOUR's text through, and
   that is the neighbour's own overflow:hidden. */
.dt-actions-cell{text-align:right;overflow:visible!important}
.dt-table td.dt-actions-cell,
.dt-table th.dt-actions-cell{
  position:sticky;right:0;background:var(--field);
  box-shadow:-9px 0 9px -9px rgba(27,42,58,.28);
}
.dt-table td.dt-actions-cell{z-index:1}
/* Above the body's sticky cells AND above the sticky header row, because it is
   the one cell that is both. */
.table-wrap thead th.dt-actions-cell{z-index:3}
/* The picked row's wash has to reach the pinned cell too, or a selected row has
   one square of white at the end of it. */
.dt-table tr.dt-picked td.dt-actions-cell{background:var(--approved-wash)}
/* Same, for the review queue's own-entry treatment, which paints --paper. */
.dt-table tr.own-entry td.dt-actions-cell{background:var(--paper)}
.dt-kebab{
  width:30px;height:30px;line-height:1;font-size:1.05rem;
  border:1px solid transparent;background:none;border-radius:var(--r-sm);
  color:var(--ink-soft);cursor:pointer;
}
.dt-kebab:hover{background:var(--paper);border-color:var(--rule-strong);color:var(--ink)}

/* ---- the row menu -------------------------------------------------------- */
/* A sibling of .table-wrap, not a child of the row: .table-wrap is a scroll
   container and would clip anything positioned inside it. placeMenu() sets
   top/left from the button's own rect. */
.dt-menu{
  position:absolute;z-index:50;min-width:14rem;
  background:var(--field);border:1px solid var(--rule-strong);
  border-radius:var(--r);box-shadow:var(--sh-lg);padding:.3rem;
}
.dt-menu-who{margin:.25rem .5rem .35rem;color:var(--ink-soft)}
.dt-menu-item{
  display:block;width:100%;text-align:left;background:none;border:0;
  font:inherit;font-size:.9375rem;padding:.45rem .55rem;
  border-radius:var(--r-sm);cursor:pointer;color:var(--ink);
}
.dt-menu-item:hover{background:var(--paper)}
.dt-menu-item[disabled]{color:var(--ink-soft);cursor:default}
.dt-menu-danger{color:var(--rejected)}
.dt-menu hr{border:0;border-top:1px solid var(--rule);margin:.3rem 0}
.dt-menu .btn{display:block;width:100%;text-align:left;border:0;background:none;padding:.45rem .55rem}
.dt-menu-backdrop{position:fixed;inset:0;z-index:40}

/* ---- the rest ------------------------------------------------------------ */
/* INLINE-BLOCK, NOT INLINE-FLEX, AND THE SWAP IS THE HEADER HALF OF THE CIT-3
   FIX. `text-overflow:ellipsis` on the <th> cannot reach the anonymous text
   inside a flex container, so a header longer than its column was sliced and —
   with the th having no overflow of its own — drew straight over its neighbour.
   `CRDWASVER` was "CREDITS" and "WAIVER" doing exactly that.

   THE TAP TARGET IS NOT GIVEN UP (WCAG 2.5.8). It was inline-flex only to get
   `align-items:center` to make the box 24px tall; left as a bare line box it
   measured 17px, which the screenshot pass caught. `line-height:24px` on an
   inline-block does the same job without a formatting context, and
   vertical-align:middle keeps the sort arrow on the text's own line. */
.dt-sort{
  background:none;border:0;padding:0;font:inherit;color:inherit;
  cursor:pointer;text-align:inherit;letter-spacing:inherit;text-transform:inherit;
  display:inline-block;max-width:100%;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  line-height:24px;min-height:24px;vertical-align:middle;
}
.dt-arrow{font-size:.7em}
.dt-columns{position:relative}
.dt-columns summary{
  cursor:pointer;min-height:32px;display:inline-flex;align-items:center;
  padding:.15rem .7rem;border:1px solid var(--rule-strong);border-radius:var(--r-sm);
  font-size:.875rem;list-style:none;background:var(--field);
}
.dt-columns summary::-webkit-details-marker{display:none}
/* Floated over the table rather than pushing it down, so opening it to hide one
   column does not move the row you were looking at. */
.dt-columns[open] .dt-columns-list{
  position:absolute;z-index:30;top:calc(100% + .3rem);right:0;min-width:12rem;
  background:var(--field);box-shadow:var(--sh);
  border:1px solid var(--rule-strong);
  flex-direction:column;gap:.25rem;
}
.dt-columns-list{
  display:flex;flex-wrap:wrap;gap:.3rem .9rem;padding:.5rem .7rem;margin-top:.4rem;
  background:var(--paper);border:1px solid var(--rule);border-radius:var(--r-sm);
}
.dt-prompt{margin-bottom:.7rem}
.dt-results{margin:.4rem 0 .6rem;padding-left:1.1rem}
.dt-more{padding:.7rem 0 0;border-top:1px solid var(--rule);margin-top:.5rem}

/* A link inside a table cell. Inline it is as tall as the line box — 23px, one
   short of the 24px minimum tap target (WCAG 2.5.8).

   INLINE-BLOCK AND TRUNCATING ITSELF — the cell half of the CIT-3 fix, and the
   same mechanism as .dt-sort above. The td already has overflow:hidden and
   text-overflow:ellipsis; neither reaches inside a flex container, so a
   certificate called `ell-workshop-certificate-2026.pdf` was clipped mid-word
   with nothing to say it had been. `max-width:100%` is what gives the ellipsis
   something to happen at, and line-height keeps the 24px the flex box was there
   for. */
.cell-link{
  display:inline-block;max-width:100%;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  line-height:24px;min-height:24px;vertical-align:middle;
}

/* Capabilities as chips. Three independent switches read as three things on one
   line; a sentence would read as one thing, which is what a role picker looks
   like and is the conflation the whole schema is arranged to avoid. */
.dt-chips{display:inline-flex;gap:.25rem;align-items:center}
.dt-chip{
  font-size:.6875rem;font-family:var(--data);letter-spacing:.02em;line-height:1.5;
  background:var(--paper);border:1px solid var(--rule);border-radius:var(--r-xs);
  padding:.05rem .35rem;color:var(--ink-soft);white-space:nowrap;
}
.dt-chip--adm{background:var(--approved-wash);border-color:var(--approved);color:var(--approved)}
.dt-em{color:var(--ink-soft);font-size:.875rem}
/* A "no" that means something is missing. Colour AND the word (WCAG 1.4.1). */
.dt-no{color:var(--rejected)}


/* Columns that exist at one width and not the other. The phone summary line is
   the only one so far: it folds the columns hidden below into one muted line. */
.dt-narrow-only{display:none}

/* Somebody who cannot sign in. Softened rather than struck through or hidden:
   she is still on the roster, still has history, and giving access back is one
   of the actions on this very row. */
.dt-table tr.dt-inactive td{color:var(--ink-soft)}
.dt-table tr.dt-inactive td:nth-child(2) strong{font-weight:400}

/* THE PHONE, AND IT IS A DIFFERENT LAYOUT RATHER THAN A NARROWER ONE.
   Six columns at 390px is not a table, so a row becomes a block: checkbox at
   the left, "⋯" at the right, then name, email, capabilities, and one muted
   line carrying whatever the columns above would have said.

   640px, not 600px, because that is where the mock puts it and because the
   narrowest tablet is still a worse table than it is a list. */
@media (max-width:640px){
  .dt-controls{gap:.4rem}
  .dt-search{flex:1 0 100%}
  .dt-count{order:2}
  /* THE PHONE'S SELECT-ALL, TURNED BACK ON. It is display:none above this width
     because the column header carries it there; below it the header is off
     screen and this is the only one there is. Setting order and flex without
     setting display left it hidden at BOTH widths, which is the same defect as
     before with a different cause — a screenshot run caught it the same way, by
     being unable to click it. */
  .dt-selectall{
    display:flex;align-items:center;gap:.5rem;order:5;flex:1 0 100%;
    min-height:44px;font-family:var(--legible);color:var(--ink-soft);
  }
  .dt-selectall input{flex:none}

  .dt-table,.dt-table tbody,.dt-table tr,.dt-table td{display:block;width:auto}
  /* THE DESKTOP FLOOR IS SWITCHED OFF HERE, and it has to be. ui/table.js sets
     --dt-min from how many COLUMNS are on, which measures a layout that does not
     exist below this breakpoint — a row is a block and the columns are stacked
     inside it. Left on, a roster would claim ~900px on a 390px phone and push the
     whole page sideways, which is the exact failure the .table-wrap note further
     up records from last time.

     min-width IS OVERRIDDEN, NOT THE CUSTOM PROPERTY, and the difference is not
     style: --dt-min arrives as an INLINE style on the <table>, and an inline
     declaration beats any stylesheet rule short of !important. min-width is only
     ever set in this file, so a later rule at equal specificity simply wins. */
  .dt-table{min-width:0}
  .dt-table colgroup{display:none}
  .dt-table thead{
    position:absolute;width:1px;height:1px;margin:-1px;padding:0;
    overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
  }
  /* 2.6rem ON THE RIGHT, DOWN FROM 2.9 — measured off the 390 ledger shot. The
     right inset exists to keep row content clear of the absolutely positioned
     "⋯", which is 34px at right:.3rem and therefore needs 39; 2.9rem reserved
     46.4 and the five pixels of that were the difference between a ledger row's
     hours figure sitting beside its date and dropping onto a line of its own.
     41.6 still clears the control. */
  .dt-table tr{
    position:relative;padding:.6rem 2.6rem .6rem 2.2rem;
    border-bottom:1px solid var(--rule);
  }
  .dt-table tr.dt-picked{background:var(--approved-wash)}
  .dt-table tr.dt-picked td{background:none}
  .dt-table td{
    height:auto;padding:0;border:0;white-space:normal;overflow:visible;
    text-overflow:clip;
  }
  .dt-table td.dt-pick{position:absolute;left:.6rem;top:.85rem;width:auto}
  /* absolute, NOT sticky, and with the pinned column's ground and edge shadow
     taken back off. Both exist above the breakpoint to say "this column is
     reserved space that content scrolls under"; here there are no columns and
     nothing scrolls sideways, so they would be a white box with a shadow floating
     in the corner of every row. */
  .dt-table td.dt-actions-cell,
  .dt-table tr.dt-picked td.dt-actions-cell,
  .dt-table tr.own-entry td.dt-actions-cell{
    position:absolute;right:.3rem;top:.55rem;width:auto;
    background:none;box-shadow:none;
  }
  .dt-kebab{width:34px;height:34px}

  /* The first data column is the name and reads as the heading of the block. */
  .dt-table td:nth-of-type(2){font-size:1rem;line-height:1.3}
  .dt-table td .sub{display:block;margin:0;color:var(--ink-soft)}

  /* Everything the summary line covers goes away, and the summary line arrives.
     td.dt-wide-only, not .dt-wide-only: the rule three lines up that turns every
     cell into a block is (0,1,1) and would out-specify a bare class. It did, and
     the phone printed every fact twice — once as its own line and again in the
     summary line meant to replace them. */
  .dt-table td.dt-wide-only{display:none}
  /* ONE LINE, TRUNCATED, RATHER THAN THREE — CIT-3. This is the `a · b · c` meta
     line under every phone row, and at 390px "Category E · Mandatory areas:
     suicide prevention, mental illness · Not yet reported to the state" wrapped to
     three, which made a three-line row a six-line one and was the ugliest thing on
     the phone. Cut visibly, per the rule the table now follows everywhere: an
     ellipsis says there is more, and the facts themselves are columns on a wider
     screen and in the download. */
  .dt-table td.dt-narrow-only{
    display:block;margin-top:.3rem;font-size:.8125rem;color:var(--ink-soft);
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;
  }

  /* ==========================================================================
     A ROW THAT IS A BLOCK HAS A SHAPE — CIT-3
     ==========================================================================
     The block used to be the COLUMN ORDER stacked, which is a decision about a
     desktop table applied to a phone. On the ledger that meant the status stamp
     on one line, the date on another, the activity on a third, the hours on a
     fourth and then the meta line — five paragraphs per entry on the screen a
     teacher opens most, almost always on a phone.

     A row is a flex container here so that two opt-in cell flags can place
     themselves: `dt-phone-title` is what the block is about and goes first,
     `dt-phone-inline` joins one strip beneath it. Everything else keeps a full
     width line of its own, IN DOM ORDER, which is exactly what display:block gave
     it — so a table that declares neither flag renders identically to before and
     nothing else in the product had to be touched.

     `order` is what makes the title first even though the activity column is
     third; reordering the columns instead would have moved them on the desktop
     too, where the order is right.
     ========================================================================== */
  .dt-table tr{display:flex;flex-wrap:wrap;align-items:baseline}
  .dt-table td{order:2;flex:1 0 100%}
  .dt-table td.dt-phone-title{order:0;flex:1 0 100%;font-size:1rem;line-height:1.3}
  /* auto basis and a gap to its right, so three short facts sit side by side and
     wrap as a group if the reader's font size makes them too wide to.

     .875rem AND A .45rem GAP, BOTH MEASURED OFF THE 390 SCREENSHOT. At the table's
     own .9375rem the ledger's strip came to about 272px against 270px of room
     between the tick box and the "⋯", so the hours figure wrapped onto a line of
     its own — which is most of what this whole block exists to stop. Ten pixels
     back from the type and four from each gap is the difference, and the strip is
     three short facts read together rather than three values in a column, so a
     step down in size is what it should have been anyway. It still wraps rather
     than clipping if a reader's own font size takes it past the width, which is
     the right failure (WCAG 1.4.4). */
  .dt-table td.dt-phone-inline{
    order:1;flex:0 0 auto;width:auto;margin:.4rem .5rem 0 0;font-size:.875rem;
  }
  /* AND THE STRIP IS NOT A COLUMN, SO IT IS NOT --data. The monospace rule in the
     brand spec is "hours are columns and columns must align", and it is right
     everywhere it applies — but below this breakpoint there are no columns, and a
     390px row cannot afford the width tabular figures cost. "11 Feb 2026" is 92px
     in mono and 76 in the interface face, which is the sixteen pixels between the
     hours figure sitting beside the date and dropping onto a line of its own. The
     screenshot is what measured it; two earlier attempts (a smaller size, a
     tighter gap) each recovered about five. */
  .dt-table td.dt-phone-inline .mono{font-family:var(--ui);letter-spacing:normal}
  /* The num alignment is a column property and there are no columns here — left
     as it was, a right-aligned "6.00" would sit alone at the far edge of the
     strip it belongs to. */
  .dt-table td.dt-phone-inline.num{text-align:left}
  /* Out of flow, so out of the ordering too — stated rather than left to be
     rediscovered, because an absolutely positioned flex child ignores `order` and
     a reader checking why the checkbox is not first deserves the answer here. */
  .dt-table td.dt-pick,.dt-table td.dt-actions-cell{flex:none}

  .dt-filters{gap:.6rem}
  .dt-filter{flex:1 1 46%}
  .dt-filter input,.dt-filter select{width:100%;min-width:0}

  /* THE BULK BAR STICKS TO THE BOTTOM. On a phone the selection is made by
     scrolling, so a bar at the top is a bar you have scrolled away from by the
     time you want it. */
  .dt-bulk{
    position:sticky;bottom:0;z-index:5;margin:0 0 .5rem;
    box-shadow:0 -6px 18px rgba(27,42,58,.12);
  }
  /* One action per line on a phone. Two 46% columns put "Take 5 off the
     committee" against "Stop 27 filing hours" and clipped both. */
  .dt-bulk{gap:.35rem}
  .dt-bulk .actions{flex-direction:column;align-items:stretch;width:100%}
  .dt-bulk .btn{width:100%;text-align:center}
  .dt-bulk-count{flex:1 0 100%}

  .dt-menu{left:.6rem!important;right:.6rem!important;min-width:0;width:auto}
  .dt-menu-item{padding:.6rem .7rem;font-size:1rem}
}

/* `table.stacks` — about fifty lines of card treatment for a hand-built table —
   stood here and is gone with Part 4b. Its last three callers (the ledger, the
   import preview, the import report) are the shared data table now, and .dt has
   its own phone treatment: .dt-wide-only drops the columns that do not fit and
   .dt-narrow-only brings a summary line in their place, so each fact is shown
   once at either width rather than twice.

   Deleted rather than left: `stackable()` went with it, and a mutation pattern
   guarding a function nothing calls is a pattern that reports coverage it does
   not have. The proof this changed nothing visible is the Part 0 screenshot set
   at 390, which is where the pattern's own defects were found in the first
   place. */


/* A held row on the filing desk — CIT-5b. The reason under the activity wraps
   on a desktop, where every other cell is one line: the sentence IS the
   information, and an ellipsis would cut off the rule it names. On a phone
   the word "Held" is wider than the checkbox inset it stands in. */
td .dt-held-why{white-space:normal;margin:.3rem 0 0}
.dt-table td.dt-held{font-size:.8125rem}
@media (max-width:640px){
  .dt-table tr:has(> td.dt-held){padding-left:3.1rem}
}
