/* =============================================================================
   Ledgerline & Co. — Timesheet  (DEMO FORK)
   app/static/app.css

   Design system from the Claude Design pass (2026-08-03), ADAPTED to this
   repo's actual markup. The tokens, type scale, spacing scale, colour
   application, table treatment, banner glyphs, tab strip, chip and login
   designs are as delivered. Everything marked `ADAPTED:` is a place where the
   delivered rule collided with markup the designer could not see — each one
   would have been a visible break, so each is fixed here with its reason.

   Plain CSS. No build step, no framework, no external request. Self-hosted
   fonts only. Light theme only. WCAG AA throughout.

   Every rule consumes a token from the :root block — reskin for the next firm
   by editing that block alone.
   ============================================================================= */

/* ---------------------------------------------------------------------------
   FONTS — self-hosted, subset to Latin.
   Mulish = UI + every number.  Philosopher = brand + page titles, never a
   figure (its "1" is 305u against 584u for "0" — ragged in any column).
   font-display:swap so a slow disk never blocks the hot path.

   ADAPTED: the delivered file pointed at `fonts/mulish-latin-400.woff2`.
   The real assets live in `static/brand/` under different names, and this
   stylesheet is served from `/static/`, so every @font-face would have 404'd
   and the whole product would have rendered in a system fallback — silently,
   because a missing font is not an error.
   --------------------------------------------------------------------------- */
/* SITE AESTHETIC (2026-10-03). The faces are now the ones ayushmutha.in uses: Figtree for the
   UI and every number (it has tabular figures), IBM Plex Mono for the small uppercase labels.
   Mulish and Philosopher are still in brand/ but nothing references them. */
@font-face{
  font-family:'Figtree'; font-style:normal; font-weight:400;
  font-display:swap; src:url('brand/figtree-400.woff2') format('woff2');
}
@font-face{
  font-family:'Figtree'; font-style:normal; font-weight:500;
  font-display:swap; src:url('brand/figtree-500.woff2') format('woff2');
}
@font-face{
  font-family:'Figtree'; font-style:normal; font-weight:600;
  font-display:swap; src:url('brand/figtree-600.woff2') format('woff2');
}
@font-face{
  font-family:'Figtree'; font-style:normal; font-weight:700;
  font-display:swap; src:url('brand/figtree-700.woff2') format('woff2');
}
@font-face{
  font-family:'Figtree'; font-style:normal; font-weight:800;
  font-display:swap; src:url('brand/figtree-800.woff2') format('woff2');
}
@font-face{
  font-family:'IBM Plex Mono'; font-style:normal; font-weight:500;
  font-display:swap; src:url('brand/plexmono-500.woff2') format('woff2');
}

/* ---------------------------------------------------------------------------
   DESIGN TOKENS
   --------------------------------------------------------------------------- */
:root{
  /* SITE AESTHETIC (2026-10-03). The palette below is ayushmutha.in's: the sage desk, paper
     sheets, navy ink and the rust accent. The job split is kept - navy READS (links, focus,
     selected fills), rust ACTS (primary buttons, the active nav item). The older comments in
     this block describe the indigo/rust set this replaced; the reasoning about the split still
     holds, the hexes in them do not. */
  /* --- Palette (DEMO FORK, 2026-08-29) ---
     The five hexes here used to be the CLIENT'S OWN BRAND COLOURS. That made the
     palette an identity carrier as surely as the wordmark was: navy + hooker's green
     on warm grey IS their look, and a demo wearing it is still their software with
     the labels changed.
     This set is deliberately a different system, not a nudge. Their split was green
     for ACTION and blue for READING; the split-by-job idea is sound and is kept, but
     both hues change and the neutrals go cool instead of warm — indigo reads, rust
     acts. Every pair below is MEASURED; the ratios in these comments are real. */
  --brand-indigo:#1E2833;    /* reads */
  --brand-rust:#B4441F;      /* acts  */
  --brand-ink:#1E2833;       /* deep indigo-black, not a neutral black */
  --brand-line:#CBC8BE;
  --brand-mist:#E3E7E0;
  /* Kept as an alias: `.chip-dark` and `.strip .today` reference it by name and the
     token is about "the darkest ink", not about a brand hue. */
  --brand-black:#1E2833;

  /* --- Ink. Cool-derived from --brand-ink so text sits in the palette's family. */
  --ink:#1E2833;             /* body      · 17.0:1 on surface */
  --ink-2:#3A4450;           /* secondary ·  7.4:1 */
  --ink-3:#56606A;           /* muted     ·  5.3:1 — still AA */
  --ink-on-accent:#FFFFFF;   /* 7.9:1 on --accent, 5.2:1 on --green */

  /* --- Surfaces. Cool grey page, white cards — same reasoning as before: a wall of
         client names should never glare. */
  --bg:#D9DED8;
  --surface:#FBF9F4;
  --surface-2:#F1EEE6;

  --line:#E2DED3;
  --line-2:#CBC8BE;

  /* --- Accent (indigo): the READING layer — links, focus rings, selected fills. */
  --accent:#1E2833;
  --accent-2:#2C3A47;
  --accent-bg:#E6EAE4;       /* 6.7:1 for --accent text on it */

  /* --- The ACTION layer — primary buttons, active nav pill, header and footer
         rules. Named `--green` ONLY because forty-odd rules in this stylesheet
         already reference that token; renaming would be a large diff for no
         behavioural gain. The comment is here so nobody wonders why `--green` is
         orange. Rust was picked against their green precisely so the two systems
         cannot be mistaken for one another.
         White on it measures 5.2:1, on the hover 7.4:1. The focus ring stays indigo
         so it can never be confused with a primary button's own fill. --- */
  --green:#B4441F;
  --green-2:#963818;
  --green-bg:#FBE9E1;        /* 6.4:1 for --green-2 text on it */

  /* --- Status. Semantic only. `--ok` is a true green because a success state that
         is not green fails the one job it has — and green is no longer a brand
         colour here, which is what makes that free. Darkened from #15803D, which
         measured 4.41:1 on its own tint and missed AA. --- */
  --ok:#146B34;      --ok-bg:#E8F3EC;   /* 5.8:1 */
  --warn:#8A5A1E;    --warn-bg:#F7EBD6; /* 5.1:1 */
  --bad:#9A2C2C;     --bad-bg:#F5E7E7;  /* 6.3:1 */
  --info:#1E2833;    --info-bg:#E6EAE4;

  /* Footer text on --brand-ink. The old value cleared AA by 0.08 and carried a
     "do NOT lighten it" warning; in this role on this darker footer the same text
     measures 9.9:1, so the warning is retired rather than inherited. */
  --foot-ink:#C9CBC3;

  /* --- Type. The display face was Philosopher, a traditional book serif, which
         together with the serif wordmark is what made the app read as an old
         practice's internal tool. This is a PRODUCT, so the display layer is the UI
         sans at heavy weight with tight tracking. No new font files are needed:
         Philosopher's woff2 is simply no longer referenced by the display token. */
  --font-ui:'Figtree', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  --font-brand:'Figtree', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  --font-mono:'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --fs-100:0.75rem;   /* 12 — tags, hints */
  --fs-200:0.8125rem; /* 13 — table body, meta */
  --fs-300:0.875rem;  /* 14 — dense controls */
  --fs-400:1rem;      /* 16 — body + all inputs (16 stops iOS focus-zoom) */
  --fs-500:1.125rem;  /* 18 — sub-headings */
  --fs-600:1.5rem;    /* 24 — card titles */
  --fs-700:2.125rem;  /* 34 — page title */
  --fs-800:2.75rem;   /* 44 — login brand */
  --lh:1.5;
  --lh-tight:1.25;

  /* --- Spacing scale (4px base). --- */
  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px;
  --sp-5:20px; --sp-6:24px; --sp-7:32px; --sp-8:40px;

  /* --- Radii — small. CA software should read precise, not soft/consumer. */
  --radius:10px; --radius-sm:6px; --radius-lg:14px; --radius-pill:999px;

  --bw-1:1px; --bw-2:2px;

  /* --- Shadows — whisper-quiet; elevation comes from lines, not blur. */
  --shadow-1:0 1px 2px rgba(30,40,51,.06), 0 1px 1px rgba(30,40,51,.04);
  --shadow-2:0 4px 14px rgba(30,40,51,.10), 0 1px 3px rgba(30,40,51,.06);

  --tap:44px;
}

/* ---------------------------------------------------------------------------
   RESET / BASE
   --------------------------------------------------------------------------- */
*,*::before,*::after{ box-sizing:border-box; }
html{ -webkit-text-size-adjust:100%; }
body{
  margin:0;
  font-family:var(--font-ui);
  font-size:var(--fs-400);
  line-height:var(--lh);
  color:var(--ink);
  background:var(--bg);
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  /* Flex column pins the footer to the bottom on short pages (the login page)
     instead of floating it halfway up. Load-bearing — see .wrap / .foot below. */
  display:flex;
  flex-direction:column;
  min-height:100vh;
}
/* Numbers are load-bearing here. tabular-nums is redundant with Mulish but
   guarantees aligned columns if the font ever falls back to a system face. */
.num,td.num,th.num,.stat .v,.strip div,.ta-item .uses,input,table{
  font-variant-numeric:tabular-nums;
}

h1,h2,h3,h4{ margin:0 0 var(--sp-3); line-height:var(--lh-tight); font-weight:700; color:var(--ink); }
h1{ font-size:var(--fs-700); }
h2{ font-size:var(--fs-600); }
h3{ font-size:var(--fs-500); }
h4{ font-size:var(--fs-400); }
p{ margin:0 0 var(--sp-3); }
a{ color:var(--accent); text-decoration:underline; text-underline-offset:2px; text-decoration-thickness:1px; }
a:hover{ color:var(--accent-2); }
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,
textarea:focus-visible,summary:focus-visible,[tabindex]:focus-visible{
  outline:var(--bw-2) solid var(--accent); outline-offset:2px; border-radius:var(--radius-sm);
}
::selection{ background:var(--accent-bg); }

