/* ═══════════════════════════════════════════════════════════════════════════
   The report surface: ribbon · scope bar · filter strip · canvas · authoring
   rail · page tabs.
   ───────────────────────────────────────────────────────────────────────────
   Every colour is an app.css token. The grid is CSS Grid, not absolute
   positioning, so reflow, the single-column mobile collapse and printing all
   come free; a tile writes only --c/--r/--cw/--ch.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Flex, not grid, and deliberately so.
   The first version used grid-template-rows with an explicit `grid-row: 5` on
   the rail. Two of the bars between them (#filterstrip, #refreshbar) are hidden
   most of the time, and a display:none grid item leaves the row it was counted
   into — so the canvas auto-placed into row 3 while the rail stayed pinned to
   row 5, and the rail rendered BELOW the canvas instead of beside it. Counting
   grid rows across conditionally-hidden siblings is not a layout, it is a
   coincidence. A flex column with an explicit middle row cannot drift. */
#dashapp{
  display:flex; flex-direction:column;
  height:100%; min-height:0; overflow:hidden;
}
:root{--rail-w:320px;}

#ribbon, #slicers, #filterstrip, #refreshbar, #pagetabs{flex:none;}

/* The middle band: canvas beside the rail. */
#dashmid{flex:1; min-height:0; display:flex; align-items:stretch;}
#canvas{flex:1; min-width:0; min-height:0;}
#rail{flex:none; width:var(--rail-w); min-height:0;}

/* ── ribbon ─────────────────────────────────────────────────────────────── */
.rpt{
  display:flex; align-items:flex-start; gap:16px; flex-wrap:wrap;
  padding:10px 18px 8px; border-bottom:1px solid var(--border-color);
  background:var(--bg-surface-alt);
}
.rpt__name{
  font-size:16px; font-weight:680; color:var(--text-primary);
  border:1px solid transparent; border-radius:var(--radius-sm);
  padding:3px 7px; margin:-3px -7px 0; min-width:110px; align-self:center;
}
.rpt__name:hover{border-color:var(--border-color); background:var(--bg-card);}
.rpt__name:focus{outline:none; border-color:var(--color-accent);
  background:var(--bg-card);}

.ribbon__groups{display:flex; gap:18px; flex-wrap:wrap; margin-left:auto;}
.ribbon__group{display:flex; flex-direction:column; gap:3px;}
.ribbon__glabel{
  font-size:9.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  color:var(--text-faint); padding-left:2px;
}
.ribbon__row{display:flex; align-items:center; gap:3px; flex-wrap:wrap;}
/* A ribbon command. Height comes from --ctl-h-sm like every other dense
   control, so a row of them lines up with the segmented control beside it. They
   used to be 25px next to a 30px button, because only the padding was set. */
.rbtn{
  display:inline-flex; align-items:center; justify-content:center; gap:6px;
  height:var(--ctl-h-sm); padding:0 9px; flex:none;
  border:1px solid transparent; border-radius:var(--radius-sm); background:none;
  font:inherit; font-size:var(--fs-chrome); font-weight:550; line-height:1;
  color:var(--text-secondary); cursor:pointer; white-space:nowrap;
  transition:background var(--transition-fast), color var(--transition-fast),
             border-color var(--transition-fast);
}
.rbtn:focus-visible{outline:2px solid var(--color-accent); outline-offset:1px;}
.rbtn:hover{background:var(--bg-hover); color:var(--text-primary);}
.rbtn svg{width:15px; height:15px; flex:none;}
.rbtn.is-active{background:var(--color-accent-soft); color:var(--color-accent);
  border-color:var(--color-accent-soft-strong);}
.rbtn--primary{background:var(--color-accent); color:var(--color-accent-on);
  border-color:var(--color-accent);}
.rbtn--primary:hover{background:var(--color-accent-dark);
  color:var(--color-accent-on);}
.rbtn[disabled]{opacity:.45; cursor:not-allowed;}

/* ── scope bar and filter strip ─────────────────────────────────────────── */
.slicers{
  display:flex; align-items:center; gap:9px; flex-wrap:wrap;
  padding:7px 18px; border-bottom:1px solid var(--border-color);
  background:var(--bg-card);
}
.slicers__label{
  font-size:9.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  color:var(--text-faint);
}
.slicer{display:flex; align-items:center; gap:5px;}
/* The scope controls were the worst offenders: 21px tall beside a 26px ribbon
   button, and native selects showing the OS arrow — a different glyph, colour
   and inset on every platform, three of them side by side. Now they share the
   dense control height and the app's own chevron. */
