/* GENERATED FILE -- DO NOT EDIT.
   Source: shared/ui.css. Regenerate with: node tools/sync-ui.mjs
   Edits here are silently overwritten and will not reach the other apps. */

/* =========================================================================
   resale1 shared UI
   =========================================================================

   The one place the three apps agree on what they look like.

   They had drifted into three palettes -- the MCP warm and light, the
   orchestrator and the website each their own blue, on their own dark -- three
   copies of the same button, and a 13px base that was small on a large display
   and unreadable on a laptop. Anything defined here is defined once.

   EDIT shared/ui.css AND RUN tools/sync-ui.mjs. Each app serves its own copy
   because Docker build contexts are per-service and cannot reach a sibling
   directory; the copies are generated and should never be edited in place.

   Theming: every colour is a variable on :root, so a running page can be
   restyled by setting a handful of them -- which is what the website's admin
   theme controls do. `data-theme="light"` and `data-theme="dark"` force a mode;
   with neither, the system preference decides.
   ========================================================================= */

:root {
  color-scheme: light dark;

  /* --- type ---------------------------------------------------------------
     Base was 13px everywhere and too small. 15px reads comfortably at arm's
     length without reflowing any of the tables. */
  --font: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  --fs-xs: 12.5px;
  --fs-sm: 14px;
  --fs: 15px;
  --fs-lg: 17px;
  --fs-h2: 19px;
  --fs-h1: 21px;

  --radius: 7px;
  --radius-lg: 10px;

  /* --- light ------------------------------------------------------------ */
  --bg: #fbfbfa;
  --panel: #ffffff;
  --border: #e2e1de;
  --border-strong: #cfcec9;
  --text: #1b1a18;
  --muted: #63625e;
  --accent: #2f6f4e;
  --accent-contrast: #ffffff;
  --danger: #a8321f;
  --warn: #855e00;
  --good: #2f6f4e;
  --code-bg: #f4f3f0;
  --row-alt: #faf9f7;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #15161a;
    --panel: #1c1e23;
    --border: #2e3137;
    --border-strong: #3c4046;
    --text: #e7e6e3;
    --muted: #9a9893;
    --accent: #6bbd8f;
    --accent-contrast: #10251b;
    --danger: #e0806e;
    --warn: #d3a75c;
    --good: #6bbd8f;
    --code-bg: #121317;
    --row-alt: #191b1f;
    --shadow: none;
  }
}

/* An explicit choice beats the system preference, in both directions. */
:root[data-theme="dark"] {
  --bg: #15161a;
  --panel: #1c1e23;
  --border: #2e3137;
  --border-strong: #3c4046;
  --text: #e7e6e3;
  --muted: #9a9893;
  --accent: #6bbd8f;
  --accent-contrast: #10251b;
  --danger: #e0806e;
  --warn: #d3a75c;
  --good: #6bbd8f;
  --code-bg: #121317;
  --row-alt: #191b1f;
  --shadow: none;
}

/* --- base ---------------------------------------------------------------- */

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

code, pre, .mono, textarea, table.grid td, table.grid th {
  font-family: var(--font-mono);
}

a { color: var(--accent); }

h1 { font-size: var(--fs-h1); margin: 0; font-weight: 600; }
h2 { font-size: var(--fs-h2); margin: 0 0 10px; font-weight: 600; }
h3 { font-size: var(--fs-lg); margin: 0 0 6px; font-weight: 600; }

.muted { color: var(--muted); }
.meta { color: var(--muted); font-size: var(--fs-sm); margin: 8px 0 0; }
.spacer { flex: 1; }

/* --- app shell ----------------------------------------------------------
   Sidebar rather than a top bar. Eleven top-level tabs had outgrown a single
   row: they wrapped, they could not be grouped, and there was nowhere to put a
   sub-item. A column has room to say which section a page belongs to. */

.shell {
  display: grid;
  grid-template-columns: 216px 1fr;
  min-height: 100vh;
}

.sidebar {
  background: var(--panel);
  border-right: 1px solid var(--border);
  padding: 14px 10px 24px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}

.sidebar .brand {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 4px 10px 14px;
}
.sidebar .brand strong { font-size: var(--fs-lg); font-weight: 600; letter-spacing: -0.01em; }
.sidebar .brand span { font-size: var(--fs-xs); color: var(--muted); }

