/* Seeds the real stylesheet. Brand colour sampled from the logo: #623627
     is 84.6% of its opaque pixels, and it reads 10.11:1 on white (AAA). */
  :root {
    --brand:        #623627;
    --brand-dark:   #4b291e;
    --ink:          #241a15;
    --muted:        #6f6259;
    --line:         #ded5cd;
    --line-soft:    #ece5df;   /* lighter --line, for within-group column rules */
    --field:        #fff;
    --bg:           #f6f2ee;
    --radius:       10px;
    --tap:          44px;      /* minimum touch target, report §7.10 */
    --z:            1;         /* page scale — see the zoom rule below */
  }

  /* Every length in this sheet is px, so nothing gave way as the window got
     narrower: a five-outlet Laba Rugi is 1592px of columns and did not fit the
     content column on a 1520px screen, and the report had to be read at browser
     zoom 80%. `zoom` moves that into the stylesheet — layout runs at
     viewport / --z CSS px (1900 on that screen), then paints scaled back down,
     which is what browser zoom was already doing.
     ponytail: one knob beats retuning ~30 px values, and it keeps the table's
     column widths honest — they are still the widths the <colgroup> says.
     Touch opts back out, and it is the second query that does it, not a width —
     scaling by 0.8 also scales what a finger has to hit (--tap lands at 35px)
     and what iOS measures before it zooms a focused input (16px lands at
     12.8px). Both are touch constraints, so a touch pointer answers them,
     rather than a guess about which widths are tablets. Written as an opt-out
     and not as `and (pointer: fine)` deliberately: a device that reports no
     pointer at all then still gets the scale, where requiring `fine` would
     silently drop it.
     Viewport units are NOT rescaled by zoom (verified: 100vh inside zoom .8
     paints 0.8 × the screen), so every vh below divides by --z. */
  @media (min-width: 861px) { :root { --z: 0.8; } }
  @media (pointer: coarse)  { :root { --z: 1; } }
  body { zoom: var(--z); }

  * { box-sizing: border-box; }

  /* Scrollbar chrome hidden everywhere, scrolling itself untouched: wheel,
     touch, and keyboard all still work, and no layout shifts when a box goes
     from fitting to not fitting. Firefox/Chromium 121+ take the standard
     property; the -webkit rule covers older Chromium and Safari.
     ponytail: one global rule beats per-container rules on .nav, .side and
     .table-scroll. If some box later needs its bar back, override there. */
  * { scrollbar-width: none; }
  *::-webkit-scrollbar { display: none; }

  /* .table-scroll IS that later box (see its own rule): once it got a bounded
     height so its sticky caption/header could freeze, it became a real
     internally-scrolling region with ~3500px of body rows a reader cannot
     see all at once. A hidden scrollbar there isn't tidy, it's a dead end —
     the box sits inside an otherwise-short page, so the ordinary page scroll
     runs out almost immediately (chart + ~10 rows + the "Sumber" footer is
     all that fits before the page itself has nowhere further to go), and
     with no visible thumb nothing on screen says "more rows live inside this
     box, scroll *here*." Restoring a thin bar is the one visible cue for it. */
  .table-scroll { scrollbar-width: thin; }
  .table-scroll::-webkit-scrollbar { display: block; width: 10px; height: 10px; }
  .table-scroll::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
  .table-scroll::-webkit-scrollbar-track { background: transparent; }

  body {
    margin: 0;
    min-height: calc(100vh / var(--z));
    background: var(--bg);
    color: var(--ink);
    font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    /* 16px minimum: iOS Safari zooms the page on focus for anything smaller. */
  }

  .card {
    width: 100%;
    max-width: 420px;
    background: #fff;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 32px 24px 28px;
  }

  /* The logo sits above the form, per instruction. Sized by width so the
     wordmark stays legible at 360px and does not dominate on a desktop. */
  .logo {
    display: block;
    width: 100%;
    max-width: 260px;
    height: auto;
    margin: 0 auto 28px;
  }

  h1 {
    margin: 0 0 4px;
    font-size: 20px;
    font-weight: 650;
    letter-spacing: -0.01em;
  }
  /* The dashboard H1 is the page's own name and the loudest single word on it.
     Push it: stronger weight, tighter tracking, larger size. The product's
     title deserves more than a body heading — a meeting screen is named by its
     title, and a 20px generic H1 reads as "admin page," not as "the screen we
     agreed to look at together." Only the dashboard's H1 — no other page in
     the app is a monitor surface, so no other H1 gets this treatment. */
  .dash h1 {
    font-size: 30px;
    font-weight: 700;
    letter-spacing: -0.025em;
    line-height: 1.05;
  }

  .sub { margin: 0 0 24px; color: var(--muted); font-size: 14px; }

  label {
    display: block;
    margin-bottom: 6px;
    font-size: 14px;
    font-weight: 600;
  }

  input {
    width: 100%;
    min-height: var(--tap);
    padding: 10px 12px;
    margin-bottom: 18px;
    font-size: 16px;
    font-family: inherit;
    color: var(--ink);
    background: var(--field);
    border: 1px solid var(--line);
    border-radius: 8px;
  }

  /* Visible focus is not optional — outlet phones get used in bad light. */
  input:focus-visible, button:focus-visible, a:focus-visible, summary:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
  }

  .hint { margin: -12px 0 18px; font-size: 13px; color: var(--muted); }

  button {
    width: 100%;
    min-height: var(--tap);
    font-size: 16px;
    font-weight: 650;
    font-family: inherit;
    color: #fff;
    background: var(--brand);
    border: 0;
    border-radius: 8px;
    cursor: pointer;
  }
  button:hover { background: var(--brand-dark); }

  .reset {
    margin: 20px 0 0;
    padding-top: 18px;
    border-top: 1px solid var(--line);
    font-size: 14px;
    color: var(--muted);
    text-align: center;
  }
  .reset a { color: var(--brand); }

  footer {
    margin-top: 20px;
    font-size: 12px;
    color: var(--muted);
    text-align: center;
  }

  /* No dark theme, deliberately. Report §7.13: projectors and printed handouts
     both favour light, and a second theme is a second thing to keep correct.
     A dark logo on a dark background also needs a second logo asset. */
  :root { color-scheme: light; }


/* --- Shell (layout.html) ------------------------------------------------- */

body.centred {
  min-height: calc(100vh / var(--z));
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
}

/* Sidebar left, page right. Grid rather than a fixed-position aside: the
   content column then never has to know the sidebar's width. */
.shell {
  min-height: calc(100vh / var(--z));
  display: grid;
  grid-template-columns: 244px minmax(0, 1fr);
}

/* Fixed to the viewport height, so on a short window the nav scrolls inside it
   rather than spilling its rows out past the white panel. */
.side {
  position: sticky;
  top: 0;
  align-self: start;
  height: calc(100vh / var(--z));
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* Top padding clears the fixed .navtoggle, so the button never lands on the
     wordmark. Centring the logo inside a narrower box would have done it too,
     and would have moved the logo off the sidebar's centre line to do it. */
  padding: 56px 14px 20px;
  background: #fff;
  border-right: 1px solid var(--line);
}
.side .brand {
  display: block;
  padding: 10px 10px 22px;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--line);
}
.side .brand img {
  display: block;
  width: 100%;
  max-width: 180px;
  height: auto;
  margin: 0 auto;
}

/* --- Hiding the sidebar --------------------------------------------------- */

/* display:none rather than a width or a transform: the sidebar leaves the
   accessible tree with it, so a screen reader and the tab order agree with what
   is on screen. Nothing animates, because the state is restored on every page
   load and an animation would replay on each one. */
.nav-off .shell { grid-template-columns: minmax(0, 1fr); }
.nav-off .side  { display: none; }
/* With the sidebar gone the button sits over the page's own top-left corner. */
.nav-off .page  { padding-top: 56px; }

/* Fullscreen (F11, or a page that calls requestFullscreen) auto-hides the
   sidebar the same way .nav-off does — a fullscreened dashboard is a
   presentation surface, it wants the whole screen. The .navtoggle stays fixed
   and visible, so leaving fullscreen (or clicking it) brings the nav back. */
@media (display-mode: fullscreen) {
  .shell { grid-template-columns: minmax(0, 1fr); }
  .side  { display: none; }
  .page  { padding-top: 56px; }
}
:root:fullscreen .shell { grid-template-columns: minmax(0, 1fr); }
:root:fullscreen .side  { display: none; }
:root:fullscreen .page  { padding-top: 56px; }

.navtoggle {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 20;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  padding: 0;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 9px;
  color: var(--muted);
  cursor: pointer;
}
.navtoggle:hover { background: var(--bg); color: var(--brand); }
.navtoggle svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
}

.page { max-width: 1100px; margin: 0 auto; padding: 24px 16px; }
.page h1 { font-size: 22px; margin: 0 0 12px; }

.empty {
  padding: 40px 20px;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  color: var(--muted);
  text-align: center;
}