/* ADAPTED — where Philosopher stops.
   The delivered CSS confined the brand serif to `.nav-brand` / `.login-brand`.
   In this app both of those contain an <img>, not text, so Philosopher would
   have rendered NOWHERE — two woff2 files shipped and self-hosted for zero
   visible use, and the brand gone from the product entirely.
   It is restored on `h1` only, because every h1 here is a fixed English page
   title we control and none carries a digit: "New entry", "My week",
   "My team", "Reports", "Master data", "Security", "Timesheet".
   The designer's rule still holds where it matters: h2 stays Mulish because
   those DO carry figures ("Submission compliance — 1–15 Aug 2026"), and the one
   h1 that renders data — the drilldown key, which can be a month like
   "2026-07" — opts out with `.data-title`. */
/* DEMO FORK: the display face is the UI sans now, so it needs weight and negative
   tracking to still read as a display layer — at 400/normal it is indistinguishable
   from body copy and the page loses its hierarchy entirely. */
h1,.nav-brand,.login-brand,.brand{ font-family:var(--font-brand); font-weight:800; letter-spacing:-.02em; }
h1{ color:var(--brand-ink); letter-spacing:-.022em; }
h1.data-title{ font-family:var(--font-ui); color:var(--ink); letter-spacing:0; }

/* ---------------------------------------------------------------------------
   LAYOUT SHELL — .wrap / .nav / .foot
   --------------------------------------------------------------------------- */
/* `min-width:0` + `width:100%` are load-bearing, not defensive. A flex item
   defaults to `min-width:auto`, so it refuses to shrink below its widest
   content — which once made .wrap 438px wide inside a 375px phone and brought
   back the horizontal page scroll item 14 exists to prevent. Wide tables
   scroll inside their own .scroll-x; the PAGE must not. Asserted by
   test_flex_body_children_can_shrink. */
.wrap{
  max-width:960px; margin:0 auto; padding:var(--sp-6) var(--sp-4);
  flex:1 0 auto; min-width:0; width:100%;
}
.wrap.wide{ max-width:1240px; }   /* reports, master data and the team view */

/* --- THE RAIL (2026-08-29, demo fork) -----------------------------------------
   This was a horizontal bar. Nine text links across the top of a 375px phone is what
   the 2026-08-26 note under `.nav-links` in the mobile block is about: the bar ran to
   ~500px and took the whole page sideways with it, and the fix was to let it wrap to
   three rows. A rail does not have that failure mode, because a tenth destination
   costs 46px of a column nobody is short of rather than a row of the page.

   84px, icon over caption. The chosen design was 58px and icon-only; that drops the
   leave count, which exists precisely so an approver learns about a request without
   being on the leave page, and it makes nine destinations nine hovers. 26px is a
   cheap price for keeping both. --- */
.nav{
  display:flex; flex-direction:column; align-items:stretch; gap:0;
  width:84px; flex-shrink:0; padding:var(--sp-3) 0 var(--sp-2);
  background:var(--surface); overflow-y:auto; overscroll-behavior:contain;
  /* The one hairline every page shows still carries the action colour. It has moved from
     under the bar to the right edge of the rail — same rule, rotated with the layout. */
  border-right:var(--bw-2) solid var(--green);
}
.nav-brand{
  display:flex; align-items:center; justify-content:center;
  padding:0 var(--sp-2) var(--sp-4);
}
/* The SQUARE mark, not the wordmark. The wordmark is a ~4.6:1 lockup: inside an 84px
   column it would have to come down to 18px tall to fit, which is not a logo any more. */
.nav-brand img{ height:30px; width:auto; display:block; }

.nav .nav-links{ display:flex; flex-direction:column; align-items:stretch; gap:2px; padding:0 var(--sp-2); }
/* Column, centred, caption under the glyph. `min-height:var(--tap)` is gone because the
   whole tile is 52px tall and is itself the target. */
.nav .nav-links a{
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:var(--sp-1);
  padding:var(--sp-2) 2px; border-radius:var(--radius);
  color:var(--ink-2); text-decoration:none;
  font-weight:700; font-size:10.5px; line-height:1.15; letter-spacing:.005em; text-align:center;
}
.nav .nav-links a:hover{ background:var(--surface-2); color:var(--ink); }
/* Active: the filled chip is kept, and gains the inset rule the list pane uses for a
   selected row — one selection signal, used in both columns, so "where am I" and "which
   row am I on" read as the same idea at two scales. */
.nav .nav-links a.on{
  background:var(--green-bg); color:var(--green-2);
  box-shadow:inset 3px 0 0 var(--green);
}
/* Icons are drawn, not fonted: no extra request, and they inherit the link colour, so the
   active state needs no second rule to recolour them. */
.rail-i{
  width:20px; height:20px; flex-shrink:0;
  fill:none; stroke:currentColor; stroke-width:1.75;
  stroke-linecap:round; stroke-linejoin:round;
}
.rail-l{ display:block; }
.rail-sep{ display:block; height:1px; background:var(--line); margin:var(--sp-2) var(--sp-1); }

/* The count of leave requests waiting on this person (2026-08-26). Deliberately the SAME warn
   palette as `.tag.pending` further down: pending is one colour in this app, and a red badge
   would read as an error rather than as a queue. Tabular numerals so 8 and 11 are the same
   width and the rail does not shift as approvals land. */
.nav .nav-links a .count{
  display:inline-flex; align-items:center; justify-content:center;
  min-width:16px; height:16px; margin-left:var(--sp-1); padding:0 4px;
  border-radius:var(--radius-pill); background:var(--warn); color:#fff;
  font-size:10px; font-weight:700; font-variant-numeric:tabular-nums; line-height:1;
  vertical-align:1px;
}

/* `margin-top:auto` does at the bottom of a column exactly what `margin-left:auto` did at
   the end of a row. */
.nav-user{
  margin-top:auto; display:flex; flex-direction:column; align-items:center; gap:var(--sp-1);
  padding:var(--sp-4) var(--sp-2) 0; margin-left:0;
  color:var(--ink-2); font-size:var(--fs-300); text-align:center;
}
/* Indigo, not rust: this is an identity, not an action. It is the one place in the rail the
   READING colour appears as a fill, which is what keeps it from reading as a button. */
.rail-avatar{
  display:flex; align-items:center; justify-content:center;
  width:30px; height:30px; flex-shrink:0;
  background:var(--accent); color:var(--ink-on-accent);
  font-size:11px; font-weight:700; line-height:1;
}
.rail-who{
  font-size:10.5px; font-weight:600; line-height:1.2; color:var(--ink-2);
  overflow-wrap:anywhere;
}
.nav-user a{ font-size:10.5px; }

/* Painted the exact brand blue the supplied wordmark sits on, so the asset's
   baked-in flat background is invisible — no keying, therefore no fringing on
   the anti-aliased letterforms. */
.foot{
  background:var(--brand-ink); color:var(--foot-ink); margin-top:var(--sp-8);
  flex-shrink:0; min-width:0; width:100%;
  /* Green enters as a rule, NOT as the band's fill. The wordmark-light asset has the brand blue
     baked into its own flat background, so repainting this band any other colour would put a blue
     rectangle around the logo. The top rule is the safe place for the second colour. */
  border-top:3px solid var(--green);
}
.foot-in{
  max-width:1240px; margin:0 auto; padding:var(--sp-5) var(--sp-4);
  display:flex; align-items:center; gap:var(--sp-4); flex-wrap:wrap;
  font-size:var(--fs-200);
}
/* ADAPTED: the delivered CSS had no `.foot img` rule at all. The footer
   wordmark is print-scale source art — unsized it renders at natural width and
   detonates the layout. Height-sized, like the nav mark. */
