/*
 * Just Kickoffz — one stylesheet, no build step.
 *
 * The frame is the point: .shell is exactly the viewport and never scrolls, a list page is a
 * column (bar, grid, pager), and the grid takes whatever height is left. public/js/rows.js
 * measures that and asks the server for exactly the rows that fit, which is why the pager says
 * "30 rows/screen" rather than a fixed 25 — and why dragging a long list never takes the sidebar
 * and the header away with it.
 *
 * Two rules keep that arithmetic honest and are worth knowing before editing anything below:
 * a row's height is DECLARED (--row-h) rather than measured, and cells are one line with an
 * ellipsis. Both exist because measuring a rendered row made the answer depend on the answer —
 * fewer rows meant no scrollbar, a wider page, no wrapping, more rows, and the scrollbar back.
 */
:root{
  --bg:#F1F4F9; --card:#FFFFFF; --side:#F7F9FC; --row-alt:#FAFBFD; --row-hover:#EEF3FC;
  --line:#D5DEEB; --line-soft:#E6ECF5; --ink:#0B1524; --ink-2:#26374F; --muted:#6B7C95;
  --label:#8494AB; --accent:#2563EB; --accent-deep:#1D4ED8; --accent-pale:#EFF4FF;
  --ok:#15803D; --ok-bg:#ECFDF3; --warn:#B45309; --warn-bg:#FFF7ED; --bad:#B91C1C; --bad-bg:#FEF2F2;
  --info:#1D4ED8; --info-bg:#EFF4FF; --wait:#7C3AED; --wait-bg:#F5F0FF;
  --shadow-rgb:11,21,36;
}
*{box-sizing:border-box}
/* `hidden` means hidden, whatever the element's class also says. The browser's own rule is
   [hidden]{display:none} and a user-agent rule loses to any author one — so .filters{display:flex}
   would win and a panel would stay on screen with the attribute faithfully set. */
[hidden]{display:none !important}
body{margin:0;background:var(--bg);color:var(--ink);font:13px/1.5 "Segoe UI",Inter,system-ui,sans-serif;
  font-variant-numeric:tabular-nums}
a{color:var(--accent-deep);text-decoration:none}
a:hover{text-decoration:underline}
h1{font-size:19px;margin:0}
h2{font-size:14px;margin:0 0 10px;letter-spacing:.2px}
h3{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--label);margin:0 0 8px}

/* ---------- shell ---------- */
.shell{display:flex;height:100vh;overflow:hidden}
.side{width:216px;flex:0 0 216px;background:var(--side);border-right:1px solid var(--line);
  display:flex;flex-direction:column;height:100%}
/* One gutter down the whole sidebar: the brand, the group headings, every row and the footer all
   start at the same x. The 6px inset on .nav is what the hover background sits in; 6 + 6 lands
   the text on the same 12px as everything else. The brand block and the top bar are the same
   height so their bottom borders form one unbroken line across the seam. */
.side{--side-gutter:12px;--side-inset:6px;--bar-height:56px}
.brand{display:flex;align-items:center;gap:9px;border-bottom:1px solid var(--line);
  height:var(--bar-height);padding:0 var(--side-gutter);box-sizing:border-box;text-decoration:none}
.brand:hover{text-decoration:none}
.brand .mark{flex:0 0 26px;width:26px;height:26px;display:grid;place-items:center;border-radius:7px;
  background:var(--accent);color:#fff}
.brand .mark svg{display:block}
.brand .name{font-size:14px;font-weight:800;letter-spacing:.3px;color:var(--ink);white-space:nowrap}
.brand .name i{font-style:normal;color:var(--accent)}
.nav{padding:10px var(--side-inset);flex:1;overflow-y:auto}
.nav .grp{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--accent-deep);
  font-weight:800;padding:14px calc(var(--side-gutter) - var(--side-inset)) 6px;
  margin-top:8px;border-top:1px solid var(--line-soft)}
.nav .grp:first-of-type{margin-top:4px;border-top:0}
/* the icon keeps its own column so labels line up whatever shape the glyph is */
.nav .nav-ico{flex:0 0 15px;opacity:.72}
.nav a:hover .nav-ico,.nav a.on .nav-ico{opacity:1}
.nav a{display:flex;align-items:center;gap:8px;border-radius:6px;color:var(--ink-2);font-size:12.5px;
  padding:7px calc(var(--side-gutter) - var(--side-inset))}