.slicer{display:flex; align-items:center; gap:6px; min-width:0;}
.slicer select,.slicer input{
  height:var(--ctl-h-sm); padding:0 10px; max-width:176px; min-width:0;
  border:1px solid var(--border-color); border-radius:var(--radius-sm);
  background:var(--bg-card); color:var(--text-primary);
  font:inherit; font-size:var(--fs-chrome); line-height:1; outline:none;
  transition:border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.slicer select{
  appearance:none; -webkit-appearance:none; -moz-appearance:none;
  padding-right:26px; cursor:pointer; text-overflow:ellipsis;
  background-image:var(--select-arrow);
  background-repeat:no-repeat; background-position:right 8px center;
  background-size:12px 12px;
}
.slicer select option{background:var(--bg-card); color:var(--text-primary);}
.slicer select:hover,.slicer input:hover{border-color:var(--text-faint);}
.slicer select:focus,.slicer input:focus{
  border-color:var(--color-accent);
  box-shadow:0 0 0 3px var(--color-accent-soft);
}
/* The scope label carries its own explanation on hover/focus, so the sentence
   that used to occupy a full row under this bar is one keystroke away instead
   of permanently on screen. The dotted underline is what makes that
   discoverable — an affordance with no mark is a secret. */
.slicers__label--help{
  cursor:help; text-decoration:underline dotted var(--border-strong);
  text-underline-offset:3px;
}
.slicers__label--help:hover,.slicers__label--help:focus-visible{
  color:var(--text-muted); text-decoration-color:var(--text-faint);
}

/* Mixed freshness: a chip on the controls' OWN row, pushed to the far end.
   This was a full-width band carrying a two-clause sentence — the warning has
   to be glanceable, the explanation does not, and a paragraph that is always
   on screen stops being read within a day. The short form states the fact and
   offers the fix; the sentence survives as the chip's tooltip. */
.slicers__mixed{
  display:flex; align-items:center; gap:var(--sp-2);
  margin-left:auto; min-width:0;
  height:var(--ctl-h-sm); padding:0 var(--sp-2) 0 var(--sp-3);
  border:1px solid var(--color-warning-soft);
  border-radius:var(--radius-pill);
  background:var(--color-warning-soft);
  color:var(--color-warning);
  font-size:var(--fs-micro); font-weight:650; white-space:nowrap;
}
.slicers__mixed svg{width:13px; height:13px; flex:none;}
.slicers__mixed__txt{cursor:help; min-width:0; overflow:hidden; text-overflow:ellipsis;}
.slicers__mixed .btn{flex:none; height:20px; padding:0 8px; font-size:var(--fs-micro);}

@media (max-width:900px){
  /* No room to share the row — let the chip wrap rather than squeeze the
     selects, which are the thing being operated. */
  .slicers__mixed{margin-left:0; flex-basis:100%; justify-content:flex-start;}
}

.filterstrip{
  display:flex; align-items:center; gap:7px; flex-wrap:wrap;
  padding:7px 18px; border-bottom:1px solid var(--border-color);
  background:var(--color-info-soft);
}
.fchip{
  display:inline-flex; align-items:center; gap:5px; padding:2px 4px 2px 10px;
  border-radius:var(--radius-pill); background:var(--bg-card);
  border:1px solid var(--border-color); font-size:11.5px;
  color:var(--text-secondary);
}
.fchip__x{
  border:0; background:none; padding:2px; border-radius:50%; cursor:pointer;
  color:var(--text-faint); display:grid; place-items:center;
}
.fchip__x:hover{background:var(--bg-hover); color:var(--color-danger);}
.fchip__x svg{width:11px; height:11px;}

.refreshbar{
  display:flex; align-items:center; gap:11px; padding:7px 18px;
  background:var(--color-info-soft); border-bottom:1px solid var(--border-color);
  font-size:12.5px; color:var(--text-secondary);
}
.refreshbar .bar{flex:1; max-width:280px;}

/* ── canvas ─────────────────────────────────────────────────────────────── */
.dash-canvas{
  overflow-y:auto; padding:14px 18px 30px;
  display:grid; grid-template-columns:repeat(12,1fr);
  grid-auto-rows:40px; gap:12px; align-content:start;
}
.dash-canvas--grid{
  background-image:repeating-linear-gradient(
    to right, var(--color-accent-soft) 0, var(--color-accent-soft) 1px,
    transparent 1px, transparent calc((100% - 11 * 12px) / 12 + 12px));
  background-position:18px 0; background-attachment:local;
}
.dash-canvas--busy{cursor:grabbing; user-select:none;}

.tile{
  grid-column:var(--c,1) / span var(--cw,3);
  grid-row:var(--r,1) / span var(--ch,3);
  background:var(--bg-card); border:1px solid var(--border-color);
  border-radius:var(--radius-md); display:flex; flex-direction:column;
  min-width:0; min-height:0; overflow:hidden; position:relative;
  touch-action:none;
  /* The same resting whisper .tapcard gives an interactive card, declared here
     too so a non-interactive tile is not the one flat sheet on the canvas. */
  box-shadow:var(--shadow-xs);
}
/* A visual is a white sheet on a grey canvas — no shadow at rest, because a
   canvas of shadowed tiles reads as a pile rather than a report. Elevation is
   reserved for a visual that is being POINTED AT or MOVED: hovering lifts it a
   hair, dragging lifts it properly. That is the whole hover language of a report
   tool, and it is what makes a static grid feel responsive without animating
   anything that carries data. */
/* The tile's hover comes from the shared .tapcard (app.css) when the tile is
   interactive. What stays here is the SELECTION frame, which is specific to a
   report canvas. */
.tile--nobg{background:transparent;}
.tile--noborder{border-color:transparent;}
.tile--shadow{box-shadow:var(--shadow-md);}
.tile:focus-visible{outline:2px solid var(--color-accent); outline-offset:2px;}
/* Selection is an inset accent frame, not an outline: an outline sits OUTSIDE
   the border box and overlapped the neighbouring tile across a 10px gap. */
.tile.is-selected{
  border-color:var(--color-accent);
  box-shadow:0 0 0 1px var(--color-accent) inset, var(--shadow-sm);
}
/* STATUS TINTS THE WHOLE BORDER; it does not own the left edge.

   It used to: `border-left:3px solid var(--color-warning)` on a stale tile. But
   the left edge is where the shared .tapcard draws the card HUE, so the two
   meanings collided on one channel — an amber left stripe meant "stale", a teal
   one meant "this card is teal", and on a stale tile you got two 3px bars side
   by side. Worse, because most tiles on a real dashboard are stale, the canvas
   looked like it had one amber accent everywhere and no colour identity at all.

   A tinted border plus the badge that already sits in the footer says "stale"
   at least as clearly, and it leaves the left edge to mean one thing.
   And it is a WHISPER of a tint, not a coloured frame. At 55% amber a 1px
   border reads as heavy — and since nearly every tile on a real dashboard is
   stale, the canvas became a grid of amber boxes with the report inside them
   fighting for attention. The footer badge is the label; the border only needs
   to agree with it. Elevation (--shadow-xs, from .tapcard) is what says "sheet
   on a canvas", which is a job colour should not have been doing. */
.tile.is-stale,.tile.is-error{
  border-color:color-mix(in srgb, var(--color-warning) 24%, var(--border-color));
}
/* Denied keeps a little more, because it is the one status that changes what
   the tile is allowed to show — and it also has the soft background below. */
.tile.is-denied{
  border-color:color-mix(in srgb, var(--color-danger) 32%, var(--border-color));
}
/* A denied tile is the one status worth more than a border: its figures are
   blanked and it is telling the reader their access changed. */
.tile.is-denied{background:var(--color-danger-soft);}

/* The hue dot, exactly as on the Home figures — same meaning, same mark, so a
   card recognisably carries its colour in both places. It sits on the title's
   first line rather than centred, because a title can wrap. */
.tile__hue{
  width:6px; height:6px; border-radius:50%; flex:none; margin-top:5px;
  background:var(--card-accent, var(--color-accent));
}
.tile.is-hidden-card{opacity:.42;}
.tile.is-dragging{opacity:.92; box-shadow:var(--shadow-pop); z-index:30;
  cursor:grabbing;}
.tile.is-kbd{outline:2px dashed var(--color-accent); outline-offset:2px;}
.tile.just-added{animation:tilein .5s ease-out;}
@keyframes tilein{from{opacity:0; transform:scale(.97);} to{opacity:1;}}

.tile__ghost{
  grid-column:var(--c,1) / span var(--cw,3);
  grid-row:var(--r,1) / span var(--ch,3);
  border:2px dashed var(--color-accent); border-radius:var(--radius-lg);
  background:var(--color-accent-soft); pointer-events:none; z-index:5;
}

.tile__head{display:flex; align-items:flex-start; gap:6px;
  padding:9px 10px 4px; flex:none;}
[data-mode="edit"] .tile__head{cursor:grab;}
.tile__titles{min-width:0; flex:1;}
.tile__title{
  font-size:var(--fs-chrome); font-weight:650; color:var(--text-primary);
  letter-spacing:-.005em;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.tile__sub{font-size:11px; color:var(--text-faint); overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap;}
.tile__hacts{display:flex; align-items:center; gap:1px; flex:none; opacity:0;
  transition:opacity var(--transition-fast);}
.tile:hover .tile__hacts,.tile:focus-within .tile__hacts,
.tile.is-selected .tile__hacts{opacity:1;}
.tile__act{
  border:0; background:none; padding:3px; border-radius:var(--radius-sm);
  color:var(--text-faint); cursor:pointer; display:grid; place-items:center;
  flex:none;
}
.tile__act:hover{background:var(--bg-hover); color:var(--text-primary);}
.tile__act[disabled]{opacity:.3; cursor:not-allowed;}
.tile__act svg{width:14px; height:14px;}
.tile__lvl{font-size:10px; color:var(--text-faint); padding:0 3px;
  max-width:9ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.tile__grip{color:var(--text-faint); display:grid; place-items:center;
  cursor:grab; flex:none; margin-top:1px;}
.tile__grip svg{width:14px; height:14px;}

.tile__body{flex:1; min-height:0; padding:0 10px 6px; display:flex;
  flex-direction:column; gap:7px; overflow:auto;}
.tile__foot{display:flex; align-items:center; gap:6px; padding:5px 10px 8px;
  border-top:1px solid var(--border-color); flex:none; flex-wrap:wrap;}
.tile__asof{font-size:10px; color:var(--text-faint);}
.tile__lock{color:var(--text-faint); display:grid; place-items:center;}
.tile__lock svg{width:12px; height:12px;}
.tile__foot .badge{font-size:9px; padding:1px 5px;}

.tile__strip{padding:6px 8px; font-size:11.5px; display:flex;
  flex-direction:column; gap:5px; flex:none;}
.tile__strip-acts{display:flex; gap:5px; flex-wrap:wrap;}
.tile__empty{flex:1; min-height:0; display:flex; flex-direction:column; gap:6px;
  align-items:flex-start; justify-content:center;}
.tile__nodata{font-size:25px; color:var(--text-faint); font-weight:600;}
/* ── the loading state ────────────────────────────────────────────────────
   A 2px bar on the tile's top edge, and the content stays readable underneath.

   What was here: `.tile__busy`, an opaque overlay with a spinner and a line of
   stage text. Two things were wrong with it. It hid the figure the reader
   already had, and during a refresh that OLD number is the most useful thing on
   the tile -- it is what the new one gets compared against. And the text
   described the machinery ("Validating the generated SQL for safety") rather
   than the reader's answer.

   The bar sits INSIDE the tile's radius because `.tile` is `overflow:hidden`,
   so it clips to the corner without needing its own rounding. */
.tile__prog{
  position:absolute; top:0; left:0; right:0; height:2px; z-index:9;
  background:color-mix(in srgb, var(--card-accent, var(--color-accent)) 12%,
                       transparent);
  pointer-events:none;
}
.tile__prog__fill{
  height:100%; width:0;
  background:var(--card-accent, var(--color-accent));
  /* LINEAR, and exactly as long as the creep tick that schedules the next step
     (PROG_TICK_MS in page-dash.js). That equality is what makes the motion one
     continuous glide rather than a row of little eased animations with a pause
     between each -- the first version used a 520ms expo curve on a 420ms tick,
     so every step decelerated to a near-stop and then jerked forward again. */
  transition:width 400ms linear;
}
/* A real stage arriving IS a jump, so it gets a curve. Applied for that one
   change only, then removed, so it never interferes with the glide. */
.tile__prog.is-step .tile__prog__fill{
  transition:width 520ms cubic-bezier(.16, 1, .3, 1);
}
/* Restoring after render() rebuilt the tile: commit the width with no
   transition at all, so a re-render is invisible instead of replaying. */
.tile__prog.is-instant .tile__prog__fill{transition:none;}
/* The run to 100%. Finishing FAST is what leaves the impression the whole thing
   was fast -- the tail is the part people remember -- and the fade waits for the
   fill to arrive rather than overlapping it. */
.tile__prog.is-done .tile__prog__fill{
  transition:width 220ms cubic-bezier(.4, 0, 1, 1);
}
/* The fade WAITS for the fill (220ms) and then takes 180ms, so the bar is gone
   at 400ms -- which is PROG_DONE_MS, the moment page-dash swaps the tile. The
   three numbers are one decision written in two files. */
.tile__prog.is-done{
  opacity:0;
  transition:opacity 180ms var(--ease-out) 220ms;
}

/* The content dims, it does not disappear. 62% is enough to say "this is being
   replaced" while leaving the figure legible -- the point of the whole change. */
.tile.is-busy .tile__body,
.tile.is-busy .tile__foot{
  opacity:.62;
  transition:opacity var(--transition-base) var(--ease-out);
}

@media (prefers-reduced-motion:reduce){
  /* The bar still reports progress -- it is information, not decoration -- but
     it steps instead of gliding, and the creep between stages is what would
     read as unnecessary motion. */
  .tile__prog__fill,
  .tile__prog.is-step .tile__prog__fill,
  .tile__prog.is-done .tile__prog__fill,
  .tile__prog.is-done,
  .tile.is-busy .tile__body,
  .tile.is-busy .tile__foot{transition:none;}
}

.tile__handle{position:absolute; z-index:6;}
.tile__handle::after{content:''; position:absolute; inset:-7px;}
.tile__handle--e{right:0; top:22px; bottom:22px; width:7px; cursor:ew-resize;}
.tile__handle--s{left:22px; right:22px; bottom:0; height:7px; cursor:ns-resize;}
.tile__handle--se{
  right:0; bottom:0; width:15px; height:15px; cursor:nwse-resize;
  background:linear-gradient(135deg, transparent 46%, var(--border-strong) 46%,
             var(--border-strong) 58%, transparent 58%);
  border-bottom-right-radius:var(--radius-lg);
}
.tile__handle--e:hover,.tile__handle--s:hover{
  background:var(--color-accent-soft-strong);}

/* ── the visuals ────────────────────────────────────────────────────────── */
.viz-chart{flex:1; min-height:90px; position:relative;}
.viz-empty{flex:1; display:grid; place-items:center; text-align:center;
  padding:14px;}

/* The chart's own inline data table — the same rows "Show as table" already
   builds in a modal, offered here without leaving the dashboard. `flex:none`
   on both: unlike `.viz-chart`, neither should stretch to fill the tile when
   the table is closed, which is what made a chart look squashed under a
   nearly-empty band the first time this was tried.

   The toggle is a MICRO-LABEL (Meridian #2) — the same object as a column
   header or a tile title, not a one-off button style: --fs-micro, 650,
   uppercase, +.06em, in --text-faint. Its height is --ctl-h-sm, the token
   every dense-toolbar control claims (#4), not padding improvising one. */
.viz-tablebtn{
  flex:none; align-self:flex-start; display:flex; align-items:center;
  gap:var(--sp-1); height:var(--ctl-h-sm);
  margin-top:var(--sp-1); padding:0 var(--sp-2) 0 var(--sp-1);
  border:none; border-radius:var(--radius-sm); background:transparent;
  color:var(--text-faint); font-size:var(--fs-micro); font-weight:650;
  letter-spacing:.06em; text-transform:uppercase; cursor:pointer;
}
.viz-tablebtn svg{width:13px; height:13px; flex:none;}
.viz-tablebtn:hover{color:var(--text-muted); background:var(--bg-hover);}
.viz-tablebtn:focus-visible{outline:2px solid var(--color-accent); outline-offset:1px;}

/* The smooth reveal: a 0fr->1fr grid track animates to the table's real
   height without either guessing a max-height or measuring it in JS — the
   two usual ways this kind of transition gets built, and the two usual ways
   it breaks (clipped early on a tall table, or fixed pixels that show a
   snap once every table is a different length). `.viz-tablewrap__inner`
   supplies the overflow:hidden the grid technique needs while the row is
   still shrinking. */
.viz-tablewrap{
  flex:none; display:grid; grid-template-rows:0fr;
  transition:grid-template-rows var(--transition-base) var(--ease-out);
}
.viz-tablewrap.is-open{grid-template-rows:1fr;}
.viz-tablewrap__inner{overflow:hidden; min-height:0;}
.viz-tablewrap.is-open .viz-tablewrap__inner{padding-top:var(--sp-2);}

/* Meridian #6, "count the boxes": the TILE is already the one bordered,
   filled family on this screen. .tablewrap draws its own border+--bg-card
   fill for its usual home (a modal, itself a lifted surface where a nested
   box reads as content, not as a second frame) — inline in a tile that is
   already that exact surface, the same rule makes it a box drawn inside an
   identical box. Stripped here, and only here: the modal keeps its own. */
.viz-tablewrap__inner .tablewrap{
  border:none; border-radius:0; background:transparent;
}

@media (prefers-reduced-motion:reduce){
  .viz-tablewrap{transition:none;}
}

/* A figure sits just under the title rather than floating in the middle of a
   tall tile: `justify-content:center` left a big empty band above and below the
   number whenever the tile was taller than the figure needed. */
/* ── the figure visuals ─────────────────────────────────────────
   The two tiles a reader looks at first, and the two that most looked like a
   form field with a large number in it. Four changes, all of them about making
   the FIGURE the subject:

     * It is CENTRED in the tile, not stacked at the top. A number pinned to the
       top-left of a 3x3 tile with empty space beneath reads as unfinished; the
       same number optically centred reads as the point of the tile.
     * Bigger and tighter: up to 40px at -0.035em tracking, which is where a
       figure stops being text and starts being a display number.
     * Tabular numerals and a fixed line box, so a value that counts up or
       refreshes cannot shift the layout by a pixel as its digits change.
     * A secondary series is genuinely secondary — about half the size, muted,
       and behind a divider — rather than the same weight one step down. */
.viz-card,.viz-kpi{
  flex:1; min-height:0;
  display:flex; flex-direction:column; justify-content:center;
  gap:var(--sp-2); padding:2px 0 4px;
}
.viz-card__item{display:flex; flex-direction:column; gap:2px; min-width:0;}
.viz-card__value,.viz-kpi__value{
  font-size:clamp(24px, 3.4vw, 40px); font-weight:700;
  letter-spacing:-.035em; line-height:1.02;
  color:var(--text-primary); font-variant-numeric:tabular-nums;
  /* Never wrap a figure: 12,34,567 broken across two lines is not a number any
     more. It truncates and keeps its tooltip instead. */
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0;
}
.viz-card__item.is-secondary{
  padding-top:var(--sp-2); border-top:1px solid var(--border-color);
}
.viz-card__item.is-secondary .viz-card__value{
  font-size:clamp(15px, 1.6vw, 19px); font-weight:650;
  letter-spacing:-.02em; color:var(--text-secondary);
}
.viz-card__label,.viz-kpi__label{
  font-size:var(--fs-micro); font-weight:650; letter-spacing:.06em;
  text-transform:uppercase; color:var(--text-muted);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0;
}
.viz-kpi__meta{display:flex; align-items:center; gap:var(--sp-2);
  flex-wrap:wrap; min-width:0;}
.viz-kpi__spark{height:40px; margin-top:var(--sp-1); flex:none;}
/* The delta carries a GLYPH as well as a colour. The direction of a change is
   exactly the kind of thing that must never be colour-alone, and the success
   green is 5.79:1 as text but indistinguishable from the danger red to a
   red-green colourblind reader. */
.viz-delta{
  display:inline-flex; align-items:center; gap:3px;
  font-size:11.5px; font-weight:700; font-variant-numeric:tabular-nums;
}
.viz-delta::before{font-size:9px; line-height:1;}
.viz-delta--up::before{content:"\25B2";}
.viz-delta--down::before{content:"\25BC";}
.viz-delta--flat::before{content:"\2013";}
.viz-delta--up{color:var(--status-good);}
.viz-delta--down{color:var(--color-danger);}
.viz-delta--flat{color:var(--text-muted);}

.meter__track{height:7px; border-radius:var(--radius-pill);
  background:var(--bg-surface-alt); overflow:hidden;
  border:1px solid var(--border-color);}
.meter__fill{height:100%; border-radius:var(--radius-pill);
  transition:width var(--transition-base);}
.meter__fill--good{background:var(--status-good);}
.meter__fill--warn{background:var(--status-warn);}
.meter__fill--bad{background:var(--status-critical);}
.meter__label{display:flex; align-items:center; gap:5px; font-size:11px;
  color:var(--text-muted);}
.meter__mark{display:grid; place-items:center;}
.meter__mark svg{width:12px; height:12px;}

.viz-gauge{flex:1; display:flex; flex-direction:column; align-items:center;
  justify-content:center; gap:2px;}
.viz-gauge__svg{width:100%; max-width:210px; height:auto;}
.viz-gauge__value{font-size:clamp(19px,2.6vw,28px); font-weight:680;
  letter-spacing:-.02em; color:var(--text-primary); margin-top:-10px;}
.viz-gauge__sub{font-size:11px; color:var(--text-muted); text-align:center;}

.viz-multirow{flex:1; min-height:0; overflow:auto; display:flex;
  flex-direction:column;}
.viz-multirow__row{display:flex; align-items:center; gap:10px; padding:7px 2px;
  border-bottom:1px solid var(--border-color);}
.viz-multirow__row:last-child{border-bottom:0;}
.viz-multirow__row.is-clickable{cursor:pointer;}
.viz-multirow__row.is-clickable:hover{background:var(--bg-hover);}
.viz-multirow__label{flex:1; min-width:0; font-size:12.5px;
  color:var(--text-secondary); overflow:hidden; text-overflow:ellipsis;
  white-space:nowrap;}
.viz-multirow__vals{display:flex; gap:14px; flex:none;}
.viz-multirow__val{display:flex; flex-direction:column; align-items:flex-end;}
.viz-multirow__num{font-size:14px; font-weight:650; color:var(--text-primary);
  font-variant-numeric:tabular-nums;}
.viz-multirow__slabel{font-size:9.5px; color:var(--text-faint);}

/* EXACTLY ONE ELEMENT SCROLLS, and it is the one wrapping only the rows.

   This rule has now been wrong in both directions, which is why it is spelled
   out. First `.tablewrap` carried `overflow-x:auto` from app.css while
   `.viz-table` also scrolled, so the sticky header stuck to the inner box and
   did not move when the tile scrolled. Then the table gained a toolbar and a
   sort caption — and an ancestor scrollport would have carried BOTH of them
   sideways with the columns and up out of view with the rows.

   The answer for both: `.tablescroll` is the scrollport, and every ancestor is
   a flex column that gives it a height to be short of. That is also what makes
   the sticky header mean anything — it needs a scrolling ancestor WITH a
   bounded height, and until now it never had one. */
.viz-table{
  flex:1; min-height:0; overflow:hidden; margin:0 -10px;
  display:flex; flex-direction:column;
}
.viz-table .tablewrap{
  border:0; border-radius:0; flex:1 1 auto; min-height:0;
  display:flex; flex-direction:column;
}
.viz-table .tablescroll{flex:1 1 auto; min-height:0;}
.viz-matrix table.data th[scope="row"]{
  text-align:left; font-weight:600; color:var(--text-secondary);
  background:var(--bg-card); position:sticky; left:0;
}
.viz-matrix tfoot td,.viz-table tfoot td{
  border-top:2px solid var(--border-strong); font-weight:650;
  background:var(--bg-surface-alt);
}
td.has-bar{position:relative;}
.cell-bar{
  position:absolute; left:0; top:3px; bottom:3px; z-index:0;
  background:var(--color-accent-soft-strong); border-radius:2px;
  pointer-events:none;
}
.cell-bar--neg{background:var(--color-danger-soft);}
td.has-bar>*:not(.cell-bar){position:relative; z-index:1;}
.cell-tone--good{background:var(--color-success-soft);}
.cell-tone--warn{background:var(--color-warning-soft);}
.cell-tone--bad{background:var(--color-danger-soft);}
.cell-tone--neutral{background:var(--bg-hover);}
tr.is-clickable{cursor:pointer;}
tr.is-clickable:hover td{background:var(--bg-hover);}

.viz-treemap{flex:1; min-height:100px; position:relative; border-radius:6px;
  overflow:hidden;}
.viz-treemap__cell{
  position:absolute; padding:5px 7px; overflow:hidden;
  /* A 2px surface gap does the separating — no stroke, which would add
     data-weight ink that is not data. */
  box-shadow:inset 0 0 0 1.5px var(--bg-card);
}
.viz-treemap__cell.is-clickable{cursor:pointer;}
.viz-treemap__cell.is-clickable:hover{filter:brightness(1.12);}
.viz-treemap__label{font-size:11px; font-weight:650; color:#fff;
  text-shadow:0 1px 2px rgba(0,0,0,.45); overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap;}
.viz-treemap__value{font-size:10.5px; color:rgba(255,255,255,.92);
  text-shadow:0 1px 2px rgba(0,0,0,.45);}

.viz-funnel{flex:1; min-height:0; overflow:auto; display:flex;
  flex-direction:column; gap:8px;}
.viz-funnel__row{display:flex; flex-direction:column; gap:3px;}
.viz-funnel__row.is-clickable{cursor:pointer;}
.viz-funnel__row.is-clickable:hover .viz-funnel__bar{filter:brightness(1.12);}
.viz-funnel__head{display:flex; align-items:baseline; gap:8px;}
.viz-funnel__label{flex:1; min-width:0; font-size:12px;
  color:var(--text-secondary); overflow:hidden; text-overflow:ellipsis;
  white-space:nowrap;}
.viz-funnel__value{font-size:12.5px; font-weight:650; color:var(--text-primary);
  font-variant-numeric:tabular-nums;}
.viz-funnel__track{height:16px; background:var(--bg-surface-alt);
  border-radius:4px; overflow:hidden;}
.viz-funnel__bar{height:100%; border-radius:4px;
  transition:width var(--transition-base);}
.viz-funnel__conv{font-size:10px; color:var(--text-faint);}

.viz-slicer{flex:1; min-height:0; display:flex; flex-direction:column; gap:6px;}
.viz-slicer__search{height:var(--ctl-h-sm); padding:0 9px;
  font-size:var(--fs-chrome);}
.viz-slicer__list{flex:1; min-height:0; overflow-y:auto; display:flex;
  flex-direction:column;}
.viz-slicer__item{display:flex; align-items:center; gap:7px; padding:3px 2px;
  font-size:12.5px; cursor:pointer; border-radius:var(--radius-sm);}
.viz-slicer__item:hover{background:var(--bg-hover);}
.viz-slicer__text{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis;
  white-space:nowrap;}
.viz-slicer__n{font-size:10px; color:var(--text-faint);
  font-variant-numeric:tabular-nums;}
.viz-slicer__range{display:flex; align-items:center; gap:6px;}
.viz-slicer__range .input{height:var(--ctl-h-sm); padding:0 8px;
  font-size:var(--fs-chrome); min-width:0;}

.viz-text{flex:1; font-size:13px;}

/* ── focus mode ─────────────────────────────────────────────────────────── */
.focus{grid-column:1 / -1; display:flex; flex-direction:column; gap:10px;
  min-height:0;}
.focus__head{display:flex; align-items:center; gap:11px; flex-wrap:wrap;}
.focus__title{font-size:15px; font-weight:680; color:var(--text-primary);}
.focus__filters{display:flex; align-items:center; gap:7px; flex-wrap:wrap;}
.focus__body{
  flex:1; min-height:min(62vh,520px); background:var(--bg-card);
  border:1px solid var(--border-color); border-radius:var(--radius-lg);
  padding:14px; display:flex; flex-direction:column; gap:9px; overflow:auto;
}
.focus__note{font-size:12px;}

/* ── authoring rail ─────────────────────────────────────────────────────── */
#rail{
  display:flex; flex-direction:column; min-height:0;
  border-left:1px solid var(--border-color); background:var(--bg-panel);
}
/* The View-mode stand-in for the tab strip: same height and rule, so the rail
   does not change shape when the extra panes appear in Edit mode. */
.rail__title{
  flex:none; padding:9px 12px; border-bottom:1px solid var(--border-color);
  font-size:10.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  color:var(--text-muted); background:var(--bg-card);
}
#rail-tabs{display:flex; border-bottom:1px solid var(--border-color); flex:none;}
/* The rail's pane tabs, in the same component language as every other tab in
   the product: a fixed 34px height, and the current one wears the 2px accent
   edge rather than a colour change. They were 27px with an 11px label and their
   own idea of an active state. */
.railtab{
  flex:1; position:relative;
  display:inline-flex; align-items:center; justify-content:center;
  height:34px; padding:0 6px;
  border:0; background:none; font:inherit;
  font-size:11.5px; font-weight:600; line-height:1;
  color:var(--text-muted); cursor:pointer; white-space:nowrap;
  transition:color var(--transition-fast), background var(--transition-fast);
}
.railtab:hover{background:var(--bg-hover); color:var(--text-primary);}
.railtab.is-active{color:var(--text-primary); background:var(--bg-card);}
.railtab.is-active::after{
  content:''; position:absolute; left:0; right:0; bottom:-1px; height:2px;
  border-radius:2px 2px 0 0; background:var(--color-accent);
}
.railtab:focus-visible{outline:2px solid var(--color-accent); outline-offset:-2px;}
#rail-body{flex:1; min-height:0; overflow-y:auto; padding:10px 12px 40px;
  display:flex; flex-direction:column; gap:8px;}

.pane__grouplabel{
  font-size:9.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  color:var(--text-faint); margin-top:6px;
}
.pane__note{margin-bottom:2px;}
.pane__empty{padding:10px 2px;}
.pane__sec{border-top:1px solid var(--border-color); padding-top:2px;}
.pane__sec>summary{
  cursor:pointer; list-style:none; padding:6px 0; font-size:11.5px;
  font-weight:640; color:var(--text-secondary); display:flex;
  align-items:center; gap:6px;
}
.pane__sec>summary::-webkit-details-marker{display:none;}
.pane__sec>summary::before{content:'▸';
  transition:transform var(--transition-fast); color:var(--text-faint);}
.pane__sec[open]>summary::before{transform:rotate(90deg);}
.pane__secbody{padding:2px 0 8px; display:flex; flex-direction:column; gap:6px;}
.pane__check{display:flex; align-items:center; gap:7px; font-size:12px;
  color:var(--text-secondary); cursor:pointer;}
#rail-body .field{margin:0;}
#rail-body .label{font-size:10.5px; margin-bottom:2px;}
/* The rail is dense, so its controls step down one size — but they step down
   TOGETHER. Four different heights in one settings pane is what made it look
   thrown together. */
#rail-body .input,#rail-body .select{
  height:var(--ctl-h-sm); padding:0 8px; font-size:var(--fs-chrome);}
