/* Stampio web UI — design system (ported from st-stampio-poc @ poc-final).
   Tokens + base + shared components. Screen-specific styles are added as the
   worker/admin screens are ported (Steps 2b–2c). Themes: light / dark / dim. */

@font-face { font-family: "Geist"; src: url("fonts/Geist-Variable.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; }
@font-face { font-family: "Geist Mono"; src: url("fonts/GeistMono-Variable.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; }

/* ---- Contrast contract (STAM-170) --------------------------------------------------
   Every ink/surface pairing that carries TEXT in this file, measured. WCAG AA for normal
   text is 4.5:1; figures are light / dark / dim, computed on 8-bit sRGB so they match
   what an ordinary hex contrast checker reports. RECOMPUTE rather than trust this block —
   the claim it replaced said `--muted` stayed readable in all three themes, which was
   false in dim at 3.83:1 and looked entirely fine on screen for three months.

     --text       on --bg --bg-grad --surface --surface-2      9.24 – 16.51   ok
     --muted      on --surface --surface-2                     4.62 –  5.78   ok
     --muted      on --bg --bg-grad                            4.59 –  6.19   ok, see below
     --accent-ink on --accent --accent-2  (.btn-primary
                   .cal-cell.is-today .cal-daynum)             4.82 –  8.89   ok
     --accent-2   on --accent-weak  (.role-pill .flash-ok
                   .status-active .clock-badge.is-in .session.is-forced
                   .cal-bar--holiday)                          4.97 –  6.26   ok
     --text       on the hue-tinted calendar bar (STAM-180)    9.81 – 14.02   ok
                   `.cal-bar--absence`, whose background is
                   color-mix(in oklab, oklch(0.70 0.14 <hue>) 20%, var(--surface)).
                   MEASURED over all NINE palette hues plus the unset fallback (--accent-h),
                   in all three themes — 30 pairings, worst case 9.81:1 in dim at hue 340.
                   scripts/contrast.py does the mix as an Oklab lerp and quantises through
                   `hexof`; re-run it if the 20% or the 0.70/0.14 person colour ever moves.
                   The 3px `.cal-bar-rail` and the `.cal-worked` dot carry NO text and are
                   therefore not rows here, on the same footing as `--faint`'s one border.
     --red        on --red-weak  (.login-err .notice .flash-error
                   .status-syg .session.is-open .flag)     3.95 / 4.59 / 3.68  FAILS light+dim
     --red        on --surface   (.btn-danger)              4.73 / 6.29 / 4.37  FAILS dim

   The two `--red` rows are PRE-EXISTING, are worse than anything STAM-170 fixed, and are
   deliberately NOT fixed here — they have their own ticket. `--red` is also a border and
   an icon colour, so moving it is a whole-token change that must be re-measured against
   every row above, not nudged until one of them clears.

   Surfaces are --bg, --bg-grad, --surface, --surface-2, --accent-weak and --red-weak.
   `--muted` is NOT safe on the last two — 4.27/4.39 on --accent-weak, 4.15/4.20 on
   --red-weak — and no rule puts it there today. Do not be the first. `--text` is safe on
   all six.

   `--muted` on --bg/--bg-grad is held to AA deliberately even though it may not paint
   today: the only rule placing it there (`.sub` bare in `.shell-main`) currently holds
   nothing but links, and an unstyled <a> takes the UA link colour rather than --muted.
   One plain text node changes that, so the token is kept safe rather than the pairing
   argued away.

   `--faint` is NOT a text colour. It failed AA in all three themes on every surface
   (2.13–3.50:1) and styles exactly one thing: the `.stamp-out` left border — decoration,
   not state, since the pill spells out "IND"/"UD". */
:root {
  --font-sans: "Geist", system-ui, -apple-system, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
  --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px; --s7: 48px;
  --r-sm: 8px; --r-lg: 18px;
  --ease: cubic-bezier(.2, .7, .3, 1); --t: 180ms var(--ease);
  --accent-h: 264;
  --bg: oklch(0.984 0.003 95); --bg-grad: oklch(0.972 0.004 95);
  --surface: oklch(1 0 0); --surface-2: oklch(0.975 0.003 260);
  --text: oklch(0.24 0.012 264); --muted: oklch(0.544 0.010 264); --faint: oklch(0.70 0.008 264);
  --border: oklch(0.915 0.004 264); --border-2: oklch(0.875 0.005 264);
  --accent: oklch(0.55 0.145 var(--accent-h)); --accent-2: oklch(0.50 0.150 var(--accent-h));
  --accent-ink: oklch(0.99 0 0); --accent-weak: oklch(0.95 0.035 var(--accent-h));
  --ring: oklch(0.55 0.145 var(--accent-h) / 0.35);
  --red: oklch(0.575 0.165 25); --red-weak: oklch(0.95 0.04 25);
  --shadow-2: 0 2px 6px oklch(0.2 0.02 264 / .06), 0 8px 24px oklch(0.2 0.02 264 / .08);
}
[data-theme="dark"] {
  --bg: oklch(0.165 0.008 264); --bg-grad: oklch(0.185 0.010 264);
  --surface: oklch(0.205 0.011 264); --surface-2: oklch(0.232 0.012 264);
  --text: oklch(0.945 0.005 264); --muted: oklch(0.66 0.010 264); --faint: oklch(0.52 0.010 264);
  --border: oklch(0.305 0.012 264); --border-2: oklch(0.355 0.014 264);
  --accent: oklch(0.70 0.135 var(--accent-h)); --accent-2: oklch(0.76 0.130 var(--accent-h));
  --accent-ink: oklch(0.16 0.01 264); --accent-weak: oklch(0.30 0.060 var(--accent-h));
  --ring: oklch(0.70 0.135 var(--accent-h) / 0.45);
  --red: oklch(0.70 0.150 25); --red-weak: oklch(0.32 0.075 25);
  --shadow-2: 0 2px 8px oklch(0 0 0 / .30), 0 12px 30px oklch(0 0 0 / .40);
}
[data-theme="dim"] {
  --bg: oklch(0.875 0.022 79); --bg-grad: oklch(0.895 0.024 76);
  --surface: oklch(0.948 0.016 78); --surface-2: oklch(0.912 0.018 78);
  --text: oklch(0.305 0.022 64); --muted: oklch(0.475 0.020 66); --faint: oklch(0.660 0.018 70);
  --border: oklch(0.838 0.022 76); --border-2: oklch(0.788 0.026 74);
  --accent: oklch(0.53 0.140 var(--accent-h)); --accent-2: oklch(0.48 0.148 var(--accent-h));
  --accent-ink: oklch(0.99 0 0); --accent-weak: oklch(0.90 0.045 var(--accent-h));
  --ring: oklch(0.53 0.140 var(--accent-h) / 0.34);
  --red: oklch(0.56 0.165 27); --red-weak: oklch(0.90 0.055 32);
  --shadow-2: 0 2px 8px oklch(0.30 0.04 70 / .10), 0 12px 28px oklch(0.30 0.04 70 / .14);
}

*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font-family: var(--font-sans); color: var(--text);
  background: radial-gradient(1200px 700px at 85% -10%, var(--bg-grad), transparent 60%), var(--bg);
  -webkit-font-smoothing: antialiased; }

/* Brand mark — a styled element, no image needed. */
.brand { display: flex; align-items: center; justify-content: center; gap: var(--s3); }
.brand-mark { width: 40px; height: 40px; border-radius: 12px; background: linear-gradient(145deg, var(--accent), var(--accent-2)); position: relative; box-shadow: 0 2px 8px var(--ring); flex: none; }
.brand-mark::after { content: ""; position: absolute; inset: 11px; border-radius: 3px; border: 2.5px solid var(--accent-ink); border-top-color: transparent; border-right-color: transparent; }
.brand-name { font-weight: 600; font-size: 24px; letter-spacing: -0.02em; }

/* Inputs / buttons */
.field { display: flex; flex-direction: column; gap: 6px; }
.field label { font-size: 12px; font-weight: 600; color: var(--muted); letter-spacing: 0.01em; }
.input { height: 38px; padding: 0 var(--s3); border-radius: var(--r-sm); border: 1px solid var(--border-2);
  background: var(--surface); color: var(--text); font-size: 14px; font-family: inherit;
  transition: border-color var(--t), box-shadow var(--t); width: 100%; }
.input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring); }
.input::placeholder { color: var(--muted); }
.btn-primary { height: 42px; justify-content: center; display: inline-flex; align-items: center;
  border-radius: var(--r-sm); border: 1px solid transparent; background: var(--accent); color: var(--accent-ink);
  font-size: 14px; font-weight: 600; font-family: inherit; cursor: pointer; box-shadow: 0 2px 8px var(--ring);
  transition: background var(--t); text-decoration: none; padding: 0 var(--s4); }
.btn-primary:hover { background: var(--accent-2); }
.btn-ghost { height: 38px; display: inline-flex; align-items: center; gap: var(--s2); padding: 0 var(--s4);
  border-radius: var(--r-sm); border: 1px solid var(--border-2); background: var(--surface); color: var(--text);
  font-size: 14px; font-weight: 550; font-family: inherit; cursor: pointer; text-decoration: none;
  transition: border-color var(--t), background var(--t); }
.btn-ghost:hover { border-color: var(--accent); }

/* Login */
.login-stage { min-height: 100vh; display: grid; place-items: center; padding: var(--s5);
  background: radial-gradient(900px 600px at 50% -20%, var(--accent-weak), transparent 55%); }
.login-card { width: 100%; max-width: 380px; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: var(--s7) var(--s6) var(--s6); box-shadow: var(--shadow-2); text-align: center; }
.login-card .brand { margin-bottom: var(--s4); }
.login-lead { color: var(--muted); margin: 0 0 var(--s6); font-size: 14px; }
.login-form { display: flex; flex-direction: column; gap: var(--s4); text-align: left; }
.login-form .btn-primary { margin-top: var(--s2); }
.login-err { display: none; align-items: center; gap: var(--s2); background: var(--red-weak); color: var(--red);
  border-radius: var(--r-sm); padding: var(--s2) var(--s3); font-size: 13px; font-weight: 550; }
.login-err.show { display: flex; animation: shake .4s; }
@keyframes shake { 0%,100%{transform:none} 20%,60%{transform:translateX(-4px)} 40%,80%{transform:translateX(4px)} }

/* App shell (Step 2a scaffold; screens land on top in 2b–2c) */
.topbar { display: flex; align-items: center; justify-content: space-between; gap: var(--s3) var(--s4);
  flex-wrap: wrap; padding: var(--s4) var(--s5); border-bottom: 1px solid var(--border); background: var(--surface); }
.topbar .brand-name { font-size: 20px; }
.topbar-right { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end;
  gap: var(--s2) var(--s4); }
/* Wrap, never squeeze: `Log ud` is an 80px button that flex-shrink pressed to 60px and
   clipped its own label, before the bar itself overflowed the viewport (STAM-76). */
.topbar-right > * { flex: none; }
.topbar .who { color: var(--muted); font-size: 14px; }
.topbar .who strong { color: var(--text); font-weight: 600; }
.role-pill { padding: .1rem .5rem; border-radius: 1rem; font-size: 12px; font-weight: 600;
  background: var(--accent-weak); color: var(--accent-2); }
.shell-main { max-width: 960px; margin: 0 auto; padding: var(--s7) var(--s5); }
.placeholder { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg);
  padding: var(--s6); box-shadow: var(--shadow-2); }