.alert, .notice {
  padding: 10px 12px;
  margin: 0 0 18px;
  border-radius: 8px;
  font-size: 14px;
}
.alert  { background: #fbeae6; border: 1px solid #e0b4a8; color: #7a2d18; }
.notice { background: #eef3ee; border: 1px solid #bcd0bd; color: #2f5233; }


/* --- Navigation ---------------------------------------------------------- */

/* The scrolling part: the logo and the account block stay put, the module list
   is what gives way. min-height:0 is what lets a flex child shrink enough to
   scroll at all. */
.nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.nav form { margin: 0; }

.nav a {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: var(--tap);
  padding: 0 12px;
  border-radius: 12px;
  color: var(--muted);
  font-size: 15px;
  font-weight: 600;
  text-decoration: none;
}
.nav a:hover { background: var(--bg); color: var(--ink); }

/* The active module. Brand on white is 10.11:1, so white on brand is too. */
.nav a.on { background: var(--brand); color: #fff; }
.nav a.on:hover { background: var(--brand-dark); color: #fff; }

/* The collapsible group heading. Same row shape as a nav link, but it toggles
   instead of navigating, so it never takes the active pill — that stays on the
   child, and two rows never claim to be the page you are on. */
.navgroup > summary {
  display: flex;
  align-items: center;
  min-height: var(--tap);
  border-radius: 12px;
  cursor: pointer;
  list-style: none;          /* Firefox */
}
.navgroup > summary::-webkit-details-marker { display: none; }
.navgroup > summary:hover { background: var(--bg); }

/* The label fills the row so the toggle area is the chevron. */
.navgroup > summary > a {
  flex: 1;
  min-height: var(--tap);
  padding-right: 4px;
}
.navgroup[open] > summary > a { color: var(--ink); }

/* A group whose section has no landing page of its own (SQ, HRD) labels itself
   with a .grouplabel instead of a link, so it reads the same as the divisions
   that have no tools yet — same word, same weight, just with a chevron. It
   still has to fill the row, and the margin that separates standalone headings
   would push it off-centre inside the summary. */
.navgroup > summary > .grouplabel { flex: 1; margin-top: 0; }

/* A division: a heading over its tools, not a control. Lighter and smaller than
   a nav row so it reads as a label rather than something that failed to click. */
.grouplabel {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 38px;
  margin-top: 10px;
  padding: 0 12px;
  color: var(--muted);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
.grouplabel svg { opacity: 0.75; }

.navgroup .chev {
  width: 16px;
  height: 16px;
  margin: 0 12px 0 auto;
  color: var(--muted);
  transition: transform .15s;
}
.navgroup[open] .chev { transform: rotate(90deg); }

/* Dashboard views. The rail is what makes them read as children of the row
   above rather than as seven more modules. */
.subnav {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 2px 0 4px 21px;
  padding-left: 9px;
  border-left: 1px solid var(--line);
}
/* 34px, under the 44px touch minimum, is deliberate and desktop-only: the
   sidebar becomes a strip below 860px and the media query puts --tap back. */
.subnav a { min-height: 34px; padding: 0 10px; border-radius: 9px; font-size: 14px; }

/* Stroke icons inherit the row's colour, so the active pill needs no second set. */
.nav svg, .account svg {
  flex: none;
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* --- Account, at the foot of the sidebar --------------------------------- */

/* margin-top:auto pins it to the bottom however short the module list is; the
   negative side margins cancel the sidebar's padding so the block runs edge to
   edge and the sign-out button lands on the sidebar's own right edge. */
.account {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: auto -14px 0;
  padding: 12px 6px 4px 14px;
  border-top: 1px solid var(--line);
}
/* The form is the flex item, not the button inside it — the auto margin has to
   live here or the sign-out sits wherever the NIK happens to end. */
.account form { margin: 0 0 0 auto; }

.account .avatar {
  flex: none;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--bg);
  border: 1px solid var(--line);
  color: var(--brand);
}

/* min-width:0 lets a long NIK ellipsis instead of pushing the button off. */
.account-id { display: grid; min-width: 0; line-height: 1.25; }
.account-id strong,
.account-id small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-id strong { font-size: 14px; font-weight: 650; }
.account-id small { font-size: 11px; color: var(--muted); letter-spacing: 0.04em; }

/* Icon-only, so the tap target comes from padding, not from a label. */
.account .signout {
  display: grid;
  place-items: center;
  width: var(--tap);
  height: var(--tap);
  padding: 0;
  background: none;
  border: 0;
  border-radius: 10px;
  color: var(--muted);
  cursor: pointer;
}
.account .signout:hover { background: var(--bg); color: var(--brand); }

/* Under ~860px the sidebar becomes a scrolling strip across the top. Still no
   overlay drawer: sliding a panel over the page needs a focus trap, a scrim and
   an Escape handler, where .nav-off just stops rendering the strip. The toggle
   hides it in both layouts and costs none of that. */
@media (max-width: 860px) {
  .shell { grid-template-columns: minmax(0, 1fr); }
  .side {
    position: static;
    height: auto;
    flex-direction: row;
    align-items: center;
    gap: 8px;
    /* Left padding clears the fixed button here too — in a row the strip starts
       exactly where the button is. */
    padding: 10px 12px 10px 56px;
    overflow: visible;
    overflow-x: auto;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }
  /* The strip is a row: the logo goes back to a fixed height beside the items. */
  .side .brand {
    flex: none;
    padding: 0 12px 0 4px;
    margin: 0;
    border-bottom: 0;
    border-right: 1px solid var(--line);
  }
  .side .brand img { width: auto; height: 28px; max-width: none; }
  .nav { flex: 0 0 auto; flex-direction: row; align-items: center; gap: 4px; overflow: visible; }
  .nav a, .grouplabel { width: auto; white-space: nowrap; }
  .grouplabel { margin-top: 0; }
  /* In a row the rail and the indent mean nothing, and fingers need the target. */
  .subnav { flex-direction: row; gap: 4px; margin: 0; padding: 0; border-left: 0; }
  .subnav a { min-height: var(--tap); }
  /* The strip is one row, so "bottom" becomes "far end". */
  .account {
    margin: 0 0 0 auto;
    padding: 4px 0 4px 8px;
    border-top: 0;
  }
  .account .avatar { width: 30px; height: 30px; }
  .account-id small { display: none; }
}

/* Dashboard page heading. */
.welcome { font-size: clamp(22px, 3vw, 30px); margin: 0 0 4px; letter-spacing: -0.02em; }
.welcome-sub { margin: 0 0 24px; color: var(--muted); font-size: 15px; }


/* --- Laba Rugi ----------------------------------------------------------- */

/* The report needs the width; the login card does not. .wide widens only here. */
.page.wide { max-width: 1600px; }

.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 12px;
  margin: 0 0 16px;
}
.filters label { margin: 0; }
.filters span { display: block; margin-bottom: 6px; }
.filters select {
  min-height: var(--tap);
  padding: 10px 12px;
  font: inherit;
  font-size: 16px;   /* iOS zooms on focus below 16px */
  color: var(--ink);
  background: var(--field);
  border: 1px solid var(--line);
  border-radius: 8px;
}
.filters button { width: auto; padding: 0 20px; }

.scope-line { margin: 0 0 16px; font-size: 15px; }
.scope-line span { color: var(--muted); }

/* Four figures, readable from the back of a room (PRD A11). */
.tiles {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  margin: 0 0 20px;
}
.tile {
  padding: 16px 18px;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.tile-label { display: block; margin-bottom: 6px; font-size: 13px; color: var(--muted); }
.tile-value {
  display: block;
  font-size: clamp(20px, 2.2vw, 30px);
  font-weight: 700;
  letter-spacing: -0.02em;
  /* Tabular figures so a column of numbers lines up and a changing period does
     not make the tile jitter. */
  font-variant-numeric: tabular-nums;
}
.tile-value.neg { color: #a3301a; }

/* Same card language as .tile/.table-scroll (white, bordered, radius) — .card
   supplies that; this only adds the padding and spacing a chart card needs
   that a login card (.card's other user) does not. */
.chart-card { max-width: none; padding: 16px 18px; margin: 0 0 20px; }

/* The table scrolls inside this box, never the page. A horizontally scrolling
   page is how a phone user loses the description column and the row heading at
   the same time.
   `max-height` is load-bearing, not cosmetic — `overflow-x: auto` computes
   `overflow-y` to `auto` too (CSS can't have one axis scroll and the other
   stay `visible`; the non-`visible` axis wins), so this box IS a scroll
   container in Y whether a height is set or not. Without a bound, its own
   height just grows to fit all ~200 rows and nothing inside it ever scrolls,
   which means `position: sticky` on the caption/header below has nothing to
   stick within — they compute their offset against a scrollport that never
   moves and just scroll away with the page. With the bound, this box gets a
   real internal scrollbar (both axes) and the sticky caption + header row
   freeze at its top exactly like an Excel freeze pane, which is the point. */
.table-scroll {
  overflow-x: auto;
  max-height: 70vh;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/* `separate`, not `collapse`, and this is load-bearing: under `collapse` the
   borders belong to the table and are painted by it, so a border on a sticky
   cell is left behind the moment the box scrolls. Both sticky things here would
   lose their edge — the pinned .desc column and the .thead row. Under
   `separate` the cell owns its border and carries it. It also keeps every rule
   1px without collapse arithmetic, because the rules below only ever set one
   side of any given boundary. */
table.pnl {
  border-collapse: separate;
  border-spacing: 0;
  /* Fixed layout, and the widths come from the <colgroup> in the template. A
     definite `width` is what lets the fixed algorithm run at all; with `auto`
     the browser silently falls back to content-based sizing, which is the bug
     this replaced. `100%` never squeezes below the column total — the fixed
     algorithm takes the larger of the two — so the table keeps its natural
     2752px and .table-scroll scrolls it. */
  table-layout: fixed;
  width: 100%;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}

/* One declaration per column kind, and every column of that kind is that width.
   ponytail: 140px holds 15 digits+separators at 14px tabular, which clears the
   largest figure in the data today (1.124.851.016,0). Under fixed layout a
   longer one overflows its cell rather than widening the column — bump c-val
   and c-grand together if the numbers ever outgrow it. */
table.pnl col.c-desc  { width: 300px; }
table.pnl col.c-val   { width: 140px; }
table.pnl col.c-pct   { width: 72px; }
table.pnl col.c-grand { width: 160px; }
/* The "Semua angka dalam Rupiah" note. It lives above .table-scroll, not in
   it — see the comment in laba-rugi.html. Nothing about it is sticky because
   nothing scrolls underneath it. */
.table-note {
  margin: 0 0 6px;
  font-size: 13px;
  line-height: 20px;
  color: var(--muted);
}
/* Columns lead, rows recede: a rule on every column boundary, and horizontal
   rules only where a block of figures ends. Two weights, because one uniform
   rule dissolves the pairing this table is built from — an outlet is a value
   and its %, and those two belong together. The soft rule falls inside a pair,
   the full rule closes the outlet. */
table.pnl th, table.pnl td {
  padding: 6px 12px;
  border-right: 1px solid var(--line-soft);
  white-space: nowrap;
}
table.pnl .pct   { border-right: 1px solid var(--line); }
table.pnl .desc  { border-right: 1px solid var(--line); }
table.pnl .grand { border-right: 0; }  /* .table-scroll draws that edge */
table.pnl th.head { border-right: 0; } /* spans the row — it has no neighbour */

/* The only horizontal rules: under the header, and above each row that closes a
   block. `border-top` rather than `border-bottom` because the useful line is the
   one shutting the list of accounts above a subtotal — the accounting
   convention, and the only direction CSS can reach (no previous-sibling
   selector). Section and block headings need none: their fills separate them. */
table.pnl thead th { border-bottom: 1px solid var(--line); }
tr.k-subtotal > *, tr.k-total > *, tr.k-result > *,
tr.k-section > *, tr.k-block > * { border-top: 1px solid var(--line); }

table.pnl td { text-align: right; }
table.pnl thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--bg);
  font-size: 12px;
  font-weight: 650;
  text-align: right;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* Pinned description column. Needs its own background or the scrolling figures
   show through it. */
table.pnl .desc {
  position: sticky;
  left: 0;
  z-index: 1;
  background: #fff;
  text-align: left;
  font-weight: 400;
  /* The one column allowed to wrap. Its width is now the <col>'s and nothing a
     cell does can change that, so a long account name wraps to a second line
     instead of spilling across the figures. Figures stay nowrap. */
  white-space: normal;
}
table.pnl thead .desc { z-index: 3; background: var(--bg); }

table.pnl .grand { background: #faf7f4; font-weight: 650; }
table.pnl thead .grand { background: var(--bg); }

/* The '%' columns (value / gross revenue) are read-only context, muted. */
table.pnl .pct { color: var(--muted); font-size: 12px; }
table.pnl thead .pct { color: var(--muted); font-size: 11px; }

table.pnl .zero { color: #c3b9b1; }
table.pnl .neg  { color: #a3301a; }

/* Hierarchy comes from `kind`, set by the loader, not from parsing labels.
   The `k-` prefix is not decoration. These names come straight from the row
   kind — ACCOUNT, TOTAL, SECTION — and unprefixed they are exactly the words a
   layout class already wants. `.account` is the sidebar's signed-in user block
   (line 351) and it carries `display: flex`, so every `<tr class="account">`
   stopped being a table row and became a flex container: 121 rows sized their
   own cells and left the column grid, which is what pushed the TOTAL rows'
   figures off the right edge. Keep the prefix on anything derived from data. */
tr.k-section .desc {
  padding-top: 18px;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--brand);
}
tr.k-block .desc   { padding-top: 12px; font-weight: 650; }
tr.k-account .desc { padding-left: 28px; color: var(--muted); }
tr.k-subtotal      { background: #fbf9f7; }
tr.k-subtotal .desc, tr.k-subtotal td { font-weight: 600; }
tr.k-total, tr.k-result { background: #f3ece7; }
tr.k-total .desc, tr.k-total td, tr.k-result .desc, tr.k-result td { font-weight: 700; }
tr.k-result .desc { color: var(--brand); }

/* Full-width section / block headings (a single spanning cell). */
table.pnl th.head {
  text-align: left;
  background: #f3ece7;
  font-weight: 700;
  letter-spacing: 0.04em;
}

/* The heading LABEL is pinned, not its cell. These rows are one cell spanning
   the table, so nothing of them sits in the first column: scroll right to read
   an outlet and 'PENDAPATAN' and 'Penjualan Barang Dagangan' had gone, leaving
   figures under no heading. Sticky on the cell itself does nothing — a sticky
   box may only shift inside its containing block, and this cell already fills
   the row. The inner span has room, so it travels along the band instead.
   `left` matches the cell's own padding, so nothing moves until it has to. */
table.pnl th.head > span {
  position: sticky;
  left: 12px;
  display: inline-block;
}
tr.k-block th.head > span { left: 20px; }
tr.k-section th.head {
  background: var(--brand);
  color: #fff;
  font-size: 13px;
  text-transform: uppercase;
}
/* Indented, because a block heading is always inside a section: 'OPS.' sits
   under 'BEBAN USAHA'. The lighter fill alone left the two reading as peers. */
tr.k-block th.head { padding-top: 14px; padding-left: 20px; color: #5a4636; }

/* Upload block and its flash result. */
.upload {
  margin-bottom: 24px;
  padding: 14px 16px;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.upload form { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.upload .hint { flex: 1 1 100%; font-size: 13px; color: var(--muted); }
.upload input[type=file] { font-size: 14px; }
.flash { padding: 10px 14px; border-radius: var(--radius); margin-bottom: 10px; font-size: 14px; }
.flash.ok  { background: #eef6ec; color: #1e6b33; border: 1px solid #cde5cb; }
.flash.err { background: #fdeeee; color: #a3301a; border: 1px solid #f3cccc; white-space: pre-wrap; }

.download {
  margin-left: auto;
  font-size: 14px;
  font-weight: 600;
  color: var(--brand);
  text-decoration: none;
}
.download:hover { text-decoration: underline; }

.source { margin: 14px 0 0; font-size: 13px; color: var(--muted); }

/* ==========================================================================
   Dashboard Company
   ==========================================================================
   The chart palette, decided once here and binding on every later module
   (ADR-0002 §5a left palette, mark specs and tile design to the first
   dashboard). Validated with the dataviz six-checks against the card surface
   (#fff), adjacent pairlist — every chart on the page is a bar, line or stack:

     [PASS] lightness band, chroma floor, contrast >= 3:1
     [PASS] CVD separation, worst adjacent dE 11.2 (deutan), target >= 8
     [PASS] normal-vision floor, worst adjacent dE 18.7, floor >= 15

   The ORDER is the safety mechanism. Rust and gold measure dE 11.4 to each
   other and FAIL the normal-vision floor when adjacent, so teal sits between
   them permanently. Re-run the validator before reordering; three remains the
   cap for any chart using s1-s3 alone, and two on all-pairs forms (scatter,
   bubble, choropleth) if one is ever added. §1's category stack extends this
   to six slots (s4-s6 below) as its own separately-validated set — that
   extension does not raise the cap here for s1-s3's other uses.

   These duplicate the SERIES constant in src/main/frontend/charts.js, which is
   the one place the charts themselves read. Kept in both because the swatches
   in a legend are CSS and the marks are JS; change them together. */
:root {
  --s1: #A8502A;   /* rust — slot 1 */
  --s2: #008C80;   /* teal — slot 2 */
  --s3: #AD7A00;   /* gold — slot 3 */

  /* §1's category stack only (Haircut/Package/Treehouse/F&B/Produk/Layanan
     Lainnya) — six slots, not a bump of the three-slot cap above. Reuses s1-s3
     for the first three buckets (same brand hues) and adds three more,
     validated together as one adjacent-pair set:

       node scripts/validate_palette.js \
         "#A8502A,#008C80,#AD7A00,#3A6EA5,#B5487A,#5C7A29" \
         --mode light --surface "#ffffff"
       → ALL CHECKS PASS. One WARN: adjacent pair s5<->s4 (magenta/blue) sits
         in the 6-8 CVD floor band (deutan 7.2) — legal only with mandatory
         secondary encoding, which this chart already has (direct-labelled
         legend, 2px surface gap between stacked segments). Every other
         adjacent pair and the normal-vision floor (>=15) clear clean.

     Six is the cap for THIS chart specifically (one fixed stack order,
     adjacent-pair form) - re-run the validator before reordering, adding a
     7th, or reusing s4-s6 anywhere pairs could appear in a different order
     (a legend re-sort, a second stacked chart with a different category
     order) - the adjacent pairlist depends on order. */
  --s4: #3A6EA5;   /* blue — slot 4, F&B */
  --s5: #B5487A;   /* magenta — slot 5, Produk */
  --s6: #5C7A29;   /* green — slot 6, Layanan Lainnya */
}

/* `.page` sets `margin: 0 auto`, and an auto inline margin on a grid item
   disables stretch — the item becomes shrink-to-fit. On a table page that is
   harmless (the table drives the width); here it made `main` 919px inside a
   1756px column, so the card grid only ever saw room for two columns and the
   right third of a projector stayed empty. An explicit width restores the fill
   without touching `.page` itself, which Laba Rugi depends on. */
.dash { width: 100%; }

.dash .flash.err { margin-bottom: 20px; }

  .dash-head {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 20px;
  align-items: flex-start;
  justify-content: space-between;
  margin-bottom: 20px;
}
.dash-head h1 { margin-bottom: 4px; font-size: clamp(24px, 3.4vw, 40px); }
.dash-head .scope-line { margin: 0; }
.dash-head-titles { min-width: 0; flex: 1 1 240px; }

/* Page title block is centred on every viewport (was left, with the delta chips
   and controls filling the row's right side via space-between). Three-column
   grid: delta chips left, title centred, mode/period controls right — all on the
   one band, top-aligned, so the header stays one row tall. */
.dash-head {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: start;
}
.dash-head-titles { grid-area: 1 / 2; text-align: center; align-self: center; }
.dash-head .deltas { grid-area: 1 / 1; justify-self: start; }
.dash-head .controls { grid-area: 1 / 3; justify-self: end; margin-left: 0; }
/* When the sidebar is hidden the fixed .navtoggle sits over the page's top-left
   corner — push the delta chips clear of it. */
.nav-off .dash-head .deltas,
:root:fullscreen .dash-head .deltas { margin-left: 46px; }
@media (display-mode: fullscreen) { .dash-head .deltas { margin-left: 46px; } }

.deltas { display: flex; flex-wrap: wrap; gap: 8px; }
.dash-head .deltas .scope-line { flex-basis: 100%; margin: 2px 0 0; font-size: 13px; }
.delta {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 13px;
  color: var(--muted);
  background: #fff;
}
.delta strong { font-size: 14px; font-variant-numeric: tabular-nums; }
/* Target-mode-by-default (2026-09-06): whichever chip leads (Target, or the
   picked comparison once ?bandingkan= is set) gets a filled, bolder treatment;
   the other becomes a plain-bordered secondary. Same .delta shape either way -
   no new pill component, just which one reads as "the headline". */
.delta.delta-primary { background: #f5efe8; border-color: var(--brand); font-weight: 600; }
.delta.delta-primary strong { font-size: 15px; }
.delta.delta-secondary { font-size: 12px; padding: 5px 10px; }
.delta.delta-secondary strong { font-size: 13px; }
/* The arrow, not the colour, is what says "up" — a delta cue beside a series
   colour must never rest on hue alone. */
.delta.up strong { color: #1e6b33; }
.delta.up::after { content: "\25B2"; font-size: 9px; color: #1e6b33; }
/* No red pair for "down" - the page's own cross-cutting rule (see
   dashboard-chart-choices.md) is that green/red is never available, so a
   decline stays the base muted colour with a down arrow, not a red one. */
.delta.down::after { content: "\25BC"; font-size: 9px; }

/* The controls wrapper: the mode toggle and the period picker, side-by-side
   as a single right-aligned group. Each control has a small uppercase caption
   above it naming what it is (Tampilan / Periode), so a sighted user has the
   same context the aria-label gives a screen reader. */
.controls {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  margin-left: auto;
}
.controls-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.controls-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  /* Reserve the label's width so the toggle and picker right-align, not the
     labels: the controls are what matter, the labels are context for them. */
  min-width: 64px;
  text-align: right;
}

/* The weekly/monthly meeting-view toggle. Same pill language as .delta so it
   reads as part of the header, not a new control style. */
.mode-toggle { display: flex; gap: 2px; padding: 2px; border: 1px solid var(--line); border-radius: 999px; }
.mode-toggle a {
  padding: 6px 12px;
  border-radius: 999px;
  font-size: 13px;
  color: var(--muted);
  text-decoration: none;
}
.mode-toggle a.active { background: var(--ink); color: #fff; }

/* Monthly-only period picker, same pill language as .mode-toggle so the two
   read as one control group in the header. */
.period-picker select {
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 13px;
  color: var(--ink);
  background: transparent;
}
/* The noscript fallback button inherits no style by default and would render
   as the platform's default chrome - ugly next to the pill controls. Match
   the same border/radius/color language so it never reads as a different
   control if it ever shows. */
.period-picker button {
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 13px;
  color: var(--ink);
  background: #fff;
  cursor: pointer;
}

/* iOS Safari auto-zooms the viewport on focus when a form control's computed
   font-size is below 16px. The page is server-rendered, not a touch-first
   dashboard, so the desktop font can stay compact — but on screens where
   this control might be touched, bump it.

   A phone width alone was too narrow a guard. The one-screen 4-column layout
   is the only context where the header stays compact, and it is gated on
   `not (pointer: coarse)`; everywhere else — a 768px iPad portrait, a 1024px
   iPad landscape, a touch laptop — the pill renders at the 13px compact size
   and tapping "Periode" zooms the viewport. Coarse pointer is the real trigger
   for iOS zoom, so target it in addition to the existing phone floor; a
   fine-pointer desktop window does not zoom and stays compact. */
@media (max-width: 640px), (pointer: coarse) {
  .mode-toggle a,
  .period-picker select,
  .period-picker button { font-size: 16px; }
}

/* §2/§3 have no weekly-grain source (CompanyDashboardQuery) - dimmed, not
   hidden, so the real monthly figure underneath stays legible rather than
   needing its own placeholder state. §4 joins them in weekly mode. */
.dash-inactive { opacity: 0.4; pointer-events: none; }

/* Cards parked for a later rebuild (2026-09-07, §3/§4/§5 on request) - dims
   the real content underneath (still real figures, not demo() - just not
   ready to show in a meeting yet) and names itself with a centred badge
   instead of a "Tidak tersedia" sentence buried in the card's own flow.
   pointer-events:none also drops the is-clickable modal open, same reasoning
   as .dash-inactive. */
.dash-soon { position: relative; opacity: 0.35; pointer-events: none; }
.dash-soon::after {
  content: "SOON";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font: 700 2rem/1 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  letter-spacing: 0.15em;
  color: var(--ink);
  background: color-mix(in srgb, var(--bg) 70%, transparent);
}

/* Two columns on a desktop or projector, one on a phone. Cards are equal-width
   and independent; nothing reflows across the gap. */
.grid {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
  align-items: start;
}
@media (max-width: 540px) {
  /* minmax(420px,...) would overflow a 360px phone before auto-fit collapses. */
  .grid { grid-template-columns: minmax(0, 1fr); }
}
.dash-card { max-width: none; padding: 20px; }
.dash-card .tiles { margin-bottom: 16px; gap: 10px; }
.dash-card .tiles .tile { padding: 12px 14px; background: var(--bg); }
.dash-card .tile-value { font-size: clamp(19px, 1.7vw, 26px); }
/* Full width whatever the column count is — a leaderboard of eleven bars is
   unreadable in a third of a screen. */
.span-all { grid-column: 1 / -1; }

.dash-card h2 {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 16px;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
}
/* §5's title runs bigger than the other cards', and drops its number badge, on request. */
.dash .grid > [data-section="5"] > h2 { font-size: 18px; }
.dash .grid > [data-section="5"] > h2 .num { display: none; }
/* §5 carries four equal KPI tiles (Occupation / Kehadiran / SQ Score / Kuota) —
   always 2x2, never a single wide row, and the target sits on its own line under
   the value rather than wrapping mid-figure. */
/* Overrides the one-screen block's `grid-auto-flow: column` (as-many-cols-as-tiles):
   §5's four tiles wrap 2x2 instead of a single cramped four-across row. */
.dash .grid > [data-section="5"] > .tiles {
  grid-auto-flow: row;
  grid-template-columns: 1fr 1fr;
}
/* "48,6%" big; "/ 70,0%" muted on its own line under it so the headline figure
   has the tile's full width to grow into. */
.dash .grid > [data-section="5"] .tile-den {
  display: block; font-weight: 500; color: var(--muted);
  font-size: 0.42em; white-space: nowrap; margin-top: 0.15em;
}
.dash-card h2 .num {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  flex: 0 0 24px;
  /* The badges are this product's signature on the page. They earned the
     brand color already; the missing half of the look is a soft brand tint
     BEHIND the brand number — a hairline ring of the same hue a few shades
     up. The number sits in a field that says "Barbertology owns this," not
     "an ordered list got rendered." The radius matches the card itself
     (var(--radius)) on purpose: badges look like part of the card, not a
     foreign sticker. */
  background: #f3e4dc;
  color: var(--brand);
  border: 1px solid #e8d0c2;
  border-radius: 8px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}

.tile-foot { display: block; margin-top: 4px; font-size: 12px; color: var(--muted); }
/* §1 lead line: mark a prior month that the current one fell short of — the
   header carries the MoM %, this just puts the direction on the figure itself. */
.tile-foot.is-down::before { content: "\25BE\00A0"; color: #a3301a; }
.chart-cap { margin: 0 0 8px; font-size: 13px; font-weight: 600; color: var(--muted); }
.chart-foot { margin: 8px 0 0; font-size: 12px; color: var(--muted); }

/* §1 used to carry a bespoke `.headline` band here (an outsized revenue number
   with no tile box and its own --vh scale). It now uses the shared
   `.tiles`/`.tile` chrome like every other card — see templates/dashboard/company.html §1. */
.chart-foot strong { font-variant-numeric: tabular-nums; }
.muted { color: var(--muted); }

/* --- meters: one value against one target, no axis ---------------------- */
.meter {
  position: relative;
  display: block;
  height: 10px;
  margin-top: 8px;
  background: var(--line-soft);
  border-radius: 999px;
  overflow: hidden;
}
.meter-lg { height: 16px; margin: 4px 0 0; overflow: visible; }
/* A mark overhangs the track by 3px top and bottom, so a meter carrying one
   cannot clip. Keyed on the mark itself rather than on a modifier class: the
   overflow is a consequence of the content, and §6's small meter needed it the
   moment it gained a prior-period mark. */
.meter:has(.meter-mark) { overflow: visible; }
.meter-fill {
  display: block;
  height: 100%;
  background: var(--s1);
  border-radius: 999px;
}
.meter-fill.alt { background: var(--s3); }
/* The pace/target marker. Drawn over the fill, so "behind schedule" is a gap
   you see rather than two percentages you subtract. */
.meter-mark {
  position: absolute;
  top: -3px;
  bottom: -3px;
  width: 3px;
  margin-left: -1px;
  background: var(--ink);
  border-radius: 2px;
}
.meter-legend {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin: 8px 0 0;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

.note {
  margin: 12px 0 0;
  padding: 10px 12px;
  background: #fdf6e6;
  border: 1px solid #ecd9a8;
  border-radius: var(--radius);
  font-size: 13px;
  line-height: 1.45;
}

.placeholder {
  display: grid;
  gap: 4px;
  margin-top: 16px;
  padding: 14px;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  font-size: 13px;
  color: var(--muted);
}
.placeholder strong { color: var(--ink); font-size: 13px; }

/* --- legend: required for two series or more --------------------------- */
.legend { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; font-size: 13px; }
.legend-row { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 10px; }
.legend li { display: flex; align-items: center; gap: 8px; }
.legend strong { margin-left: auto; font-variant-numeric: tabular-nums; }
.legend-row strong { margin-left: 0; }
/* Legend share (d.kategoriPersen): five of the six §1 bands are single-percent
   slivers at card height, so the legend carries the mix the area can't show.
   This is the card's most informative figure, so it reads as real type, not as
   a muted annotation: full size, tabular, and ink-weight (not the muted
   secondary colour that under-sold it) so it holds up beside the label. */
.legend-row .pct { color: var(--ink); font-size: 1em; font-weight: 600; font-variant-numeric: tabular-nums; }
.swatch { width: 12px; height: 12px; flex: 0 0 12px; border-radius: 3px; background: var(--muted); }
/* k- prefix: these come from a loop index, i.e. from data (see docs/STATE.md).
   The slot numbers must match the SERIES array order in charts.js — the marks
   are painted there and the swatches here, and a mismatch makes the legend lie
   without erroring. */
.k-s1 { background: var(--s1); }
.k-s2 { background: var(--s2); }
.k-s3 { background: var(--s3); }
.k-s4 { background: var(--s4); }
.k-s5 { background: var(--s5); }
.k-s6 { background: var(--s6); }

/* The column is a sliver; the legend gets everything else. The band only has to
   be wide enough to read three stacked segments, while each legend row carries
   four figures (label, count, share, change) — a 0.8fr/1.2fr split starved the
   side that had more to say and clipped the delta chips at the card's edge.
   A fixed 64px rather than a fraction: the band's job does not get easier on a
   wider card, so extra width belongs to the legend at every size. */
/* container-type, not a viewport media query: this card sits in a grid whose
   own width depends on the sidebar and how many cards share a row, so a
   viewport breakpoint can stay "desktop" while this card itself is phone-
   narrow (found live 2026-09-07: sidebar expanded shrank the card to ~150px
   at a 1280px viewport, and the legend's fixed em tracks don't shrink - they
   just overflowed past the card edge, unreadable). Querying the card's own
   inline size catches that case the same way it catches an actual phone. */
.mix { display: grid; gap: 10px; grid-template-columns: 64px minmax(0, 1fr); align-items: center; container-type: inline-size; }
@container (max-width: 340px) { .mix { grid-template-columns: minmax(0, 1fr); } }
/* The legend reads in PAINT order, top to bottom, and what that means depends on
   which way the band runs — which is why this rule has been added, removed and
   added again as §6 changed shape. A vertical stack paints its first slice at
   the BOTTOM, so a top-down legend left in DOM order points at the slices upside
   down. `order` flips the visual rows to match the column while leaving the DOM
   alone, so reading and tab order stay in data order, and it keeps `.legend` as
   `display: grid` (a flex column-reverse fights the viewport-mode
   `align-items: stretch` further down and clumps every row against the bottom).
   Swatch colour stays tied to each row's own loop index, not its new visual
   position, so k-s1..k-s6 still point at the right slice.
   If this card ever goes horizontal again, delete these six rules — left-to-
   right already matches a top-down legend. */
.mix .legend li:has(.k-s1) { order: 6; }
.mix .legend li:has(.k-s2) { order: 5; }
.mix .legend li:has(.k-s3) { order: 4; }
.mix .legend li:has(.k-s4) { order: 3; }
.mix .legend li:has(.k-s5) { order: 2; }
.mix .legend li:has(.k-s6) { order: 1; }

/* This legend is a values TABLE — swatch, service, job count — so it is laid
   out as one, same reasoning as before (git blame for the fuller history).
   Share% and the vs-prior-period delta were both dropped (2026-09-07): the
   count is the figure asked for, and dropping the other two columns is what
   lets the label stay full-width instead of fighting for room in a ~220-300px
   card. The label track is `minmax(0, 1fr)` with ellipsis, not a fixed em
   width - it is the one column that can safely lose width before anything
   breaks, so it is the one that flexes; count stays a fixed em track (each
   `<li>` is its own grid, so only fixed-length tracks align across rows). */
.mix .legend {
  gap: 4px;
  font-size: 12px;
}
.mix .legend li {
  display: grid;
  grid-template-columns: 12px minmax(0, 1fr) 4.2em;
  column-gap: 8px;
  align-items: center;
}
.mix .legend li > span:not(.swatch) {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mix .legend li > strong { text-align: right; }

.pair { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); margin-top: 16px; }
.pair strong { font-size: 18px; font-variant-numeric: tabular-nums; }

/* --- outlet chips ------------------------------------------------------- */
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 0; }
.chip {
  padding: 4px 10px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 12px;
}
.chip-new { background: #e6f4f2; border-color: var(--s2); color: #0b5f57; font-weight: 600; }

/* Charts own their box; the axis labels are measured from the rendered text, so
   the host needs a real width to measure against on the first frame. */
[data-chart] { width: 100%; min-height: 60px; }
[data-chart] svg { display: block; overflow: visible; }


/* --- Dashboard Company: one screen, no fold ------------------------------
   The ten sections are read together — on a laptop, on a projector, in a
   meeting where nobody scrolls for you. So on a wide screen the page is
   exactly one viewport tall and the grid is a fixed 4 x 3 of cells: §9 and §10
   take two columns each on the last row, which is what makes 10 sections land
   in 12 slots. Nothing is below the fold because there is no fold, at 720p or
   at 1080p or at anything between.

   Two rules to keep in mind when editing a card:

   - Every card clips (`min-height: 0` + `overflow: hidden`). A card whose text
     outgrows its cell scrolls inside itself; the PAGE never scrolls. That is
     the guarantee, and it holds no matter what a later card puts in itself.
   - Type and spacing are in `--vh`, not px, so 720p and 1080p differ in scale
     rather than in what is on screen. `--vh` divides by `--z` for the same
     reason every other viewport length in this sheet does: `zoom` scales the
     paint, not the unit (see the note at the top of the file).

   Charts get whatever the card has left, so their height is measured at
   runtime rather than declared — `heightOf` in src/main/frontend/charts.js.
   The `data-height` attributes in the template are the fallback for the OTHER
   branch of this query, not dead values.

   Below 1000px, or on a touch device, none of this applies and the page scrolls
   as it did: ten cards on a phone screen is ten unreadable slivers. Touch
   answers here for the same reason it opts out of `--z` above — a finger and a
   projector want opposite things.

   Written as an opt-out for the same reason `--z` is, and it is not theoretical:
   a device with no pointing device at all reports `pointer: none`, matches
   neither `fine` nor `coarse`, and `and (pointer: fine)` silently drops it into
   the phone layout on a 1080p screen. Headless Chrome is one such device, which
   is how this was found. */
@media (min-width: 1000px) and (not (pointer: coarse)) {

  /* `.page.dash`, not `.dash`: Laba Rugi's `.page.wide` caps the column at
     1600px and would otherwise win on specificity, leaving a fifth of a 1080p
     screen empty on either side of a layout whose whole point is that it uses
     the screen it is given. */
  .page.dash {
    --vh: calc(1vh / var(--z));
    --pad: calc(1.1 * var(--vh));
    display: flex;
    flex-direction: column;
    gap: var(--pad);
    height: calc(100vh / var(--z));
    max-width: none;
    padding: var(--pad);
    overflow: hidden;
  }
  /* .nav-off pushes .page down by 56px to clear the fixed nav button. Here that
     would spend 56px of the one screen on empty space, so the banner steps
     around the button sideways instead. */
  .nav-off .dash { padding-top: var(--pad); }

  .dash .flash.err {
    margin: 0;
    padding: calc(0.45 * var(--vh)) 10px;
    font-size: clamp(10px, calc(1.15 * var(--vh)), 13px);
  }
  /* After the reset above, not before it: both selectors weigh the same, so the
     later one is the one that survives. */
  .nav-off .dash > .flash { margin-left: 46px; }

  .dash-head { align-items: start; gap: calc(0.5 * var(--vh)) 20px; margin: 0; }
  /* The H1 fills the header band: 4*--vh scales it to the page's name at every
     viewport height. Cap 44px, floor 24px so it stays a title, not a banner or
     a body heading. Title cell is align-self:center so it sits centred against
     the two-row delta/scope column. */
  .dash-head h1 { margin: 0; font-size: clamp(24px, calc(4 * var(--vh)), 44px); }
  .dash-head .scope-line { font-size: clamp(10px, calc(1.3 * var(--vh)), 15px); }
  .dash-head-titles { min-width: 0; }
  .delta { padding: calc(0.3 * var(--vh)) 10px; font-size: clamp(10px, calc(1.15 * var(--vh)), 13px); }
  .delta strong { font-size: clamp(10px, calc(1.25 * var(--vh)), 14px); }
  .controls-label { font-size: clamp(9px, calc(1.0 * var(--vh)), 11px); }
  .mode-toggle a { padding: calc(0.3 * var(--vh)) 10px; font-size: clamp(10px, calc(1.15 * var(--vh)), 13px); }
  .period-picker select { padding: calc(0.3 * var(--vh)) 10px; font-size: clamp(10px, calc(1.15 * var(--vh)), 13px); }

  .dash .grid {
    flex: 1;
    min-height: 0;
    gap: var(--pad);
    /* Boss note 2026-08-31: three rows. Row 1 is §1 Revenue and §2 YTD in two
       wide columns; row 2 is §9 Outlet Leaderboard and §10 Barbertologist
       Leaderboard, likewise; row 3 is the six small boxes — §3–§8 — one per
       column. Six columns: mains span 3, smalls 1.
       Source order in company.html stays 1..10 (reading/tab/screen-reader
       order must match the numbering), so each card is placed explicitly
       rather than left to auto-flow, which would interleave 3–8 between the
       mains. */
    grid-template-columns: repeat(6, minmax(0, 1fr));
    /* The mains carry the page's headline charts and the leaderboards carry
       eleven and ten bars against small number cards, so the first two rows
       are given the extra. Row 2 (the two leaderboards) is given a touch more
       than row 1 — eleven / ten labelled bars need the vertical room more than
       §1/§2's rail-plus-chart faces do — with the smalls in row 3 giving it up
       (they are numbers-only and were the loosest of the three). */
    grid-template-rows: minmax(0, 1.28fr) minmax(0, 1.42fr) minmax(0, 0.95fr);
    /* NOT `start`, which the scrolling layout uses. A card that sizes to its own
       content has no height to give its chart, and an SVG sized `height: 100%`
       against an auto-height parent falls back to its viewBox ratio — which is
       computed from the height we just handed it. That is a loop, and it grows
       by 1/--z per frame until the card is millions of pixels tall. Stretching
       the card is what makes its height definite and closes it. */
    align-items: stretch;
  }
  /* Main rows: §1/§2 on top, §9/§10 beneath, three columns each. */
  .dash .grid > [data-section="1"]  { grid-area: 1 / 1 / 2 / 4; }
  .dash .grid > [data-section="2"]  { grid-area: 1 / 4 / 2 / 7; }
  .dash .grid > [data-section="9"]  { grid-area: 2 / 1 / 3 / 4; }
  .dash .grid > [data-section="10"] { grid-area: 2 / 4 / 3 / 7; }
  /* Small row: §3–§8, one per column. */
  .dash .grid > [data-section="3"]  { grid-area: 3 / 1 / 4 / 2; }
  .dash .grid > [data-section="4"]  { grid-area: 3 / 2 / 4 / 3; }
  .dash .grid > [data-section="5"]  { grid-area: 3 / 3 / 4 / 4; }
  .dash .grid > [data-section="6"]  { grid-area: 3 / 4 / 4 / 5; }
  .dash .grid > [data-section="7"]  { grid-area: 3 / 5 / 4 / 6; }
  .dash .grid > [data-section="8"]  { grid-area: 3 / 6 / 4 / 7; }
  /* Boss note 2026-08-31: §4 reads as numbers only on the one-screen card —
     the two tiles carry target and actual, the trend sparkline and the cost
     matrix stay behind. Their hosts stay in the DOM because charts.js clones
     them for the §4 modal; hiding is display-only, the modal is untouched. */
  .dash .grid > [data-section="4"] :is([data-chart], .spark, .chart-cap) { display: none; }
  /* Boss note 2026-09-01, revised 2026-09-02: §1 rehaul — the one-screen monthly
     card face carries the per-product target scorecard ([data-s1-target], filled
     by charts.js) in place of the revenue chart. Laid out as ONE column, top to
     bottom: h2 · the target progress bar and its caption · the lead revenue line
     · the scorecard filling the rest. The earlier two-column version put two
     figures in a narrow left rail against a six-row table and read lopsided —
     the rail sat half-empty while the table's bars were pinched. Single column
     gives the bars the card's full width and kills the imbalance.
     The chart, its caption, the axis/shadow foot notes and the six-category
     swatch legend all go (display:none, so they take no grid cell); `>` so the
     scorecard's own .chart-cap nested under [data-s1-target] survives. The chart
     host stays in the DOM for the §1 modal to clone.
     `:has(> [data-s1-target])` — charts.js removes that host in weekly mode
     (no per-product target for a week), so this only applies when the scorecard
     is actually present; weekly falls back to the stacked flex card.
     (Selector was missing the `:has()` scope itself until 2026-09-06 - it hid
     the chart unconditionally, so weekly mode showed neither the scorecard
     nor the chart it was supposed to fall back to: a blank card.) */
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) > :is([data-chart], .chart-cap, .chart-foot, .legend) { display: none; }
  /* §1 one-screen monthly face (redesign 2026-09-02, "rail + scorecard"): the
     title, then two columns — a left rail carrying Total Revenue and % dari
     Target as big brand figures, and the per-product target scorecard filling
     the rest. `:has(> [data-s1-target])` so this fires only when the scorecard
     is present (charts.js drops that host in weekly mode → narrow fallback). */
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) {
    display: grid;
    grid-template-columns: minmax(168px, 0.9fr) minmax(0, 2.6fr);
    grid-template-rows: auto minmax(0, 1fr);
    column-gap: calc(1.6 * var(--vh));
    row-gap: calc(0.6 * var(--vh));
  }
  /* §1 face carries a larger title and drops the "1" badge — the card is the
     page's lead figure, not one item in a numbered list. Source order is
     untouched (screen-reader / tab order still 1..10). */
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) > h2 {
    grid-area: 1 / 1 / 2 / -1;
    font-size: clamp(16px, calc(2.3 * var(--vh)), 23px);
    letter-spacing: 0.04em; margin-bottom: 0;
  }
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) > h2 .num { display: none; }

  /* The full-width revenue-vs-target bar is off on this face (boss call
     2026-09-02) — the % lives big in the rail, the deep-dive is in the modal. */
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) > .s1-progress-wrap { display: none; }

  /* Left rail: Total Revenue over % dari Target, each a small uppercase label
     above a big brand figure, split from the scorecard by a vertical rule. Both
     tiles show here (the narrow layout still owns the meter). */
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) > .tiles {
    grid-area: 2 / 1 / 3 / 2;
    margin: 0; padding: 0 calc(1.5 * var(--vh)) 0 0; border: 0;
    border-right: 1px solid var(--line);
    display: flex; flex-direction: column;
    gap: 0;
  }
  /* Each rail figure is its own equal-height segment, its content centred in it,
     with a hairline between segments. */
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) .tiles .tile {
    flex: 1 1 0;
    display: flex; flex-direction: column; justify-content: center; gap: calc(0.3 * var(--vh));
    padding: 0.6em 0; background: none; border: 0;
  }
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) .tiles .tile + .tile {
    border-top: 1px solid var(--line);
  }
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) .tile-label {
    margin: 0; letter-spacing: 0.03em; text-transform: uppercase; color: var(--muted);
    font-size: clamp(9px, calc(1.1 * var(--vh)), 12px);
  }
  /* nowrap: the figure ("Rp 1,2 M", "62,3%") is one token and must never break
     across lines — in the narrow rail (worse in fullscreen, taller viewport →
     clamp at its cap) that wrap overflowed and clipped against the card's
     `overflow: hidden`. The denominator is `inline-block` so it drops to the
     next line as a whole unit when there's no room, and trails inline when
     there is. §2's YTD rail shares this exact treatment. */
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) .tile-value {
    color: var(--brand); font-weight: 700; line-height: 1.04; letter-spacing: -0.01em;
    /* Cap dropped 33->26 / mid 3->2.4 vh: the miliar figures carry three
       decimals now ("Rp 1,200 M" / "Rp 13,000 M"), and at the old size the
       nowrap token overflowed the min-width rail. §2 keeps this in step. */
    font-size: clamp(16px, calc(2.4 * var(--vh)), 26px);
    white-space: nowrap;
  }
  /* Revenue / % go red when the month is short of target (same negative colour
     as the gap figure). The muted denominator span keeps its own colour. */
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) .tile-value.neg { color: #a3301a; }
  /* "/ target" denominator trailing each rail figure — muted, lighter, ~half the
     figure's size, sitting on its baseline. */
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) .tile-den {
    display: inline-block; color: var(--muted); font-weight: 500;
    font-size: 0.55em; letter-spacing: 0; white-space: nowrap;
  }
  /* The gap is its own rail segment (boss call): a "Kurang" / "Lewat" label, then
     the rupiah figure at the same size as the %. */
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) .tile-gap {
    line-height: 1.1; font-variant-numeric: tabular-nums;
  }
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) .tile-foot {
    display: block; margin: calc(0.2 * var(--vh)) 0 0; color: var(--muted);
    font-size: clamp(9px, calc(1.15 * var(--vh)), 12px); font-variant-numeric: tabular-nums;
  }

  /* Scorecard fills the right column. */
  .dash .grid > [data-section="1"]:has(> [data-s1-target]) > [data-s1-target] {
    grid-area: 2 / 2 / 3 / -1; align-self: stretch;
    display: flex; flex-direction: column; min-height: 0;
    gap: calc(0.15 * var(--vh));
  }
  /* Caption sits right on the table — no inherited chart-cap margin. */
  .dash .grid > [data-section="1"] [data-s1-target] .chart-cap { margin: 0; }
  /* Per-product target rupiah foot is modal-only; dropping it buys the rows height. */
  .dash .grid > [data-section="1"] [data-s1-target] .chart-foot { display: none; }
  /* Header sizes to its text; the five product rows split the rest evenly so bar
     height, type and gaps all scale with the card. `min-height: 0` lets the 1fr
     rows shrink on a short viewport rather than overflow. */
  .dash .grid > [data-section="1"] .k-bars {
    font-size: clamp(12px, calc(1.7 * var(--vh)), 16px); gap: calc(0.5 * var(--vh)) 12px;
    flex: 1 1 0; min-height: 0;
    grid-template-rows: auto repeat(5, minmax(0, 1fr));
  }
  .dash .grid > [data-section="1"] .k-bar-track { height: clamp(12px, calc(2 * var(--vh)), 20px); }
  /* Five columns on the face now, matching the modal: name · bar · %Target ·
     Aktual · Selisih. */
  .dash .grid > [data-section="1"] .k-bars.k-target {
    grid-template-columns: max-content minmax(120px, 1fr) 3.8em 5em 5.6em;
  }
  .dash .grid > [data-section="1"] .k-bar-head { font-size: clamp(9px, calc(1.15 * var(--vh)), 12px); }
  /* :not(.k-bar-head) — the header's own SELISIH label stays header-sized. */
  .dash .grid > [data-section="1"] .k-bar-row:not(.k-bar-head) .k-bar-shift {
    font-size: clamp(12px, calc(1.7 * var(--vh)), 16px);
  }
  /* No left inset edge on the card-face scorecard rows — every product misses
     this aspirational target so it lit every row, reading as noise. The row tint
     carries the pass/fail on its own here; the §1 modal keeps the edge. */
  .dash .grid > [data-section="1"] .k-bar-row:is(.is-bad, .is-good, .is-watch) {
    padding-block: 1px; box-shadow: none;
  }
  /* No category dot on the card face — the stacked bar and legend it keyed to
     are hidden here, so it was colour without a referent. The §1 modal keeps
     its dots (the stacked bar lives there). */
  .dash .grid > [data-section="1"] .k-target .k-dot { display: none; }
  .dash .grid > [data-section="1"] .t-bar-track::after { opacity: 0.5; width: 2px; }

  /* §2 chases §1's one-screen face (boss call 2026-09-02): a left rail of big
     brand figures (Realisasi YTD · % target · Proyeksi akhir tahun) over the
     cumulative YTD chart in the right column, split by a vertical rule. Gated
     on the chart host, which Thymeleaf renders only in bulanan mode — weekly
     has no host and falls back to the stacked flex card. Source order stays 2. */
  /* Same rail geometry as §1's one-screen face — keep these in step. */
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) {
    display: grid;
    grid-template-columns: minmax(168px, 0.9fr) minmax(0, 2.6fr);
    grid-template-rows: auto minmax(0, 1fr);
    column-gap: calc(1.6 * var(--vh));
    row-gap: calc(0.6 * var(--vh));
  }
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) > h2 {
    grid-area: 1 / 1 / 2 / -1;
    font-size: clamp(16px, calc(2.3 * var(--vh)), 23px);
    letter-spacing: 0.04em; margin-bottom: 0;
  }
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) > h2 .num { display: none; }
  /* The pace meter, its caption and legend belong to the stacked layout — the
     two rail tiles carry tercapai and pace-ideal on this face. */
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) > :is(.chart-cap, .meter, .meter-legend) {
    display: none;
  }
  /* Left rail — same treatment as §1's: each figure its own equal-height
     segment, centred, brand colour, a hairline between. */
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) > .tiles {
    grid-area: 2 / 1 / 3 / 2;
    margin: 0; padding: 0 calc(1.5 * var(--vh)) 0 0; border: 0;
    border-right: 1px solid var(--line);
    display: flex; flex-direction: column; gap: 0;
  }
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) .tiles .tile {
    flex: 1 1 0;
    display: flex; flex-direction: column; justify-content: center; gap: calc(0.3 * var(--vh));
    padding: 0.6em 0; background: none; border: 0;
  }
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) .tiles .tile + .tile {
    border-top: 1px solid var(--line);
  }
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) .tile-label {
    margin: 0; letter-spacing: 0.03em; text-transform: uppercase; color: var(--muted);
    font-size: clamp(9px, calc(1.1 * var(--vh)), 12px);
  }
  /* Identical to §1's rail figure — nowrap token, inline-block denominator that
     drops as a unit when the rail can't hold it inline (see §1's note). */
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) .tile-value {
    color: var(--brand); font-weight: 700; line-height: 1.04; letter-spacing: -0.01em;
    /* In step with §1's rail — smaller cap so the 3-decimal miliar figures fit. */
    font-size: clamp(16px, calc(2.4 * var(--vh)), 26px);
    white-space: nowrap;
  }
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) .tile-value.neg { color: #a3301a; }
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) .tile-den {
    display: inline-block; color: var(--muted); font-weight: 500;
    font-size: 0.55em; letter-spacing: 0; white-space: nowrap;
  }
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) .tile-foot {
    display: block; margin: calc(0.2 * var(--vh)) 0 0; color: var(--muted);
    font-size: clamp(9px, calc(1.1 * var(--vh)), 12px); font-variant-numeric: tabular-nums;
  }
  /* Chart fills the right column. */
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) > [data-chart="ytd"] {
    grid-area: 2 / 2 / 3 / -1; align-self: stretch; min-height: 0;
  }

  /* §9 one-screen face: title only, like §1/§2. The chart-cap ("Semua outlet
     terhadap target sendiri, <periode>") repeats what the h2 and the axis
     already say at meeting distance; it stays in the DOM as the §9 modal's
     data-label source. The chart-foot legend (dash = 100% target, rule =
     company average, full colour = top/bottom 3) stays — that encoding is not
     self-evident. */
  .dash .grid > [data-section="9"] > .chart-cap { display: none; }
  /* Title only, sized like §1/§2's one-screen face — the leaderboard reads as
     a headline, not item 9 in a list. Source order stays 9 (tab / screen-reader
     order untouched). */
  .dash .grid > [data-section="9"] > h2 {
    font-size: clamp(16px, calc(2.3 * var(--vh)), 23px);
    letter-spacing: 0.04em;
  }
  .dash .grid > [data-section="9"] > h2 .num { display: none; }

  /* §10 one-screen face: the two-column ranked list ([data-barber-strip], filled
     by charts.js) stands in for the 20-row Haircut/Package chart — 20 grouped
     bars can't fit this cell legibly. The chart host, its swatch legend and the
     chart-cap all hide (display:none, so no grid cell) but stay in the DOM for
     the §10 modal to clone / use as data-label. The strip's own column heads
     ("10 Teratas" / "10 Terbawah") already say what it is. Title sized like §9. */
  .dash .grid > [data-section="10"] > :is([data-chart], .legend, .chart-cap) { display: none; }
  .dash .grid > [data-section="10"] > h2 {
    font-size: clamp(16px, calc(2.3 * var(--vh)), 23px); letter-spacing: 0.04em;
  }
  .dash .grid > [data-section="10"] > h2 .num { display: none; }
  .dash .grid > [data-section="10"] { display: flex; flex-direction: column; }
  .dash .grid > [data-section="10"] > [data-barber-strip] {
    display: block; flex: 1; min-height: 0; overflow: hidden;
  }

  /* The six smalls — §3–§8 — are the page's compact class: real small cards
     (boss note 2026-08-31, "real real small"). Numbers / small charts only, so
     every intrinsic block is tightened one notch below the shared .dash-card
     scale and the chart's height floor drops, letting the mains' 1.4fr
     columns read as a step above rather than as an equal card. */
  .dash .grid > [data-section="3"],
  .dash .grid > [data-section="4"],
  .dash .grid > [data-section="5"],
  .dash .grid > [data-section="6"],
  .dash .grid > [data-section="7"],
  .dash .grid > [data-section="8"] {
    padding: calc(0.45 * var(--vh)) calc(0.55 * var(--vh));
  }
  .dash .grid > [data-section="3"] h2,
  .dash .grid > [data-section="4"] h2,
  .dash .grid > [data-section="5"] h2,
  .dash .grid > [data-section="6"] h2,
  .dash .grid > [data-section="7"] h2,
  .dash .grid > [data-section="8"] h2 {
    margin: 0 0 calc(0.3 * var(--vh));
    font-size: clamp(8px, calc(0.95 * var(--vh)), 11px);
  }
  .dash .grid > [data-section="3"] h2 .num,
  .dash .grid > [data-section="4"] h2 .num,
  .dash .grid > [data-section="5"] h2 .num,
  .dash .grid > [data-section="6"] h2 .num,
  .dash .grid > [data-section="7"] h2 .num,
  .dash .grid > [data-section="8"] h2 .num {
    width: 14px; height: 14px; flex: 0 0 14px; font-size: 8px;
  }
  /* §5's title runs bigger than its siblings on request. */
  .dash .grid > [data-section="5"] h2 {
    font-size: clamp(11px, calc(1.3 * var(--vh)), 15px);
  }
  /* §5 has no chart to eat the card's height — the four tiles take it instead.
     The 2x2 grid stretches to fill (`.dash-card` is already flex-column), each
     tile centring its own stack, so the figures read as large as the box allows. */
  .dash .grid > [data-section="5"] > .tiles {
    flex: 1 1 auto; min-height: 0;
    grid-template-rows: 1fr 1fr;
  }
  .dash .grid > [data-section="5"] .tiles .tile {
    display: flex; flex-direction: column; justify-content: space-between;
    min-height: 0; padding: calc(0.7 * var(--vh)) calc(0.9 * var(--vh));
  }
  .dash .grid > [data-section="3"] .tiles,
  .dash .grid > [data-section="4"] .tiles,
  .dash .grid > [data-section="5"] .tiles,
  .dash .grid > [data-section="6"] .tiles,
  .dash .grid > [data-section="7"] .tiles,
  .dash .grid > [data-section="8"] .tiles {
    gap: calc(0.25 * var(--vh));
    margin: 0 0 calc(0.3 * var(--vh));
  }
  .dash .grid > [data-section="3"] .tiles .tile,
  .dash .grid > [data-section="4"] .tiles .tile,
  .dash .grid > [data-section="5"] .tiles .tile,
  .dash .grid > [data-section="6"] .tiles .tile,
  .dash .grid > [data-section="7"] .tiles .tile,
  .dash .grid > [data-section="8"] .tiles .tile {
    padding: calc(0.2 * var(--vh)) calc(0.35 * var(--vh));
  }
  .dash .grid > [data-section="3"] .tile-value,
  .dash .grid > [data-section="4"] .tile-value,
  .dash .grid > [data-section="5"] .tile-value,
  .dash .grid > [data-section="6"] .tile-value,
  .dash .grid > [data-section="7"] .tile-value,
  .dash .grid > [data-section="8"] .tile-value {
    font-size: clamp(9px, calc(1.15 * var(--vh)), 15px);
  }
  /* The small cards are the page's compact class, so the balance needs help:
     each card's lead figure IS its at-a-glance headline and should read a clear
     step above its supporting figures. Sized on --vh so it scales with the card;
     a single line of short tabular figures, so the bump costs no height.
     Which figure is the lead is per-card, not "first child": §4's two tiles are
     a target/actual PAIR and the actual is the result, so §4 (excluded from the
     blanket rule) emphasizes its aktual tile while §7–§8 keep their first. §3
     carries no tiles and is harmless here. §6's two tiles (Total Job, %
     Permintaan Barber) are both independent headline figures, not a lead +
     supporting pair, so both get the lift. */
  /* §5 excluded: four equal KPI tiles, no lead figure to emphasise. */
  .dash .grid > [data-section="6"] .tiles .tile:first-child .tile-value,
  .dash .grid > [data-section="6"] .tiles .tile:nth-child(2) .tile-value,
  .dash .grid > [data-section="7"] .tiles .tile:first-child .tile-value,
  .dash .grid > [data-section="8"] .tiles .tile:first-child .tile-value {
    font-size: clamp(12px, calc(2.0 * var(--vh)), 24px);
    letter-spacing: -0.01em;
  }
  .dash .grid > [data-section="4"] .tiles .tile:nth-child(2) .tile-value {
    font-size: clamp(12px, calc(2.0 * var(--vh)), 24px);
    letter-spacing: -0.01em;
  }
  /* §5's 2x2 layout has the vertical room the old single row didn't — all four
     ratio figures read as headlines, a step above §6-§8's supporting tiles. */
  .dash .grid > [data-section="5"] .tile-value {
    font-size: clamp(16px, calc(3.2 * var(--vh)), 36px);
    letter-spacing: -0.02em; line-height: 1.05;
  }
  .dash .grid > [data-section="5"] .tile-label {
    font-size: clamp(9px, calc(1.2 * var(--vh)), 13px);
  }
  .dash .grid > [data-section="5"] .tile-foot {
    font-size: clamp(9px, calc(1.15 * var(--vh)), 12px);
  }
  .dash .grid > [data-section="3"] .tile-label,
  .dash .grid > [data-section="4"] .tile-label,
  .dash .grid > [data-section="5"] .tile-label,
  .dash .grid > [data-section="6"] .tile-label,
  .dash .grid > [data-section="7"] .tile-label,
  .dash .grid > [data-section="8"] .tile-label,
  .dash .grid > [data-section="3"] .chart-cap,
  .dash .grid > [data-section="4"] .chart-cap,
  .dash .grid > [data-section="5"] .chart-cap,
  .dash .grid > [data-section="6"] .chart-cap,
  .dash .grid > [data-section="7"] .chart-cap,
  .dash .grid > [data-section="8"] .chart-cap,
  .dash .grid > [data-section="3"] .chart-foot,
  .dash .grid > [data-section="4"] .chart-foot,
  .dash .grid > [data-section="5"] .chart-foot,
  .dash .grid > [data-section="6"] .chart-foot,
  .dash .grid > [data-section="7"] .chart-foot,
  .dash .grid > [data-section="8"] .chart-foot {
    font-size: clamp(7px, calc(0.8 * var(--vh)), 9px);
  }
  .dash .grid > [data-section="3"] [data-chart],
  .dash .grid > [data-section="4"] [data-chart],
  .dash .grid > [data-section="5"] [data-chart],
  .dash .grid > [data-section="6"] [data-chart],
  .dash .grid > [data-section="7"] [data-chart],
  .dash .grid > [data-section="8"] [data-chart] {
    min-height: calc(2.5 * var(--vh));
  }
  .dash .grid > [data-section="3"] .chips,
  .dash .grid > [data-section="4"] .chips,
  .dash .grid > [data-section="5"] .chips,
  .dash .grid > [data-section="6"] .chips,
  .dash .grid > [data-section="7"] .chips,
  .dash .grid > [data-section="8"] .chips { font-size: clamp(8px, calc(0.9 * var(--vh)), 10px); }
  .dash .grid > [data-section="3"] .legend,
  .dash .grid > [data-section="4"] .legend,
  .dash .grid > [data-section="5"] .legend,
  .dash .grid > [data-section="6"] .legend,
  .dash .grid > [data-section="7"] .legend,
  .dash .grid > [data-section="8"] .legend { font-size: clamp(8px, calc(0.9 * var(--vh)), 10px); }

  .dash-card {
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
    padding: calc(1 * var(--vh)) calc(1.1 * var(--vh));
  }
  .dash-card h2 {
    gap: 7px;
    margin: 0 0 calc(0.8 * var(--vh));
    font-size: clamp(9px, calc(1.15 * var(--vh)), 13px);
  }
  /* A title that cannot fit its column truncates rather than taking a second
     line out of the chart below it. */
  .dash-card h2 > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .dash-card h2 .num {
    width: 19px;
    height: 19px;
    flex: 0 0 19px;
    border-radius: 6px;
    font-size: 10px;
  }

  /* auto-fit + minmax(200px) resolves to ONE column inside a quarter-width
     card, which stacks two tiles where the design has them side by side.
     Explicit column flow instead: as many columns as there are tiles, always. */
  .dash-card .tiles {
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    grid-template-columns: none;
    gap: calc(0.6 * var(--vh));
    margin: 0 0 calc(0.8 * var(--vh));
  }
  .dash-card .tiles:last-child { margin-bottom: 0; }
  .dash-card .tiles .tile { padding: calc(0.55 * var(--vh)) calc(0.75 * var(--vh)); }
  .dash-card .tile-label { margin-bottom: 2px; font-size: clamp(9px, calc(1.05 * var(--vh)), 12px); }
  /* 1.55, not 2: three tiles across a quarter-width card leave ~86px for the
     text, and the miliar figures ("Rp 18,000 M", three decimals now) wrap
     mid-figure at 720p one notch larger. */
  .dash-card .tile-value { font-size: clamp(11px, calc(1.55 * var(--vh)), 22px); }
  /* §2's YTD card is the year's balance at a glance, the second headline on the
     one-screen meeting view — so its leading figure (Revenue YTD) reads a step
     above its neighbours. §1's own rail already sizes its lead figures larger
     via the [data-s1-target] block, so it is deliberately excluded here. The
     lift is gentler than §1's: Revenue YTD is a long tabular string (longer
     still with three-decimal miliar) and must not wrap mid-figure. */
  .dash .grid > [data-section="2"] .tiles .tile:first-child .tile-value {
    font-size: clamp(14px, calc(2 * var(--vh)), 24px);
    letter-spacing: -0.02em;
  }
  /* On the one-screen face the rail figures are all one size (like §1's), so
     the stacked layout's first-child lift above is normalised back. */
  .dash .grid > [data-section="2"]:has(> [data-chart="ytd"]) .tiles .tile:first-child .tile-value {
    font-size: clamp(16px, calc(2.4 * var(--vh)), 26px);
    letter-spacing: -0.01em;
  }
  .dash-card .tile-foot { margin-top: 1px; font-size: clamp(8px, calc(0.95 * var(--vh)), 11px); }

  /* The chart-cap belongs to the chart, not to the gap above it: its bottom
     margin separates the caption from the chart it captions; the space above
     it is owned by the preceding .tiles' own bottom margin. */
  .dash-card .chart-cap { margin: 0 0 calc(0.35 * var(--vh)); font-size: clamp(9px, calc(1.05 * var(--vh)), 12px); }
  .dash-card .chart-foot { margin: calc(0.35 * var(--vh)) 0 0; font-size: clamp(8px, calc(0.95 * var(--vh)), 11px); }

  /* The one flexible box on a card. Everything else keeps its intrinsic height,
     so the chart is what gives way — and the floor stops it giving way to
     nothing. Basis 0, not auto: an auto basis is the chart's own content, and
     the chart's content is whatever height we last gave it.

     --chart-fill is read by charts.js, and it is the whole reason the flag
     exists rather than the media query being written out twice: it says "this
     box's height belongs to the layout, measure it" and it says so from the one
     place that decides that. Without it the chart is sized by `data-height`. */
  /* No `overflow: hidden` here, deliberately. The library caps the gutter it
     gives y-axis categories, so a long one ("Kuota Barbertologist" in a quarter
     of a 720p screen) is drawn a few pixels outside the viewBox — which is what
     `[data-chart] svg { overflow: visible }` above is for. The card's padding
     absorbs the spill, and the card clips, so the one-screen guarantee is still
     enforced one level up. */
  .dash-card [data-chart] { --chart-fill: 1; flex: 1 1 0; min-height: calc(6 * var(--vh)); }
  /* §2's YTD chart is the one card whose fixed content (tiles + meter +
     legend + foot) over-subscribes its cell at 720p/768p by a
     few px — the generic 6*--vh floor is what pushed it over. Its cumulative
     lines read fine shorter; the meter above carries the headline. */
  .dash-card [data-chart='ytd'] { min-height: calc(4 * var(--vh)); }
  /* Axis and value labels are the one text on a card that did NOT scale with
     --vh: they are painted by the library at a fixed size (see the
     `[data-chart] text` rule below). At 720p that is the same size inside a
     card two-thirds the height, which is what made §4's nine cost lines and
     §9/§10's ten-and-eleven row names collide into each other.

     The FLOOR is what matters here, and it is a physical measurement, not a
     CSS one. `body { zoom: 0.8 }` above means a CSS px paints at 0.8px, so the
     old clamp(8px … 11px) reached the eye as 6,4–8,8px — well under the 12px
     readability floor, on a page whose own comments say it is read off a
     meeting-room projector. 15px CSS is 12px painted; the 13px floor is 10,4px
     at 720p, which is the trade that keeps ten cards on one screen.

     Set on the HOST, not on `text`, and that placement is load-bearing: the
     renderer reserves each axis gutter from the label size it is TOLD (every
     axis passes `tickLabels.fontSize`), and charts.js resolves that number by
     reading this property off the host before any label exists. Sizing `text`
     directly instead is what clipped §9's and §10's outlet names off the left
     edge — CSS painted 15px labels into a gutter cut for 11px ones. */
  .dash-card [data-chart] { font-size: clamp(13px, calc(1.4 * var(--vh)), 15px); }
  /* §4 carries two charts. The trend is a shape, not a table - it keeps a
     sparkline's height so the cost matrix under it gets a row per line. */
  .dash-card [data-chart].spark { flex: 0 0 auto; height: clamp(22px, calc(3.2 * var(--vh)), 40px); min-height: 0; }
  /* Weekly mode drops the cost matrix under it (no weekly source), so the trend
     is the card's only chart and takes the room the matrix would have had
     rather than leaving a third of the card blank. */
  .dash-inactive [data-chart].spark { flex: 1 1 0; height: auto; min-height: calc(6 * var(--vh)); }
  .dash-card .mix { flex: 1 1 0; min-height: 0; align-items: stretch; gap: calc(0.9 * var(--vh)); }
  .dash-card .mix [data-chart] { min-height: 0; }

  /* Found live 2026-08-26 at 1512x694: six legend rows (Haircut..Layanan
     Lainnya) each carry a header-style `.delta` pill (border + padding +
     ~19px line height), needing ~123px total against the ~82px `.mix` had
     left to give — `.mix` has no overflow of its own, so the two rows that
     did not fit did not clip, they painted straight through into `.tiles`
     below (Package's count and Haircut's — the majority category at 66,6%
     of the period — both unreadable behind the % Permintaan Barber tile).
     Stripped to plain coloured text, the same treatment §1's own `.pct`
     already gets in this same legend family, and tightened to a single
     line: six rows now fit inside whatever `.mix` is given, no pill chrome
     to outgrow it. */
  /* Loosened from the 1px/1.1 this comment's own fix originally landed on -
     §6's tiles block dropped from two uneven boxes to one merged row the same
     day (2026-08-27), which gave `.mix` more of the card back. Re-verified at
     1512x694, the exact size the original crowding was caught at: still zero
     overflow on `.mix`/`.legend`/the card itself. */
  .dash-card .mix .legend { gap: 3px; }
  .dash-card .mix .legend li { line-height: 1.3; }
  .dash-card .mix .legend .delta { background: none; border: 0; padding: 0; border-radius: 0; }

  /* flex-shrink: 0 — found live 2026-08-23: at 1366x768/1280x720 the §2 meter
     collapsed to 0px tall. The card's flex column was over-subscribed (the
     chart's min-height floor refuses to give way), so flex squeezed the only
     item without a content floor — an empty-height bar — down to nothing,
     while .meter-mark (top/bottom -3px) kept painting outside the collapsed
     box: the user saw the target tick and no bar at all. Text items survive
     shrinking via min-height:auto's min-content floor; a meter has none. */
  .dash-card .meter { height: clamp(6px, calc(0.85 * var(--vh)), 10px); margin-top: calc(0.5 * var(--vh)); flex-shrink: 0; }
  .dash-card .meter-lg { height: clamp(9px, calc(1.3 * var(--vh)), 16px); margin-top: calc(0.3 * var(--vh)); flex-shrink: 0; }
  .dash-card .meter-legend { margin-top: calc(0.4 * var(--vh)); font-size: clamp(9px, calc(1.05 * var(--vh)), 12px); }

  /* §2 is the only card with no flex-growing chart/placeholder
     ([data-chart]/.placeholder above), so nothing else claims the stretched
     row's leftover height - margin-top: auto pushes .note (its only user) to
     the card's bottom edge instead of leaving that space dead below it. */
  .dash-card .note.note-top { margin-top: 0; }
  .dash-card .note {
    margin-top: auto;
    padding: calc(0.5 * var(--vh)) calc(0.7 * var(--vh));
    font-size: clamp(9px, calc(1.05 * var(--vh)), 12px);
    line-height: 1.35;
  }
  .dash-card .placeholder {
    flex: 1 1 auto;
    align-content: center;
    min-height: 0;
    margin-top: calc(0.8 * var(--vh));
    padding: calc(0.8 * var(--vh));
    font-size: clamp(9px, calc(1.05 * var(--vh)), 12px);
  }
  .dash-card .placeholder strong { font-size: clamp(9px, calc(1.05 * var(--vh)), 12px); }

  .dash-card .legend { gap: calc(0.3 * var(--vh)); font-size: clamp(9px, calc(1.05 * var(--vh)), 12px); }
  .dash-card .legend-row { margin-top: calc(0.2 * var(--vh)); gap: calc(0.2 * var(--vh)) 12px; }
  .dash-card .swatch { width: 9px; height: 9px; flex: 0 0 9px; border-radius: 2px; }

  .dash-card .pair { margin-top: calc(0.4 * var(--vh)); gap: calc(0.6 * var(--vh)); }
  .dash-card .pair strong { font-size: clamp(11px, calc(1.35 * var(--vh)), 16px); }
  /* The annotation lines beside a figure ("3.900 booking · Juni: 24,5%",
     "Bulan lalu: 27,2%") inherit the card's body size, which left them reading
     larger than the tile LABEL they sit under. Footnote size, like every other
     annotation on a card. */
  .dash-card .tile .muted,
  .dash-card .pair .muted { font-size: clamp(8px, calc(0.95 * var(--vh)), 11px); }
  .dash-card .pair .muted { margin-left: 6px; }

  /* §5/§8 render this instead of an axis-only chart when the division has not
     keyed the month. Centred: it stands in for a chart, so it should read as a
     state of the card, not as a stray line of text pinned to one corner. */
  .dash-card .placeholder { justify-items: center; text-align: center; }

  /* Eleven outlet names wrap to three or four rows in a quarter-width card.
     They stay at the bottom of the card and clip there rather than pushing the
     chart above them out of the cell. */
  .dash-card .chips { flex: 0 1 auto; min-height: 0; overflow: hidden; gap: 4px; margin-top: calc(0.5 * var(--vh)); }
  .dash-card .chip { padding: 2px 7px; font-size: clamp(8px, calc(0.95 * var(--vh)), 11px); }
}
/* The scrolling layout runs at --z: 1, so this size is also the painted size —
   15px, over the 12px readability floor. On the HOST for the same reason as the
   one-screen rule above: charts.js reads it back and hands it to every axis, so
   the gutter and the paint agree. `text` inherits rather than declaring, so
   there is exactly one number. */