#rail-body .select{padding-right:24px; background-position:right 7px center;}
#rail-body .textarea{padding:6px 8px; font-size:var(--fs-chrome); height:auto;}
#rail-body .hint{font-size:10.5px;}

/* gallery */
.gallery{display:flex; flex-direction:column; gap:3px;}
.gallery__grid{display:grid; grid-template-columns:repeat(6,1fr); gap:3px;}
.gallery__item{
  aspect-ratio:1; display:grid; place-items:center; border:1px solid transparent;
  border-radius:var(--radius-sm); background:var(--bg-card); cursor:pointer;
  color:var(--text-muted);
}
.gallery__item:hover{border-color:var(--border-strong);
  color:var(--text-primary);}
.gallery__item.is-active{background:var(--color-accent-soft);
  border-color:var(--color-accent); color:var(--color-accent);}
.gallery__item[disabled]{opacity:.4; cursor:not-allowed;}
.gallery__item svg{width:17px; height:17px;}

/* field pills */
.fieldpill{
  display:flex; align-items:center; gap:7px; padding:4px 8px;
  border:1px solid var(--border-color); border-radius:var(--radius-sm);
  background:var(--bg-card); font-size:12px; color:var(--text-secondary);
  cursor:grab;
}
.fieldpill:hover{border-color:var(--color-accent); color:var(--text-primary);}
.fieldpill__icon{display:grid; place-items:center; color:var(--text-faint);
  flex:none;}