.placeholder h1 { margin: 0 0 var(--s3); font-size: 22px; letter-spacing: -0.02em; }
.placeholder p { color: var(--muted); margin: 0 0 var(--s2); line-height: 1.5; }
.notice { background: var(--red-weak); color: var(--red); border-radius: var(--r-sm);
  padding: var(--s3) var(--s4); font-weight: 550; }

/* Admin roster (Step 2b) */
.sub { color: var(--muted); font-size: 13px; }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg);
  padding: var(--s5) var(--s6); box-shadow: var(--shadow-2); margin-bottom: var(--s5); }
.card h2 { margin: 0 0 var(--s4); font-size: 18px; letter-spacing: -0.01em; }
.flash { border-radius: var(--r-sm); padding: var(--s3) var(--s4); font-weight: 550; margin-bottom: var(--s4); }
.flash-ok { background: var(--accent-weak); color: var(--accent-2); }
.flash-error { background: var(--red-weak); color: var(--red); }
.grid-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--s3);
  align-items: end; container-type: inline-size; }
.grid-form .span-all { grid-column: 1 / -1; }
/* The RFID-tag field is the only one holding TWO controls (input + "Aflæs tag"), so one
   150px track left the input ~49px — too narrow to show the "AA BB CC DD" its own
   placeholder advertises, which defeats the point of a button that fills it from the
   kiosk feed (STAM-76). Two tracks fit both; :has() picks the field out without adding a
   class to the template.
   Queried on the CONTAINER, not the viewport: the same viewport holds a wide form in a
   card and a narrow one in a roster row, and asking for two tracks where only one exists
   invents an implicit track the 150px floor cannot compress (measured: 215px of tracks
   inside a 196px card). 312px is where auto-fit yields that second column — 2 * 150px +
   one 12px gap — and has to be a literal because container queries cannot read custom
   properties; keep it in step with the track floor above. Below it the field is one
   full-width column, which is wider than a track anyway. `container-type: inline-size`
   on .grid-form above is what makes the query resolvable. */