[data-chart] { font-size: 15px; color: var(--muted); }
/* `fill` is deliberately NOT set here. The renderer emits a mark's own fill as
   a PRESENTATION ATTRIBUTE, and any stylesheet rule outranks one — so a blanket
   `fill: var(--muted)` silently repainted every label that had asked for a
   colour. §6's in-band count was white-on-rust by intent and painted muted
   brown on rust instead (~1.5:1, unreadable), and §9's highlighted rows lost
   their dark ink and rendered identically to the receded ones.
   Marks that want the default emit `fill="currentColor"` and pick up the
   `color` above; marks that name a colour now keep it. */
[data-chart] text { font-size: inherit; }

/* The chart library's native tooltip surface, themed to match the app instead
   of its browser-default look. These custom properties are read by the
   @tanstack/charts tooltip element's own inline styles, so restating the
   variables here is the supported override path — no internals touched. */
.ts-chart-tooltip {
  --ts-chart-tooltip-background: #fff;
  --ts-chart-tooltip-border: 1px solid var(--line);
  --ts-chart-tooltip-border-radius: 8px;
  --ts-chart-tooltip-shadow: 0 10px 28px rgb(36 26 21 / 0.14);
  /* 15px, matching the chart type it sits next to. 12px was the smallest text
     on the page and `zoom: 0.8` painted it at 9,6px — a figure someone squints
     at is a figure they read wrong. */
  --ts-chart-tooltip-font: 500 15px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* The default is `min(24rem, 80%)` and the 80% is the whole bug: the tooltip
     is absolutely positioned INSIDE its chart host, so the percentage resolves
     against the host, not the page. §6's host is `.mix`'s fixed 64px chart
     track (a ~119px fraction back when this was written), which capped the
     box at ~51px; with the library's inline
     `overflow-wrap: anywhere` on top, "Classic Haircut" did not wrap, it
     SHATTERED — one letter per line, a 277px column of characters.
     Re-based on the viewport so a narrow host cannot starve it. 280px still
     clears the narrowest card this appears in, so a wide tooltip is never
     clipped by the card's own `overflow: hidden`. */
  --ts-chart-tooltip-max-width: min(280px, 92vw);
}