.fieldpill__icon svg{width:13px; height:13px;}
.fieldpill__name{min-width:0;}

/* wells */
.wells{display:flex; flex-direction:column; gap:6px;}
.well{
  border:1px dashed var(--border-strong); border-radius:var(--radius-sm);
  padding:6px 7px; background:var(--bg-card);
}
.well.is-over{border-color:var(--color-accent);
  background:var(--color-accent-soft);}
.well__head{display:flex; align-items:center; gap:6px; margin-bottom:4px;}
.well__label{font-size:10.5px; font-weight:640; color:var(--text-muted);}
.well__req{font-size:9px; color:var(--color-warning); font-weight:600;}
.well__drop{display:flex; flex-direction:column; gap:4px;}
.well__hint{font-size:10.5px; color:var(--text-faint); padding:2px 0;}
.wellchip-wrap{display:flex; flex-direction:column; gap:3px;}
.wellchip{
  display:flex; align-items:center; gap:5px; padding:3px 4px 3px 8px;
  border-radius:var(--radius-sm); background:var(--bg-surface-alt);
  border:1px solid var(--border-color); font-size:11.5px;
}
.wellchip__text{flex:1; min-width:0;}
.wellchip__lvl{font-size:9px; font-weight:700; color:var(--color-accent);
  background:var(--color-accent-soft); border-radius:3px; padding:0 4px;}