@container (min-width: 312px) {
  .grid-form .field:has(.tag-input) { grid-column: span 2; }
}
.grid-form .field-btn { display: flex; align-items: flex-end; }
.grid-form .field-btn button { width: 100%; }
.emp-card { border: 1px solid var(--border); border-radius: var(--r-lg); padding: var(--s4) var(--s5);
  margin-bottom: var(--s4); background: var(--surface-2); }
.emp-head { display: flex; align-items: center; gap: var(--s3); margin-bottom: var(--s3); }
.emp-head strong { font-size: 15px; }
.emp-card .grid-form { margin-bottom: var(--s3); }
.emp-card .login-form { padding-top: var(--s3); border-top: 1px dashed var(--border); }
.hue-dot { width: 14px; height: 14px; border-radius: 50%; border: 1px solid var(--border-2);
  background: var(--surface); flex: none; }
.delete-form { text-align: right; margin-top: var(--s2); }
.btn-danger { height: 34px; padding: 0 var(--s4); border-radius: var(--r-sm); border: 1px solid var(--border-2);
  background: var(--surface); color: var(--red); font-size: 13px; font-weight: 600; font-family: inherit;
  cursor: pointer; transition: background var(--t), border-color var(--t); }
.btn-danger:hover { background: var(--red-weak); border-color: var(--red); }