/* Section headers in the sidebar ("Yours", "Analysis", ...). They have to read
   as headings, not as one more muted link: full text colour, heavier, a rule
   above that separates one group from the last, and more room before the
   group than inside it so the eye groups the links underneath with it. */
.sidebar .group {
  color: var(--text);
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  font-weight: 700;
  margin: 16px 0 4px;
  padding: 14px 10px 2px;
  border-top: 1px solid var(--border);
}

.sidebar nav { display: contents; }

.sidebar button.navitem {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-radius: 6px;
  padding: 7px 10px;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--muted);
  cursor: pointer;
}
.sidebar button.navitem:hover { background: var(--code-bg); color: var(--text); }
.sidebar button.navitem[aria-current="true"] {
  background: color-mix(in srgb, var(--accent) 13%, transparent);
  color: var(--text);
  font-weight: 600;
}
.sidebar button.navitem .badge {
  margin-left: auto;
  font-size: 11px;
  color: var(--muted);
  font-weight: 400;
}

.sidebar .foot { margin-top: auto; padding: 12px 10px 0; font-size: var(--fs-xs); color: var(--muted); }

.content { min-width: 0; display: flex; flex-direction: column; }

.topbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 22px;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
  position: sticky;
  top: 0;
  z-index: 5;
}

main { padding: 20px 22px 40px; max-width: 1500px; width: 100%; }

@media (max-width: 860px) {
  .shell { grid-template-columns: 1fr; }
  .sidebar { position: static; height: auto; flex-direction: row; flex-wrap: wrap; overflow-x: auto; }
  .sidebar .group { display: none; }
  .sidebar .foot { display: none; }
  .sidebar button.navitem { width: auto; }
}

/* --- panels -------------------------------------------------------------- */

.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 16px 18px;
  margin-bottom: 16px;
  box-shadow: var(--shadow);
}
.panel > p:first-of-type { margin-top: 0; }
.panel p.lede { color: var(--muted); font-size: var(--fs-sm); margin: 0 0 14px; }

.split { display: grid; grid-template-columns: 300px 1fr; gap: 16px; align-items: start; }
@media (max-width: 900px) { .split { grid-template-columns: 1fr; } }

.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 10px 0 0; }

/* --- form controls ------------------------------------------------------
   Every control type is named. A type left out of this list -- and several
   were, including every select on every page -- renders as a raw browser
   widget in the middle of an otherwise styled form.

   `input:not([type])` is not decoration. An attribute selector only matches
   when the attribute is present, so `input[type=text]` does NOT match a bare
   <input> -- which defaults to text and looks identical in the markup. Those
   fields fell through every rule here and rendered at the browser's default
   width of about twenty characters: the sign-in username on the website and the
   orchestrator, and half the metric editor. */

.field { margin-bottom: 14px; max-width: 640px; }
.field label { display: block; font-weight: 500; margin-bottom: 4px; font-size: var(--fs-sm); }
.field .hint { color: var(--muted); font-size: var(--fs-xs); margin-bottom: 6px; }
.field.wide { max-width: none; }

input:not([type]), input[type=text], input[type=email], input[type=password],
input[type=number], input[type=search], input[type=url], input[type=tel], input[type=date],
input[type=time], input[type=datetime-local], select, textarea {
  background: var(--code-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 10px;
  font: inherit;
  font-size: var(--fs-sm);
  width: 100%;
}

/* Sized to their content rather than to one cap: a port number does not need
   the width of an email address, and a connection string needs more. */
input[type=number] { max-width: 190px; }
input.short { max-width: 130px; }
input.wide, textarea.wide { max-width: none; }

select {
  /* Appearance must be reset before a background can be drawn on a select, or
     the platform widget paints over it and the field looks unstyled. */
  appearance: none;
  -webkit-appearance: none;
  padding-right: 32px;
  max-width: 340px;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 14px center, right 9px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
select.wide { max-width: none; }

textarea { min-height: 130px; resize: vertical; line-height: 1.45; }

input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
  outline-offset: 1px;
  border-color: var(--accent);
}
input:disabled, select:disabled, textarea:disabled { opacity: 0.55; cursor: not-allowed; }

input[type=checkbox], input[type=radio] { accent-color: var(--accent); width: 15px; height: 15px; }

label.inline {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: var(--fs-sm);
  cursor: pointer;
}

/* --- buttons ------------------------------------------------------------- */

button {
  font: inherit;
  font-size: var(--fs-sm);
  border-radius: var(--radius);
  padding: 8px 14px;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
}
button:hover { border-color: var(--border-strong); }
button:disabled { opacity: 0.5; cursor: not-allowed; }

button.action, button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
  font-weight: 500;
}
button.action:hover, button.primary:hover { filter: brightness(1.07); }