/* The aggregate and type pickers inside a well chip. Native selects, so they
   get the drawn chevron too — three OS arrows inside one 11px chip was the
   busiest corner of the whole interface. */
.wellchip__agg,.wellchip__type{
  appearance:none; -webkit-appearance:none; -moz-appearance:none;
  height:20px; padding:0 16px 0 4px; max-width:96px;
  border:1px solid var(--border-color); border-radius:3px;
  background:var(--bg-card); color:var(--text-secondary); font:inherit;
  font-size:10.5px; line-height:1; cursor:pointer; text-overflow:ellipsis;
  background-image:var(--select-arrow);
  background-repeat:no-repeat; background-position:right 3px center;
  background-size:9px 9px;
}
.wellchip__agg:focus,.wellchip__type:focus{
  outline:none; border-color:var(--color-accent);}
.wellchip__x{border:0; background:none; padding:2px; border-radius:4px;
  cursor:pointer; color:var(--text-faint); display:grid; place-items:center;}
.wellchip__x:hover{background:var(--bg-hover); color:var(--color-danger);}
.wellchip__x svg{width:11px; height:11px;}

/* colours */
.swatches{display:flex; gap:4px; flex-wrap:wrap;}
.swatch{
  width:20px; height:20px; border-radius:5px; border:2px solid transparent;
  cursor:pointer; padding:0;
}
.swatch.is-active{border-color:var(--text-primary);}