/* Worker dashboard (Step 2c) — raw logged time */
.dash-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s4); flex-wrap: wrap; }
.dash-head h1 { margin: 0 0 4px; font-size: 22px; letter-spacing: -0.02em; }
.dash-badges { display: flex; align-items: center; gap: var(--s2); }
.status-pill { padding: .15rem .6rem; border-radius: 1rem; font-size: 12px; font-weight: 600; }
.status-active { background: var(--accent-weak); color: var(--accent-2); }
.status-syg { background: var(--red-weak); color: var(--red); }
.status-fri, .status-ferie { background: var(--surface-2); color: var(--muted); border: 1px solid var(--border-2); }
.clock-badge { padding: .15rem .6rem; border-radius: 1rem; font-size: 12px; font-weight: 600; }
.clock-badge.is-in { background: var(--accent-weak); color: var(--accent-2); }
.clock-badge.is-out { background: var(--surface-2); color: var(--muted); border: 1px solid var(--border-2); }
/* ---- Worker timeline: two layers per day (REQ-029 / STAM-48) -----------------------
   Intent: show what the kiosk RECORDED beside what pairing DERIVED from it, so a gesture
   that earns no hours is still visibly on the page.
   Decisions:
     - The two layers are SIDE BY SIDE on a laptop and STACKED on a phone (STAM-48). They
       are two readings of the same day, so putting them in one column at every width
       invites reading the sessions as more stamps. The collapse rides the existing 700px
       breakpoint rather than adding a second one, and every track is `minmax(0, 1fr)`:
       an implicit `min-width: auto` on a grid child is what turns one long unbreakable
       string into a page-wide horizontal scrollbar, which is the failure
       tests/test_ui_layout.py exists to catch.
     - An uncounted stamp is DASHED and muted, never hidden, struck through or greyed to
       illegibility (STAM-48/REQ-029): it is a real recorded datapoint that simply earned
       no hours, and `--muted` is the token that stays readable in all three themes.
       SUPERSEDED in part (STAM-170): the dashed-not-greyed decision stands and this
       ticket reinforces it, but the readability claim was measured and found FALSE —
       `--muted` was 3.83:1 on `--surface-2` in dim, against WCAG AA's 4.5:1 for normal
       text. Recorded rather than quietly corrected because the claim was believed for
       three months and nothing about the screen looked wrong. It is true as of STAM-170,
       which darkened dim's `--muted` until every surface in every theme clears 4.5:1 —
       a property the token now has rather than one it was assumed to have.
     - `--faint` is retired as a TEXT colour (STAM-170) and the labels here use `--muted`.
       It failed AA in all three themes (2.13–3.50:1), and darkening it enough to pass
       lands it within 0.013–0.018 L of `--muted` in light and dim: the same grey to the
       eye, two tokens to maintain, and a name promising a tier that AA has no room for.
       The layer labels keep their tier through 11px/uppercase/600/letter-spacing, which
       was doing the work anyway.
     - The weekend cue INVERTS its polarity (STAM-170): weekend `.day-date`/`.day-wd` are
       `--text` where they used to be `--faint`. The cue had been built out of the contrast
       deficit itself — weekends were the days too washed out to read — so it could not
       survive the fix in its original direction. It survives in the opposite one, because
       the base row is already `--muted`: weekends now read as EMPHASIS rather than as
       de-emphasis. No new token, no layout change, and 16.51/15.29/11.55 on `--surface`.
       SUPERSEDES this ticket's own first answer, which DELETED the rule on the grounds
       that AA left nothing for the cue to be. That was wrong — it only left nothing in the
       fainter direction — and it is recorded rather than quietly replaced because the
       reasoning error is the reusable part: a contrast fix removes the option of saying
       "less important" with colour, and does nothing to the option of saying "more".
     - An open session is styled with the RED token because it credits zero — the worker
       has to notice. A force-out gets the ACCENT instead: those hours are real and
       credited, so alarming red would be a lie; the flag chip below carries the detail.
     - Flag chips are the only red-weak block on the day card, so the day's anomalies read
       as one channel (REQ-030) rather than as scattered styling.
   Not this: colour-coding by absence type, an editable session (STAM-50), or any new
   colour token — the three themes each define the palette once, and a fourth hue would
   have to be designed in all of them for one chip. */