button.ghost { background: none; color: var(--muted); }
button.ghost:hover { color: var(--text); background: var(--code-bg); }

button.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, transparent); background: none; }
button.danger:hover { background: color-mix(in srgb, var(--danger) 10%, transparent); }

button.small { padding: 4px 9px; font-size: var(--fs-xs); }

/* --- tables -------------------------------------------------------------- */

table.grid { border-collapse: collapse; width: 100%; font-size: var(--fs-sm); }
table.grid th, table.grid td {
  border-bottom: 1px solid var(--border);
  padding: 7px 10px;
  text-align: left;
  vertical-align: top;
}
table.grid th { color: var(--muted); font-weight: 600; font-size: var(--fs-xs); white-space: nowrap; }
table.grid tr:nth-child(even) td { background: var(--row-alt); }
table.grid td.num { text-align: right; font-variant-numeric: tabular-nums; }
.tablewrap { overflow-x: auto; }

/* --- lists (master/detail) ---------------------------------------------- */

.list { max-height: 660px; overflow-y: auto; }
.list button {
  display: block; width: 100%; text-align: left; background: none;
  border: none; border-radius: 6px; padding: 6px 9px; font: inherit;
  font-size: var(--fs-sm); color: var(--text); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.list button:hover { background: var(--code-bg); }
.list button[aria-current="true"] { background: var(--code-bg); font-weight: 600; }
.list .group {
  color: var(--muted); font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.05em; margin: 12px 0 4px; padding: 0 9px;
}
.count { color: var(--muted); font-weight: 400; font-size: var(--fs-xs); }

/* --- output, status, messages ------------------------------------------- */

pre.out {
  background: var(--code-bg); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 12px; overflow: auto;
  max-height: 480px; font-size: var(--fs-sm); margin: 0;
}
code { background: var(--code-bg); border-radius: 4px; padding: 1px 5px; font-size: 0.92em; }

.pill {
  display: inline-block; padding: 2px 8px; border-radius: 11px;
  font-size: var(--fs-xs); border: 1px solid var(--border); white-space: nowrap;
}
.pill.ok, .pill.good { color: var(--good); border-color: color-mix(in srgb, var(--good) 40%, transparent); }
.pill.rejected, .pill.warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 45%, transparent); }
.pill.error, .pill.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, transparent); }

.err, .notice {
  padding: 11px 13px; border-radius: var(--radius); margin: 10px 0 0;
  white-space: pre-wrap; font-size: var(--fs-sm);
}
/*
 * An empty one is not a message.
 *
 * Where these are created per message, an empty one never exists. Where the
 * element is in the markup and filled in later -- the sign-in form does this,
 * so the layout does not jump when a message appears -- it renders as a red
 * box with nothing in it, and the page looks like it is reporting a failure
 * before anybody has done anything.
 */
.err:empty, .notice:empty { display: none; }
.err {
  border: 1px solid color-mix(in srgb, var(--danger) 45%, transparent);
  background: color-mix(in srgb, var(--danger) 8%, transparent);
  color: var(--danger);
}
.notice {
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

/* --- sign-in --------------------------------------------------------- */

.gate { max-width: 460px; margin: 90px auto; }

/* --- "still working" ------------------------------------------------- */
/*
 * The sidebar indicator shown while an answer is being written.
 *
 * A pulsing dot rather than a spinner: it has to sit inside a nav item at text
 * size, and at that size a spinner is a smudge. The dot also survives
 * prefers-reduced-motion becoming still without becoming invisible.
 */
.sidebar button.navitem.working { color: var(--accent); font-weight: 600; }
.sidebar button.navitem.working .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  flex: 0 0 auto;
  animation: pulse 1.4s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
@media (prefers-reduced-motion: reduce) {
  .sidebar button.navitem.working .dot { animation: none; }
}

/* --- card grid ------------------------------------------------------- */
/*
 * Tiles, for things you pick from rather than read through -- playbooks being
 * the case this was built for. A list of long instruction bodies is a wall of
 * text you scroll past; a grid of names with a Run button is something you scan.
 *
 * auto-fill with a minimum rather than a fixed column count, so it is three
 * across on a desktop and one on a phone without a breakpoint being written for
 * either.
 */
.cardgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 12px;
  margin-top: 12px;
}
.tile {
  display: flex;
  flex-direction: column;
  gap: 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 14px;
  background: var(--panel);
}
.tile h3 { margin: 0; font-size: var(--fs-sm); }
/* The description takes the slack, so the buttons line up across a row
   regardless of how much any one of them has to say. */