/* `width` is NOT set inline by the library, so this one lands normally and lets
   the box hug its content up to the cap above. */
.ts-chart-tooltip { width: max-content; }

/* These two ARE inline, and inline beats a stylesheet — `!important` is the
   only way to reach them, and it is why the max-width above goes through the
   custom property instead. Without it the row's grid gave the label a 0px
   track and printed it underneath the figure. */
.ts-chart-tooltip {
  overflow-wrap: normal !important;
  word-break: normal !important;
}
/* A row is a grid of swatch | label | figure, and the library sets the template
   inline as `0.55rem minmax(0px, 1fr) auto`. That `0px` minimum is the second
   half of the bug: the label track is allowed to shrink below its own text, so
   "Realisasi kumulatif" overflowed its 80px track and printed underneath the
   rupiah figure beside it.
   `min-content` as the floor means the track is never narrower than the label's
   longest WORD — so the label wraps onto a second line when the box runs out of
   room, which is the one behaviour that neither overlaps nor shatters. */
.ts-chart-tooltip__row {
  grid-template-columns: 0.55rem minmax(min-content, 1fr) auto !important;
}
.ts-chart-tooltip__title { white-space: nowrap; }

/* Pendapatan per Layanan (/dashboard/revenue-per-layanan) - a plain ranked
   data table, not table.pnl's freeze-pane machinery: one screen, no sticky
   caption, no per-outlet column pairs. Shares .table-scroll for the long tail
   of products; the header row freezes inside it the same way. */