/* conditional-format rules */
.rules{display:flex; flex-direction:column; gap:4px;}
.rule{display:grid; grid-template-columns:1fr 42px 60px 1fr 26px; gap:3px;
  align-items:center;}
.rule select,.rule input{height:24px; padding:0 5px; font-size:10.5px;
  min-width:0;}
.rule select{padding-right:20px; background-position:right 5px center;
  background-size:10px 10px;}

/* filter cards */
.filtercard{
  border:1px solid var(--border-color); border-radius:var(--radius-sm);
  background:var(--bg-card); overflow:hidden;
}
.filtercard__head{display:flex; align-items:center; gap:6px; padding:5px 7px;
  background:var(--bg-surface-alt); border-bottom:1px solid var(--border-color);}
.filtercard__title{flex:1; min-width:0; font-size:11.5px; font-weight:600;
  color:var(--text-secondary);}
.filtercard__x{border:0; background:none; padding:2px; border-radius:4px;
  cursor:pointer; color:var(--text-faint); display:grid; place-items:center;}
.filtercard__x:hover{background:var(--bg-hover); color:var(--color-danger);}
.filtercard__x svg{width:11px; height:11px;}
.filtercard__body{padding:6px 7px; display:flex; flex-direction:column; gap:5px;}
.filtercard__values{max-height:150px; overflow-y:auto; display:flex;
  flex-direction:column; gap:1px;}