.tile .fill { flex: 1 1 auto; }
.tile .row { margin-top: 0; }

/* --- KPI cards --------------------------------------------------------
   A KPI tile is wider than a playbook tile because it carries a chart, so the
   grid gets its own minimum rather than reusing the 240px one -- at 240 the
   ninety-day line is too cramped to read a shape from, which is the only
   reason it is there.

   Everything below is variables and layout. No colour is written literally, so
   the admin theme reaches these the way it reaches everything else. */

/* Wide enough for a label and five figures without the grid having to scroll.
   380px fitted the four-column version and was outgrown when the fiscal year
   column arrived. */
.kpigrid { grid-template-columns: repeat(auto-fill, minmax(440px, 1fr)); }

.kpitile { gap: 10px; }

.kpitop { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.kpitop h3 { font-size: var(--fs); }

/* The headline figure. Tabular numerals so a column of these does not jitter
   as the digits change. */
.kpivalue {
  font-size: 24px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

.kpichart { position: relative; }
.kpichart svg {
  width: 100%;
  height: 130px;
  display: block;
  overflow: visible;
}
/* Reserves its own line, so the tile does not resize as the pointer moves
   across the chart -- which looks like the page twitching. */
.kpihover { min-height: 1.2em; }

/* Two rows of small figures under the chart: the store's own periods, then
   what to compare them with. auto-fit keeps them on one line where there is
   room and wraps rather than shrinking where there is not. */
.kpistats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  gap: 6px 10px;
}
.kpistat { display: flex; flex-direction: column; gap: 1px; }
.kpistat-label { font-size: var(--fs-xs); color: var(--muted); }
.kpistat-value { font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }

.small { font-size: var(--fs-xs); }

/* --- KPI comparison grid ----------------------------------------------
   Periods across, subjects down, so "this store over 30 days" sits directly
   above the typical store, its quartile and the system over the same 30 days.
   The comparison people actually make is vertical.

   Column count is set in script, because the periods come from the server --
   the fiscal year is a table lookup, not a rule this file could know. */

.kpicompare {
  display: grid;
  gap: 2px 8px;
  align-items: baseline;
  overflow-x: auto;
}
/* The row being judged.
   It is the only row on the card that is a fact about the thing you selected;
   the other three are context. It was striped with --row-alt, which is the
   same barely-there tint used to make long tables readable and is far too
   quiet for the one row the eye should land on first.

   Tinted with the accent rather than a grey so it reads as "this is you", and
   the tint is mixed from --accent so it follows the admin theme instead of
   being a colour written here. The label gets a solid edge to anchor the row's
   left end, and both cells sit a shade heavier than their neighbours. */
.kpicompare-own {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  font-weight: 600;
  color: var(--text);
  padding-top: 2px;
  padding-bottom: 2px;
}
.kpicompare-own.kpicompare-label {
  border-left: 3px solid var(--accent);
  padding-left: 6px;
  border-radius: var(--radius) 0 0 var(--radius);
}
.kpicompare-own.kpicompare-value:last-child { border-radius: 0 var(--radius) var(--radius) 0; }
/* A touch larger than the context rows, which is enough to separate them
   without making the card look like it has two type sizes fighting. */
.kpicompare-own.kpicompare-value { font-size: var(--fs); padding-right: 4px; }
.kpicompare-head {
  font-size: var(--fs-xs);
  color: var(--muted);
  text-align: right;
}
.kpicompare-head:first-child { text-align: left; }
.kpicompare-label { font-size: var(--fs-xs); color: var(--text); white-space: nowrap; }
.kpicompare-value {
  font-size: var(--fs-sm);
  text-align: right;
  font-variant-numeric: tabular-nums;
  /* A figure that wraps splits mid-number and reads as two figures, which is
     worse than a card that scrolls. */
  white-space: nowrap;
}
.kpicompare-head { white-space: nowrap; }

/* Sits under the headline and under the chart, naming the period each covers.
   They are different periods on purpose -- the range picks the figure, the
   chart always shows the last ninety days -- so both say so. */
.kpicaption { margin: 0; }

/* The trend direction, coloured by whether it is good for THIS metric rather
   than by which way it points. A falling queue is an improvement. */
.trend-good { color: var(--good); }
.trend-bad { color: var(--danger); }

/* --- KPI picker -------------------------------------------------------
   Checkboxes rather than a multi-select: there are forty metrics in five
   subjects, and a native multi-select hides the grouping, needs a modifier key
   to add a second choice, and shows four rows at a time.

   auto-fill so it is several columns wide on a desktop and one on a phone,
   with a minimum wide enough for the longest label ("Buy Containers
   Processed") not to wrap. */
.kpipick {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 2px 14px;
  margin: 4px 0 12px;
}
.kpipick label.inline { font-size: var(--fs-sm); }

/* --- the value of closing a gap ---------------------------------------
   Set apart from the figures above it because it is a different kind of claim:
   everything else on the card is measured, and this is a projection. It should
   read as an argument for doing something, not as another fact. */
.kpiopportunity {
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
  background: color-mix(in srgb, var(--warn) 8%, transparent);
  border-radius: var(--radius);
  padding: 8px 10px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.kpiopportunity-head { font-weight: 600; font-size: var(--fs-sm); }

/* --- the KPI filter bar -----------------------------------------------
   One row across the top, and it stays put.

   The controls used to sit in a card that scrolled away, so eight cards down
   there was nothing on screen saying which period or which stores were being
   shown -- and the figures alone do not say, which is how a number gets quoted
   against the wrong month.

   Sticky at the top of the scroll area rather than fixed, so it occupies real
   layout space and nothing hides underneath it. The negative margins let it
   span the panel's padding and read as a bar rather than a floating card. */
.kpibar {
  position: sticky;
  top: 0;
  z-index: 4;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  /* Pulled out over #panel's own 18px padding so the bar spans the full width
     and reads as a bar rather than a floating card. #panel is the scrolling
     element here, which is what makes sticky stick to the top of the content
     area rather than the window. */
  margin: -18px -18px 14px;
  padding: 10px 18px;
}

/* wrap, not scroll: on a narrow window the controls take a second line rather
   than hiding half of themselves behind an edge nobody thinks to drag. */
.kpibar-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.kpibar-title { font-size: var(--fs-lg); margin-right: 4px; }
/* A hairline before the chart option, which configures the cards rather than
   the figures, so the two groups are not read as one list of filters. */
.kpibar-sep {
  border-left: 1px solid var(--border);
  padding-left: 10px;
  margin-left: 2px;
}
/* Pushed right and allowed to shrink first, since it is a restatement of the
   controls beside it rather than a control itself. */
.kpibar-summary { margin-left: auto; text-align: right; }

.kpibar select, .kpibar input { font-size: var(--fs-sm); padding: 3px 6px; }

/* The picker drops out of the bar, so it scrolls with it and cannot be left
   open somewhere off screen. Capped and scrollable because forty checkboxes
   would otherwise fill the window. */
.kpipicker {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  max-height: 46vh;
  overflow-y: auto;
}

@media (max-width: 860px) {
  .kpibar { margin: -18px -18px 14px; }
  .kpibar-summary { margin-left: 0; width: 100%; text-align: left; }
}

/* --- modal ------------------------------------------------------------
   Used for setting a goal: a decision that wants the rest of the page held
   still while it is made, and that has to be cancellable without losing where
   you were. */
.modalback {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  z-index: 50;
}
.modalbox {
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  width: min(560px, 100%);
  max-width: 100%;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}
.modalhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--border);
}
/* The body scrolls, the header and footer do not, so Save is reachable however
   long the form gets. */