table.grid {
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}
table.grid th, table.grid td {
  padding: 6px 12px;
  border-bottom: 1px solid var(--line-soft);
  text-align: left;
  white-space: nowrap;
}
table.grid th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--bg);
  font-size: 12px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  border-bottom: 1px solid var(--line);
}
table.grid td.num, table.grid th.num { text-align: right; }
table.grid tfoot td {
  border-top: 1px solid var(--line);
  border-bottom: 0;
  font-weight: 650;
}

/* ============================================================================
   Section detail modal — opens when a section card is clicked.

   A native <dialog> in the top layer: background inert, UA-centred, height
   capped to the viewport with the body scrolling inside it.
   Cards are clickable on every viewport ≥ 1000px; below that the one-screen
   contract already shows the page as a scrolling grid, and a tap target that
   opens a modal on top would conflict with the natural scroll. The card
   itself signals its affordance via .dash-card.is-clickable + a subtle hover.
   ============================================================================ */
.dash-card.is-clickable { cursor: pointer; }
/* The hover reads as "this opens something", not as a link to somewhere else:
   the card borders onto brand and gains a hint of lift, and a chevron appears
   on the title row. A header only, so nothing on the card jumps, and the
   chevron is added by CSS (::after) so no markup change is needed. */
.dash-card.is-clickable:not(.dash-inactive):hover {
  border-color: var(--brand);
  box-shadow: 0 14px 30px -20px rgba(36, 26, 21, 0.35);
}
/* One subtle cue that the detail lives behind the card: a tiny chevron that
   appears at the end of the title row on hover. Placed on the h2 (the flex
   container), not on the title span, so a title that runs to its column's edge
   cannot clip it; kept out of the layout until hover so the header never
   shifts. */