.filtercard__val{display:flex; align-items:center; gap:6px; font-size:11.5px;
  cursor:pointer; padding:1px 0;}
.filtercard__range{display:flex; gap:5px;}
.filtercard__range .input,.filtercard__range .select{min-width:0;}

/* selection pane */
.selrow{display:flex; align-items:center; gap:4px; padding:3px 4px;
  border-radius:var(--radius-sm);}
.selrow:hover{background:var(--bg-hover);}
.selrow.is-active{background:var(--bg-active);}
.selrow__name{flex:1; min-width:0; text-align:left; border:0; background:none;
  font:inherit; font-size:12px; color:var(--text-secondary); cursor:pointer;}
.selrow__btn{border:0; background:none; padding:3px; border-radius:4px;
  cursor:pointer; color:var(--text-faint); display:grid; place-items:center;}
.selrow__btn:hover{background:var(--bg-card); color:var(--text-primary);}
.selrow__btn.is-on{color:var(--color-accent);}
.selrow__btn svg{width:12px; height:12px;}
/* The OTHER lock, read-only here. A card can be pinned to the grid
   (card.locked, this row’s padlock) and separately set never to refresh
   (card.user.locked, the Format pane). Both used to be called "Lock", which
   meant a reader trying to stop a refresh was pinning the tile instead. */