.day-card { display: flex; flex-direction: column; gap: var(--s3); padding: var(--s3) var(--s5); }
.day-card.is-empty { padding: var(--s2) var(--s5); }
.day-head { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.day-date { font-family: var(--font-mono); font-size: 13px; color: var(--muted); }
.day-wd { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.day-hours { margin-left: auto; font-family: var(--font-mono); font-size: 14px; font-weight: 600; }
.day-card.is-weekend .day-date, .day-card.is-weekend .day-wd { color: var(--text); }
.layers { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--s3) var(--s5); }
.layer-label { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase;
  letter-spacing: .05em; margin-bottom: 6px; }
.stamps, .sessions { display: flex; flex-wrap: wrap; gap: var(--s2); }
.stamp { font-family: var(--font-mono); font-size: 13px; padding: .2rem .55rem; border-radius: var(--r-sm);
  background: var(--surface-2); border: 1px solid var(--border-2); border-left-width: 3px; }
.stamp-in { border-left-color: var(--accent); }
.stamp-out { border-left-color: var(--faint); }
.stamp.is-uncounted { border-style: dashed; color: var(--muted); background: transparent; }
.session { font-family: var(--font-mono); font-size: 13px; padding: .2rem .55rem; border-radius: var(--r-sm);
  background: var(--surface-2); border: 1px solid var(--border-2); display: inline-flex;
  align-items: baseline; gap: var(--s2); }
.session.is-open { background: var(--red-weak); border-color: var(--red); color: var(--red); }
.session.is-forced { background: var(--accent-weak); border-color: var(--accent); color: var(--accent-2); }
.session-note { font-family: var(--font-sans); font-size: 11px; font-weight: 600; }
.flags { display: flex; flex-wrap: wrap; gap: var(--s2); }
.flag { font-size: 12px; font-weight: 600; padding: .15rem .55rem; border-radius: 1rem;
  background: var(--red-weak); color: var(--red); }
.period { display: flex; align-items: center; justify-content: space-between; gap: var(--s3) var(--s4);
  flex-wrap: wrap; margin-top: var(--s4); padding-top: var(--s4); border-top: 1px solid var(--border); }
/* Sans + tabular figures, not the mono face the smaller figures use: at 26px a monospace
   comma sits in a full-width cell and "26,70" reads as "26 , 70" — a pay figure is the last
   place to make someone look twice. Tabular numerals keep the column aligned regardless. */
.period-hours { font-size: 26px; font-weight: 600; letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums; }
.period-nav { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.period-nav .btn-ghost { height: 34px; font-size: 13px; padding: 0 var(--s3); }
.period-range { font-family: var(--font-mono); font-size: 13px; color: var(--muted); }
.unattributed .notice { margin: 0; }
.deferred { opacity: .85; }
.deferred-list { margin: var(--s2) 0 0; padding-left: 1.1rem; color: var(--muted); font-size: 14px; }
.deferred-list li { margin: 4px 0; }
.theme-toggle { font-size: 13px; }

/* ---- Phone widths (STAM-76) -------------------------------------------------------
   Intent: keep every top-bar control — above all `Log ud` — reachable and tappable at
   any width. The first breakpoint in this stylesheet.
   Decisions:
     - The bar wraps; nothing is hidden or collapsed behind a menu (STAM-76). The worker
       dashboard is self-service (REQ-006) and most plausibly read on a phone, so a
       missing log-out leaves one named employee's attendance data on an open session on
       a possibly borrowed device. Wrapping is declared on the base rules too, so the
       range just above this breakpoint degrades instead of clipping.
     - Breakpoint at 700px because that is where the five-item row measurably stops
       fitting (brand 126px + controls 488px + gaps and padding ≈ 678px) — measured, not
       a device guess.
     - "Logget ind som <user>" takes its own line rather than being ellipsised: inside
       the control row it was squeezed to 59px and wrapped to four lines, which told the
       worker nothing and made the bar taller than the fix does.
   Not this: a CSS framework, a build step, a hamburger menu, or any restyling — the only
   defect is that two containers did not fit their contents. */
@media (max-width: 700px) {
  .topbar { padding: var(--s3) var(--s4); }
  .topbar-right { width: 100%; justify-content: flex-start; column-gap: var(--s3); }
  .topbar .who { flex: 1 1 100%; order: -1; }
  /* One column: two 150px-ish layers side by side in a 360px viewport leaves neither
     wide enough for an `07:00–16:00 9,00 t` pill, and the card is read top-down anyway. */
  .layers { grid-template-columns: minmax(0, 1fr); }
  /* `.day-card` only — the shared `.card` gutter at phone widths is a live design
     question (STAM-77), and narrowing it here would decide it for every page by accident
     and move the geometry tests/test_ui_layout.py's known-overflow budget is set against. */
  .day-card, .day-card.is-empty { padding: var(--s3) var(--s4); }
}

/* ---- Calendar month grid (STAM-180 / REQ-043) --------------------------------------
   Intent: one month of the company, drawn as a Monday-first grid whose bars run across
   week boundaries. Appended at the END of this file and restructuring nothing above it —
   every queued calendar ticket appends here, and a restructure turns those into conflicts.
   Decisions:
     - **No new colour token, and no new hue.** The person's colour is the existing
       `oklch(0.70 0.14 <hue>)` expression the roster's hue swatches and the employee page's
       dot already use — one per-person colour scheme in this product, not two. The bar's
       fill is that colour mixed 20% into `--surface`; the head of this file carries the
       measurement for `--text` on every one of the resulting thirty surfaces.
     - **Two axes, deliberately: the LAYER is the fill, the PERSON is the 3px rail and the
       initials.** Nine palette angles cannot identify ten people — two employees already
       share hue 12 at this headcount and an eleventh gets none at all — so colour groups a
       person's days and the initials label them. Removing the initials would make the
       calendar wrong rather than plainer.
     - **A bar's right edge is square unless the span actually ends there.** That is the
       whole visual of "this run continues", and it is `calendar_view`'s edge decision
       rendered rather than a second rule: a span with no known end never reports `ends`, so
       no cell can round it off. There is deliberately NO extra "open-ended" class — it
       would put "this person is off sick" into the DOM as a machine-readable class name, on
       a screen whose one rule is that it says *that* someone is away and never *why*.
     - **Every track is `minmax(0, 1fr)` and every box in the chain sets `min-width: 0`.**
       An implicit `min-width: auto` on a grid child is what turns one long unbreakable name
       into a page-wide horizontal scrollbar — the failure tests/test_ui_layout.py exists to
       catch. The names ellipsise instead.
     - **The one breakpoint is the app's existing 700px** (STAM-76 measured it; a second
       number would be a second thing to keep true). Below it the NAMES go and the initials
       stay: seven columns inside a phone-width card leave ~44px each, which holds a rail
       and two initials and cannot hold "Anna Arbejder".
     - The nav's dead end is a `--muted` SPAN, never an opacity: dimming text with opacity
       moves it off the measured contrast pairing at the head of this file, and `--muted` on
       `--surface` is a pairing that is already in the contract.
   Not this: a day-detail card, a click target, a hover lift, any JavaScript at all, or a
     colour per absence TYPE — the type is not on this screen and must not arrive through a
     stylesheet. */
.cal-head { display: flex; align-items: baseline; gap: var(--s3) var(--s4); flex-wrap: wrap;
  margin-bottom: var(--s4); }
.cal-head h1 { margin: 0; font-size: 22px; letter-spacing: -0.02em; }
.cal-month { color: var(--muted); font-weight: 500; }
.cal-nav { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; margin-left: auto; }
.cal-nav .btn-ghost { height: 34px; font-size: 13px; padding: 0 var(--s3); }
.cal-nav .is-inert { color: var(--muted); cursor: default; }
.cal-nav .is-inert:hover { border-color: var(--border-2); }

/* Hairline grid: a 1px gap over a border-coloured backdrop, so the lines cost no borders. */
.cal-grid { border: 1px solid var(--border); border-radius: var(--r-sm); overflow: hidden;
  background: var(--border); }
.cal-dows, .cal-cells { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 1px; }
.cal-dow { background: var(--surface-2); color: var(--muted); font-family: var(--font-mono);
  font-size: 10px; letter-spacing: .08em; text-transform: uppercase; padding: 6px 8px; }
.cal-cell { display: flex; flex-direction: column; gap: 3px; min-height: 98px; min-width: 0;
  padding: 5px 5px 6px; background: var(--surface); }
.cal-cell.is-weekend { background: var(--surface-2); }
.cal-cell.is-out { background: var(--bg); }
.cal-cell.is-out .cal-daynum { color: var(--muted); }
.cal-daytop { display: flex; align-items: center; gap: 5px; min-height: 18px; }
.cal-daynum { font-family: var(--font-mono); font-size: 12px; font-variant-numeric: tabular-nums; }
.cal-cell.is-today .cal-daynum { background: var(--accent); color: var(--accent-ink);
  border-radius: 4px; padding: 1px 5px; font-weight: 600; }
/* The worked layer: a quiet mark, no figure (Ask, 2026-08-12 — docs/module-calendar.md,
   question C). Decoration, so it carries no contrast row; the title says what it means. */
.cal-worked { width: 5px; height: 5px; border-radius: 50%; flex: none;
  background: color-mix(in oklab, var(--accent) 55%, transparent); }

/* Lanes push to the BOTTOM of the cell, so bars in one week line up whatever sits above
   them. `visibility: hidden` and not `display: none`: the empty lane must still occupy its
   row, which is the whole reason the spacers exist. */
.cal-lanes { display: flex; flex-direction: column; gap: 2px; margin-top: auto; min-width: 0; }
.cal-lane { height: 17px; min-width: 0; }
.cal-lane.is-empty { visibility: hidden; }
.cal-more { font-family: var(--font-mono); font-size: 10px; color: var(--muted); padding-left: 2px; }

.cal-bar { --cal-hue: var(--accent-h); display: flex; align-items: center; gap: 4px;
  height: 17px; min-width: 0; padding: 0 5px 0 4px; border-radius: 4px; overflow: hidden;
  font-size: 11px; line-height: 1; color: var(--text); }
.cal-bar--absence { background: color-mix(in oklab, oklch(0.70 0.14 var(--cal-hue)) 20%, var(--surface)); }
.cal-bar--holiday { background: var(--accent-weak); color: var(--accent-2); }
.cal-bar-rail { width: 3px; flex: none; align-self: stretch; border-radius: 1px;
  background: oklch(0.70 0.14 var(--cal-hue)); }
.cal-bar--holiday .cal-bar-rail { background: var(--accent-2); }
.cal-bar-ini { font-family: var(--font-mono); font-size: 10px; letter-spacing: .02em; flex: none; }
.cal-bar-txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/* Squared inner corners and no rail on a continuation: this is what makes seven separate
   cells read as one bar. The edge names are `calendar_view`'s own. */
.cal-bar.is-middle, .cal-bar.is-end { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.cal-bar.is-start, .cal-bar.is-middle { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.cal-bar.is-middle .cal-bar-rail, .cal-bar.is-end .cal-bar-rail { display: none; }

/* The key travels with the calendar: colour-coded layers are only honest if it does. */
.cal-key { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4); margin-top: var(--s4); }
.cal-key-i { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); }
.cal-key-sw { width: 14px; height: 10px; border-radius: 3px; flex: none; }
.cal-key-sw--absence { background: color-mix(in oklab, oklch(0.70 0.14 var(--accent-h)) 20%, var(--surface));
  box-shadow: inset 3px 0 0 oklch(0.70 0.14 var(--accent-h)); }
.cal-key-sw--holiday { background: var(--accent-weak); box-shadow: inset 3px 0 0 var(--accent-2); }
.cal-key-sw--worked { width: 10px; height: 10px; border-radius: 50%;
  background: color-mix(in oklab, var(--accent) 55%, transparent); }

@media (max-width: 700px) {
  /* Seven columns inside a phone-width card leave ~44px each. The rail and the initials
     fit; a name does not, and an ellipsised "A…" says less than "AA" does. */
  .cal-bar-txt { display: none; }
  .cal-cell { min-height: 76px; padding: 3px 3px 4px; }
  .cal-dow { padding: 5px 4px; font-size: 9px; letter-spacing: .04em; }
  .cal-bar { padding: 0 3px 0 3px; gap: 3px; }
  /* Measured at 420px: "+2 mere" wrapped onto two lines and made its cell taller than the
     six beside it, so one busy day bent the whole week's baseline. */
  .cal-more { font-size: 9px; white-space: nowrap; padding-left: 0; }
  .cal-nav { margin-left: 0; width: 100%; }
}