.dash-card.is-clickable:not(.dash-inactive):hover h2::after {
  content: "\203A";
  margin-left: auto;
  color: var(--brand);
  font-size: 1.15em;
  line-height: 1;
  flex: none;
}
.dash-card.is-clickable:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.dash-inactive.is-clickable { cursor: not-allowed; }

/* Native <dialog>: showModal() puts it in the top layer (centred by the UA,
   above everything, background inert, Esc + focus trap for free). The box is
   height-capped and its body scrolls, so a long section table is reachable
   instead of painted off the bottom of a fixed overlay. */
dialog#section-modal {
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: var(--field);
  color: var(--ink);
  width: min(1100px, calc(100vw - 48px));
  max-height: calc(100vh - 48px);
  box-shadow: 0 24px 48px -12px rgba(36, 26, 21, 0.4);
}
/* Every section-detail modal (§1's own grid + §2–§10's `body.dataset.grid`
   layout) gets the same frame. `zoom: calc(1 / var(--z))` cancels the app's
   own `body { zoom: var(--z) }` (0.8 on desktop, 1 on touch — see line ~35) so
   the modal always paints at true 1:1 scale — legible, not the 0.8 squint — and
   its `100vw`/`100vh` sizing lands in real pixels. Consistent near-full size
   with a margin around it, not edge-to-edge. Per-section layout tuning is in
   the @media block below. */
dialog#section-modal:has(.modal-body.sec-1),
dialog#section-modal:has(.modal-body[data-grid]) {
  zoom: calc(1 / var(--z));
  width: min(1440px, calc(100vw - 160px));
  /* explicit height, not just max-height: several modals stretch a chart to
     `height: 100%` of its grid cell, which only resolves against a definite
     container height. It also keeps every modal the same size. */
  height: calc(100vh - 44px);
  max-height: calc(100vh - 44px);
}
dialog#section-modal[open] {
  display: grid;
  grid-template-rows: auto 1fr;
}
dialog#section-modal::backdrop {
  background: rgba(36, 26, 21, 0.55);
}
.modal-head {
  display: flex; align-items: center; gap: 12px;
  padding: 18px 24px;
  border-bottom: 1px solid var(--line-soft);
  background: var(--bg);
}
.modal-head h2 {
  margin: 0; flex: 1; min-width: 0;
  font-size: 20px; font-weight: 700;
  display: flex; align-items: center; gap: 10px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.modal-head h2 .num {
  display: grid; place-items: center;
  width: 28px; height: 28px; flex: 0 0 28px;
  background: #f3e4dc; color: var(--brand);
  border: 1px solid #e8d0c2; border-radius: 6px;
  font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.modal-close {
  appearance: none; background: none; border: 0; padding: 0;
  width: 32px; height: 32px; display: grid; place-items: center;
  border-radius: 6px; cursor: pointer; color: var(--muted);
  font-size: 22px; line-height: 1;
}
.modal-close:hover { background: var(--bg); color: var(--ink); }
.modal-close:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

.modal-body {
  /* The body scrolls inside the height-capped dialog. The head above it stays
     put; a long table reaches its last row. */
  padding: 24px;
  display: grid; gap: 20px;
  grid-template-columns: minmax(0, 1fr);
  align-content: start;
  overflow-y: auto;
  min-height: 0;
}

/* Chart heights inside the modal: deliberately smaller than the first pass
   so the chart + the table that follows it both fit on one laptop screen at
   1080p. Each value picked by the section that owns the chart - taller ones
   (leaderboards, the P&L cost matrix) earn their height, compact ones
   (layanan, sparklines) keep their natural aspect. */
/* --chart-fill:1 makes boxOf() read the fixed height below via clientHeight.
   font-size is the chart's tick/label size (readTextPx) - pinned so modal
   charts keep the projector-legible floor the .dash-card rule gives on-card. */
.modal-body [data-chart] { min-height: 280px; --chart-fill: 1; height: 320px; font-size: 15px; }
.modal-body [data-chart].spark { height: 100px; min-height: 100px; }
.modal-body [data-chart='revenue'] { min-height: 240px; height: 260px; }
.modal-body [data-chart='ytd'] { min-height: 280px; height: 320px; }
.modal-body [data-chart='outlet'] { min-height: 240px; height: 280px; }
.modal-body [data-chart='leaderboardOutlet'] { min-height: 360px; height: 400px; }
.modal-body [data-chart='leaderboardBarbertologist'] { min-height: 320px; height: 360px; }
.modal-body [data-chart='layanan'] { min-height: 160px; height: 200px; }
.modal-body [data-chart='headcount'] { min-height: 180px; height: 220px; }
.modal-body [data-chart='rate'] { min-height: 240px; height: 280px; }
.modal-body [data-chart='coa'] { min-height: 240px; height: 280px; }
.modal-body [data-chart='netProfitTrend'] { min-height: 140px; height: 180px; }
.modal-body [data-chart='pelanggan'] { min-height: 240px; height: 280px; }

.modal-section {
  display: grid; gap: 10px;
  grid-template-columns: minmax(0, 1fr); /* content stretches to the modal width, not to max-content */
}
.modal-section h3 {
  margin: 0; font-size: 12px; font-weight: 650;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted);
}
.modal-tiles {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.modal-tile {
  background: var(--bg); border: 1px solid var(--line-soft); border-radius: 8px;
  padding: 12px 14px;
  display: grid; gap: 3px;
}
.modal-tile-label { font-size: 11px; color: var(--muted); font-weight: 600; letter-spacing: 0.02em; }
.modal-tile-value { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.2; }
.modal-tile-foot { font-size: 11px; color: var(--muted); }
.modal-empty { margin: 0; color: var(--muted); font-size: 13px; }
.modal-chart-legend {
  display: flex; flex-wrap: wrap; gap: 4px 14px;
  margin: 6px 0 2px; font-size: 12px; color: var(--ink);
}
.modal-chart-legend span { display: inline-flex; align-items: center; gap: 6px; }
.modal-chart-legend i {
  width: 10px; height: 10px; border-radius: 2px; flex: none;
}

/* §1 detail-modal hero (monthly + weekly) — a flat cream panel, thin hairline
   border: the big figure, the comparison stats, and a category-filled pace rail
   with last period's position ticked. */
.s1-hero {
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #f6ece4;
  padding: 12px 16px;
  display: grid;
  gap: 10px 26px;
}
@media (min-width: 700px) {
  .s1-hero { grid-template-columns: minmax(0, max-content) 1fr; align-items: center; }
  .s1-hero .s1-pace, .s1-hero .s1-hero-note, .s1-hero .s1-hero-wide { grid-column: 1 / -1; }
}
.s1-hero-main { display: grid; gap: 3px; align-content: start; }
.s1-hero-eyebrow {
  font-size: 10px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--brand);
}
.s1-hero-num {
  font-size: 30px; font-weight: 800; line-height: 1.05;
  font-variant-numeric: tabular-nums; letter-spacing: -0.01em; color: var(--ink);
}
.s1-hero-sub {
  font-size: 12px; font-weight: 600; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.s1-hero-stats {
  display: grid; gap: 12px 20px;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.s1-stat { display: grid; gap: 2px; align-content: start; }
.s1-stat-label {
  font-size: 10px; font-weight: 700; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--muted);
}
.s1-stat-value {
  font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink);
}
.s1-stat-value.is-up { color: #1e6b33; } /* green up / no red down — see .delta */
.s1-stat-foot { font-size: 11px; color: var(--muted); }
.s1-pace { display: grid; gap: 6px; padding-top: 10px; }
.s1-pace-track {
  position: relative; height: 26px; border-radius: 7px; background: var(--line-soft);
}
/* the target-pace end of the track */
.s1-pace-track::after {
  content: ""; position: absolute; right: 0; top: -4px; bottom: -4px;
  width: 2px; border-radius: 1px; background: var(--brand);
}
.s1-pace-fill {
  display: flex; height: 100%; border-radius: 7px 2px 2px 7px; overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(36, 26, 21, 0.06);
  background: var(--brand); /* a segmentless rail (§2) still shows a fill */
}
/* a bridge / composition rail (§4 margin, §6 job-mix, §7 base) is the whole
   100%, so the target-pace end tick has nothing to mark */
.s1-pace-track.no-end::after { content: none; }
/* each category segment centres its juta value; a segment with no room for one
   just renders as a colour band (charts.js only appends the span where it fits) */
.s1-pace-fill i {
  display: flex; align-items: center; justify-content: center;
  height: 100%; min-width: 0; overflow: hidden;
}
.s1-pace-fill i + i { box-shadow: inset 1px 0 0 rgba(246, 236, 228, 0.4); }
.s1-pace-fill i span {
  font-size: 10px; font-weight: 650; font-style: normal; color: #fff;
  font-variant-numeric: tabular-nums; letter-spacing: 0.01em;
  padding: 0 4px; white-space: nowrap;
}
.s1-pace-ghost { position: absolute; top: -4px; bottom: -4px; width: 2px; background: var(--ink); }
.s1-pace-ghost::before {
  content: ""; position: absolute; left: 50%; top: -4px;
  width: 6px; height: 6px; background: var(--ink);
  transform: translateX(-50%) rotate(45deg);
}
.s1-pace-ghost::after {
  content: attr(data-month);
  position: absolute; left: 50%; top: -17px; transform: translateX(-50%);
  font-size: 9px; font-weight: 700; letter-spacing: 0.02em; color: var(--ink); white-space: nowrap;
}
.s1-pace-labels {
  display: flex; justify-content: space-between;
  font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums;
}
.s1-hero-note { margin: 0; font-size: 11px; color: var(--muted); }
.k-dot {
  display: inline-block; width: 9px; height: 9px; border-radius: 2px;
  margin-right: 7px; vertical-align: baseline;
}

/* §1 detail modal — outlet contribution as a compact ranked list (both the
   monthly and weekly views use it). An 11-row bordered table alone stood taller
   than the whole modal. Row: rank · name · dotted leader · juta · share — the
   leader ties the name to its figure across the gap and the figures line up on
   the right edge of each column. */
.s1-contrib { display: grid; gap: 3px; }
.s1-contrib-row {
  display: grid;
  grid-template-columns: 1.4em max-content minmax(12px, 1fr) 4.6em 3em;
  align-items: baseline; column-gap: 6px;
  padding: 2.5px 0;
  font-size: 12px; font-variant-numeric: tabular-nums;
}
.s1-contrib-rank { color: var(--muted); font-size: 10px; text-align: right; }
.s1-contrib-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); }
.s1-contrib-lead { align-self: center; height: 0; border-bottom: 1px dotted var(--line); }
.s1-contrib-jt { text-align: right; white-space: nowrap; color: var(--ink); font-weight: 600; }
.s1-contrib-pct { text-align: right; white-space: nowrap; color: var(--muted); }

/* §1's Komposisi Layanan — a sorted horizontal bar list (charts.js
   katCompositionBars). The .k-bars grid owns the columns; each row is a subgrid
   so every track shares one width (bars only compare if the baseline does) and
   the figures line up on the right. The 1fr track eats the column's free width,
   so the block fills the modal cell the snug table used to float in. */
.k-bars {
  display: grid;
  grid-template-columns: max-content minmax(36px, 1fr) 4em 4.6em;
  gap: 5px 10px;
  font-size: 12px; font-variant-numeric: tabular-nums;
}
/* Two independent .k-bars side by side (§6's per-outlet list once the roster
   is long) - each keeps its own subgrid, so tracks only line up within a
   column, not across both. */
.k-bars-cols {
  display: grid;
  grid-template-columns: repeat(var(--k-cols, 2), 1fr);
  gap: 5px 14px;
}
.k-bars:has(.k-bar-shift) {
  grid-template-columns: max-content minmax(36px, 1fr) 4em 4.6em 3.9em;
}
/* Target-per-produk scorecard (charts.js katTargetList) — §1 modal AND card
   face. Short product names, wider "−528,3"-style gaps than Tren Bulanan's
   "+12,4%": its own column widths, plus left room so the name clears the
   is-bad / is-good inset edge. */
/* .k-bars.k-target, not .k-target — matches .k-bars:has(.k-bar-shift)'s
   specificity so the later rule wins the column override. */
.k-bars.k-target { grid-template-columns: max-content minmax(20px, 1fr) 4em 5.4em 6em; }
.k-target .k-bar-name { padding-left: 4px; }
/* Hidden by default; the one-screen monthly layout switches it on for §1
   (see the dashboard @media block) where it stands in for the revenue chart. */
[data-s1-target] { display: none; }
/* Same deal for the §1 progress-bar caption: the one-screen face shows it in
   place of the "% dari Target" tile; the narrow layout keeps that tile and
   leaves this hidden, so the figure is never on screen twice. */
.s1-progress-cap { display: none; }