.selrow__flag{
  flex:none; font-size:9.5px; font-weight:650; letter-spacing:.02em;
  color:var(--color-warning); background:var(--color-warning-soft);
  border-radius:3px; padding:1px 5px; white-space:nowrap; cursor:help;
}

/* The effective question, shown verbatim in the Format pane. Monospace and
   wrapping: it is a string that gets sent somewhere, not prose, and the
   whole point is being able to read all of it. */
.panequestion{
  font-family:var(--font-mono); font-size:11.5px; line-height:1.5;
  color:var(--text-primary); background:var(--bg-app);
  border:1px solid var(--border-color); border-radius:var(--radius-sm);
  padding:6px 8px; white-space:pre-wrap; word-break:break-word;
  max-height:120px; overflow:auto;
}
.panequestion--was{color:var(--text-faint);}

/* ── page tabs (bottom, as in Power BI) ─────────────────────────────────────
   The strip is the report's own footer, so it is quiet: no borders on the
   inactive tabs, one accent rule on the active one. The count sits on the right
   and never scrolls with the tabs — which is why the tabs get their own
   overflow container rather than the bar scrolling as a whole. */
.pagetabs{
  display:flex; align-items:center; gap:12px; padding:0 18px;
  min-height:38px;
  border-top:1px solid var(--border-color); background:var(--bg-surface-alt);
}
.pagetabs__strip{
  display:flex; align-items:stretch; gap:2px; min-width:0; flex:1;
  overflow-x:auto; scrollbar-width:thin;
}
.pagetabs__strip::-webkit-scrollbar{height:5px;}
.ptab{
  display:inline-flex; align-items:center; gap:7px;
  padding:0 13px; height:100%; min-height:36px;
  border:0; border-top:2px solid transparent;
  background:none; font:inherit; font-size:12.5px; color:var(--text-muted);
  cursor:pointer; white-space:nowrap; flex:none;
  transition:color var(--transition-fast), background var(--transition-fast);
}
.ptab:hover{background:var(--bg-hover); color:var(--text-primary);}
.ptab.is-active{
  background:var(--bg-card); color:var(--text-primary); font-weight:620;
  border-top-color:var(--color-accent);
}
.ptab:focus-visible{outline:2px solid var(--color-accent); outline-offset:-2px;}
/* 22ch, with the full name reachable on hover via Airo.setLabel — a page named
   "Q3 payables review, north region" used to end at "Q3 payables review, n…"
   with no way to see the rest. */
.ptab__name{max-width:22ch; overflow:hidden; text-overflow:ellipsis;
  white-space:nowrap;}
/* How many visuals the page holds. Small, and only when non-zero — a row of
   zeroes is noise. */
.ptab__n{
  flex:none; font-size:10px; font-weight:700; line-height:1;
  padding:3px 5px; border-radius:var(--radius-pill);
  background:var(--bg-surface-alt); color:var(--text-faint);
}
.ptab.is-active .ptab__n{background:var(--color-accent-soft); color:var(--color-accent);}
.ptab--add{padding:0 11px; color:var(--text-faint);}
.ptab--add:hover{color:var(--color-accent);}
.ptab--add svg{width:15px; height:15px;}
.ptab--capped{cursor:default; color:var(--text-faint); font-size:11px;}
.ptab--capped:hover{background:none;}
/* Rename in place. Sized to the tab so the strip does not jump when it opens. */
.ptab__edit{
  width:14ch; min-width:8ch; border:1px solid var(--color-accent);
  border-radius:var(--radius-sm); background:var(--bg-card);
  color:var(--text-primary); font:inherit; font-size:12.5px; font-weight:620;
  padding:3px 7px; outline:none;
}
.pagetabs__count{flex:none; white-space:nowrap;}

/* ── empty state ────────────────────────────────────────────────────────── */
.dash-empty{grid-column:1 / -1; padding:7vh 20px; text-align:center;}
.dash-empty__acts{display:flex; gap:9px; justify-content:center; flex-wrap:wrap;
  margin-top:14px;}

/* ── narrow screens ─────────────────────────────────────────────────────── */
@media (max-width:1100px){
  :root{--rail-w:280px;}
  .gallery__grid{grid-template-columns:repeat(5,1fr);}
}
@media (max-width:900px){
  /* The rail stacks under the canvas rather than squeezing it to nothing. */
  #dashmid{flex-direction:column;}
  #rail{width:auto; max-height:52vh;
    border-left:0; border-top:1px solid var(--border-color);}
  .dash-canvas{grid-template-columns:1fr; grid-auto-rows:auto;}
  /* The stored layout is PRESERVED — only the presentation collapses. */
  .tile{grid-column:1 / -1 !important; grid-row:auto !important; min-height:180px;}
  .tile__handle,.tile__grip{display:none;}
}

@media print{
  .rpt,.slicers,.refreshbar,#rail,.pagetabs,.tile__hacts,.tile__handle,
  .tile__grip{display:none !important;}
  #dashapp{height:auto; overflow:visible;}
  #dashmid{display:block;}
  .dash-canvas{overflow:visible; padding:0;}
  .tile{break-inside:avoid; box-shadow:none;}
}
