/* Shared chrome for kpi.twtsc.org. Cached 7 days by Caddy — bump ?v= in every page on change.
   Lifted from platform-ui's platform.css so the two portals look like one system; the
   KPI-specific block is at the bottom. */
:root{
  --bg:#F4F6F8;--card:#fff;--ink:#242c33;--mut:#6b7480;--line:#dde3ea;
  --brand:#732034;--brand-dk:#5a1828;--brandink:#fff;
  --good:#198754;--warn:#b7791f;--bad:#b02a37;
  --shadow:0 2px 8px rgba(0,0,0,.08);--radius:14px;
}
*{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--ink);font-size:14px}
header{background:var(--brand);color:#fff;border-bottom:1px solid var(--brand-dk);padding:.7rem 1.2rem;display:flex;align-items:center;gap:1rem;position:sticky;top:0;z-index:5;box-shadow:var(--shadow)}
header h1{font-size:1.05rem;margin:0;font-weight:800;letter-spacing:.4px;display:flex;align-items:center;gap:.5rem}
.grow{flex:1}
.who{color:rgba(255,255,255,.85);font-size:.85rem}
.topnav{display:flex;gap:.2rem;margin-left:1rem}
.topnav a{color:rgba(255,255,255,.82);text-decoration:none;font-weight:700;font-size:.85rem;padding:.3rem .7rem;border-radius:999px}
.topnav a:hover{background:rgba(255,255,255,.12);color:#fff}
.topnav a.active{background:rgba(255,255,255,.2);color:#fff}
button{font:inherit;cursor:pointer;border:2px solid var(--line);background:var(--card);color:var(--ink);border-radius:10px;padding:.45rem .8rem;font-weight:700}
button.primary{background:var(--brand);color:var(--brandink);border-color:var(--brand)}
header button{background:rgba(255,255,255,.12);color:#fff;border-color:rgba(255,255,255,.5)}
select,input{font:inherit;border:2px solid var(--line);border-radius:10px;padding:.4rem .55rem;background:#fff;color:var(--ink)}
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:1rem 1.1rem;box-shadow:var(--shadow)}
.hide{display:none !important}
.msg{font-size:.85rem;color:var(--mut);margin-top:.5rem;min-height:1rem}
.err{color:var(--bad)}
/* Scoped to .msg deliberately: a bare .warn would also catch .kpi.warn and recolour the tile
   container, not just its value. Used when a push SUCCEEDED but shipped fewer units than were
   approved — there is no .ok class, so a clamped result would otherwise look identical to a
   clean one. */
.msg.warn{color:var(--warn);font-weight:600}
.wrap{max-width:1200px;margin:0 auto;padding:1.4rem 1.2rem}

/* B3 — declared breakpoints, never auto-fit. auto-fit derives the column count from the container
   width, so the same page renders 3 tiles on one laptop and 5 on another and nobody can describe
   the layout. 1 phone → 2 tablet → 4 desk, stated. Pages carry 4–7 tiles; 5 wraps to 4+1 on purpose
   rather than silently re-flowing every tile at an unpredictable width. */
.kpis{display:grid;grid-template-columns:1fr;gap:.8rem;margin-bottom:1.2rem}
@media(min-width:600px){.kpis{grid-template-columns:repeat(2,1fr)}}
@media(min-width:1000px){.kpis{grid-template-columns:repeat(4,1fr)}}
.kpi{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:.8rem 1rem;box-shadow:var(--shadow)}
.kpi .k{font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--mut)}
.kpi .v{font-size:1.35rem;font-weight:800;margin-top:.2rem}
.kpi .s{font-size:.72rem;color:var(--mut);margin-top:.1rem}
.kpi.warn .v{color:var(--warn)}
.kpi.bad .v{color:var(--bad)}

/* Tables */
.table-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:var(--radius);background:var(--card);box-shadow:var(--shadow)}
/* C20/C21 — a sticky <th> needs a scroll container with a BOUNDED height or it has no scroll room
   and the freeze is decorative. .table-wrap alone is content-height, so opt in with .scroll on any
   table whose body can grow. Opt-in, not global: nine other pages render short tables where an
   inner scrollbox would be worse than the page scroll they have today.
   ⚠️ C21: test every sticky-header table at 1 and 2 rows — a sticky th in an overflow container
   can pin over the first body rows and make a 1-row result read as "no data". */
.table-wrap.scroll{max-height:min(62vh,620px);overflow-y:auto}
table{border-collapse:collapse;width:100%;font-size:.83rem}
th,td{padding:.5rem .7rem;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap}
/* z-index is not optional (C20): without it body rows paint THROUGH the sticky header on scroll. */
th{background:#eef3f7;font-size:.7rem;text-transform:uppercase;letter-spacing:.04em;color:var(--mut);position:sticky;top:0;z-index:2}
td.num,th.num{text-align:right;font-variant-numeric:tabular-nums}
tbody tr:hover{background:#f7fafc}

/* C6 — a sortable header is a real <button>, so it is reachable by keyboard and announced as a
   control. The arrow is ALWAYS visible (a hover-only affordance is undiscoverable and dead on a
   touch screen); it is faint until the column is the active sort. */
th.sortable{padding:0}
th.sortable>button{width:100%;background:none;border:0;border-radius:0;padding:.5rem .7rem;font:inherit;
  font-size:.7rem;text-transform:uppercase;letter-spacing:.04em;color:var(--mut);font-weight:700;
  cursor:pointer;display:flex;align-items:center;gap:.35rem;text-align:left}
th.sortable.num>button{justify-content:flex-end}
th.sortable>button:hover{background:#e3ebf2;color:var(--ink)}
th.sortable .arw{font-size:.62rem;opacity:.35}
th.sortable[aria-sort="ascending"] .arw,
th.sortable[aria-sort="descending"] .arw{opacity:1;color:var(--brand)}
th.sortable[aria-sort="ascending"]>button,
th.sortable[aria-sort="descending"]>button{color:var(--ink)}

/* C32 — loading is skeleton rows at the real column count, so the header and the layout hold still.
   A spinner replacing the table jumps the page and loses the column headings you were reading. */
.skel{display:block;height:.62rem;border-radius:4px;background:#e6ecf2}
tr.skel-row td{padding:.55rem .7rem}
@media(prefers-reduced-motion:no-preference){
  .skel{background:linear-gradient(90deg,#e6ecf2 25%,#f2f6fa 37%,#e6ecf2 63%);background-size:400% 100%;
    animation:skelshine 1.4s ease-in-out infinite}
  @keyframes skelshine{0%{background-position:100% 50%}100%{background-position:0 50%}}
}

/* C13–C17 — pagination bar. The count line is mandatory and never hidden (C15). */
.pager{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;padding:.55rem .7rem;
  border-top:1px solid var(--line);background:#fbfcfd;font-size:.78rem;color:var(--mut)}
.pager .count{font-weight:700;color:var(--ink)}
.pager button{padding:.25rem .6rem;font-size:.78rem;border-width:1px}
.pager button[disabled]{opacity:.45;cursor:not-allowed}
.pager select{padding:.2rem .4rem;font-size:.78rem;border-width:1px}

h2{margin:1.4rem 0 .7rem;font-size:1rem;color:var(--brand)}
h2:first-child{margin-top:0}
.toolbar{display:flex;gap:.6rem;align-items:center;margin-bottom:1rem;flex-wrap:wrap}
.muted{color:var(--mut)}
@media(max-width:700px){.topnav{display:none}}

/* G4 — WCAG 2.2 AA 2.4.11/2.4.13. There was NO focus style anywhere on this portal: a keyboard user
   could not see where they were on any of the 10 pages. :focus-visible, not :focus, so a mouse click
   does not leave a ring behind. The dark variant is for controls sitting on the maroon header. */
:where(a,button,select,input,textarea,[tabindex]):focus-visible{
  outline:3px solid var(--brand);outline-offset:2px;border-radius:6px}
header :where(a,button):focus-visible,
.topnav a:focus-visible{outline:3px solid #fff;outline-offset:2px}
/* Some browsers drop the default ring once any outline rule exists — restore it explicitly. */
:where(a,button,select,input):focus:not(:focus-visible){outline:none}

/* C22 — a secondary value under a primary one, in ONE cell rather than two columns. Shared because
   three pages now render it and a fourth will; a page-local copy is how two of them end up with
   different line-heights. */
.sub{display:block;font-size:.7rem;color:var(--mut);font-weight:400;margin-top:.1rem;
  white-space:normal;line-height:1.3}


/* =============================================================================================
   KPI module
   ============================================================================================= */

/* 🔴 NO DATA IS NOT A BLANK AND NOT A ZERO (doctrine 1). It gets its own treatment — struck-through
   weight, muted, in small caps — because a blank cell and a "0" both read as an answer, and the
   whole point of this state is that nobody answered. */
.nodata{font-size:.7rem;font-weight:800;letter-spacing:.04em;color:var(--mut);
  background:#eef1f4;border:1px dashed #c6cfd8;border-radius:6px;padding:.05rem .35rem}

/* RAG. `.none` is deliberately its own colour and not simply absent: an uncoloured cell in a status
   column reads as "fine" to everybody who has ever seen a status table. */
.rag{display:inline-block;font-size:.68rem;font-weight:800;border-radius:999px;padding:.12rem .55rem;
  border:1px solid transparent;white-space:nowrap}
.rag.green{background:#e4f3ea;color:#14663f;border-color:#b6ddc6}
.rag.amber{background:#fdf3e0;color:#8a5a08;border-color:#efd6a3}
.rag.red{background:#fbe9ea;color:#8e1f29;border-color:#eec1c5}
.rag.none{background:#eef1f4;color:var(--mut);border-color:#d6dee6}

/* The entry card — one sub-KPI, its figure, and the two sentences. Not a table row: the commentary
   is the point, and a textarea inside a table cell is unusable on a phone, which is where a weekly
   update actually gets written. */
.entry{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);
  padding:.9rem 1rem;box-shadow:var(--shadow);margin-bottom:.8rem}
.entry.locked{background:#fbfcfd}
/* 🔴 NOT SCOPED TO `.entry`, AND THAT IS THE FIX RATHER THAN THE SLOPPINESS.
   These five were written as `.entry .ehead` and the admin screen uses the same markup WITHOUT an
   `.entry` wrapper — so its header rendered with no flex, no gap and no type scale, and
   "Fill Rate" / "FILL_RATE" / "owner tapasvi@…" ran together into one unreadable string. TJ found it
   on the first KPI he created. A header component that only works inside one container is not a
   component; either the CSS is unscoped or the markup carries the wrapper, and unscoping is the
   honest one because three screens already use this header. */
.ehead{display:flex;align-items:baseline;gap:.6rem;flex-wrap:wrap;margin-bottom:.6rem}
.ename{font-weight:800;font-size:.95rem}
.ekpi{font-size:.72rem;color:var(--mut);font-weight:700;text-transform:uppercase;letter-spacing:.05em}
.etag{font-size:.68rem;font-weight:800;border-radius:999px;padding:.1rem .5rem;
  background:#eef3f7;color:var(--mut);border:1px solid var(--line)}
.fields{display:grid;grid-template-columns:1fr;gap:.7rem}
@media(min-width:760px){.fields{grid-template-columns:repeat(2,1fr)}}
.wide{grid-column:1/-1}
.entry textarea{font:inherit;width:100%;border:2px solid var(--line);border-radius:10px;
  padding:.5rem .6rem;background:#fff;color:var(--ink);min-height:62px;resize:vertical}
.entry input[type=number],.entry input[type=text],.entry input[type=date]{width:100%}
.computed{font-size:1.15rem;font-weight:800}
.foot{display:flex;align-items:center;gap:.7rem;flex-wrap:wrap;margin-top:.7rem;
  padding-top:.7rem;border-top:1px solid var(--line);font-size:.76rem;color:var(--mut)}

/* The KPI header on the owner's screen — and the ONLY place the report link is asked for.
   It sits OUTSIDE the period cards on purpose: the link belongs to the KPI, not to the week, and a
   field rendered inside a weekly card reads as part of that week's entry however the data is
   actually stored. TJ read it exactly that way, which is the only reading the layout supported. */
.ghead{background:var(--card);border:1px solid var(--line);border-top:3px solid var(--brand);
  border-radius:var(--radius) var(--radius) 0 0;padding:.7rem 1rem;box-shadow:var(--shadow);
  margin-bottom:-1px}
.ghead .gname{font-weight:800;font-size:1rem;letter-spacing:-.01em}
.ghead .grow-row{display:flex;gap:.6rem;align-items:center;flex-wrap:wrap;margin-top:.45rem}
.ghead input[type=url]{flex:1;min-width:220px}
.kpigroup{margin-bottom:1.3rem}
.kpigroup .entry{border-radius:0;margin-bottom:-1px}
.kpigroup .entry:last-child{border-radius:0 0 var(--radius) var(--radius);margin-bottom:0}

/* The lock. A button, not an icon in a span — reopening a closed period is an action and has to be
   reachable by keyboard and announced as one. */
.lockbtn{font-size:.76rem;padding:.25rem .6rem;border-width:1px}
.lockbtn.open{border-color:var(--warn);color:var(--warn)}

/* A warning that has to stop somebody, used for "this period has already been published from" and
   "this target re-colours closed periods". Louder than .msg.warn on purpose. */
.loud{background:#fff8e6;border:1px solid #f0d391;color:#7a5a12;border-radius:10px;
  padding:.6rem .8rem;font-size:.82rem;font-weight:600;margin:.6rem 0}
.loud.bad{background:#fbe9ea;border-color:#eec1c5;color:#8e1f29}

/* The attention strip on the dashboard: rows where a number moved and nobody said why. Doctrine 2
   says that is the most interesting row in the report, so it sits above the table, not inside it. */
.attention{background:#fff;border:1px solid var(--line);border-left:4px solid var(--warn);
  border-radius:10px;padding:.7rem .9rem;margin-bottom:1rem;font-size:.84rem}
.attention b{color:var(--ink)}
.attention ul{margin:.4rem 0 0;padding-left:1.1rem;color:var(--mut)}

/* Admin: a definition row and its target history. */
.defrow{display:grid;gap:.5rem;grid-template-columns:1fr;align-items:end;
  padding:.7rem 0;border-bottom:1px solid var(--line)}
@media(min-width:900px){.defrow{grid-template-columns:2fr 1.4fr 1fr 1fr auto}}
.tiny{font-size:.72rem;color:var(--mut)}
.retired{opacity:.55}
.retired .ename{text-decoration:line-through}
fieldset{border:1px solid var(--line);border-radius:var(--radius);padding:.9rem 1rem;margin:0 0 1rem}
legend{font-size:.76rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--brand);padding:0 .3rem}