/* §10 card face — two-column ranked list (charts.js [data-barber-strip] loop),
   the one-screen stand-in for the 20-row Haircut/Package chart, which cannot
   fit this cell legibly. Hidden by default; the one-screen @media block
   switches it on for §10 and hides the chart + legend (kept in the DOM for the
   modal). Bar = each barber's total jobs as a share of the #1 total, painted
   as the name cell's own background so a row stays one text-line tall. */
[data-barber-strip] { display: none; }
/* Fills the §10 cell top-to-bottom: the strip stretches (flex:1 on the one-screen
   rule), each column is a full-height grid, and align-content:space-between
   spreads the 10 rows down the card so there is no dead space below rank 10 —
   the row PITCH grows, the bars stay one text-line tall. */
.bstrip {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 0 clamp(14px, 2.4vw, 34px); height: 100%; min-height: 0;
}
.bstrip-col {
  display: grid; grid-template-columns: 2ch 1fr max-content;
  column-gap: 8px; align-content: space-between;
}
.bstrip-head {
  grid-column: 1 / -1; margin: 0; text-align: center;
  font-size: clamp(9px, calc(1.15 * var(--vh)), 12px);
  letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--muted); font-weight: 650;
}
.bstrip-row {
  display: grid; grid-column: 1 / -1; grid-template-columns: subgrid;
  align-items: center; line-height: 1.15;
  font-size: clamp(10px, calc(1.5 * var(--vh)), 13.5px);
  font-variant-numeric: tabular-nums;
}
.bstrip-rank { color: var(--muted); text-align: right; font-size: 0.82em; }
.bstrip-name {
  color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  padding: 2px 7px; border-radius: 3px;
  background-image: linear-gradient(to right,
    color-mix(in srgb, var(--s1) 28%, transparent),
    color-mix(in srgb, var(--s1) 28%, transparent));
  background-size: var(--bar, 0%) 100%;
  background-repeat: no-repeat; background-position: left center;
}
.bstrip-num { text-align: right; font-weight: 600; color: var(--ink); }
.k-bar-row {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
}
.k-bar-head {
  font-size: 10px; letter-spacing: 0.03em; text-transform: uppercase;
  color: var(--muted); font-weight: 650;
}
.k-bar-head span { text-align: right; white-space: nowrap; }
.k-bar-name { color: var(--ink); white-space: nowrap; }
.k-bar-track {
  height: 12px; border-radius: 3px; overflow: hidden;
  background: var(--line-soft);
}
.k-bar-track i { display: block; height: 100%; min-width: 2px; border-radius: 3px; }
.k-bar-jt { text-align: right; font-weight: 600; color: var(--ink); white-space: nowrap; }
.k-bar-pct { text-align: right; color: var(--muted); white-space: nowrap; }
/* Komposisi: mix-shift vs last month — directional, not good/bad (charts.js
   ppShift). Tren: MoM. Both right-aligned in the last column. */
.k-bar-shift { text-align: right; color: var(--muted); font-size: 11px; }

/* Tren Bulanan: the bar is % of that month's own target (0–100, tick at 100),
   in brand rather than the per-service colours. */
.t-bar-track { position: relative; }
.t-bar-track i { background: var(--brand); }
.t-bar-track::after {
  content: ""; position: absolute; right: 0; top: 0; bottom: 0;
  width: 2px; background: var(--ink); opacity: 0.35;
}

/* §8's headcount bridge as its own widget (charts.js waterfallBlock): step
   label · the step's own ± · the running total. */
.m-wf {
  display: grid; gap: 2px;
  font-size: 12px; font-variant-numeric: tabular-nums;
}
.m-wf-row {
  display: grid; grid-template-columns: 1fr max-content 4.5em;
  align-items: baseline; column-gap: 12px; padding: 3px 6px;
}
.m-wf-label { color: var(--ink); }
.m-wf-delta { text-align: right; color: var(--muted); }
.m-wf-run { text-align: right; font-weight: 700; color: var(--ink); }

/* Modal highlight rows — doctrine in charts.js (applyMark) and
   docs/dashboard-highlight-rules.md. This page carries no red (see .delta):
   "watch" is a warm ochre, "good" the same green .delta.up uses. A row tint
   plus a 2px inset edge, so the mark survives the row gaps in .k-bars /
   .s1-contrib and still reads if the tint is missed. */
.k-bar-row.is-watch, .s1-contrib-row.is-watch, .modal-table tr.is-watch, .m-wf-row.is-watch {
  background: #f7ece0; box-shadow: inset 2px 0 0 #a75d17;
}
.k-bar-row.is-good, .s1-contrib-row.is-good, .modal-table tr.is-good, .m-wf-row.is-good {
  background: #e8f1ea; box-shadow: inset 2px 0 0 #1e6b33;
}
/* `is-bad` = a pass/fail miss (§1's per-product target scorecard). The one red
   on the page — the app's own negative colour (.tile-value.neg, .flash.err),
   reserved for "this number did not hit its target", never for a nudge. */