.foot img{ height:34px; width:auto; display:block; }
.foot small{ color:var(--foot-ink); line-height:1.6; }
.foot a{ color:#FFFFFF; }
/* ADAPTED: `.sep` is a 1px vertical rule ELEMENT here, not a text character, so
   it needs width and stretch — `color:` alone rendered nothing at all. */
.foot .sep{ width:1px; align-self:stretch; background:rgba(255,255,255,.22); }
.foot .right{ margin-left:auto; text-align:right; }

/* --- PILL NAV — a horizontal strip of mutually-exclusive choices ---
   The pivot's dimension switcher (`_pivot_table.html`) and the security log's event
   filter (`security.html`) both used `.nav-links`, borrowed from the old horizontal
   header because it already looked like a row of tabs. On 2026-08-30 `.nav-links`
   became the vertical RAIL, and both of them silently turned into what the rail is:
   nine full-width rows, centred, 10px captions, the active one tinted with an inset
   rule. Nothing errored — the pivot picker just quietly became a stacked list that
   pushed the actual table off the screen.

   THE LESSON IS THE SELECTOR, NOT THE PAGES. Restyling a class in place is safe only
   if that class has one meaning; `.nav-links` had three. The rail rules above are now
   scoped to `.nav .nav-links` so they cannot reach anything outside the nav again, and
   these two get a class that says what they are. --- */
.pillnav{ display:flex; flex-wrap:wrap; align-items:center; gap:var(--sp-1); }
.pillnav a{
  display:inline-flex; align-items:center; min-height:36px;
  padding:0 var(--sp-3); border:var(--bw-1) solid var(--line); border-radius:var(--radius);
  color:var(--ink-2); text-decoration:none; font-weight:600; font-size:var(--fs-300);
  white-space:nowrap;
}
.pillnav a:hover{ background:var(--surface-2); color:var(--ink); border-color:var(--line-2); }
/* INDIGO, not rust, and the distinction is the palette's own rule. Rust ACTS and
   marks where you are or what is selected — the rail tile, the selected week row.
   Indigo READS. A pivot dimension and a date preset are both "which slice am I
   looking at", which is reading, and `.chip.on` right above this strip on /admin
   already answers it in solid indigo. Rust here put two different selected-states,
   in two colours, one card apart, meaning the same thing. */
.pillnav a.on{ background:var(--accent); color:var(--ink-on-accent); border-color:var(--accent); }
.pillnav a.on:hover{ background:var(--accent-2); border-color:var(--accent-2); color:var(--ink-on-accent); }

/* ---------------------------------------------------------------------------
   THE WORKBENCH  (2026-08-29, demo fork)

   Three columns, left to right: the rail, an optional LIST PANE, and the
   workspace. Only the workspace and the pane scroll, and they scroll
   independently — which is the whole argument for the layout. You can be four
   screens deep in a form with the week still on screen beside it, and moving
   between entries never costs a page load or a scroll position.

   `body` stays `flex-direction:column` (the signed-out pages are still a page
   with a footer under it, and test_flex_body_children_can_shrink reads that
   rule). `body.app` — set by base.html only when somebody is signed in — turns
   it on its side.
   --------------------------------------------------------------------------- */
body.app{
  flex-direction:row;
  /* The app fills the viewport and does NOT scroll; its columns do. `min-height`
     alone would let a long report grow the body and drag the rail off the top. */
  height:100vh; overflow:hidden;
}

.shell{
  display:flex; flex-direction:column;
  flex:1 0 auto; min-width:0; width:100%;
}
/* The scrolling column. `min-width:0` for the same reason `.wrap` needs it — a flex
   item defaults to min-width:auto and a 1020px grid table would push the pane and the
   rail off the left of the screen rather than scroll inside itself. */
body.app .shell{ flex:1 1 auto; overflow-y:auto; overscroll-behavior:contain; }

/* Content starts at the left edge of the workspace rather than centring in it: the
   rail and pane are already doing the job the centred container used to do, and a
   form that drifts right as the window widens is the thing that made this app read
   as a website with a nav bar. */
body.app .wrap{ margin:0; max-width:1080px; padding:var(--sp-6) var(--sp-7); }
body.app .wrap.wide{ max-width:none; }

/* --- THE WORKSPACE — the third column ---
   The rail and the pane were not the half that made this app read as somebody else's.
   The workspace was: rounded cards with a drop shadow, stacked down a centred container,
   34px page titles, 13px sentence-case field labels. That is a WEBSITE's vocabulary, and
   changing the colours inside it left it recognisable.

   Almost all of the change below is made by REDEFINING FOUR TOKENS, which is the promise
   the top of this file makes ("every rule consumes a token from the :root block"). It is
   worth writing down that the promise held: sixty-odd rules turn square and flat here
   without one of them being edited.

   Signed-in surfaces only. The login and /demo screens keep the softer card, because a
   single centred box on an empty page is the one place the old treatment was right. --- */
body.app{
  /* Square. CA software should read like a ruled ledger, not like a card feed — and the
     radius was the single largest carrier of "consumer web app" in the old look. */
  --radius:10px; --radius-sm:6px; --radius-lg:14px; /* SITE AESTHETIC 2026-10-03: was 0/0/0; the site is rounded, so the app is too */
  /* Elevation comes from lines. The shadow under every card is what made the page read as
     a stack of floating things rather than one surface with structure on it. */
  --shadow-1:none; --shadow-2:none;
}

/* Titles: 34px was a marketing headline on a screen whose job is 30 rows of client names.
   22px at 800 with tight tracking is a WORKING title — it names the thing you are editing
   without taking a fifth of the viewport to do it. */
body.app .wrap h1{ font-size:1.375rem; font-weight:800; letter-spacing:-.025em; }
body.app .wrap h2{ font-size:1rem; font-weight:800; letter-spacing:-.015em; }
body.app .wrap h3{ font-size:var(--fs-300); font-weight:800; }

/* The page header is a BAND across the top of the workspace, not the top of a card: white,
   full width, hairline under it, and the title sits against the left edge where the eye
   already is. The cards below it then carry only content. */
body.app .wrap > .card:first-child{ border-top:none; }
body.app .card{ padding:var(--sp-5) var(--sp-6); margin-bottom:var(--sp-4); }

/* Field labels become the small caps the detail pane was drawn with. This is the change
   that is felt on the hot path: eleven of them on the entry form, and at 10px/800/.12em
   they stop competing with the values underneath them for the same visual weight. */
body.app .field label{
  font-size:10px; font-weight:800; letter-spacing:.12em; text-transform:uppercase;
  color:var(--ink-3);
}
/* Not the hint and not the "remembered" pill: those are sentences, and .12em tracking on a
   sentence is unreadable. They opt back out of everything the label just opted into. */
body.app .field label .hint,
body.app .field label .sticky{
  font-size:var(--fs-100); font-weight:400; letter-spacing:0; text-transform:none;
}
body.app .stat .k{ font-size:10px; letter-spacing:.12em; }
body.app .stat .v{ font-size:1.75rem; }
/* Statistics are a ROW of figures separated by hairlines, not four boxes with four borders.
   Same numbers, a quarter of the ink. */
body.app .stats{ gap:0; border:var(--bw-1) solid var(--line); background:var(--surface); }
body.app .stats .stat{ border:none; border-left:var(--bw-1) solid var(--line); }
/* SITE AESTHETIC 2026-10-03: with rounded corners back, the ROW is the rounded thing; a cell that
   rounds its own corners notches the hairlines between figures. */
body.app .stats{ border-radius:var(--radius); overflow:hidden; }
body.app .stats .stat{ border-radius:0; }
body.app .stats .stat:first-child{ border-left:none; }

/* The header BAND. A card whose first child is a head pulls that head out to the card's own
   edges and rules it off, so the title sits on a white strip with the content beneath it -
   which is the shape the detail pane was drawn as. Doing it in CSS rather than by adding a
   wrapper to eleven templates means no page can forget it. */
body.app .card > .card-head:first-child,
body.app .card > .head-row:first-child{
  margin:calc(var(--sp-5) * -1) calc(var(--sp-6) * -1) var(--sp-5);
  padding:var(--sp-4) var(--sp-6);
  border-bottom:var(--bw-1) solid var(--line);
}

/* Square, like every other control. The pill was the last rounded thing on the entry form, and
   one round object among twelve square ones reads as a mistake rather than as emphasis. The
   hour quick-picks are the ones this is really about: they sit directly under a square input
   holding the same value. */
body.app .chip{ border-radius:var(--radius-sm); }

/* The section eyebrow — rust, tiny, tracked. One per screen at most: it says what kind of
   thing the title beneath it is, which on a screen with no breadcrumb is the whole
   orientation. */
.eyebrow{
  font-family:var(--font-mono); font-size:11px; font-weight:500; letter-spacing:.08em; text-transform:uppercase;
  color:var(--green); margin-bottom:var(--sp-1);
}

/* --- LIST PANE — the middle column ---
   Rendered only by /  and /week (base.html's `pane` block). It is the week, always
   on screen, always scannable: day headers, one row per entry, one selected. */
.pane{
  display:flex; flex-direction:column;
  width:352px; flex-shrink:0; min-width:0;
  background:var(--surface); border-right:var(--bw-1) solid var(--line);
  overflow-y:auto; overscroll-behavior:contain;
}
/* Sticky so the week total stays readable while the list runs past it — on a heavy
   week this list is 30 rows and the number at the top is why you opened it. */
.pane-head{
  position:sticky; top:0; z-index:2;
  padding:var(--sp-4) var(--sp-4) var(--sp-3);
  background:var(--surface); border-bottom:var(--bw-1) solid var(--line);
}
.pane-head h2{ margin:0; font-size:var(--fs-500); font-weight:700; letter-spacing:-.02em; }
.pane-when{ display:flex; align-items:baseline; justify-content:space-between; gap:var(--sp-2); }
.pane-when .muted{ font-size:var(--fs-100); white-space:nowrap; }
/* Hours logged against the week's own denominator. A 4px rule, not a `.meter`: this is a
   glance, not a gauge, and the meter's 10px is a component in its own right. */
.pane-total{ display:flex; align-items:center; gap:var(--sp-2); margin-top:var(--sp-2); }
.pane-total .v{ font-size:var(--fs-500); font-weight:700; font-variant-numeric:tabular-nums; }
.pane-total .muted{ font-size:var(--fs-100); white-space:nowrap; }
.pane-bar{ flex:1 1 auto; min-width:0; height:4px; background:var(--line); }
.pane-bar span{ display:block; height:4px; background:var(--green); }

.pane-day{
  display:flex; align-items:baseline; justify-content:space-between; gap:var(--sp-2);
  padding:var(--sp-2) var(--sp-4) var(--sp-1);
  background:var(--surface-2); border-bottom:var(--bw-1) solid var(--line);
  font-size:10px; font-weight:700; letter-spacing:.13em; text-transform:uppercase;
  color:var(--ink-3);
}
.pane-day .h{ font-variant-numeric:tabular-nums; letter-spacing:0; }
/* A whole row is the target, not a link inside it. */
.pane-row{
  display:block; padding:var(--sp-3) var(--sp-4);
  border-bottom:var(--bw-1) solid var(--line);
  color:var(--ink); text-decoration:none;
}
.pane-row:hover{ background:var(--surface-2); }
/* The selected row. An inset rule plus the action tint — the same pair the active rail
   tile uses, so selection means one thing across both columns. */
.pane-row.on{ background:var(--green-bg); box-shadow:inset 3px 0 0 var(--green); }
.pane-row .c{ font-size:var(--fs-200); font-weight:700; }
.pane-row .m{
  display:flex; align-items:baseline; justify-content:space-between; gap:var(--sp-2);
  margin-top:2px; font-size:var(--fs-100); color:var(--ink-3);
}
.pane-row.on .m{ color:var(--green-2); }
.pane-row .m .h{ font-weight:700; font-variant-numeric:tabular-nums; white-space:nowrap; }
.pane-row .m .w{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* Leave that has materialised into an entry is not something you can edit here, and a row
   that looks editable and is not is worse than a row that says so. */
.pane-row.locked .c{ font-weight:600; color:var(--ink-2); }
.pane-empty{ padding:var(--sp-6) var(--sp-4); color:var(--ink-3); font-size:var(--fs-200); }
/* The other end of the pair. The pane marks the row you clicked; the table marks the row you
   landed on, so a jump into a 30-row week does not leave you hunting for where it put you.
   `tr.on` is set by pane.js from the hash; `tr:target` is the browser doing the same thing with
   script off, and both are here so neither is the only one that works. */
#entry-table tbody tr.on > td,
#entry-table tbody tr:target > td{ background:var(--green-bg); }
#entry-table tbody tr.on > td:first-child,
#entry-table tbody tr:target > td:first-child{ box-shadow:inset 3px 0 0 var(--green); }
/* Sticky to the bottom for the same reason `.actions` is on a phone: the one action this
   column offers must not be somewhere you have to scroll to. */
.pane-foot{
  position:sticky; bottom:0; margin-top:auto;
  padding:var(--sp-3) var(--sp-4);
  background:var(--surface); border-top:var(--bw-1) solid var(--line);
}
.pane-foot button,.pane-foot .btn{ width:100%; }
/* A link that has to look like the primary button — `.pane-foot` on /week points at the
   entry form, which is a navigation, not a submit. */
.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:var(--sp-2);
  min-height:var(--tap); padding:0 var(--sp-4); border-radius:var(--radius);
  font-size:var(--fs-300); font-weight:700; text-decoration:none;
  background:var(--green); color:#FFFFFF; border:var(--bw-1) solid var(--green);
}
.btn:hover{ background:var(--green-2); border-color:var(--green-2); color:#FFFFFF; }

/* ---------------------------------------------------------------------------
   CARD
   --------------------------------------------------------------------------- */
.card{
  background:var(--surface); border:var(--bw-1) solid var(--line);
  border-radius:var(--radius-lg); box-shadow:var(--shadow-1);
  padding:var(--sp-6); margin-bottom:var(--sp-5);
}
.card-head{ display:flex; align-items:flex-start; justify-content:space-between; gap:var(--sp-4); }
.card-head h1,.card-head h2{ margin-bottom:var(--sp-1); }
.card-head .muted{ font-size:var(--fs-300); }
.head-row{ display:flex; align-items:flex-start; justify-content:space-between; gap:var(--sp-4); flex-wrap:wrap; }
.actions{ display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap; }

/* ---------------------------------------------------------------------------
   FORMS
   --------------------------------------------------------------------------- */
.row{ display:flex; gap:var(--sp-4); flex-wrap:wrap; }
.field{ display:flex; flex-direction:column; gap:var(--sp-2); flex:1 1 220px; min-width:0; margin-bottom:var(--sp-4); }
.field label{
  display:flex; align-items:baseline; gap:var(--sp-2); flex-wrap:wrap;
  font-size:var(--fs-300); font-weight:600; color:var(--ink-2);
}
.field label .hint{ font-weight:400; color:var(--ink-3); font-size:var(--fs-100); }
/* The "remembered from your last entry" affordance. A pill, so it reads as a
   state rather than as part of the label. */
.field label .sticky{
  font-weight:400; color:var(--accent); font-size:var(--fs-100);
  background:var(--accent-bg); padding:0 var(--sp-2); border-radius:var(--radius-pill);
}
.field label .sticky:empty{ display:none; }   /* filled by script, not always present */

input,select,textarea{
  font-family:var(--font-ui);
  font-size:var(--fs-400);              /* 16px — never smaller (stops iOS zoom) */
  color:var(--ink); background:var(--surface);
  border:var(--bw-1) solid var(--line-2); border-radius:var(--radius);
  padding:var(--sp-2) var(--sp-3); min-height:var(--tap); width:100%;
  transition:border-color .12s ease, box-shadow .12s ease;
}
textarea{ min-height:calc(var(--tap) * 1.6); line-height:var(--lh); resize:vertical; padding-top:var(--sp-3); }
input::placeholder,textarea::placeholder{ color:var(--ink-3); }

/* A TICKBOX IS NOT A TEXT FIELD. The block above is written for fields you type in, and a
   checkbox that inherits `width:100%` + `min-height:var(--tap)` becomes a 44px square that
   stretches across its row. There were already two patches for this - `.radios input` and a
   hand-applied `.w-auto` - and both are per-place, so the 14 checkboxes on the admin master page
   (which have neither) have been rendering oversized. Fixed at the ELEMENT, once, where nothing
   has to remember it. `align-self:center` matters for the wrapping case: a two-line label is
   exactly when a stretched box got enormous. */
input[type=checkbox],input[type=radio]{
  width:15px; height:15px; min-width:0; min-height:0; flex:none; align-self:center;
  margin:0; padding:0; accent-color:var(--accent);
}
input:focus,select:focus,textarea:focus{
  outline:none; border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-bg);
}
input:disabled,select:disabled,textarea:disabled{
  background:var(--surface-2); color:var(--ink-3); cursor:not-allowed;
}

/* Buttons. Default = quiet secondary outline. .primary = the one action per view. */
button{
  font-family:var(--font-ui); font-size:var(--fs-300); font-weight:700;
  color:var(--ink); background:var(--surface);
  border:var(--bw-1) solid var(--line-2); border-radius:var(--radius);
  min-height:var(--tap); padding:0 var(--sp-4);
  display:inline-flex; align-items:center; justify-content:center; gap:var(--sp-2);
  cursor:pointer; white-space:nowrap;
  transition:background .12s ease, border-color .12s ease, color .12s ease;
}
button:hover{ background:var(--surface-2); border-color:var(--ink-3); }
button:active{ background:var(--line); }
button:disabled{ color:var(--ink-3); background:var(--surface-2); border-color:var(--line); cursor:not-allowed; }
/* Primary was Raisin Black, on the reasoning that a blue primary is one blue among many on a
   screen full of blue links and chips. That reasoning holds and is why primary is NOT blue now
   either — it is the brand green (2026-08-13). Green is used nowhere else as a fill, so "Save and
   add another" is still the single unambiguous target on the hot path, and it now says which
   firm's software you are in. */
button.primary{ background:var(--green); color:#FFFFFF; border-color:var(--green); }
button.primary:hover{ background:var(--green-2); border-color:var(--green-2); }
/* DEMO FORK re-skin: this was #1E4839, a hardcoded DARK GREEN from the old palette —
   the one press state the token swap could not reach, so every primary button flashed
   green on click against a rust fill. Darkened from --green-2 by the same step the old
   pair used. */
button.primary:active{ background:#7A2C12; border-color:#7A2C12; }
button.link{
  background:none; border:none; color:var(--accent); padding:0 var(--sp-1);
  min-height:var(--tap); text-decoration:underline; text-underline-offset:2px; font-weight:600;
}
button.link:hover{ background:none; color:var(--accent-2); }
button.link.danger{ color:var(--bad); }
button.link.danger:hover{ color:#7C2020; }

.checks{ display:flex; flex-wrap:wrap; gap:var(--sp-2) var(--sp-4); }
.checks label{
  display:inline-flex; align-items:center; gap:var(--sp-2);
  min-height:32px; font-size:var(--fs-300); font-weight:400; color:var(--ink);
}
.checks input{ width:auto; min-height:auto; accent-color:var(--accent); }
.checks .hint{ color:var(--ink-3); }
/* ADAPTED: `.radios` stays a COLUMN. It is only ever the propagate choice, whose
   two options are full sentences ("This has genuinely changed — past reports
   keep the old name…"). Wrapped side by side they crush to two words a line. */
.radios{ display:flex; flex-direction:column; gap:var(--sp-2); }
.radios label{
  display:flex; align-items:flex-start; gap:var(--sp-2);
  padding:var(--sp-2) var(--sp-3); font-size:var(--fs-300); font-weight:400;
  border:var(--bw-1) solid var(--line); border-radius:var(--radius);
}
.radios label:has(input:checked){ border-color:var(--accent); background:var(--accent-bg); }
.radios input{ margin-top:4px; }   /* sizing now comes from the element rule above */

/* `.adder` — a <details> disclosure wrapping an inline add-form. Collapsed by
   default: on the master page the LIST is what you came for. */
.adder{ border:var(--bw-1) dashed var(--line-2); border-radius:var(--radius); background:var(--surface-2); margin-bottom:var(--sp-4); }
.adder > summary{
  list-style:none; cursor:pointer; display:flex; align-items:center; gap:var(--sp-2);
  min-height:var(--tap); padding:0 var(--sp-4);
  font-size:var(--fs-300); font-weight:700; color:var(--accent);
}
.adder > summary::-webkit-details-marker{ display:none; }
.adder > summary::before{ content:'+'; font-size:var(--fs-500); }
.adder[open] > summary::before{ content:'\2013'; }
.adder[open] > summary{ border-bottom:var(--bw-1) dashed var(--line); }
.adder > *:not(summary){ padding:var(--sp-4); }
.inline-reset{
  display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap;
  margin-top:var(--sp-4); padding-top:var(--sp-4); border-top:var(--bw-1) solid var(--line);
}
.inline-reset input{ width:auto; flex:1 1 220px; }

/* ---------------------------------------------------------------------------
   TABLES — the product's centre of gravity.
   Restraint over borders: horizontal hairlines only, no verticals. Column
   RHYTHM (consistent padding + right-aligned tabular figures) carries the grid;
   vertical rules would only add noise to a 40-row scan.
   --------------------------------------------------------------------------- */
table{ width:100%; border-collapse:collapse; font-size:var(--fs-200); color:var(--ink); }
th{
  text-align:left; font-weight:700; color:var(--ink-2);
  font-size:var(--fs-100); letter-spacing:.03em; text-transform:uppercase;
  padding:var(--sp-2) var(--sp-3); vertical-align:bottom;
  border-bottom:var(--bw-2) solid var(--line-2);   /* silver header rule */
  white-space:nowrap;
}
td{ padding:var(--sp-3); border-bottom:var(--bw-1) solid var(--line); vertical-align:top; }
td.num,th.num{ text-align:right; font-variant-numeric:tabular-nums; padding-right:var(--sp-4); white-space:nowrap; }
tbody tr:hover{ background:var(--accent-bg); }

/* ADAPTED: zebra banding is opt-in via `.zebra`, not automatic on every table.
   The delivered rule was `tbody tr:nth-child(even)`, but half the tables here
   interleave a HIDDEN `tr.editor` after every data row — so the stripes would
   have landed on an arbitrary subset of visible rows and read as a rendering
   fault rather than as banding. `.zebra` goes on the editor-free tables, which
   are exactly the long ones the banding is for. */
.zebra tbody tr:nth-child(even){ background:var(--surface-2); }
.zebra tbody tr:hover{ background:var(--accent-bg); }

/* A total must read AS a total: heavier top rule, bold ink, a faint fill. */
tfoot td,tfoot th,tr.total td,tr.total th{
  font-weight:700; color:var(--ink); background:var(--accent-bg);
  border-top:var(--bw-2) solid var(--ink); border-bottom:none;
  font-size:var(--fs-300);
}

.sticky-head thead th{
  position:sticky; top:0; z-index:2; background:var(--surface);
  box-shadow:0 var(--bw-2) 0 var(--line-2);
}

/* Wide tables scroll inside themselves; the PAGE never moves sideways.
   The gradients are pure-CSS scroll shadows — they tell the reader the table
   continues past the edge, which a bare cut-off does not. */
.scroll-x{
  overflow-x:auto; -webkit-overflow-scrolling:touch;
  background:
    linear-gradient(90deg, var(--surface) 30%, rgba(255,255,255,0)) left / 24px 100% no-repeat,
    linear-gradient(90deg, rgba(255,255,255,0), var(--surface) 70%) right / 24px 100% no-repeat,
    radial-gradient(farthest-side at 0 50%, rgba(30,40,51,.12), transparent) left / 12px 100% no-repeat,
    radial-gradient(farthest-side at 100% 50%, rgba(30,40,51,.12), transparent) right / 12px 100% no-repeat;
  background-attachment:local,local,scroll,scroll;
}
/* ADAPTED: the min-width that forces a scroll rather than crushing columns is
   scoped to the WIDE surfaces (reports, master data, team). Applied globally it
   forced a horizontal scroll on the employee pages' three-column tables, which
   fit a 375px phone perfectly well — friction invented on the hot path. */
.wide .scroll-x table{ min-width:640px; }
.scroll-y{ overflow-y:auto; max-height:50vh; }

/* Inline editor rows: on a 504-row list, losing scroll position on every edit
   is the difference between usable and not. */
tr.editor > td{ background:var(--accent-bg); box-shadow:inset 0 0 0 var(--bw-2) var(--accent); padding:var(--sp-4); }
tr.editor input,tr.editor select,tr.editor textarea{ min-height:38px; font-size:var(--fs-300); }
tr.on-row > td{ background:var(--accent-bg); }
tr.on-row > td:first-child{ box-shadow:inset var(--bw-2) 0 0 var(--accent); }
/* A separate class from [hidden], which the editor rows use for open/closed —
   one attribute doing two jobs is how a filtered list re-opens every editor. */
tr.filtered-out{ display:none; }
.row-actions{ display:flex; gap:var(--sp-1); justify-content:flex-end; white-space:nowrap; }

/* Stat blocks + share bars -------------------------------------------------- */
.stats{ display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:var(--sp-3); }
.stat{ background:var(--surface); border:var(--bw-1) solid var(--line); border-radius:var(--radius); padding:var(--sp-4); }
.stat .k{ display:block; font-size:var(--fs-100); font-weight:700; letter-spacing:.03em; text-transform:uppercase; color:var(--ink-3); }
.stat .v{ display:block; font-size:var(--fs-700); font-weight:700; line-height:1.1; margin-top:var(--sp-1); font-variant-numeric:tabular-nums; }

.bar{ display:block; height:8px; background:var(--line); border-radius:var(--radius-pill); overflow:hidden; }
/* Width is painted by bars.js from the row's `data-share`. `el.style.width` from
   script is NOT what `style-src` governs — CSP blocks <style> elements and
   `style=` attributes in MARKUP, not the CSSOM setter. That is the escape hatch
   that keeps a per-row width with no policy exemption. */
.bar span{ display:block; height:100%; width:0; background:var(--accent); border-radius:inherit; }

/* ---------------------------------------------------------------------------
   STATUS & FEEDBACK
   Every state carries a NON-COLOUR cue (a leading glyph badge) so it survives
   greyscale and colour-blind viewing. Plain characters, no icon font.
   --------------------------------------------------------------------------- */
/* A block with the badge hung in the left padding, not a flex row: as flex items a leading <b> and
   the sentence after it became two columns, and on a phone the bold one wrapped a word a line. */
.banner{
  display:block; position:relative;
  padding:var(--sp-3) var(--sp-4) var(--sp-3) calc(var(--sp-4) + 20px + var(--sp-3)); border-radius:var(--radius);
  border:var(--bw-1) solid var(--line-2); border-left-width:4px;
  background:var(--surface); color:var(--ink); font-size:var(--fs-300);
  margin-bottom:var(--sp-4);
}
.banner::before{
  content:''; position:absolute; left:var(--sp-4); top:var(--sp-3); width:20px; height:20px; border-radius:var(--radius-pill);
  display:inline-flex; align-items:center; justify-content:center;
  font-weight:700; font-size:var(--fs-200); color:#FFFFFF; line-height:1;
}
.banner.info{ border-left-color:var(--info); background:var(--info-bg); }
.banner.info::before{ content:'i'; background:var(--info); }
.banner.ok{ border-left-color:var(--ok); background:var(--ok-bg); }
.banner.ok::before{ content:'\2713'; background:var(--ok); }
.banner.warn{ border-left-color:var(--warn); background:var(--warn-bg); }
.banner.warn::before{ content:'!'; background:var(--warn); }
.banner.err{ border-left-color:var(--bad); background:var(--bad-bg); }
.banner.err::before{ content:'\2715'; background:var(--bad); }

/* Tags. Fill/outline treatment plus the text distinguishes them without colour.
   ADAPTED: no `text-transform:uppercase`. These are not one-word labels here —
   they carry sentences ("13 logged since submitting", "all entries submitted"),
   and a shouted sentence in a table cell is unreadable. Uppercase is kept where
   it belongs: on `th` and `.stat .k`. */
.tag{
  display:inline-flex; align-items:center; gap:var(--sp-1);
  padding:2px var(--sp-2); border-radius:var(--radius-sm);
  font-size:var(--fs-100); font-weight:700; letter-spacing:.01em;
  border:var(--bw-1) solid var(--line-2); background:var(--surface-2); color:var(--ink-2);
  white-space:nowrap;
}
.tag.draft{ background:var(--surface-2); color:var(--ink-2); border-color:var(--line-2); }
.tag.submitted{ background:var(--ok-bg); color:var(--ok); border-color:var(--ok); }
.tag.admin{ background:var(--accent-bg); color:var(--accent); border-color:var(--accent); }

/* Toast — transient, fixed to the bottom so it survives the card head scrolling
   away on a phone. Auto-hidden by entry.js. */
.toast{
  position:fixed; left:50%; bottom:var(--sp-5); transform:translate(-50%, 8px);
  max-width:min(92vw,420px); z-index:60;
  background:var(--brand-black); color:#FFFFFF; font-size:var(--fs-300);
  padding:var(--sp-3) var(--sp-4); border-radius:var(--radius); box-shadow:var(--shadow-2);
  opacity:0; pointer-events:none; transition:opacity .16s ease, transform .16s ease;
}
.toast.show{ opacity:1; transform:translate(-50%,0); }
.toast.err{ background:var(--bad); }
.toast.warn{ background:var(--warn); }

/* The day meter on the entry form: a bar plus a caption. Green until the soft
   cap, amber past it. A bar makes "how full is this day" answerable at a
   glance, which a sentence of text does not. */
.meter{ display:block; height:10px; background:var(--line); border-radius:var(--radius-pill); overflow:hidden; }
.meter > span{ display:block; height:100%; width:0; background:var(--ok); border-radius:inherit; transition:width .16s ease; }
.meter.warn > span{ background:var(--warn); }
.meter-label{
  display:flex; justify-content:space-between; gap:var(--sp-3); flex-wrap:wrap;
  font-size:var(--fs-100); color:var(--ink-3); margin-top:var(--sp-1);
  font-variant-numeric:tabular-nums;
}
.meter-label.warn{ color:var(--warn); font-weight:700; }

.muted{ color:var(--ink-3); font-size:var(--fs-200); }
.no-match{ text-align:center; color:var(--ink-3); padding:var(--sp-6) var(--sp-4); font-size:var(--fs-300); }

/* ---------------------------------------------------------------------------
   SELECTION CONTROLS — chips, presets, filters, tabs, typeahead, multiselect
   --------------------------------------------------------------------------- */
/* --- leave calendar (2026-08-26) -------------------------------------------
   Replaced a strip of eleven week chips. The unit being SELECTED is the week, so the row is the
   thing that highlights - but the click target is the individual day, because "click the week of
   the 14th" is a thought nobody has; they look for the 14th. Every cell therefore links to its
   own week and the whole row lights up. */
.cal-head{
  display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between;
  gap:var(--sp-3);
}
.cal-nav{ display:flex; align-items:center; gap:var(--sp-2); }
.cal-month{ font-size:var(--fs-400); min-width:9.5ch; text-align:center; }
.cal-arrow{ padding:0 var(--sp-3); font-size:var(--fs-500); line-height:1; }

.cal{ width:100%; border-collapse:separate; border-spacing:0; table-layout:fixed; min-width:640px; }
.cal th{
  padding:var(--sp-2) var(--sp-2); text-align:left;
  font-size:var(--fs-100); font-weight:700; letter-spacing:.04em; text-transform:uppercase;
  color:var(--ink-3);
}
.cal td{ padding:0; border:var(--bw-1) solid var(--line); }
/* Collapse the doubled interior borders without `border-collapse`, which cannot carry a
   row-level highlight cleanly. */
.cal td + td{ border-left:0; }
.cal tr + tr td{ border-top:0; }

.cal-day{
  display:flex; flex-direction:column; gap:2px; align-items:flex-start;
  min-height:76px; padding:var(--sp-2); text-decoration:none; color:inherit;
}
.cal-day:hover{ background:var(--surface-2); }
.cal-num{ font-size:var(--fs-200); font-weight:700; color:var(--ink-2); font-variant-numeric:tabular-nums; }
/* Days from the neighbouring month are drawn rather than left as holes - a 7-column grid with
   gaps is harder to read than one with greyed cells - but they must not compete. */
.cal-cell.out .cal-num{ color:var(--ink-3); opacity:.55; }
.cal-cell.off{ background:var(--surface-2); }
.cal-cell.today .cal-num{
  background:var(--accent); color:var(--ink-on-accent);
  border-radius:var(--radius-pill); padding:0 var(--sp-2); min-width:22px; text-align:center;
}
.cal-who{
  display:block; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  font-size:var(--fs-100); font-weight:600;
  border-radius:var(--radius); padding:1px var(--sp-2);
  background:var(--green-bg); color:var(--green-2);
}
/* Pending leave is a REQUEST, not a fact. It has to look different from an approved absence or
   the calendar quietly promises cover that nobody has agreed to yet. */
.cal-who.pending{ background:var(--surface-2); color:var(--ink-2); border:var(--bw-1) dashed var(--line-2); }
.cal-more{ font-size:var(--fs-100); color:var(--ink-3); padding-left:2px; }

/* The selected DAY (2026-09-16). An inset ring rather than a fill, for the same reason the week
   highlight is an edge and not one: the cell already holds two or three coloured name pills, and
   tinting the ground behind them makes both harder to read. It has to out-rank `.off` and `.out`,
   which do fill - a selected Sunday that looks unselected is the bug this replaced. */
.cal-cell.sel{ background:var(--surface); box-shadow:inset 0 0 0 2px var(--accent); }
.cal-cell.sel .cal-day:hover{ background:var(--accent-bg); }

/* The selected week. A left/right edge on the row rather than a fill, so the day pills stay
   readable - a filled row would fight every chip inside it. */
.cal-week.on td{ background:var(--accent-bg); border-color:var(--accent); }
.cal-week.on td:first-child{ box-shadow:inset 3px 0 0 var(--accent); }
.cal-week:hover td{ background:var(--surface-2); }

@media (max-width:640px){
  /* The grid keeps its 7 columns and scrolls inside its own box (.scroll-x) rather than
     reflowing: a calendar that drops columns on a phone is no longer a calendar. */
  .cal-day{ min-height:62px; }
  .cal-head{ justify-content:flex-start; }
}

.chips{ display:flex; flex-wrap:wrap; gap:var(--sp-2); align-items:center; }
.chip{
  display:inline-flex; align-items:center; justify-content:center; gap:var(--sp-2);
  min-height:var(--tap); padding:0 var(--sp-4);
  border:var(--bw-1) solid var(--line-2); border-radius:var(--radius-pill);
  background:var(--surface); color:var(--ink); font-weight:600; font-size:var(--fs-300);
  cursor:pointer; font-variant-numeric:tabular-nums;
}
.chip:hover{ border-color:var(--ink-3); background:var(--surface-2); }
/* A selected hour chip has to be unmistakable at arm's length on a phone —
   a full blue fill, not a tint. */
.chip.on{ background:var(--accent); color:var(--ink-on-accent); border-color:var(--accent); }
.chip.on:hover{ background:var(--accent-2); border-color:var(--accent-2); }
/* ADAPTED: the date presets and the removable filter chips are <a> elements, and
   the base `a` rule now underlines every link. An underlined pill reads as
   broken.

   BROADENED 2026-08-26 from `.presets .chip, .chips a.chip` to every `a.chip`. The narrow
   version was a rule about WHERE a chip sits rather than what it is, so the leave calendar's
   month arrows and its Today button - chips in `.cal-nav`, not in `.chips` - shipped underlined
   and were caught only by looking at a screenshot. A chip is never underlined; say that once,
   here, instead of re-scoping this selector every time a chip appears somewhere new. */
a.chip{ text-decoration:none; }

.presets{ display:flex; flex-wrap:wrap; gap:var(--sp-2); align-items:center; margin-bottom:var(--sp-4); }
.filter-grid{ display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:var(--sp-4); align-items:start; }
.filter-grid .field{ margin-bottom:0; }

.tabs{ display:flex; gap:var(--sp-1); border-bottom:var(--bw-1) solid var(--line); flex-wrap:wrap; margin-top:var(--sp-4); }
.tabs a{
  display:inline-flex; align-items:center; min-height:var(--tap);
  padding:0 var(--sp-3); margin-bottom:-1px; border-radius:0;
  border-bottom:var(--bw-2) solid transparent; background:none;
  color:var(--ink-2); font-weight:600; font-size:var(--fs-300); text-decoration:none;
}
.tabs a:hover{ color:var(--ink); background:none; }
.tabs a.on{ color:var(--accent); border-bottom-color:var(--accent); background:none; }

/* Typeahead — the client picker, on the hot path. 504 options. */
.typeahead{ position:relative; }
.ta-list{
  position:absolute; z-index:40; left:0; right:0; top:calc(100% + 4px);
  max-height:320px; overflow-y:auto;
  background:var(--surface); border:var(--bw-1) solid var(--line-2);
  border-radius:var(--radius); box-shadow:var(--shadow-2);
  display:none; padding:var(--sp-1);
}
.ta-list.show{ display:block; }
.ta-item{
  display:flex; align-items:baseline; justify-content:space-between; gap:var(--sp-3);
  padding:var(--sp-3); border-radius:var(--radius-sm); cursor:pointer; min-height:var(--tap);
  font-size:var(--fs-300);
}
.ta-item:hover{ background:var(--surface-2); }
.ta-item.sel{ background:var(--accent-bg); }        /* keyboard-highlighted row */
.ta-item .grp{ color:var(--ink-3); font-size:var(--fs-100); font-weight:700; letter-spacing:.03em; white-space:nowrap; }
.ta-item .uses{ color:var(--ink-3); font-size:var(--fs-100); font-variant-numeric:tabular-nums; white-space:nowrap; }
/* Say what is hidden rather than silently truncating at 8. */
.ta-more{
  padding:var(--sp-2) var(--sp-3); color:var(--ink-3); font-size:var(--fs-100);
  text-align:center; border-top:var(--bw-1) solid var(--line);
}

/* Multiselect — the <select multiple> stays in the DOM as the value store,
   visually hidden but still submitted. Never display:none on it: a
   display-hidden select is skipped by some assistive tech. */
.ms{ position:relative; }
.ms-pills{ display:flex; flex-wrap:wrap; gap:var(--sp-2); align-items:center; margin-bottom:var(--sp-2); }
.ms-pills:empty{ margin-bottom:0; }
.ms-pill{
  display:inline-flex; align-items:center; gap:var(--sp-2); max-width:100%;
  padding:var(--sp-1) var(--sp-2) var(--sp-1) var(--sp-3); min-height:32px;
  background:var(--accent-bg); color:var(--accent); border:var(--bw-1) solid var(--accent);
  border-radius:var(--radius-pill); font-size:var(--fs-200); font-weight:600;
}
.ms-pill > button{
  min-height:auto; padding:0 var(--sp-1); border:none; background:none;
  color:inherit; font-size:var(--fs-400); line-height:1;
}
.ms-pill > button:hover{ color:var(--bad); background:none; }
.ms-list{ max-height:240px; }

.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

/* ---------------------------------------------------------------------------
   DAY STRIP — the working days of the week, one cell each.
   The one genuinely visual element in the product. Load is encoded by blue
   intensity (no new hues); today gets a ring that is independent of load.

   ADAPTED: the delivered rule was a fixed 14-column grid of square cells. The
   strip is Mon–Sat only, so it is 12–14 cells depending on the month — a fixed
   14 leaves a hole or overflows — and square cells at 1240px wide would be
   ~85px tall, about three times the intended height. Auto-flow columns size to
   whatever the month actually has.
   --------------------------------------------------------------------------- */
.strip{ display:grid; grid-auto-flow:column; grid-auto-columns:1fr; gap:var(--sp-1); }
.strip div{
  min-height:38px; border-radius:var(--radius-sm);
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:1px; padding:var(--sp-1) 2px; font-size:var(--fs-100); font-weight:700; line-height:1.1;
  color:var(--ink-3); background:var(--surface-2);
  border:var(--bw-1) solid var(--line); font-variant-numeric:tabular-nums;
  cursor:pointer;                      /* clicking a day retargets the date field */
}
.strip .has{ background:var(--accent); color:var(--ink-on-accent); border-color:var(--accent); }
/* DEMO FORK re-skin: border was #B9C6D3, a blue-grey mixed against the OLD navy accent.
   Against indigo it read as a stray cool-grey edge on an otherwise violet chip. Same
   role, re-mixed for this palette. */
.strip .low{ background:var(--accent-bg); color:var(--accent); border-color:#C3CBC4; }
/* Dashed = "nothing here", legible without colour. */
.strip .none{ background:var(--surface); color:var(--ink-3); border-style:dashed; border-color:var(--line-2); }
.strip .today{ box-shadow:0 0 0 var(--bw-2) var(--brand-black); border-color:var(--brand-black); }
/* Sunday and the firm's holidays (2026-08-27). Present and clickable - people work peak-period
   Sundays - but faded, because "not expected" and "not allowed" must not look the same. Opacity
   rather than a colour of its own: it has to compose with the four states above, all of which a
   worked Sunday can be in. */
.strip .off{ opacity:.55; }
.strip .off.has,.strip .off.low{ opacity:1; }
/* An approved-leave day (2026-09-01). Distinct from `off`, which is faded to say "nothing is
   expected here": leave is a day that IS accounted for, so it reads as settled rather than
   missing. Without this it would be an empty tile — indistinguishable from a day forgotten. */
.strip .leave{ background:var(--ok-bg,rgba(46,160,67,.10)); border-color:var(--ok,#2ea043); }
.strip .leave span:last-child::after{ content:"leave"; font-size:.62rem; opacity:.75; }

/* ---------------------------------------------------------------------------
   MODALS
   --------------------------------------------------------------------------- */
.modal{
  position:fixed; inset:0; z-index:80;
  display:flex; align-items:center; justify-content:center; padding:var(--sp-4);
  background:rgba(30,40,51,.45);
}
.modal[hidden]{ display:none; }
.modal-card{
  background:var(--surface); border-radius:var(--radius-lg); box-shadow:var(--shadow-2);
  width:100%; max-width:620px; max-height:88vh; overflow-y:auto; padding:var(--sp-6);
}
.modal-actions{ display:flex; justify-content:flex-end; gap:var(--sp-2); margin-top:var(--sp-5); flex-wrap:wrap; }
/* The day-cap dialog carries one sentence; a 620px card around two lines reads
   as a bigger problem than it is. */
.cap-card{ max-width:460px; border-top:4px solid var(--warn); }
.cap-msg{ color:var(--ink); font-size:var(--fs-400); margin:var(--sp-3) 0 0; }

/* ---------------------------------------------------------------------------
   LOGIN — the first thing 38 people see every morning.
   --------------------------------------------------------------------------- */
.login{ display:grid; place-items:center; padding:var(--sp-7) 0; }
.login .card{ width:100%; max-width:400px; margin:0; }
.login-brand{ text-align:center; margin-bottom:var(--sp-6); }
.login-brand img{ height:76px; width:auto; max-width:100%; }
/* FIXED 2026-08-13: the sign-in page rendered with ~150px of dead space between every control.
   `.field` carries `flex:1 1 220px` for the two-up `.row` grids, where 220px is a WIDTH. `.login
   form` is `flex-direction:column`, and flex-basis resolves against the MAIN axis — so on this one
   screen 220px became a HEIGHT and each field grew to 220px tall. The margin override below was
   already here and hid nothing, because the gap was never margin. Reset the flex, not the gap:
   `flex:0 0 auto` lets each field be as tall as its label + input and no taller. */
/* `.login .field` (0,2,0) outranks the `.mb-12` / `.mb-16` utilities on the markup (0,1,0), so
   zeroing the margin here silently cancelled them and the Sign in button sat flush against the
   password box. Space the form with a flex `gap` instead: a gap cannot be cancelled by a utility
   losing a specificity fight, and it does not depend on the last child having no trailing margin. */
.login form{ display:flex; flex-direction:column; gap:var(--sp-4); }
.login .field{ margin-bottom:0; flex:0 0 auto; }
.login button.primary{ width:100%; margin-top:var(--sp-2); }

/* The two doors on the landing page (2026-09-08, requirement D4). Links, not tabs driven by a
   script — each door is its own URL, so this is a nav that happens to look like a tab strip.
   Sits above the heading because the heading is what CHANGES between the two: a reader who
   mistypes the door needs to see the choice before they read what they landed on. */
.doors{ display:flex; gap:var(--sp-2); margin:0 0 var(--sp-5); border-bottom:1px solid var(--line); }
.doors a{ padding:var(--sp-2) var(--sp-3); margin-bottom:-1px; color:var(--ink-3);
          text-decoration:none; border-bottom:2px solid transparent; font-weight:600; }
.doors a:hover{ color:var(--ink); }
.doors a.on{ color:var(--ink); border-bottom-color:var(--accent); }
/* ADAPTED: `.creds` is the "accounts are created by your administrator" note,
   not a form wrapper — the delivered rule made it a flex column, which does
   nothing useful to a paragraph. */
.creds{
  font-size:var(--fs-200); color:var(--ink-3); line-height:1.7;
  margin:var(--sp-5) 0 0; padding-top:var(--sp-4); border-top:var(--bw-1) solid var(--line);
}

/* ---------------------------------------------------------------------------
   UTILITIES — these exist so the CSP can forbid inline styles outright. Every
   `style="margin-top:12px"` was a reason to keep `style-src 'unsafe-inline'`,
   and a CSP with unsafe-inline is a CSP that mostly lies.
   Deliberately a SMALL fixed set, not a framework: the point is to delete
   inline styles, not to invent a second styling system competing with the
   semantic classes above.
   --------------------------------------------------------------------------- */
.mt-8{ margin-top:8px; }   .mt-10{ margin-top:10px; } .mt-12{ margin-top:12px; }
.mt-14{ margin-top:14px; } .mt-16{ margin-top:16px; }
.mb-10{ margin-bottom:10px; } .mb-12{ margin-bottom:12px; }
.mb-14{ margin-bottom:14px; } .mb-16{ margin-bottom:16px; }
.my-10{ margin-top:10px; margin-bottom:10px; } .my-14{ margin-top:14px; margin-bottom:14px; }

.push{ margin-left:auto; }
/* ADAPTED: `.wrapping` means FLEX-WRAP here. It is applied to `.nav-links` on the
   reports and security pages so nine pivot-dimension links wrap instead of
   overflowing. The delivered file redefined it as `white-space:normal`, which
   silently removed the wrap and pushed the dimension nav off the page — a
   rename in all but name, and exactly the quiet breakage a shared utility name
   invites. */
.wrapping{ flex-wrap:wrap; }
.nowrap{ white-space:nowrap; }
.w-auto{ width:auto; }
.w-full{ width:100%; }
.w-80{ width:80px; }
.w-140{ width:140px; }
.w-320{ width:320px; max-width:100%; }
/* `[hidden]` must beat any author display. The modals set `display:flex`, and
   several rows are shown and hidden by toggling the attribute from script. */
[hidden],.is-hidden{ display:none !important; }

/* ---------------------------------------------------------------------------
   THE ENTRY LAYOUT SWITCH + THE DAY GRID  (2026-08-18)

   Two layouts for the same eleven fields. Both ship in the markup; `data-entry-view` on <html>
   (set in <head> by view-pref.js, before the first paint) decides which one is shown. The
   pressed state of the switch is driven off the SAME attribute rather than off a class that
   script adds, so the button and the panel can never disagree — including in the frame before
   any script at the bottom of the page has run.
   --------------------------------------------------------------------------- */
.head-tools{ display:flex; align-items:center; gap:var(--sp-3); flex-wrap:wrap; }
.seg{
  display:inline-flex; background:var(--surface);
  border:var(--bw-1) solid var(--line-2); border-radius:var(--radius); overflow:hidden;
}
.seg button{
  border:none; border-radius:0; min-height:36px; padding:0 var(--sp-3);
  font-size:var(--fs-200); font-weight:700; color:var(--ink-2); background:var(--surface);
}
.seg button + button{ border-left:var(--bw-1) solid var(--line); }
.seg button:hover{ background:var(--surface-2); color:var(--ink); }
/* The active pill is green, the same "this is the action layer" green as the nav pill and the
   primary button — a switch that reads as part of the same system rather than a third idiom. */
:root[data-entry-view="form"] .seg [data-view="form"],
:root[data-entry-view="grid"] .seg [data-view="grid"]{
  background:var(--green-bg); color:var(--green-2);
}

:root[data-entry-view="grid"] #panel-form,
:root[data-entry-view="grid"] #panel-added,
:root[data-entry-view="form"] #panel-grid{ display:none; }
/* The grid needs the wide shell; the single form is deliberately narrow (a form is easier to
   read at 940px than at 1600px), so the width follows the LAYOUT, not the page. */
:root[data-entry-view="grid"] .wrap{ max-width:1240px; }

.pseudo-label{
  display:block; font-size:var(--fs-300); font-weight:700; color:var(--ink-2);
}
.day-readout{ margin:0; padding:var(--sp-2) 0; color:var(--ink-3); font-size:var(--fs-400); }
.only-narrow{ display:none; }

/* The table scrolls inside itself; the PAGE never moves sideways. Same treatment as `.scroll-x`
   on the report tables, including the pure-CSS scroll shadows that say the table continues past
   the edge. The dropdowns that hang off every row escape the clip by being positioned in
   viewport coordinates — see `.ta-floating` and typeahead.js's `floating` option. */
.grid-scroll{
  overflow-x:auto; -webkit-overflow-scrolling:touch;
  background:
    linear-gradient(90deg, var(--surface) 30%, rgba(255,255,255,0)) left / 24px 100% no-repeat,
    linear-gradient(90deg, rgba(255,255,255,0), var(--surface) 70%) right / 24px 100% no-repeat,
    radial-gradient(farthest-side at 0 50%, rgba(30,40,51,.12), transparent) left / 12px 100% no-repeat,
    radial-gradient(farthest-side at 100% 50%, rgba(30,40,51,.12), transparent) right / 12px 100% no-repeat;
  background-attachment:local,local,scroll,scroll;
}
/* Positioned from script in VIEWPORT coordinates, so it is not clipped by the scroll container
   its input lives in. `position` and `z-index` are the stylesheet's job; only left/top/width come
   from script (which CSP permits — it governs markup, not the CSSOM setter). */
.ta-list.ta-floating{ position:fixed; z-index:40; }

.grid-table{ min-width:1020px; }
.grid-table td{ padding:var(--sp-2); vertical-align:top; }
/* Controls are shorter than the 44px tap target used everywhere else. That target exists for
   thumbs; this layout is explicitly the laptop one (the phone gets told so, in the card), and at
   44px a six-row grid does not fit on a screen — which would cost the layout the only thing it
   is for. The single form keeps the full target and stays the phone surface. */
.grid-table input,.grid-table select,.grid-table textarea{
  min-height:34px; font-size:var(--fs-300); padding:var(--sp-1) var(--sp-2);
}
.grid-table textarea{ min-height:54px; padding-top:var(--sp-1); }
.grid-table .g-cell > * + *{ margin-top:var(--sp-1); }
.grid-table .g-n{ min-width:44px; text-align:center; color:var(--ink-3); font-weight:700; }
/* FLOORS, not fixed widths. Auto table layout handed the DETAILS column the least room of the
   four wide ones — it holds one textarea where its neighbours hold two controls each — and
   details is the only column whose content is free text somebody has to read back. Percentages
   fixed that and broke the small columns instead (a 136px financial-year select became 87px and
   clipped its own label), because percentage and fixed widths on one table compete for the same
   space. Min-widths let the small columns keep what they need and split the rest by weight.
   The total sits just under the table's own min-width, so nothing here forces a scrollbar. */
.grid-table .c-client{ min-width:190px; }
.grid-table .c-work{ min-width:175px; }
.grid-table .c-leads{ min-width:165px; }
.grid-table .c-details{ min-width:250px; }
.grid-table .g-fy-cell{ min-width:124px; }
.grid-table .g-hours-cell{ min-width:76px; }
.grid-table .g-hours-cell input{ text-align:right; }
/* Two stacked link buttons ("Copy ↑", "Remove"), not a row of them: side by side they need
   ~150px of a table that is already at its width budget, and they wrap into a ragged block the
   moment a translation is longer. */
.grid-table .g-state{ min-width:76px; white-space:nowrap; }
.grid-table .g-state button{
  display:block; width:100%; justify-content:flex-start;
  min-height:26px; padding:0; font-size:var(--fs-100);
}
/* The global row hover is for READING a report. On a grid you are typing into, a row that
   changes colour under the cursor is just movement. */
.grid-table tbody tr:hover{ background:none; }
.grid-table tfoot td{ font-size:var(--fs-400); }
.grid-table tfoot .over{ color:var(--bad); }

tr.g-bad > td{ box-shadow:inset var(--bw-2) 0 0 var(--bad); }
tr.g-saving > td{ opacity:.55; }
tr.g-done > td{ background:var(--surface-2); }
tr.g-logged > td{ background:var(--surface-2); color:var(--ink-3); }
/* The message row belongs to the row above it, so it gives up the border that would otherwise
   draw a line between a row and its own error. */
tr.g-row > td{ border-bottom:none; }
tr.g-msg > td{ padding:0 var(--sp-2) var(--sp-2); }
tr.g-msg .banner{ margin:0; }

/* ---------------------------------------------------------------------------
   MOBILE — the entry form and the week review are the phone surfaces.
   Reports are not; wide tables scroll inside .scroll-x and the page never does.
   --------------------------------------------------------------------------- */
/* ---------------------------------------------------------------------------
   COLLAPSE — below 960px there is not room for three columns, so there are not
   three columns. The rail lies back down as a strip and the pane becomes what it
   describes: a list above the thing it lists.
   --------------------------------------------------------------------------- */
@media (max-width:960px){
  /* Back to a page that scrolls, in one move. Every `overflow` below has to come
     off with it, or the columns keep their own scrollbars inside a page that also
     has one — two scrollbars, and content the outer one cannot reach. */
  body.app{ flex-direction:column; height:auto; min-height:100vh; overflow:visible; }
  body.app .shell{ overflow-y:visible; }
  body.app .wrap{ max-width:none; padding:var(--sp-6) var(--sp-4); }

  .nav{
    flex-direction:row; align-items:center; flex-wrap:nowrap;
    width:auto; padding:var(--sp-2) var(--sp-3); overflow:visible;
    border-right:none; border-bottom:var(--bw-2) solid var(--green);
  }
  .nav-brand{ padding:0 var(--sp-3) 0 0; flex-shrink:0; }
  .nav-brand img{ height:26px; }
  /* The links SCROLL sideways; they do not wrap. Wrapped, nine tiles came to four rows and
     212px of a 812px phone — a quarter of the screen spent on navigation, on every page. One
     row that scrolls is 52px and loses nothing but the promise that you can see all nine at
     once, which was never true on a phone anyway. `min-width:0` is what lets this flex item
     shrink below its content so the scroll happens INSIDE it and not in the page. */
  .nav .nav-links{
    flex-direction:row; flex-wrap:nowrap; padding:0; gap:2px;
    flex:1 1 auto; min-width:0; overflow-x:auto; overscroll-behavior-x:contain;
  }
  .nav .nav-links a{ min-width:58px; flex-shrink:0; }
  .nav .nav-links a.on{ box-shadow:none; }
  .rail-sep{ width:1px; height:28px; margin:0 var(--sp-2); align-self:center; flex-shrink:0; }
  .nav-user{
    flex-direction:row; align-items:center; gap:var(--sp-2);
    margin-top:0; margin-left:var(--sp-3); padding:0; text-align:left; flex-shrink:0;
  }
  .rail-avatar{ width:26px; height:26px; }

  .pane{
    width:auto; border-right:none; border-bottom:var(--bw-1) solid var(--line);
    /* Capped rather than free: a 30-row week above the form would put the form below
       the fold on every load, which is the opposite of what the pane is for. */
    max-height:44vh; overflow-y:auto;
  }
  .pane-foot{ position:static; }
}

@media (max-width:640px){
  .wrap{ padding:var(--sp-4) var(--sp-3); }
  .card{ padding:var(--sp-4); border-radius:var(--radius); }
  .nav{ padding:var(--sp-2) var(--sp-3); gap:var(--sp-2); min-height:0; }
  .nav-brand img{ height:34px; }
  /* The wordmark is a ~4.6:1 lockup, so the 76px desktop height comes to 351px wide — on a 375px
     phone that is edge to edge with 12px of air either side. `max-width:100%` stops it
     OVERFLOWING but cannot stop it crowding; cap the height so it lands around 260px. */
  .login-brand img{ height:56px; }
  /* WRAP, do not overflow. The bar carries up to nine links for somebody who is admin AND a
     reporting partner (the firm is both), and on 2026-08-26 it gained two more when "New entry" and
     "My week" stopped being hidden from admins - at which point the row ran to ~500px on a 375px
     screen and took the whole PAGE sideways with it. `.wrapping` already exists for exactly this
     and is applied by hand on the reports pages; the nav needs it unconditionally on a phone,
     because the number of links is a function of the viewer's roles and cannot be eyeballed once.
     Desktop is untouched - there is room there. */
  /* NOT `flex-wrap:wrap` any more - see the 960px block, which turned the strip into one
     scrolling row precisely so it could stop being four. */
  .nav .nav-links{ gap:0; }
  .nav .nav-links a{ padding:var(--sp-2) var(--sp-1); font-size:10px; min-width:54px; }
  /* The name is the first thing to go: the avatar identifies the seat and "Sign out" is the
     only thing anybody needs to reach from here. */
  .rail-who{ display:none; }
  .nav-user{ font-size:var(--fs-100); }
  .row{ flex-direction:column; gap:0; }
  .field{ flex-basis:auto; }
  .head-row{ flex-direction:column; gap:var(--sp-3); }
  .card-head{ flex-direction:column; gap:var(--sp-2); }
  .stats{ grid-template-columns:repeat(2,1fr); }

  /* Two rows of seven — a week is 6 working days, and the old 14-cell week strip across
     a 375px screen is 23px each: too small to read or to tap. */
  .strip{ grid-auto-flow:row; grid-template-columns:repeat(7,1fr); }

  /* Save must stay reachable without scrolling back up. This is item 14's whole
     point and it is load-bearing on the hot path. */
  .actions{
    position:sticky; bottom:0; z-index:30;
    background:var(--surface); padding:var(--sp-3) 0;
    border-top:var(--bw-1) solid var(--line); margin-top:var(--sp-3);
  }
  .actions button{ flex:1 1 auto; }
  .actions .muted{ display:none; }        /* "Ctrl+Enter to save" is desktop-only */

  /* The typeahead needs room above a phone keyboard — 320px shows barely two rows. */
  .ta-list{ max-height:45vh; }
  /* The × is the only way to undo a filter on a phone, so it gets a real target. */
  .ms-pill > button{ min-width:32px; min-height:32px; font-size:var(--fs-500); }

  .foot-in{ font-size:var(--fs-100); gap:var(--sp-3); }
  .foot img{ height:22px; }
  .foot .sep{ display:none; }
  .foot .right{ margin-left:0; text-align:left; width:100%; }

  .modal{ padding:0; align-items:flex-end; }
  .modal-card{ max-width:none; max-height:92vh; border-radius:var(--radius-lg) var(--radius-lg) 0 0; }

  /* The day grid is a laptop layout and says so here rather than pretending. It is still
     REACHABLE on a phone — somebody who set it on their laptop and then opens the app on the
     train must not find a page that refuses to work — it just scrolls sideways and warns. */
  .only-narrow{ display:block; }
  /* The 36px switch is a desktop density. On a phone it is a thumb target like everything else. */
  .seg{ width:100%; }
  .seg button{ flex:1 1 auto; min-height:var(--tap); }
}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{ transition:none !important; }
}

/* Print — a partner will print the pivot. Drop the chrome, keep the grid crisp. */
@media print{
  .nav,.foot,.toast,.actions,.adder,.presets{ display:none !important; }
  body{ background:#FFFFFF; }
  .card{ box-shadow:none; border:none; padding:0; margin-bottom:var(--sp-4); }
  .scroll-x{ overflow:visible; background:none; }
  table{ font-size:11px; }
  th{ border-bottom:1.5px solid #000; }
  tfoot td,tfoot th,tr.total td,tr.total th{ border-top:1.5px solid #000; background:none; }
}

/* Leave request states (2026-08-22). Same .tag vocabulary as draft/submitted, so a status reads
   the same way wherever it appears — a second badge system would be a second thing to keep in
   agreement with the first. */
.tag.pending{ background:var(--warn-bg); color:var(--warn); border-color:var(--warn); }
.tag.approved{ background:var(--ok-bg); color:var(--ok); border-color:var(--ok); }
.tag.rejected,.tag.cancelled{ background:var(--surface-2); color:var(--ink-3); border-color:var(--line-2); }

/* ---------------------------------------------------------------------------
   DEMO FRONT DOOR — /demo
   The one screen the client's instance does not have. Built entirely from the
   tokens above; no new colour, size or spacing value is introduced here.
   --------------------------------------------------------------------------- */
.demo{ max-width:1080px; margin:0 auto; padding:var(--sp-7) var(--sp-4) var(--sp-8); }

.demo-intro{ padding:var(--sp-7); }
.demo-kicker{
  font-size:var(--fs-100); font-weight:700; letter-spacing:.14em;
  color:var(--green); margin-bottom:var(--sp-3);
}
.demo-intro h1{ margin-bottom:var(--sp-4); }
.demo-lede{ color:var(--ink-2); max-width:64ch; }
.demo-lede + .demo-lede{ margin-top:var(--sp-3); }

/* Four seats since the payroll door was added, so two by two: three columns left the
   fourth alone on a row of its own, and a 3+1 orphan reads as a layout bug on a page whose
   whole job is to look deliberate. They collapse to one below 860px. */
.demo-seats{
  display:grid; grid-template-columns:repeat(2,1fr); gap:var(--sp-4);
  margin-top:var(--sp-4);
}
.demo-seat{ display:flex; flex-direction:column; padding:var(--sp-6); }
.demo-seat h2{ font-size:var(--fs-500); margin-bottom:var(--sp-2); }
.demo-seat__blurb{ color:var(--ink-2); font-size:var(--fs-300); }
.demo-seat__list{
  list-style:none; margin:var(--sp-4) 0 0; padding:0;
  border-top:var(--bw-1) solid var(--line);
}
.demo-seat__list li{
  font-size:var(--fs-200); color:var(--ink-2);
  padding:var(--sp-3) 0 var(--sp-3) var(--sp-5);
  border-bottom:var(--bw-1) solid var(--line);
  position:relative;
}
/* A rust tick rather than a bullet: the action colour is the one that should mark
   "this seat can do this", and it ties the list to the button below it. */
.demo-seat__list li::before{
  content:'';
  position:absolute; left:2px; top:calc(var(--sp-3) + .55em);
  width:8px; height:4px;
  border-left:var(--bw-2) solid var(--green);
  border-bottom:var(--bw-2) solid var(--green);
  transform:rotate(-45deg);
}
/* `margin-top:auto` is load-bearing: without it the three buttons sit at three
   different heights whenever one blurb wraps to an extra line. */
.demo-seat__go{ margin-top:auto; padding-top:var(--sp-5); }

.demo-note{ margin-top:var(--sp-4); padding:var(--sp-6); }
.demo-note h2{ font-size:var(--fs-500); margin-bottom:var(--sp-3); }
.demo-note p{ color:var(--ink-2); max-width:74ch; }
.demo-note p + p{ margin-top:var(--sp-3); }
.demo-note em{ font-style:italic; color:var(--ink); }

.demo-foot{ text-align:center; margin-top:var(--sp-6); font-size:var(--fs-200); }
.demo-backlink{ margin-bottom:var(--sp-4); font-size:var(--fs-200); }

@media (max-width:860px){
  .demo-seats{ grid-template-columns:1fr; }
}