.modalbody { padding: 16px 18px; overflow-y: auto; }
.modalfoot {
  display: flex;
  gap: 8px;
  padding: 12px 18px;
  border-top: 1px solid var(--border);
}
/* Destructive action pushed away from the one people mean to press. */
.modalfoot .danger { margin-left: auto; }

/* The measured facts a goal is being set against, shown before the inputs so
   the target is chosen with the numbers in view rather than from memory. */
.goalfacts {
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
button.small { padding: 3px 9px; font-size: var(--fs-xs); }

/* --- goal progress ----------------------------------------------------
   The verdict first, because "am I going to make it" is the question, and the
   figures underneath are the working. Coloured by outcome rather than by
   direction: a lower-is-better goal being under its line is good news and gets
   the same green as a sales goal being over. */
.goalstate {
  border-radius: var(--radius);
  padding: 10px 12px;
  margin-bottom: 14px;
  border: 1px solid var(--border);
}
.goalstate-head { font-weight: 600; font-size: var(--fs-lg); }
.goalstate.ahead {
  border-color: color-mix(in srgb, var(--good) 45%, transparent);
  background: color-mix(in srgb, var(--good) 10%, transparent);
}
.goalstate.behind {
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
  background: color-mix(in srgb, var(--danger) 10%, transparent);
}
.goalstate.unknown { background: var(--code-bg); }

/* Wide enough for the day table: five columns of figures did not fit the form
   width, and the last one was cut off rather than scrolling. */
.modalbox.wide { width: min(880px, 100%); }

/* Capped so the chart and the verdict stay on screen while the days scroll --
   ninety rows would otherwise push everything that explains them off the top.
   Both axes scroll: on a phone the table is wider than any modal can be. */
.goaldays { max-height: 40vh; overflow: auto; margin-top: 12px; }
/* The table must be allowed to exceed its wrapper, or it compresses columns
   into each other instead of letting the wrapper scroll. */
.goaldays table.grid { min-width: 520px; }
.goaldays table.grid th { position: sticky; top: 0; background: var(--panel); }

/* --- KPI picker bulk controls -----------------------------------------
   Choosing twenty of forty one box at a time is a chore, so each group carries
   its own toggle and there is a pair for the lot. The count sits next to the
   toggle because "Select these" means nothing without knowing how many are
   already on. */
.kpipick-bulk {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-bottom: 8px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
.kpipick-group {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
}
/* The group label already has its own top margin from .group; inside this row
   it would push the toggle out of line with it. */
.kpipick-group .group { margin: 0; padding: 0; }

/* --- effort grid ------------------------------------------------------
   Every KPI's effort on one screen, because effort is a comparison rather than
   a property: a 3 means something only next to the 2s and 4s around it, which
   is impossible to judge one modal at a time. */
.effortgrid {
  display: grid;
  /* Three across, fixed, rather than as many as fit.
     auto-fill packed five onto a wide screen, which left each name too narrow
     to read -- and a KPI name that has been cut in half is not something you
     can set an effort against. Three is what the longest labels
     ("Avg Buy Completion Time", "Buy Containers Processed") need at this
     font size. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 4px 24px;
  margin: 4px 0 14px;
}
@media (max-width: 1180px) { .effortgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 760px) { .effortgrid { grid-template-columns: 1fr; } }
.effortrow {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-content: space-between;
}
/* The name takes the slack so the selects line up in a column, which is what
   makes a row of them scannable. min-width lets it shrink inside the grid track
   instead of forcing the track wider; the title attribute carries the full
   definition, so a truncated name is still identifiable. */
.effortname {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--fs-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.effortrow select { flex: 0 0 auto; font-size: var(--fs-sm); padding: 2px 6px; }

/* --- why a store is behind --------------------------------------------
   The diagnosis sits under the progress figures because it answers the next
   question rather than the same one: progress says whether they will make it,
   this says what to do if they will not. */
.goalwhy {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.goalwhy h3 { margin: 0 0 4px; }
.goalwhy-summary { margin: 0 0 8px; }

/* Root cause last and highlighted: the chain reads left to right and the thing
   to act on is where it stops. */
.goalwhy-chain {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}

/* Actions, not observations, so they get list markers and room to breathe. */
.goalwhy-levers {
  margin: 4px 0 10px;
  padding-left: 20px;
  font-size: var(--fs-sm);
}
.goalwhy-levers li { margin-bottom: 4px; }

/* The working, folded. Whoever wants to check the arithmetic can; nobody has
   to scroll past it to reach the advice. */
.goalwhy-tree summary { cursor: pointer; }
.goalwhy-tree .kpicompare { margin-top: 8px; }