.k-bar-row.is-bad, .s1-contrib-row.is-bad, .modal-table tr.is-bad, .m-wf-row.is-bad {
  background: #fbeeed; box-shadow: inset 2px 0 0 #a3301a;
}
.k-bar-row.is-bad .k-bar-pct, .k-bar-row.is-bad .k-bar-shift { color: #a3301a; font-weight: 600; }
.k-bar-row.is-good .k-bar-pct, .k-bar-row.is-good .k-bar-shift { color: #1e6b33; font-weight: 600; }
.k-bar-row.is-bad .k-bar-track i { background: #a3301a; }
.k-bar-row.is-good .k-bar-track i { background: #1e6b33; }
.k-bar-row.is-watch, .k-bar-row.is-good, .k-bar-row.is-bad { padding-block: 2px; }
.k-bar-row.is-watch .k-bar-pct, .k-bar-row.is-watch .k-bar-shift,
.s1-contrib-row.is-watch .s1-contrib-pct { color: #8a4c11; font-weight: 600; }
.s1-contrib-row.is-good .s1-contrib-pct { color: #1e6b33; font-weight: 600; }
.modal-table tr.is-watch td:last-child { color: #8a4c11; font-weight: 600; }
.modal-table tr.is-good td:last-child { color: #1e6b33; font-weight: 600; }
.modal-table td.is-pos { color: #1e6b33; font-weight: 600; }
.modal-table td.is-neg { color: #a3301a; font-weight: 600; }
/* sortable-table extremes: row with the sorted column's highest / lowest value */
.modal-table tr.is-hi { background: #e8f1ea; box-shadow: inset 2px 0 0 #1e6b33; }
.modal-table tr.is-lo { background: #fbeeed; box-shadow: inset 2px 0 0 #a3301a; }
/* click-to-pick: keep one row tinted brown so a name stays findable while
   scrolling. Wins over the hi/lo tint on the same row (declared after). */
.modal-table.is-pickable tbody tr { cursor: pointer; }
.modal-table.is-pickable tbody tr:hover { background: #f4ede2; }
.modal-table tr.is-picked,
.modal-table.is-pickable tbody tr.is-picked:hover {
  background: #ecdcc4; box-shadow: inset 3px 0 0 #8a5a2b;
}
.modal-hl-cap {
  margin: 5px 0 0; font-size: 11px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* The modal's opening finding (charts.js verdict()). Reads before the tiles —
   sized like body copy, not a caption, with the same left rule the row marks
   use. `flat` = neutral (a section with no drama says so plainly). */
.modal-verdict {
  padding: 11px 15px; border-radius: 8px;
  background: var(--bg); box-shadow: inset 3px 0 0 var(--muted);
  font-size: 14px; line-height: 1.55; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.modal-verdict.is-watch { background: #f7ece0; box-shadow: inset 3px 0 0 #a75d17; }
.modal-verdict.is-good  { background: #e8f1ea; box-shadow: inset 3px 0 0 #1e6b33; }
.modal-verdict ul { margin: 0; padding-left: 17px; display: grid; gap: 4px; }

/* Tables inside the modal grow with their rows; the modal body scrolls around
   them. The bordered wrap gives the table its own card-like surface so it reads
   as a unit rather than floating in the modal's white space. */
/* Modal tables are their own thing - NOT `.grid` (that class is `display:grid`
   for the card layout and turns a <table> into a broken grid). Just a scroll
   container for the odd table wider than the modal; the rows carry their own
   rules so no enclosing box is needed. */
.modal-table-wrap { overflow-x: auto; }
.modal-table {
  /* content width, left-aligned - a snug table reads better than one justified
     across the whole modal with the columns floating apart. Boxed by an outer
     border; no inner vertical rules, rows carry the horizontal lines. */
  width: auto;
  max-width: 100%;
  border-collapse: collapse;
  border: 1px solid var(--line);   /* outer box only — no inner vertical rules */
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.modal-table td.num, .modal-table th.num { padding-left: 28px; } /* breathing room between columns */
.modal-table th, .modal-table td {
  padding: 7px 12px;
  border-bottom: 1px solid var(--line-soft);
  text-align: left;
  white-space: nowrap;
}
/* Colored header band: filled brand row, white uppercase labels. */
.modal-table th {
  font-size: 11px; font-weight: 650; color: #fff;
  text-transform: uppercase; letter-spacing: 0.03em;
  background: var(--brand);
  border-bottom: 0;
  padding: 8px 12px;
}
.modal-table td.num, .modal-table th.num { text-align: right; }
.modal-table tbody tr:last-child td { border-bottom: 0; }

/* §10's sortable headers only (charts.js sortableTableBlock, 2026-09-07) -
   every other modal table's th stays static, so this keys on the marker
   attribute the builder sets rather than a blanket .modal-table th rule. */
.modal-table th[data-sortable] {
  cursor: pointer;
  user-select: none;
}
.modal-table th[data-sortable]:hover { background: var(--brand-dark); }
.modal-table th[data-sortable].is-sorted { background: var(--brand-dark); }
.modal-table th[data-sortable]:focus-visible {
  outline: 2px solid #fff;
  outline-offset: -3px;
}

/* §1 detail modal, MONTHLY and WEEKLY alike: hero across the top; the chart
   takes two thirds of the row below with the composition and trend tables
   stacked in the last third; outlet contribution as a compact ranked list
   across the foot. Sized so a 740px laptop viewport never scrolls the body.
   Below 1000px it falls back to the single-column flow. */
@media (min-width: 1000px) {
  .modal-body.sec-1 {
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-areas:
      "hero   hero   hero"
      "chart  chart  compo"
      "chart  chart  trend"
      "outlet outlet outlet";
    row-gap: 8px;
  }
  /* align-content:start stops a short section's rows stretching to fill the
     equal-height grid cell. */
  .modal-body.sec-1 .modal-section { gap: 6px; align-content: start; }

  .modal-body.sec-1 .s1-hero   { grid-area: hero; align-self: start; }
  .modal-body.sec-1 .s1-chart  {
    grid-area: chart;
    padding: 0 24px 0 0;
    border: 0;
    border-right: 1px solid var(--line);
    align-content: start;
  }
  .modal-body.sec-1 .s1-compo  { grid-area: compo; align-self: start; }
  .modal-body.sec-1 .s1-trend  { grid-area: trend; align-self: start; }
  .modal-body.sec-1 .s1-outlet {
    grid-area: outlet;
    border-top: 1px solid var(--line);
    padding-top: 10px;
  }
  /* Fixed chart height — flex-fill fought the chart library's own box read.
     Tuned so the chart block fills its grid area (spans the compo + trend rows)
     rather than leaving a band of white under the caption. 314px is the ceiling:
     the chart section then matches compo + Target-per-produk exactly, and one
     more px pushes Kontribusi Outlet down into a scroll. */
  .modal-body.sec-1 .s1-chart [data-chart] {
    height: 314px; min-height: 314px;
  }
  .modal-body.sec-1 .s1-chart .modal-chart-legend { margin: 5px 0 0; }
  .modal-body.sec-1 .s1-chart-ritme {
    margin: 4px 0 0; font-weight: 600; color: var(--ink);
  }
  /* tighter table rows — the two tables stack in a third of the row */
  .modal-body.sec-1 .modal-table { font-size: 11px; }
  .modal-body.sec-1 .modal-table :is(th, td) { padding-block: 1px; line-height: 1.45; }
  .modal-body.sec-1 .modal-table th { padding-block: 4px; }
  .modal-body.sec-1 .modal-table :is(th, td).num { width: 4.2rem; }
  /* monthly's Tren table carries every month (7 rows vs weekly's 4) in the same
     slot — tighter line height keeps the compo + trend stack short enough that
     the body still does not scroll. */
  .modal-body.sec-1:not(.is-weekly) .modal-table :is(th, td) { line-height: 1.15; }
  /* Same no-scroll discipline for the Komposisi / Tren bar lists: the two
     stacks (6 services + up to 12 months, plus a header each) share the third
     column, and the chart beside them is sized to match their combined height,
     so the rows run tight. */
  .modal-body.sec-1 .k-bars { gap: 2px 10px; font-size: 11px; }
  .modal-body.sec-1 .k-bar-track { height: 10px; }
  .modal-body.sec-1 .k-bar-head { font-size: 9px; }
  /* Weekly §1 only carries 6 services + 4 weeks in this column (vs monthly's
     6 + up to 12), so it has the vertical room to run the Komposisi / Tren
     lists and table at a readable size instead of the monthly squeeze. */
  .modal-body.sec-1.is-weekly .k-bars { gap: 10px 14px; font-size: 14px; }
  .modal-body.sec-1.is-weekly .k-bar-track { height: 22px; }
  .modal-body.sec-1.is-weekly .k-bar-head { font-size: 11px; }
  .modal-body.sec-1.is-weekly .modal-table { font-size: 14px; width: 100%; }
  .modal-body.sec-1.is-weekly .s1-trend .modal-table-wrap { width: 100%; }
  .modal-body.sec-1.is-weekly .modal-table :is(th, td) { padding-block: 9px; line-height: 1.6; }
  .modal-body.sec-1.is-weekly .modal-table th { padding-block: 11px; }
  .modal-body.sec-1.is-weekly .s1-chart [data-chart] { height: 460px; min-height: 460px; }
  /* outlet: three top-to-bottom columns. */
  .modal-body.sec-1 .s1-contrib {
    display: block; columns: 3; column-gap: 30px;
  }
  .modal-body.sec-1 .s1-contrib-row { break-inside: avoid; padding: 1px 0; }
  /* Weekly §1 has the vertical room — run Kontribusi outlet larger. */
  .modal-body.sec-1.is-weekly .s1-contrib { column-gap: 36px; }
  .modal-body.sec-1.is-weekly .s1-contrib-row {
    font-size: 14px; padding: 4px 0; column-gap: 8px;
  }
  .modal-body.sec-1.is-weekly .s1-contrib-rank { font-size: 12px; }

  /* ---- §2–§10 enriched modals: fit-one-screen grid (charts.js gArea) --------
     Every one lays out as: hero across the top, verdict under it, then the
     chart and one or two tables sharing the columns below — sized so a 1080p
     laptop never scrolls the body, same discipline as sec-1. The named areas
     (hero/verdict/chart/a/b/c) are placed per section; the shared rules here
     do the tightening. Below 1000px this whole block is off and the body
     falls back to the single-column flow. */
  .modal-body[data-grid] {
    column-gap: 22px;
    row-gap: 8px;
    align-content: start;
  }
  .modal-body[data-grid] .ga-hero    { grid-area: hero; align-self: start; }
  .modal-body[data-grid] .ga-verdict { grid-area: verdict; align-self: start; }
  .modal-body[data-grid] .ga-chart   { grid-area: chart; align-self: start; }
  .modal-body[data-grid] .ga-a       { grid-area: a; align-self: start; }
  .modal-body[data-grid] .ga-b       { grid-area: b; align-self: start; }
  .modal-body[data-grid] .ga-c       { grid-area: c; align-self: start; }
  .modal-body[data-grid] .ga-d       { grid-area: d; align-self: start; }
  .modal-body[data-grid] .ga-e       { grid-area: e; align-self: start; }

  .modal-body[data-grid] .modal-section { gap: 6px; align-content: start; }
  .modal-body[data-grid] .modal-section h3 { font-size: 11px; }
  /* tight table rows — several share a column with the chart */
  .modal-body[data-grid] .modal-table { font-size: 11px; }
  .modal-body[data-grid] .modal-table :is(th, td) { padding-block: 2px; line-height: 1.25; }
  .modal-body[data-grid] .modal-table th { padding-block: 5px; }
  .modal-body[data-grid] .modal-table :is(th, td).num { padding-left: 18px; }
  /* the modal's default 280–400px chart heights are for a full-width chart;
     here the chart shares a row with tables, so it takes what the tallest
     table beside it needs — capped low. */
  .modal-body[data-grid] .modal-chartwrap [data-chart] { height: 188px; min-height: 172px; }
  .modal-body[data-grid] .modal-chartwrap [data-chart].spark { height: 70px; min-height: 70px; }
  .modal-body[data-grid] .s1-hero { padding: 9px 13px; gap: 7px 20px; }
  .modal-body[data-grid] .s1-hero-num { font-size: 25px; }
  .modal-body[data-grid] .s1-hero-stats { gap: 8px 16px; }
  /* The pace-descriptor text under each stat ("pace ideal 58,0%", "perlu
     Rp 1,1 M/bln") carries the modal's second-most-important numbers — 11px
     fell below comfortable reading at this viewing distance. */
  .modal-body[data-grid] .s1-stat-foot { font-size: 12px; }
  /* §6 / §9 heroes have no rail — flatten each into one row of equal cells: the
     headline reads as just another stat, same size as the rest. display:contents
     dissolves .s1-hero-stats so its .s1-stat cells become direct children of the
     .s1-hero grid alongside .s1-hero-main. */
  .modal-body.sec-6 .s1-hero,
  .modal-body.sec-9 .s1-hero {
    display: grid; grid-template-columns: repeat(4, 1fr);
    gap: 20px 28px; padding: 16px 20px; align-items: start;
  }
  .modal-body.sec-6 .s1-hero-stats,
  .modal-body.sec-9 .s1-hero-stats { display: contents; }
  .modal-body.sec-6 .s1-hero-main,
  .modal-body.sec-9 .s1-hero-main { gap: 4px; }
  .modal-body.sec-6 .s1-hero-eyebrow, .modal-body.sec-6 .s1-stat-label,
  .modal-body.sec-9 .s1-hero-eyebrow, .modal-body.sec-9 .s1-stat-label { font-size: 12px; }
  .modal-body.sec-6 .s1-hero-num, .modal-body.sec-6 .s1-stat-value,
  .modal-body.sec-9 .s1-hero-num, .modal-body.sec-9 .s1-stat-value {
    font-size: 24px; font-weight: 800; line-height: 1.1; letter-spacing: -0.01em;
  }
  .modal-body.sec-6 .s1-stat-foot,
  .modal-body.sec-9 .s1-stat-foot { font-size: 12px; }
  .modal-body[data-grid] .modal-verdict { padding: 8px 12px; font-size: 12.5px; line-height: 1.4; }
  .modal-body[data-grid] .modal-verdict ul { gap: 3px; }
  .modal-body[data-grid] .k-bars { gap: 3px 10px; font-size: 11px; }

  /* §2 — hero (category-split pace rail) / verdict + Komposisi YTD bars /
     cumulative line chart + cumulative pace table. The chart column gets the
     clearly wider share (1.5fr): the trend is what the eye scans first and a
     squeezed plot undercuts it, while the table reads fine narrower. The chart
     spans TWO rows in its column: the verdict above it is two bullets while
     the Komposisi list beside that row is tall, so a one-row chart left an
     unfillable band of whitespace under the verdict. Spanning lets the chart
     absorb that band instead. */
  .modal-body.sec-2 {
    grid-template-columns: 1.5fr 1fr;
    grid-template-areas:
      "hero    hero"
      "verdict b"
      "chart   b"
      "chart   a";
  }
  /* The height lever for the chart is its CELL, not a fixed px value: the
     chartwrap is a title + fill grid, so whatever the spanning rows give it,
     the plot takes. (A fixed height was tried and was exactly what left gaps:
     cut it shorter and the plot floats down leaving space under the title.)
     The gap we also want to close is under the table: the table block should
     reach the chart's floor, so let its cell stretch to the full row height
     and the table fill it — the two columns end together, no white space
     under "Kumulatif vs Target". */
  .modal-body.sec-2 .modal-chartwrap [data-chart] { font-size: 15px; }
  .modal-body.sec-2 .ga-a { align-self: stretch; grid-template-rows: auto 1fr; }
  .modal-body.sec-2 .ga-a .modal-table-wrap { height: 100%; }
  /* The page doctrine keeps tables content-width (snug beats justified), but
     that doctrine is about a table floating across the WHOLE modal width. In
     a stretched grid cell of its own, an auto-width box stops partway across
     the cell and reads as an unfinished edge — so here the table justifies to
     its own column, the numeric columns keeping their right alignment. */
  .modal-body.sec-2 .ga-a .modal-table { height: 100%; width: 100%; }
  /* Row 2's height is set by the Komposisi YTD bars (six rows + head) while the
     verdict beside it is two bullets, and row 3's by the table while the chart
     box is a fixed 236px — both left a strip of unowned whitespace above (and
     under) the chart that nothing could grow into. Stretch the short cells to
     their row instead: the verdict fills down, and the chartwrap becomes a
     title + fill grid so the chart itself takes the leftover height (its
     --chart-fill:1 makes boxOf re-read clientHeight, and the ResizeObserver
     repaints at the new size). */
  /* The chart fills its stretched cell via the title + fill grid below; the
     verdict no longer needs to stretch or center — its row is exactly its
     own height now that the chart, not the verdict, absorbs the leftover. */
  .modal-body.sec-2 .ga-chart { align-self: stretch; grid-template-rows: auto 1fr; }
  .modal-body.sec-2 .ga-chart [data-chart] { height: 100%; min-height: 220px; }

  /* §3 — hero / verdict / a tall step chart left, cohort bars + roster right.
     No per-month count table: the chart's own point labels carry those figures,
     and the table only ever duplicated them. The chart absorbs the leftover
     height, same discipline as sec-2's ga-chart. */
  .modal-body.sec-3 {
    grid-template-columns: 1.4fr 1fr;
    grid-template-rows: auto auto minmax(0, 1fr) auto;
    grid-template-areas:
      "hero    hero"
      "verdict verdict"
      "chart   a"
      "chart   c";
  }
  .modal-body.sec-3 .ga-chart { align-self: stretch; grid-template-rows: auto 1fr; }
  .modal-body.sec-3 .ga-chart [data-chart] { height: 100%; min-height: 220px; }
  .modal-body.sec-3 .chip { font-size: 10px; padding: 2px 7px; }
  /* Cohort bars: a DASHED mark at the portfolio average on every track, so it
     reads as a reference line rather than a second bar segment — the solid
     right-edge tick is the 100% cap. Same mark, same place, every row.
     Generalised to all modal bodies (§3 cohort bars, §6 value/request bars). */
  .modal-body .o3-track { height: 14px; }
  .modal-body .o3-track::before {
    content: ""; position: absolute; top: -3px; bottom: -3px;
    left: var(--avg, 100%); width: 0;
    border-left: 2px dashed var(--ink); opacity: 0.45;
  }
  .modal-body .o3-track::after { opacity: 0.18; }
  /* The block's rows sit inside a 2-row bar list where the house row-tint
     (full-width band + inset edge) reads as a clipped table: text flush to
     the band's edge on both sides. Here the mark moves onto the name — a
     short accent edge + padding on the name cell only, so columns stay on
     their shared subgrid — and the block gets inner padding instead. */
  .modal-body.sec-3 .ga-a .k-bars,
  .modal-body.sec-6 .ga-a .k-bars,
  .modal-body.sec-6 .ga-c .k-bars { padding: 3px 8px; }
  .modal-body.sec-3 .ga-a .k-bar-row.is-watch,
  .modal-body.sec-3 .ga-a .k-bar-row.is-good,
  .modal-body.sec-6 .ga-c .k-bar-row.is-watch,
  .modal-body.sec-6 .ga-c .k-bar-row.is-good { background: none; box-shadow: none; }
  .modal-body.sec-3 .ga-a .k-bar-row.is-watch .k-bar-name,
  .modal-body.sec-6 .ga-c .k-bar-row.is-watch .k-bar-name {
    box-shadow: inset 3px 0 0 #a75d17; padding-left: 7px; font-style: italic;
  }
  .modal-body.sec-3 .ga-a .k-bar-row.is-good .k-bar-name,
  .modal-body.sec-6 .ga-c .k-bar-row.is-good .k-bar-name {
    box-shadow: inset 3px 0 0 #1e6b33; padding-left: 7px;
  }
  .modal-body.sec-3 .ga-a .modal-hl-cap { padding: 0 8px; }
  .modal-body.sec-6 .ga-c .modal-hl-cap { padding: 0 8px; }
  /* Sized up (2026-09-07) once dropping Nilai per Job per Layanan left this
     column with far more height than a snug six-row table needed - stretched
     to the chart's full row height and enlarged the same way sec-9's table
     fills its own chart row, rather than floating small in a tall cell. */
  .modal-body.sec-6 .ga-a { align-self: stretch; grid-template-rows: auto 1fr; }
  .modal-body.sec-6 .ga-a .modal-table-wrap { height: 100%; }
  .modal-body.sec-6 .ga-a .modal-table { height: 100%; width: 100%; font-size: 13px; }
  .modal-body.sec-6 .ga-a .modal-table th,
  .modal-body.sec-6 .ga-a .modal-table td { padding: 8px 12px; }

  /* §4 — hero (bridge) / verdict / two charts stacked left / two cost tables right. */
  .modal-body.sec-4 {
    grid-template-columns: 0.85fr 1fr;
    grid-template-areas:
      "hero    hero"
      "verdict verdict"
      "chart   a"
      "chart   b";
  }
  .modal-body.sec-4 .s4-charts { display: grid; gap: 8px; align-content: start; }

  /* §5 — hero (gauges) / verdict / chart + ratio table. */
  /* No card-face chart for §5 (scorecard, not a plot) — the hero's ratioBars is
     the visual and the per-ratio trend table takes the full width below it. */
  .modal-body.sec-5 {
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "hero    hero"
      "verdict verdict"
      "a       a";
  }

  /* §6 — hero (mix rail) / jobs-trend beside the Komposisi table; the
     barber-request-per-outlet spread runs the full width below (2026-09-07:
     Nilai per Job per Layanan was dropped entirely on request, freeing that
     row's height for the outlet spread instead - it stretches into it the
     same way sec-9's chart+table stretch into freed space). The insight-badge
     verdict block was dropped in an earlier pass for the same reason: with a
     full table and a per-outlet spread already on screen, a text summary
     above them was one more thing to read before reaching the numbers. */
  .modal-body.sec-6 {
    /* Was 1.3fr chart / 1fr table - flipped (2026-09-07) so the enlarged
       Komposisi table (bigger font/padding, on request) has the width its
       five columns need instead of overflowing into a horizontal scroll. */
    grid-template-columns: 1fr 1.4fr;
    grid-template-areas:
      "hero  hero"
      "chart a"
      "c     c"
      "d     e";
    row-gap: 10px;
    column-gap: 28px;
  }
  /* d (Komposisi Permintaan, 6 cols) and e (Komposisi per Item, 8 cols) share
     the last row, e on the wider track. min-width:0 lets the tracks hold their
     fr ratio; width:100% + the wrap's overflow-x:auto is the safety net if a
     column set ever outgrows its track. */
  .modal-body.sec-6 .ga-e { align-self: start; }
  .modal-body.sec-6 .ga-d,
  .modal-body.sec-6 .ga-e { min-width: 0; }
  .modal-body.sec-6 .ga-d .modal-table,
  .modal-body.sec-6 .ga-e .modal-table { width: 100%; }
  .modal-body.sec-6 .ga-d .modal-table-wrap,
  .modal-body.sec-6 .ga-e .modal-table-wrap { overflow-x: auto; }
  .modal-body.sec-6 .ga-d .modal-table :is(th, td).num,
  .modal-body.sec-6 .ga-e .modal-table :is(th, td).num { padding-left: 14px; }
  /* Whole section tuned to fit one laptop screen without the body scrolling
     (2026-09-09): the four table/chart rows below the hero were ~130px over a
     864px viewport. Every rule here trims vertical space — nothing restructures. */
  /* Dialog fills the viewport (zoom cancel + explicit height above), but at true
     1:1 scale the content is ~120px taller than the box — these trims close that
     gap so nothing scrolls, monthly or weekly. */
  .modal-body.sec-6 { row-gap: 4px; padding-block: 8px; }
  .modal-body.sec-6 .modal-section h3 { margin-bottom: 2px; }
  .modal-body.sec-6 .s1-hero { padding: 7px 13px; }
  .modal-body.sec-6 .ga-a .modal-table { font-size: 11px; }
  .modal-body.sec-6 .ga-a .modal-table :is(th, td) { padding: 3px 11px; line-height: 1.18; }
  .modal-body.sec-6 :is(.ga-d, .ga-e) .modal-table :is(th, td) { padding-block: 1px; line-height: 1.12; }
  .modal-body.sec-6 :is(.ga-d, .ga-e) .modal-table th { padding-block: 2px; }
  /* d's six wide headers don't fit its narrower track at 11px on a toned-down
     (non-full-width) modal — 10px + a tight gutter keeps every value visible. */
  .modal-body.sec-6 .ga-d .modal-table { font-size: 10px; }
  .modal-body.sec-6 .ga-d .modal-table :is(th, td).num { padding-left: 8px; }
  .modal-body.sec-6 .ga-c .k-bars { gap: 0 10px; font-size: 10px; }
  .modal-body.sec-6 .ga-c .o3-track { height: 8px; }
  .modal-body.sec-6 .ga-c .modal-section h3 { margin-bottom: 1px; }
  .modal-body.sec-6 .ga-chart { align-self: stretch; grid-template-rows: auto 1fr; }
  .modal-body.sec-6 .ga-chart [data-chart] { height: 100%; min-height: 120px; }
  .modal-body.sec-6 .ga-c { align-self: stretch; }

  /* §7 — hero (base bar) / verdict / chart + retention-per-outlet / new-members. */
  .modal-body.sec-7 {
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "hero    hero"
      "verdict verdict"
      "chart   a"
      "b       a";
  }

  /* §8 — hero (waterfall) / verdict / chart + turnover trend. */
  .modal-body.sec-8 {
    grid-template-columns: 1.1fr 1fr;
    grid-template-areas:
      "hero    hero"
      "verdict verdict"
      "chart   a";
  }

  /* §9 — hero (strip / top performer) / chart beside the full table. The
     insight-badge verdict block was dropped (2026-09-07, on request) - the
     hero's own stat cells (biggest shortfall, biggest carrier) already say
     what the verdict text repeated in prose. */
  .modal-body.sec-9 {
    grid-template-columns: 1.05fr 1fr;
    grid-template-areas:
      "hero  hero"
      "chart a";
  }
  /* The table's row count is fixed (one per outlet) and short of the chart's
     own height, leaving a bare strip under the last row (worse once the
     verdict row above stopped padding it out). Stretched to the chart's full
     height and the table itself justified to fill it - same move sec-2's ga-a
     already makes for the same reason. */
  .modal-body.sec-9 .ga-a { align-self: stretch; grid-template-rows: auto 1fr; }
  .modal-body.sec-9 .ga-a .modal-table-wrap { height: 100%; }
  .modal-body.sec-9 .ga-a .modal-table { height: 100%; width: 100%; }
  /* Was a fixed 300px; now stretches to the row's own height (freed up by
     dropping the hero's distribution strip, 2026-09-07) so the chart claims
     that space instead of leaving it as a gap above the table's own stretch. */
  .modal-body.sec-9 .ga-chart { align-self: stretch; grid-template-rows: auto 1fr; }
  .modal-body.sec-9 .modal-chartwrap [data-chart] { height: 100%; min-height: 280px; }

  /* §10 (2026-09-07, on request): table only — no hero, verdict or chart, so
     no multi-row grid is needed at all. The single "a" area is the table, and
     it gets the full box: the point of dropping the summary blocks was
     reaching all ~20 rows without a scroll, and a leftover multi-row template
     would have left empty hero/verdict/chart bands above it doing that again. */
  .modal-body.sec-10 {
    grid-template-columns: 1fr;
    grid-template-areas: "a";
  }
  .modal-body.sec-10 .ga-a,
  .modal-body.sec-10 .ga-a .modal-table-wrap {
    height: 100%;
  }
  /* The [data-grid] tightening above (11px text, 2px row padding) was sized
     for a table sharing a row with a chart - alone in the whole box it read
     as a small table floating in a lot of empty space. Scaled back up (and
     past sec-1..9's own defaults) since this table is now the ENTIRE modal:
     the row height should be what fills the box, not what a neighbour left
     over. */
  .modal-body.sec-10 .modal-section h3 { font-size: 15px; }
  /* Same override sec-2's own stretched table cell already needed: the page
     doctrine keeps tables content-width (snug beats justified) because that
     reads best floating in a modal with other blocks beside it - alone in the
     whole box, content-width left blank margin on the right instead. */
  .modal-body.sec-10 .modal-table-wrap,
  .modal-body.sec-10 .modal-table { width: 100%; }
  .modal-body.sec-10 .modal-table { font-size: 15px; }
  .modal-body.sec-10 .modal-table :is(th, td) { padding-block: 9px; line-height: 1.3; }
  .modal-body.sec-10 .modal-table th { padding-block: 12px; }
  .modal-body.sec-10 .modal-table :is(th, td).num { padding-left: 28px; }
}

/* One-screen layout above 1000px: the cards already claim the whole viewport,
   so a modal is the only way to see a section at full size. Below 1000px the
   cards already have all the room they need and the click affordance hides,
   so the modal system is desktop-only. */
@media (max-width: 1000px) {
  .dash-card.is-clickable { cursor: default; }
}