.nav a:hover{background:var(--row-hover);text-decoration:none}
.nav a.on{background:var(--accent);color:#fff;font-weight:600}
.nav .n{margin-left:auto;background:rgba(0,0,0,.12);border-radius:9px;padding:0 6px;font-size:10.5px}
.nav a.on .n{background:rgba(255,255,255,.22)}
.nav .n.warn{background:var(--warn-bg);color:var(--warn)}
.nav a.on .n.warn{background:rgba(255,255,255,.9);color:var(--warn)}
.side .foot{border-top:1px solid var(--line);padding:10px var(--side-gutter);font-size:11px;color:var(--muted)}
.main{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column}
/* not sticky — it cannot move, because nothing around it scrolls */
.top{height:var(--bar-height,56px);background:var(--card);border-bottom:1px solid var(--line);
  display:flex;align-items:center;gap:12px;padding:0 18px;flex:0 0 auto;z-index:5}
.top .who{margin-left:auto;text-align:right;font-size:11.5px;color:var(--muted);line-height:1.3}
.top .who b{display:block;color:var(--ink);font-size:12.5px}
.content{padding:18px;flex:1;min-height:0;overflow-y:auto}

/* A LIST PAGE IS A COLUMN, AND THE GRID TAKES WHAT IS LEFT.
   The grid is sized by the frame rather than by its own rows, so its height is the same whether
   the server sent four rows or forty — which is what lets rows.js read it without the measurement
   feeding on its own result. The pager is pushed down rather than placed: margin-top:auto puts it
   against the bottom whatever is above it, so it sits in the same spot on a full page and on a
   page with four rows. */
.content:has(> [data-grid]){display:flex;flex-direction:column;overflow:hidden;gap:0}
.content:has(> [data-grid]) > *{flex:0 0 auto}
.content:has(> [data-grid]) > [data-grid]{flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
.content:has(> [data-grid]) > [data-grid] > .table-wrap{flex:1 1 auto;min-height:0;overflow:auto}
.content:has(> [data-grid]) > .pager{margin-top:auto;padding-top:12px}
/* the same frame one level down, for a grid wrapped in the form that saves it */
.content:has(> form > [data-grid]){display:flex;flex-direction:column;overflow:hidden}
.content:has(> form > [data-grid]) > *{flex:0 0 auto}
.content:has(> form > [data-grid]) > form{flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
.content:has(> form > [data-grid]) > form > [data-grid]{flex:1 1 auto;min-height:0;
  display:flex;flex-direction:column}
.content:has(> form > [data-grid]) > form > [data-grid] > .table-wrap{flex:1 1 auto;min-height:0;overflow:auto}
/* Now that the rows scroll inside the card, the column names have to stay: reading row forty of a
   list whose header has gone is guesswork. */
.content:has(> [data-grid]) thead th,
.content:has(> form > [data-grid]) thead th{position:sticky;top:0;z-index:2}

.crumb{display:flex;align-items:baseline;gap:0;min-width:0;overflow:hidden}
.crumb .up{color:var(--muted);font-size:12px;white-space:nowrap}
.crumb .sep{color:var(--muted);font-size:12px;margin:0 7px}
.crumb .title{color:var(--ink);font-size:15px;font-weight:700;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}

/* one line under the top bar: what this screen is, and what you can do on it */
.pagebar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:12px;min-height:30px}
.pagebar .lead{margin:0;color:var(--muted);font-size:12.5px;min-width:0}
.pagebar .lead b{color:var(--ink-2);font-weight:600}

/* ---------- bits ---------- */
.btn{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);background:var(--card);
  color:var(--ink-2);border-radius:7px;padding:6px 11px;font-size:12.5px;cursor:pointer;font-family:inherit}
.btn:hover{background:var(--row-hover);text-decoration:none}
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
.btn.primary:hover{background:var(--accent-deep)}
.btn.danger{color:var(--bad);border-color:#F3C6C6}
.btn.sm{padding:3px 8px;font-size:11.5px;border-radius:6px}
.btn[disabled]{opacity:.5;cursor:not-allowed}
.btn.is-busy{opacity:.85;cursor:progress}
.spin{display:inline-block;width:11px;height:11px;margin-right:6px;vertical-align:-1px;
  border:2px solid currentColor;border-right-color:transparent;border-radius:50%;
  animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* a segmented switch: several readings of one list, sharing one border so it reads as one control
   rather than as eight buttons that happen to sit together */
.seg{display:inline-flex;flex-wrap:wrap;border:1px solid var(--line);border-radius:7px;
  overflow:hidden;background:var(--card)}
.seg a{padding:6px 11px;font-size:12.5px;color:var(--ink-2);border-right:1px solid var(--line)}
.seg a:last-child{border-right:0}
.seg a:hover{background:var(--row-hover);text-decoration:none}
.seg a.on{background:var(--accent);color:#fff;font-weight:600}
/* a count inside the SELECTED segment: --muted on the accent is nearly the fill's own value, so
   inherit the segment's colour and step back with opacity instead */
.seg a.on .muted{color:inherit;opacity:.78}

.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:14px}
.card + .card{margin-top:14px}
.row{display:flex;gap:14px;align-items:flex-start;flex-wrap:wrap}
.grow{flex:1;min-width:0}
.split{display:flex;gap:14px;align-items:flex-start}
.split .col-main{flex:1;min-width:0}
.split .col-side{width:330px;flex:0 0 330px}
.muted{color:var(--muted)}
.mono{font-family:"Cascadia Mono",ui-monospace,Consolas,monospace;font-size:11.5px}
.right{text-align:right}
.center{text-align:center}
.cardbar{display:flex;align-items:center;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}
.cardbar.bottom{border-bottom:0;border-top:1px solid var(--line)}
.iconbtn{display:inline-grid;place-items:center;width:26px;height:26px;border:1px solid var(--line);
  background:var(--card);color:var(--muted);border-radius:6px;cursor:pointer;padding:0}
.iconbtn:hover{color:var(--bad);border-color:#F3C6C6;background:var(--bad-bg)}
.iconbtn[disabled]{opacity:.35;cursor:not-allowed}
.iconbtn[disabled]:hover{color:var(--muted);border-color:var(--line);background:var(--card)}
.iconbtn svg{display:block}

/* one strip of figures, where a row of cards would cost too much height */
.strip{display:flex;flex-wrap:wrap;align-items:stretch;gap:0;background:var(--card);
  border:1px solid var(--line);border-radius:10px;margin-bottom:12px;overflow:hidden}
.strip .s{display:flex;flex-direction:column;gap:2px;padding:9px 16px;min-width:118px;
  border-right:1px solid var(--line-soft)}
.strip .s:last-child{border-right:0}
.strip .l{font-size:10px;text-transform:uppercase;letter-spacing:.4px;color:var(--label);font-weight:700}
.strip .v{font-size:14px;font-weight:700}
.strip .v.good{color:var(--ok)}
.strip .v.warn{color:var(--warn)}
.strip .v.bad{color:var(--bad)}

/* ---------- tabs ---------- */
.tabs{display:flex;gap:4px;margin-bottom:14px;border-bottom:1px solid var(--line)}
.tabs a{display:flex;align-items:center;gap:7px;padding:8px 14px;border:1px solid transparent;
  border-bottom:0;border-radius:8px 8px 0 0;color:var(--ink-2);font-size:12.5px;margin-bottom:-1px}
.tabs a:hover{background:var(--row-hover);text-decoration:none}
.tabs a.on{background:var(--card);border-color:var(--line);color:var(--ink);font-weight:600}
.tabs .n{background:var(--accent-pale);border:1px solid var(--line);border-radius:9px;padding:0 6px;
  font-size:10.5px;color:var(--muted)}
.tabs a.on .n{background:var(--accent);border-color:var(--accent);color:#fff}
.tabs .tab-actions{display:flex;align-items:center;gap:6px;margin:0 0 6px auto}

/* ---------- multi-select dropdown ---------- */
/* <details> rather than a script: it opens, closes on a second click, and needs nothing loaded */
.multi{position:relative;min-width:190px}
.multi summary{list-style:none;cursor:pointer;border:1px solid var(--line);border-radius:6px;
  padding:5px 9px;background:#fff;display:flex;flex-direction:column;gap:1px;min-height:34px;
  justify-content:center}
.multi summary::-webkit-details-marker{display:none}
.multi summary::after{content:"▾";position:absolute;right:9px;top:9px;color:var(--muted);font-size:11px}
.multi summary:hover{border-color:var(--accent)}
.multi .lbl{font-size:10px;text-transform:uppercase;letter-spacing:.4px;color:var(--label);font-weight:700}
.multi .val{font-size:12.5px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  padding-right:14px}
.multi[open] summary{border-color:var(--accent)}
.multi .panel{position:absolute;z-index:200;top:calc(100% + 3px);left:0;min-width:260px;max-width:360px;
  background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px;
  box-shadow:0 8px 22px rgba(var(--shadow-rgb),.13)}
.multi .filter{margin-bottom:6px}
.multi .opts{max-height:240px;overflow-y:auto;display:flex;flex-direction:column;gap:1px}
.multi .opts label{display:flex;align-items:center;gap:7px;padding:4px 6px;border-radius:5px;
  font-size:12.5px;cursor:pointer;white-space:nowrap}
.multi .opts label:hover{background:var(--row-hover)}
.multi .opts input{width:auto;margin:0}

/* ---------- inline add rows ---------- */
.addline{display:flex;flex-wrap:wrap;gap:8px;align-items:end}
.addline .field{flex:1;min-width:120px}
.addline .field:first-child{flex:2.2}
.inline-check{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--ink-2)}
.inline{display:inline}

/* ---------- toasts: float, never take part in the layout ---------- */
/* A message that takes part in the layout pushes the grid down, which changes how many rows fit,
   which re-pages the list — so saving one row could move the whole screen. */
.toasts{position:fixed;top:64px;right:18px;z-index:60;display:flex;flex-direction:column;gap:8px;
  max-width:440px;pointer-events:none}
.toast{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-radius:9px;
  font-size:12.5px;line-height:1.45;box-shadow:0 10px 26px rgba(var(--shadow-rgb),.18);
  pointer-events:auto;animation:toast-in .16s ease-out}
.toast.ok{background:var(--ok-bg);border:1px solid #BBE5C6;color:var(--ok)}
.toast.bad{background:var(--bad-bg);border:1px solid #F3C6C6;color:var(--bad)}
.toast .x{border:0;background:transparent;color:inherit;opacity:.5;cursor:pointer;font-size:15px;
  line-height:1;padding:0 2px}
.toast .x:hover{opacity:1}
.toast.leaving{opacity:0;transform:translateX(8px);transition:opacity .2s,transform .2s}
@keyframes toast-in{from{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.toast{animation:none}.toast.leaving{transition:none}}
.errors{background:var(--bad-bg);border:1px solid #F3C6C6;color:var(--bad);padding:9px 12px;
  border-radius:8px;margin-bottom:14px;font-size:12.5px}
.errors ul{margin:4px 0 0 16px;padding:0}

/* ---------- form ---------- */
.field{display:flex;flex-direction:column;gap:4px;min-width:0}
.field label{font-size:10.5px;text-transform:uppercase;letter-spacing:.4px;color:var(--label);font-weight:700}
input[type=text],input[type=search],input[type=email],input[type=password],input[type=number],
input[type=date],input[type=url],input[type=tel],select,textarea{
  border:1px solid var(--line);border-radius:6px;padding:6px 9px;font:inherit;font-size:12.5px;
  background:#fff;color:var(--ink);width:100%}
input:focus,select:focus,textarea:focus{outline:2px solid rgba(37,99,235,.25);border-color:var(--accent)}
/* Safari and Chrome give type=search a look of their own — an inset shadow, its own radius and a
   native clear button that is not this app's grey. Reset it to the same field as every other. */
input[type=search]{-webkit-appearance:none;appearance:none}
input[type=search]::-webkit-search-decoration,
input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
textarea{min-height:66px;resize:vertical}
.readonly{border:1px dashed var(--line);border-radius:6px;padding:6px 9px;font-size:12.5px;
  background:var(--accent-pale);color:var(--ink-2)}
/* A field the server rejected. The toast says what went wrong; this says where, so a refused save
   cannot look like a completed one. */
.field.bad>input,.field.bad>select,.field.bad>textarea{border-color:#F3C6C6;background:var(--bad-bg)}
.field.bad>input:focus,.field.bad>select:focus,.field.bad>textarea:focus{
  outline:2px solid rgba(185,28,28,.2);border-color:var(--bad)}
.field .err{font-size:11px;color:var(--bad);line-height:1.35}
.field .hint{font-size:11px;color:var(--muted);line-height:1.35}
.grid2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
.grid3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
.grid4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
.checks{display:flex;flex-wrap:wrap;gap:8px 16px}
.checks label{display:flex;align-items:center;gap:6px;font-size:12.5px;text-transform:none;
  letter-spacing:0;color:var(--ink-2);font-weight:400}
.checks input[type=checkbox]{width:auto}
.formfoot{display:flex;gap:8px;align-items:center;margin-top:14px;padding-top:14px;
  border-top:1px solid var(--line-soft)}

/* ---------- table ---------- */
table{width:100%;border-collapse:collapse}
th{font-size:10.5px;text-transform:uppercase;letter-spacing:.4px;color:var(--label);text-align:left;
  padding:8px 10px;border-bottom:1px solid var(--line);white-space:nowrap;background:var(--card)}
td{padding:7px 10px;border-bottom:1px solid var(--line-soft);font-size:12.5px;vertical-align:middle;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
td.key{max-width:340px;font-weight:600}
td:has(.multi),td:has(select){overflow:visible}
td .sub{color:var(--muted);font-size:11px;margin-left:6px}
td.wrap{white-space:normal}
tbody tr:nth-child(even){background:var(--row-alt)}
tbody tr:hover{background:var(--row-hover)}
td input,td select{padding:4px 7px;font-size:12px}
.table-wrap{overflow-x:auto}
.empty{padding:26px;text-align:center;color:var(--muted)}
.totrow{display:flex;gap:12px;align-items:center;padding:8px 10px;background:var(--accent-pale);
  border-top:1px solid var(--line);font-size:12.5px;font-weight:600}
.totrow .sp{flex:1}
/* an overdue row says so at the edge, so the list scans without reading the date column */
tbody tr.late td:first-child{box-shadow:inset 3px 0 0 var(--bad)}
/* a sub-task row, indented under its parent on the detail page */
td.child .tick{color:var(--muted);margin-right:6px}

/* ---------- pills, chips, kv ---------- */
.pill{display:inline-flex;align-items:center;gap:5px;border-radius:20px;padding:2px 9px;font-size:11px;
  font-weight:600;white-space:nowrap}
.pill::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
/* One tone per meaning, named for the meaning rather than the colour — see the tone() method on
   each enum in app/Enums. A status changing colour is then a one-line edit there. */
.p-ok{background:var(--ok-bg);color:var(--ok)}
.p-bad{background:var(--bad-bg);color:var(--bad)}
.p-warn{background:var(--warn-bg);color:var(--warn)}
.p-info{background:var(--info-bg);color:var(--info)}
.p-progress{background:#E0F2FE;color:#0369A1}
.p-wait{background:var(--wait-bg);color:var(--wait)}
.p-muted{background:#F1F5F9;color:#475569}
.p-archived{background:#F1F5F9;color:#94A3B8}
.pill.sm{font-size:10px;padding:1px 7px}
a.pill{text-decoration:none}
a.pill:hover{filter:brightness(.96);text-decoration:none}
.chip{display:inline-flex;align-items:center;gap:5px;background:var(--accent-pale);
  border:1px solid var(--line);color:var(--ink-2);border-radius:20px;padding:2px 10px;
  font-size:11.5px;margin:0 5px 5px 0}
.chip.plain{background:var(--card)}
.kv{display:flex;justify-content:space-between;gap:10px;padding:5px 0;
  border-bottom:1px solid var(--line-soft);font-size:12.5px}
.kv:last-child{border-bottom:0}
.kv .k{color:var(--muted)}
.kv .v{font-weight:600;text-align:right;min-width:0}
.kv .v.warn{color:var(--warn)}
.kv .v.good{color:var(--ok)}
.kv .v.bad{color:var(--bad)}
.note{font-size:11.5px;color:var(--muted);line-height:1.5;margin:8px 0 0}
.warn-box{background:var(--warn-bg);border:1px solid #F5D9AE;border-left:3px solid var(--warn);
  border-radius:8px;padding:10px 12px;margin:0 0 12px}
.warn-box b{color:var(--warn);display:block;margin-bottom:4px}
.warn-box p{margin:0;color:var(--ink-2);font-size:12px;line-height:1.5}

/* ---------- figures ---------- */
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
.kpi{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
/* A figure that opens the list behind it. It must not read as a link — a row of eight underlined
   accent-coloured numbers is unreadable as figures — so the affordance is the border on hover. */
a.kpi{display:block;color:inherit;text-decoration:none;transition:border-color .12s ease}
a.kpi:hover{border-color:var(--accent);text-decoration:none}
a.kpi:hover .l{color:var(--accent)}
.kpi .l{font-size:10.5px;text-transform:uppercase;letter-spacing:.4px;color:var(--label);font-weight:700}
.kpi .v{font-size:23px;font-weight:700;margin-top:4px}
.kpi .v.warn{color:var(--warn)}
.kpi .v.bad{color:var(--bad)}
.kpi .v.zero{color:var(--muted)}
.bar{height:6px;border-radius:4px;background:var(--line-soft);overflow:hidden;margin:6px 0}
.bar i{display:block;height:100%;background:var(--accent)}
.bar.good i{background:var(--ok)}

/* ---------- the timeline, one entry per line ---------- */
/* Every important change to a work item lands here, and both sides of the portal read the same
   list — filtered, for a client, to the entries marked visible to them. The rail down the left is
   what makes it read as a sequence rather than as a stack of notes. */
.tl{list-style:none;margin:0;padding:0 0 0 18px;position:relative}
.tl::before{content:"";position:absolute;left:4px;top:4px;bottom:4px;width:2px;background:var(--line-soft)}
.tl li{position:relative;padding:7px 0;border-bottom:1px solid var(--line-soft);font-size:12.5px}
.tl li:last-child{border-bottom:0}
.tl li::before{content:"";position:absolute;left:-18px;top:13px;width:10px;height:10px;
  border-radius:50%;background:var(--card);border:2px solid var(--line)}
/* an entry the client can read is marked on the rail itself, so scanning the timeline answers
   "what have they been told" without opening anything */
.tl li.shared::before{border-color:var(--accent);background:var(--accent)}
.tl .who{color:var(--ink-2);font-weight:600}
.tl .when{color:var(--muted);font-size:11px;margin-left:6px}
.tl .what{display:block;color:var(--ink-2);margin-top:2px}
.tl .what .was{color:var(--muted);text-decoration:line-through;margin-right:4px}
.tl .remark{display:block;margin-top:3px;color:var(--ink);white-space:pre-wrap}

/* a comment thread, the same shape but with the body carrying the weight */
.thread{list-style:none;margin:0;padding:0}
.thread li{padding:10px 0;border-bottom:1px solid var(--line-soft)}
.thread li:last-child{border-bottom:0}
.thread .head{display:flex;align-items:center;gap:8px;margin-bottom:4px}
.thread .body{white-space:pre-wrap;color:var(--ink);font-size:12.5px;line-height:1.55}
.thread li.internal{background:var(--warn-bg);border-radius:8px;padding:10px 12px;
  border-bottom:0;margin-bottom:8px}

/* files, one line each, with the size and who put it there */
.files{list-style:none;margin:0;padding:0;font-size:12.5px}
.files li{display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--line-soft)}
.files li:last-child{border-bottom:0}
.files .nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.files .sz{color:var(--muted);font-size:11px;white-space:nowrap}

/* ---------- filters on the page bar, one line ---------- */
/* A card of filters is about 110px of a screen whose whole job is showing rows, and the fields do
   not need it: a status is one word and a client is a short name. Laid out inline beside the
   page's own note they cost nothing, and the table gets the rows back.
   No Filter button either — a select applies when it changes and the search box when you stop
   typing. The hidden submit is what makes Enter work without a script. */
.filters{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:0}
/* Longhands throughout, never the `background` shorthand: the search box carries its magnifier as
   a background image, and a shorthand here would reset background-repeat and tile a 13px icon
   across the box. */
.filters input,.filters select{width:auto;height:30px;padding:4px 8px;font-size:12px;
  border-radius:6px;background-color:var(--card)}
.filters input{min-width:150px}
.filters select{max-width:200px;cursor:pointer}
/* an active filter is tinted, and again only its colour is touched */
.filters select:not(:has(option:checked[value=""])){border-color:var(--accent);
  background-color:var(--accent-pale);font-weight:600}
.filters input:not([value=""]){border-color:var(--accent);background-color:var(--accent-pale)}
/* the search always comes first and always looks the same: leftmost, with the magnifier inside */
.filters input.search{order:-1;min-width:230px;padding-left:28px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%236B7C95' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='7.2' cy='7.2' r='4.2'/%3E%3Cpath d='M10.4 10.4l3 3'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:9px 50%;background-size:13px 13px}
.filters input.search:focus{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%231D4ED8' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='7.2' cy='7.2' r='4.2'/%3E%3Cpath d='M10.4 10.4l3 3'/%3E%3C/svg%3E")}
.filters .hidden-submit{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}
/* An <input type=checkbox> carries value="1" ticked or not, so it would collect all of the rules
   above and come out as a large accent-filled square. Reset what does not belong on a tick. */
.filters input[type="checkbox"]{width:14px;min-width:0;height:14px;padding:0;flex:none;
  border:0;background:none;accent-color:var(--accent);cursor:pointer}
.filters .inline-check{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;
  font-size:12px;color:var(--ink-2);cursor:pointer;padding:0 2px}
.filters .clear{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;
  border-radius:50%;color:var(--muted);font-size:15px;line-height:1;text-decoration:none}
.filters .clear:hover{background-color:var(--bad-bg);color:var(--bad);text-decoration:none}

/* ---------- pager ---------- */
/* The links carry `from` — a row offset — not a page number, because the number of rows on a
   screen is not a constant. See resources/views/vendor/pagination/kickoffz.blade.php. */
.pager{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:12px;font-size:12px}
.pager a,.pager .on,.pager .off{padding:3px 9px;border-radius:6px;border:1px solid var(--line);
  background:var(--card);color:var(--ink-2);line-height:1.6}
.pager a:hover{background:var(--row-hover);text-decoration:none}
.pager .on{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
.pager .off{color:var(--muted);opacity:.6}
.pager .count{margin-left:auto;border:0;background:transparent;color:var(--muted)}

/* ---------- what a page is for, behind an icon ---------- */
/* The text used to be a paragraph at the top of every screen. On a grid page that is not free:
   the page bar counts towards the height the row count is measured against, so a three-line
   explanation costs the reader two rows of their data, every visit, forever. */
.info{position:relative;display:inline-block}
.info summary{list-style:none;cursor:pointer;display:inline-flex;align-items:center;
  justify-content:center;width:24px;height:24px;border-radius:50%;color:var(--muted);
  border:1px solid transparent}
.info summary::-webkit-details-marker{display:none}
.info summary:hover{color:var(--accent);border-color:var(--line);background:var(--card)}
.info[open] summary{color:var(--accent);border-color:var(--accent)}
.info .info-pop{position:absolute;z-index:300;top:calc(100% + 6px);left:0;width:min(440px,80vw);
  background:var(--card);border:1px solid var(--line);border-radius:8px;padding:11px 13px;
  box-shadow:0 10px 30px rgba(var(--shadow-rgb),.13);font-size:12.5px;line-height:1.55;
  color:var(--muted)}
.info .info-pop b{color:var(--ink)}

/* ---------- sign in ---------- */
/* Two panels: what this is, and the way in. The left collapses on a narrow screen so a phone gets
   the form and nothing else. */
.auth{min-height:100vh;display:grid;grid-template-columns:1.1fr 1fr}
.auth-pitch{background:var(--side);border-right:1px solid var(--line);padding:44px 48px;
  display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center}
.auth-brand{display:flex;flex-direction:column;align-items:center;gap:14px;margin-bottom:28px}
.auth-brand .mark{width:74px;height:74px;border-radius:18px;display:grid;place-items:center;
  background:var(--accent);color:#fff}
.auth-brand .name{font-size:26px;font-weight:800;letter-spacing:.4px}
.auth-brand .name i{font-style:normal;color:var(--accent)}
.auth-pitch h1{font-size:24px;line-height:1.35;max-width:15em;margin-bottom:24px;color:var(--ink)}
/* the points stay left-aligned inside the centred column — centred body text is hard to read */
.auth-points{list-style:none;margin:0;padding:0;max-width:30em;text-align:left}
.auth-points li{display:flex;gap:11px;align-items:flex-start;margin-bottom:15px;
  font-size:13px;line-height:1.55;color:var(--ink-2)}
.auth-points svg{flex:0 0 15px;margin-top:2px;color:var(--accent)}
.auth-points b{color:var(--ink)}
.auth-foot{margin-top:34px;font-size:11.5px;color:var(--muted);letter-spacing:.3px}
.auth-foot b{color:var(--ink-2);font-weight:700}
.auth-form{display:grid;place-items:center;padding:32px 24px}
.auth-form .box{width:100%;max-width:352px}
.auth-form h2{font-size:20px;margin-bottom:2px}
.auth-form .sub{color:var(--muted);font-size:12.5px;margin-bottom:20px}
.auth-form .field{margin-bottom:13px}
.auth-form .btn{width:100%;justify-content:center;margin-top:6px}
.auth-form .checks{margin:2px 0 4px}
.auth-help{margin-top:18px;padding-top:14px;border-top:1px solid var(--line-soft);
  font-size:11.5px;line-height:1.5;color:var(--muted)}
@media (max-width:820px){
  .auth{grid-template-columns:1fr}
  .auth-pitch{display:none}
  .auth-form{padding:28px 20px}
}

/* ---------- how tall a row is, declared once ---------- */
/* public/js/rows.js reads this instead of measuring the tallest rendered row. Measuring made the
   answer depend on the answer — see the note at the top of this file. Screens whose rows carry
   form controls are taller and say so. If a row does render taller than it claims the page scrolls
   by a row, which is the better failure. */
[data-grid]{--row-h:36px}
[data-grid][data-rowsize="buttons"]{--row-h:42px}
[data-grid][data-rowsize="controls"]{--row-h:46px}
[data-grid][data-rowsize="pickers"]{--row-h:54px}
/* And the row is held to it: td is already nowrap with an ellipsis, so pinning the height makes
   what renders equal to what was declared rather than close to it. */
[data-grid] tbody tr{height:var(--row-h)}

/* ---------- the last ten per cent ---------- */
/* Nothing here changes a size or a position — the grid arithmetic above depends on both. */
*{scrollbar-width:thin;scrollbar-color:var(--line) transparent}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--line);border-radius:8px;
  border:2px solid transparent;background-clip:content-box}
::-webkit-scrollbar-thumb:hover{background:var(--label);background-clip:content-box}

/* ---------- the sidebar as a rail, once the screen is a tablet's ---------- */
.railtoggle{display:none;flex:0 0 auto;margin-right:2px}
.railtoggle svg{display:block}
/* .iconbtn goes red on hover because everywhere else it is a delete bin; opening a menu is not
   that kind of button */
.railtoggle:hover{color:var(--accent-deep);border-color:var(--line);background:var(--accent-pale)}
/* 216px of menu out of a 1024px tablet is a fifth of the screen spent on where you are not, and
   these are table pages: the width belongs to the columns. The panel floats over the page rather
   than pushing it — widening a flex item reflows the content beside it, and on a grid page a
   reflow re-measures how many rows fit, so merely hovering the menu would re-page the table. */
@media (max-width:1024px){
  .shell{--rail:52px;padding-left:var(--rail)}
  .side{position:fixed;top:0;left:0;bottom:0;width:var(--rail);flex:0 0 auto;
    overflow:hidden;z-index:40;transition:width .16s ease,box-shadow .16s ease}
  .side.open{width:216px;box-shadow:6px 0 20px rgba(var(--shadow-rgb),.18)}
  /* one gutter for both states, so nothing moves sideways when the panel opens */
  .side{--side-gutter:18px}
  .brand{padding:0 13px}
  .side:not(.open) .brand .name{visibility:hidden}
  /* The label leaves the layout rather than being clipped by the edge of the rail: a menu of
     half-words reads as a page that failed to load. Out of flow, so the row is still announced
     as "Waiting for client" to a screen reader and still findable by find-in-page. */
  .nav{overflow-x:hidden}
  .nav a{position:relative;white-space:nowrap}
  .side:not(.open) .nav .lbl{position:absolute;width:1px;height:1px;margin:0;padding:0;
    overflow:hidden;clip-path:inset(50%);white-space:nowrap}
  .side:not(.open) .nav{scrollbar-width:none}
  .side:not(.open) .nav::-webkit-scrollbar{display:none}
  /* a group heading clipped to 52px says "WOR" and "ACC", which is worse than saying nothing */
  .side:not(.open) .nav .grp{height:0;padding:9px 0 0;overflow:hidden;color:transparent}
  /* the count kept as a dot: the number has nowhere to go in a rail, but "there is something
     here" is the half of it worth keeping */
  .side:not(.open) .nav .n{position:absolute;top:5px;right:6px;width:7px;height:7px;padding:0;
    margin:0;border-radius:50%;background:var(--accent-deep);color:transparent;font-size:0;
    overflow:hidden}
  .side:not(.open) .nav a.on .n{background:#fff}
  /* visibility, not display: the footer keeps its space so the rail ends on the same hairline */
  .side:not(.open) .foot{visibility:hidden}
  .side .foot{white-space:nowrap}
  .railtoggle{display:inline-grid}
}
/* Touch has no walking away from a menu: the tap that dismisses it would otherwise also press
   whatever it landed on, so a scrim takes that one. A pointer gets no scrim — it would flash on
   and off every time the mouse crossed the menu on its way somewhere else. */
@media (max-width:1024px) and (hover:none){
  .side.open ~ .main::before{content:'';position:fixed;inset:0;left:var(--rail);
    background:rgba(var(--shadow-rgb),.3);z-index:30}
}
@media (prefers-reduced-motion:reduce){.side{transition:none}}

@media (max-width:860px){
  .split{flex-direction:column}
  .split .col-side{width:100%;flex:1 1 auto}
  .grid3,.grid4{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:560px){
  .grid2,.grid3,.grid4{grid-template-columns:1fr}
  .content{padding:12px}
}

/* ---------- a column header that sorts ---------- */
/* The header IS the control, so it must not read as a link: a row of eleven underlined accent
   words is unreadable as column names. The affordance is the arrow, and it only appears under the
   pointer except on the column actually in force. */
th:has(.sorter){padding:0}
.sorter{display:flex;align-items:center;gap:5px;padding:8px 10px;color:var(--label);
  font:inherit;font-size:10.5px;text-transform:uppercase;letter-spacing:.4px;font-weight:700}
.sorter:hover{color:var(--accent);background:var(--accent-pale);text-decoration:none}
.sorter.on{color:var(--accent)}
.sorter .hint{opacity:0;transition:opacity .12s ease}
.sorter:hover .hint{opacity:.55}
th.right .sorter{justify-content:flex-end}
th.center .sorter{justify-content:center}

/* ---------- type-to-search over a long list ---------- */
/* A project picker grouped by client is a wall once there are more than a few clients, so this
   filters as you type. The list goes position:fixed when open (public/js/combo.js places it),
   because a dropdown inside a table cell is clipped by the cell and again by the grid's scrolling
   wrapper — the same escape the multi-select needs. */
.combo{position:relative;min-width:230px}
.combo .combo-input{width:100%;padding-right:26px}
.combo .combo-clear{position:absolute;right:6px;top:50%;transform:translateY(-50%);border:0;
  background:none;color:var(--muted);font-size:16px;line-height:1;cursor:pointer;padding:2px 4px}
.combo .combo-clear:hover{color:var(--ink)}
.combo .combo-list{position:absolute;z-index:300;top:calc(100% + 4px);left:0;min-width:280px;
  background:var(--card);border:1px solid var(--line);border-radius:8px;padding:4px;
  box-shadow:0 12px 34px rgba(var(--shadow-rgb),.16);overflow:auto;max-height:320px}
/* the group heading sticks, so scrolling a long list never leaves you wondering whose project
   you are looking at */
.combo .combo-group{font-size:10px;text-transform:uppercase;letter-spacing:.5px;font-weight:700;
  color:var(--label);padding:7px 8px 4px;position:sticky;top:0;background:var(--card)}
.combo .combo-opt{display:flex;gap:7px;align-items:baseline;padding:5px 8px;border-radius:5px;
  cursor:pointer;font-size:12.5px;white-space:nowrap}
.combo .combo-opt b{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11.5px;color:var(--muted)}
.combo .combo-opt .nm{color:var(--ink);overflow:hidden;text-overflow:ellipsis}
/* .on is the keyboard cursor, :hover the pointer — one appearance for both, so arrowing down and
   moving the mouse do not look like two different controls */
.combo .combo-opt:hover,.combo .combo-opt.on{background:var(--accent);color:#fff}
.combo .combo-opt:hover b,.combo .combo-opt.on b,
.combo .combo-opt:hover .nm,.combo .combo-opt.on .nm,
.combo .combo-opt:hover .muted,.combo .combo-opt.on .muted{color:#fff}
.combo .combo-none,.combo .combo-hint{margin:6px 8px;color:var(--muted);font-size:12px}
.combo.busy .combo-input{opacity:.75}
/* inside a form grid the wrapper must not carry a width of its own: it sits in a column that is
   already sized, and a wrapper wider than its column spills over whatever comes next */
.field .combo,td .combo{width:100%;min-width:0}
