:root {
  --ink: #18231d;
  --muted: #66736b;
  --line: #dce4dc;
  --paper: #f7f9f6;
  --panel: #fff;
  --green: #17845d;
  --lime: #b7e46d;
  --orange: #f59e0b;
  --red: #e85445;
  --navy: #173e37;
}
* {
  box-sizing: border-box;
}
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family:
    Inter,
    ui-sans-serif,
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    sans-serif;
}
.app-shell {
  max-width: 1480px;
  margin: auto;
  padding: 0 32px 36px;
}
.topbar {
  height: 74px;
  display: flex;
  align-items: center;
  border-bottom: 1px solid var(--line);
  gap: 38px;
}
.brand {
  display: flex;
  align-items: center;
  gap: 11px;
  color: var(--ink);
  text-decoration: none;
}
.brand img {
  height: 68px;
  width: auto;
  display: block;
  margin: -8px -6px -8px -14px;
}
.brand-sep {
  width: 1px;
  height: 23px;
  background: var(--line);
}
.brand-name {
  font-weight: 780;
  font-size: 20px;
  letter-spacing: -1px;
}
.brand-mark {
  display: grid;
  place-items: center;
  width: 29px;
  height: 29px;
  background: var(--ink);
  color: var(--lime);
  border-radius: 9px;
  font-family: Georgia;
  font-size: 24px;
  font-style: italic;
}
.brand-accent {
  color: var(--green);
}
.scenario {
  display: flex;
  align-items: center;
}
.divider {
  height: 18px;
  border-left: 1px solid var(--line);
}
.top-actions {
  margin-left: auto;
  display: flex;
  gap: 10px;
  align-items: center;
}
.quiet-button,
.map-control,
.layers-button,
.link-button {
  border: 0;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  font-weight: 650;
  cursor: pointer;
}
.avatar {
  border: 0;
  background: #e6efe4;
  color: var(--green);
  width: 33px;
  height: 33px;
  border-radius: 50%;
  font-weight: 800;
}
.intro-row {
  display: flex;
  align-items: end;
  justify-content: space-between;
  padding: 16px 0 16px;
}
.eyebrow {
  font-size: 10px;
  letter-spacing: 1.25px;
  color: #718078;
  font-weight: 800;
  margin: 0 0 8px;
}
h1,
h2,
p {
  margin-top: 0;
}
h1 {
  font-size: 34px;
  letter-spacing: -1.6px;
  margin-bottom: 9px;
}
.intro-copy {
  color: var(--muted);
  font-size: 14px;
  margin: 0;
}
.legend {
  display: flex;
  gap: 18px;
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
  padding-bottom: 5px;
}
/* The status key belongs to the network map. The day-ahead plan draws no coloured elements, so there
   it was a key to nothing on the page. */
.plan-mode .legend {
  display: none;
}
.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
  margin-right: 6px;
}
.healthy {
  background: var(--green);
}
.warning {
  background: var(--orange);
}
.critical {
  background: var(--red);
}
.kpi-row {
  display: grid;
  grid-template-columns: 1.45fr 1fr 1fr 1fr 1fr;
  gap: 14px;
  margin-bottom: 18px;
}
.panel,
.health-card,
.metric-card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: 0 1px 1px #173e3705;
}
.health-card,
.metric-card {
  min-height: 124px;
  padding: 19px;
}
.card-label {
  font-size: 10px;
  letter-spacing: 1.15px;
  color: #738078;
  font-weight: 800;
}
.ready-badge {
  border-radius: 20px;
  background: #e7f6eb;
  color: #22734c;
  font-size: 9px;
  font-weight: 800;
  padding: 4px 7px;
  margin-left: 7px;
}
.health-main {
  display: flex;
  justify-content: space-between;
  margin: 14px 0 10px;
  font-size: 18px;
  font-weight: 750;
  letter-spacing: -0.4px;
}
.health-score {
  font-size: 13px;
  color: var(--muted);
  font-weight: 600;
}
.health-score strong {
  font-size: 18px;
  color: var(--ink);
}
.health-bar {
  height: 6px;
  border-radius: 99px;
  background: #e9eeea;
  overflow: hidden;
}
.health-bar span {
  display: block;
  width: 72%;
  height: 100%;
  background: linear-gradient(90deg, var(--green), var(--orange));
  border-radius: inherit;
  transition: 0.35s;
}
.health-card p,
.metric-foot {
  font-size: 11px;
  color: var(--muted);
  margin: 9px 0 0;
}
.metric-value {
  font-weight: 750;
  font-size: 24px;
  letter-spacing: -1px;
  margin-top: 15px;
}
.metric-value em {
  font-style: normal;
  color: #a0aaa4;
  font-size: 18px;
}
.metric-value small {
  font-size: 12px;
  letter-spacing: 0;
  color: #78837d;
  margin-left: 3px;
}
.danger {
  color: var(--red);
}
.positive {
  color: var(--green);
}
.workspace {
  display: grid;
  grid-template-columns: minmax(0, 1.92fr) minmax(310px, 0.8fr);
  gap: 18px;
  /* The same 18px the day-ahead plan's .day-outcome carries, and the same as the grid's own gap: the
     generalisation strip follows both of them and has no top margin of its own, so without this the
     map and the "active now" card sat right against it in the detailed day view while the plan view
     was cleanly separated. */
  margin-bottom: 18px;
}
.grid-card {
  /* Same box as the day-ahead plan's panels, on all four sides: the map used to bleed into the card
     edge at the bottom while the header sat 21px in, so the card read as unfinished. */
  padding: 21px;
  overflow: hidden;
}
.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: start;
}
.panel-header h2,
.timeline-heading h2,
.violation-head h2 {
  font-size: 17px;
  letter-spacing: -0.6px;
  margin: 0;
}
.map-control {
  background: #f2f5f1;
  padding: 7px 10px;
  border-radius: 7px;
}
.map-legend {
  display: flex;
  /* Wraps now that the injection key is split in two; the row gap keeps the wrapped line off the
     one above it. */
  flex-wrap: wrap;
  gap: 9px 17px;
  margin: 15px 0 13px;
  font-size: 11px;
  color: var(--muted);
  font-weight: 600;
}
.map-legend i {
  display: inline-block;
  vertical-align: middle;
  margin-right: 5px;
}
.line-normal,
.line-hot {
  width: 17px;
  border-top: 2px solid #8fa39a;
}
.line-hot {
  border-color: var(--orange);
  border-width: 3px;
}
.node-normal,
.node-alert {
  height: 8px;
  width: 8px;
  background: var(--green);
  border-radius: 50%;
}
.node-alert {
  background: var(--red);
  box-shadow: 0 0 0 3px #fde6e3;
}
.grid-map {
  height: 425px;
  position: relative;
  border-radius: 11px;
  background: #eaf1ea;
  overflow: hidden;
}
.grid-map:before {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0.38;
  background-image: linear-gradient(90deg, #fff 1px, transparent 1px), linear-gradient(#fff 1px, transparent 1px);
  background-size: 42px 42px;
}
.grid-map svg {
  position: relative;
  width: 100%;
  height: 100%;
}
.area-shape {
  fill: url(#mapGlow);
}
.network-lines path {
  stroke: #789087;
  stroke-width: 3;
  fill: none;
  stroke-linecap: round;
}
.hot-line {
  stroke: var(--orange);
  stroke-width: 5;
  fill: none;
  stroke-linecap: round;
  filter: drop-shadow(0 2px 2px #f59e0b66);
}
.nodes circle {
  fill: #15845e;
  stroke: #f8fff9;
  stroke-width: 4;
}
.nodes .node-danger {
  fill: var(--red);
  stroke: #fff1ef;
  stroke-width: 6;
}
.nodes .node-warning {
  fill: var(--orange);
  stroke: #fff6e5;
  stroke-width: 6;
}
.power-flow circle {
  fill: #fff;
  stroke: var(--orange);
  stroke-width: 3;
}
.map-labels {
  font-size: 12px;
  fill: #53665c;
  font-weight: 700;
}
.map-callout {
  position: absolute;
  top: 18px;
  right: 18px;
  display: flex;
  gap: 9px;
  align-items: center;
  background: #fff;
  padding: 10px 13px;
  border-radius: 9px;
  box-shadow: 0 5px 16px #2643341a;
  font-size: 12px;
}
.map-callout strong,
.map-callout small {
  display: block;
}
.map-callout small {
  color: var(--green);
  margin-top: 3px;
  font-size: 10px;
  font-weight: 700;
}
.critical-dot {
  height: 9px;
  width: 9px;
  background: var(--red);
  border-radius: 50%;
}
.layers-button {
  position: absolute;
  left: 17px;
  bottom: 15px;
  background: #fff;
  padding: 8px 11px;
  border-radius: 7px;
  box-shadow: 0 3px 9px #26433416;
}
.side-stack {
  display: grid;
  grid-template-rows: auto 1fr;
  gap: 18px;
}
.agent-card {
  padding: 19px;
  background: linear-gradient(145deg, #173f38, #1f5145);
  color: #fff;
  border: 0;
}
.card-topline {
  display: flex;
  align-items: center;
}
.agent-icon {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: 7px;
  background: var(--lime);
  color: #173e37;
  margin-right: 7px;
  font-size: 14px;
}
.agent-card .card-label {
  color: #c8ddd3;
}
.ready-badge {
  margin-left: auto;
  background: #3b6958;
  color: #dcf2dd;
}
.agent-card h2 {
  font-size: 19px;
  letter-spacing: -0.7px;
  margin: 20px 0 7px;
}
.agent-card p {
  color: #c8ddd3;
  font-size: 12px;
  line-height: 1.55;
  margin-bottom: 17px;
}
.action-preview {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  background: #ffffff12;
  border: 1px solid #ffffff1b;
  border-radius: 9px;
  padding: 11px;
  margin-bottom: 12px;
}
.action-preview span {
  display: block;
  color: #c8ddd3;
  font-size: 10px;
}
.action-preview strong {
  display: block;
  font-size: 12px;
  margin-top: 3px;
}
.action-preview .impact {
  color: var(--lime);
  font-weight: 800;
  white-space: nowrap;
  align-self: center;
}
.primary-button {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  border: 0;
  border-radius: 8px;
  background: var(--lime);
  color: #153b31;
  padding: 11px 13px;
  font-size: 12px;
  font-weight: 800;
  cursor: pointer;
}
.primary-button span {
  font-size: 17px;
}
.link-button {
  display: block;
  margin: 10px auto 0;
  color: #e1f0e6;
  font-size: 11px;
}
.violation-card {
  padding: 19px;
}
.violation-head {
  display: flex;
  justify-content: space-between;
}
.count-badge {
  display: grid;
  place-items: center;
  width: 27px;
  height: 27px;
  background: #fff0ed;
  color: var(--red);
  border-radius: 50%;
  font-size: 13px;
  font-weight: 800;
}
.issue-list {
  margin-top: 12px;
}
.issue-item {
  display: grid;
  grid-template-columns: 9px 1fr auto;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 11px 0;
  border: 0;
  border-top: 1px solid #edf1ed;
  background: transparent;
  text-align: left;
  cursor: pointer;
}
.issue-item strong,
.issue-item small {
  display: block;
  font-size: 12px;
}
.issue-item small {
  color: var(--muted);
  font-size: 10px;
  margin-top: 2px;
}
.issue-item b {
  font-size: 11px;
  color: var(--red);
}
.severity {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}
.high {
  background: var(--red);
}
.medium {
  background: var(--orange);
}
.timeline-panel {
  padding: 19px 22px 14px;
  margin-top: 18px;
}
.timeline-heading {
  display: flex;
  align-items: end;
  justify-content: space-between;
}
.timeline-state {
  color: #78837d;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0;
}
.play-controls {
  display: flex;
  gap: 5px;
}
.play-controls button {
  border: 1px solid var(--line);
  width: 29px;
  height: 29px;
  border-radius: 7px;
  background: #fff;
  color: var(--ink);
  cursor: pointer;
}
.play-controls .play {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--lime);
}
.timeline-chart {
  height: 85px;
  position: relative;
  margin-top: 7px;
  background: linear-gradient(transparent 49%, #f6e6c6 50%, transparent 51%);
  border-radius: 6px;
  overflow: hidden;
}
.timeline-chart svg {
  width: 100%;
  height: 100%;
}
.area-voltage {
  fill: #c8ead64d;
}
.line-voltage {
  stroke: var(--green);
  stroke-width: 2.6;
  fill: none;
}
.cursor {
  stroke: var(--ink);
  stroke-width: 1.3;
  stroke-dasharray: 3 3;
}
.limit {
  position: absolute;
  z-index: 1;
  right: 4px;
  color: #9a8055;
  background: #fff9;
  font-size: 9px;
}
.upper {
  top: 34px;
}
.lower {
  bottom: 2px;
}
.time-slider {
  width: 100%;
  accent-color: var(--green);
  margin: 3px 0 0;
}
.time-labels {
  display: flex;
  justify-content: space-between;
  color: #89948d;
  font-size: 10px;
}
.chart-limit {
  position: absolute;
  right: 1px;
  font-size: 9px;
  color: #76907c;
}
.chart-limit.top {
  top: 21px;
}
.chart-limit.bottom {
  bottom: 30px;
}
.bar-chart {
  height: 126px;
  border-bottom: 1px solid #dce4dc;
  margin-top: 17px;
  display: flex;
  gap: 10px;
  align-items: end;
  position: relative;
  padding: 0 18px;
}
.bar-chart span {
  flex: 1;
  background: linear-gradient(#f8c65a, var(--orange));
  border-radius: 3px 3px 0 0;
}
.hundred {
  position: absolute;
  right: 0;
  top: 10px;
  color: var(--red);
  font-size: 9px;
  font-style: normal;
  border-top: 1px dashed #e99589;
  width: 100%;
  text-align: right;
}
@media (max-width: 1000px) {
  .kpi-row {
    grid-template-columns: 1fr 1fr;
  }
  .workspace {
    grid-template-columns: 1fr;
  }
  .side-stack {
    grid-template-columns: 1fr 1fr;
    grid-template-rows: auto;
  }}
@media (max-width: 650px) {
  .app-shell {
    padding: 0 16px 25px;
  }
  .view-switcher {
    margin-left: -16px;
    margin-right: -16px;
    padding-left: 16px;
    padding-right: 16px;
  }
  .scenario,
  .legend {
    display: none;
  }
  .intro-row {
    padding-top: 20px;
  }
  .kpi-row,
  .side-stack {
    grid-template-columns: 1fr;
  }
  .grid-map {
    height: 330px;
  }
  .topbar {
    gap: 0;
  }
  .workspace {
    gap: 12px;
  }
  .map-legend {
    gap: 8px;
    font-size: 9px;
  }}

/* The three views are the page's primary navigation, so the strip carrying them stays put: it is
   pulled out to the shell's full width (hence the negative margins, which the 650px query restates
   for that breakpoint's narrower padding) and sticks under the top of the viewport, so switching
   view never costs a scroll back up. */
.view-switcher {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  margin: 0 -32px 18px;
  padding: 9px 32px;
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}
.view-tabs {
  display: inline-flex;
  background: #e8eee8;
  padding: 3px;
  border-radius: 9px;
}
.view-switcher button {
  border: 0;
  background: transparent;
  color: #65736a;
  border-radius: 7px;
  padding: 9px 15px;
  font-size: 13px;
  font-weight: 750;
  cursor: pointer;
}
.view-switcher button.active {
  background: #fff;
  color: var(--ink);
  box-shadow: 0 1px 3px #193d2d1a;
}
.red-text {
  color: var(--red);
}
.green-text {
  color: var(--green);
}
.muted-text {
  color: var(--muted);
}
.policy-map-title {
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  font-weight: 750;
}
.n {
  position: absolute;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--green);
  border: 3px solid #fff;
}
.n1 {
  left: 13%;
  top: 61%;
}
.n2 {
  left: 42%;
  top: 34%;
}
.n3 {
  left: 53%;
  top: 59%;
}
.n4 {
  left: 82%;
  top: 22%;
}
.n5 {
  left: 82%;
  top: 70%;
}
.n.alert {
  background: var(--red);
  box-shadow: 0 0 0 5px #fbe2df;
}
.n.warning {
  background: var(--orange);
  box-shadow: 0 0 0 5px #fff0d5;
}
.n.controlled {
  background: var(--green);
  box-shadow: 0 0 0 5px #daf1de;
}
.action-ring {
  position: absolute;
  width: 25px;
  height: 25px;
  border: 1px dashed var(--green);
  border-radius: 50%;
}
.r1 {
  left: 37%;
  top: 27%;
}
.r2 {
  left: 77%;
  top: 16%;
}
.run-controls {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
}
.date-run-control { display: flex; align-items: center; gap: 6px; }
.configuration-menu { position: relative; }
.configuration-menu summary {
  list-style: none; border: 1px solid var(--line); border-radius: 7px; padding: 7px 9px;
  background: #fff; color: var(--muted); font: 700 11px inherit; cursor: pointer; white-space: nowrap;
}
.configuration-menu summary::-webkit-details-marker { display: none; }
.configuration-menu summary span { margin-left: 3px; color: var(--green); }
.configuration-menu[open] summary { border-color: #bcd6c1; color: var(--green); }
.configuration-popover {
  position: absolute; top: calc(100% + 7px); right: 0; z-index: 41; width: 255px; padding: 12px;
  display: grid; gap: 10px; border: 1px solid var(--line); border-radius: 12px; background: #fff;
  box-shadow: 0 10px 28px #1a3a2a1f;
}
.configuration-popover label { display: grid; gap: 4px; color: var(--muted); font-size: 10px; font-weight: 800; letter-spacing: .4px; }
.configuration-popover select, .configuration-popover .agent-select { width: 100%; max-width: none; color: var(--ink) !important; }
.run-controls select,
.run-controls input {
  border: 1px solid var(--line);
  background: #fff;
  border-radius: 7px;
  padding: 7px;
  color: var(--ink);
  font: 600 11px inherit;
}
/* The agent picker is a statement of what the page is about, not just another run control, so it
   carries the agent's own accent rather than sitting grey next to the date field. */
/* Hides with the picker it labels - one configured run means there is nothing to choose between. */
.agent-select-label {
  font-size: 12px;
  font-weight: 750;
  color: var(--ink);
  white-space: nowrap;
  margin-right: 2px;
}
.agent-select {
  border-color: #bcd6c1 !important;
  color: var(--green) !important;
  font-weight: 750 !important;
  /* Sized to its longest option rather than capped at a width that clipped the names mid-word. */
  max-width: 300px;
  text-overflow: ellipsis;
}
.run-controls button {
  border: 0;
  border-radius: 7px;
  background: var(--ink);
  color: var(--lime);
  padding: 7px 10px;
  font: 750 11px inherit;
  cursor: pointer;
}
@media (max-width: 1000px) {
  .run-controls {
    display: none;
  }}
.real-line {
  stroke: #789087;
  stroke-width: 2;
  cursor: pointer;
}
.real-line:hover {
  stroke: #173e37;
  stroke-width: 4;
}
.real-line.warning {
  stroke: var(--orange);
  stroke-width: 3.5;
}
.real-line.critical {
  stroke: var(--red);
  stroke-width: 4;
}
.real-node {
  fill: var(--green);
  stroke: #fff;
  stroke-width: 2;
  cursor: pointer;
}
.real-node.warning {
  fill: var(--orange);
}
.real-node.critical {
  fill: var(--red);
  stroke: #fff0ed;
  stroke-width: 4;
}
.real-node:hover {
  stroke: var(--ink);
  stroke-width: 3;
}
.real-label {
  fill: #52655a;
  font:
    700 10px Inter,
    sans-serif;
  pointer-events: none;
}
.empty-issues {
  font-size: 12px;
  color: var(--muted);
  padding: 12px 0;
  margin: 0;
}
.load-marker {
  fill: #526eeb;
  stroke: #fff;
  stroke-width: 1;
  cursor: pointer;
}
.generator-marker {
  fill: #a855f7;
  stroke: #fff;
  stroke-width: 1;
  cursor: pointer;
}
.generator-marker.controllable {
  fill: #b7e46d;
  stroke: #173e37;
  stroke-width: 1.5;
}
.transformer-marker {
  fill: #fff;
  stroke: #173e37;
  stroke-width: 2;
  cursor: pointer;
}
.is-selected {
  stroke: #173e37 !important;
  stroke-width: 5 !important;
  filter: drop-shadow(0 0 4px #173e37aa);
}
circle.is-selected {
  stroke: #173e37 !important;
  stroke-dasharray: 2 2;
  animation: select-pulse 1.1s ease-in-out infinite;
}
@keyframes select-pulse {
  0%,
  100% {
    stroke-width: 5;
  }
  50% {
    stroke-width: 7;
  }
}
.legend-load,
.legend-generator {
  width: 8px;
  height: 8px;
  background: #526eeb;
  display: inline-block;
}
.legend-generator {
  background: #a855f7;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
/* Same two fills the map uses for the injection triangles, so the key can be read off the map. */
.legend-generator.controllable {
  background: #b7e46d;
}
/* The overlay writes signed numbers, so its key is a signed number rather than the split bar it used
   to be back when the readouts were drawn as P and Q bars. */
.legend-action {
  width: 11px;
  height: 9px;
  font-size: 11px;
  font-weight: 800;
  line-height: 9px;
  text-align: center;
  font-style: normal;
  color: #5b3fb0;
}
.legend-action:before {
  content: '\00b1';
}
/* The curtailment ring: hollow, because on the map it sits behind a marker. */
.legend-curtail-key {
  width: 9px;
  height: 9px;
  border: 2px solid var(--orange);
  border-radius: 50%;
  background: none;
}
/* The action overlay: what the shown policy changes at this instant, written where it changes it.
   Bars were unreadable at the size a 1.5k-bus feeder leaves for them, and the numbers are what an
   operator would be given anyway. Active and reactive power get their own hue, so a station moving
   only one of the two reads as which one without having to parse the unit. Neither hue is a status
   colour - orange and red on this map mean "outside limits", and a dispatch is not a fault.

   The white halo (paint-order: stroke, so it is painted behind the glyphs rather than over them) is
   what keeps these legible over both the schematic backdrop and the OSM tiles. Tabular figures stop
   the two stacked lines from jittering against each other digit by digit. */
/* Sizes here are fixed rather than scaled with the map: renderActionOverlay counter-scales each
   label group against the zoom, so the whole block - glyphs, halo, line spacing - is magnified as one
   piece. Scaling font-size instead used to break the text outright at high zoom: the font-size fell
   below a pixel in user units, where glyph metrics come apart, while the halo's stroke-width did not,
   so it grew over the digits it was meant to sit behind. */
.action-value,
.action-tap {
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  paint-order: stroke;
  stroke: #fff;
  stroke-width: 3.5px;
  stroke-linejoin: round;
}
/* The base of the curtailment ramp. Each P readout overrides this inline with its own point on the
   ramp, so this colour only shows on a station that is not being curtailed at all. */
.action-value.p {
  fill: #0f6b4a;
}
.action-value.q {
  fill: #5b3fb0;
}
/* The tap is the one action that belongs to the grid rather than to a station, so it is centred on
   the transformer and carries the darkest ink on the overlay. */
.action-tap {
  fill: var(--navy);
  font-weight: 800;
  text-anchor: middle;
}
/* Drawn behind a curtailed injection: the wider, the more opaque and the hotter, the more generation
   the shown policy is throwing away at that station right now. Fill and stroke are set per ring by
   renderActionOverlay's curtailment ramp; the wash stays light here so the ring tints the map rather
   than covering it, and the ring's own colour still reads at full strength on its edge. */
.curtail-halo {
  fill-opacity: 0.22;
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}
.warning-limit {
  stroke: var(--orange);
  stroke-width: 1;
  stroke-dasharray: 4 3;
  opacity: 0.9;
}
.critical-limit {
  stroke: var(--red);
  stroke-width: 1.4;
  stroke-dasharray: 4 3;
}
.lower-voltage {
  stroke: #76b99a;
  stroke-width: 1.6;
}
.day-voltage-max {
  fill: none !important;
  stroke: var(--green) !important;
  stroke-width: 2.5;
}
.day-voltage-min {
  fill: none !important;
  stroke: #76b99a !important;
  stroke-width: 2;
}
.bar-chart .bar-critical {
  background: var(--red);
}
.bar-chart .bar-warning {
  background: var(--orange);
}
.map-actions {
  display: flex;
  gap: 6px;
}
.map-attribution {
  position: absolute;
  right: 11px;
  bottom: 7px;
  z-index: 2;
  background: #ffffffe8;
  padding: 2px 4px;
  color: #5f6e65;
  font-size: 8px;
}
.map-attribution a {
  color: inherit;
}
/* The basemap is scenery, not a target: without this every tile would swallow the drag-to-pan
   pointerdown (which only fires on the SVG itself) and the click that clears the selection. */
.map-tiles image {
  pointer-events: none;
}
/* Full-strength OSM is busy enough that an orange line over it reads as part of the map. Washing the
   tiles out keeps the grid the thing you see first, while the streets stay legible underneath. */
.map-tiles {
  filter: saturate(0.45) brightness(1.08) contrast(0.94);
}
/* The decorative graph-paper backdrop is the schematic view's stand-in for a map; with a real one
   underneath it is just haze over the streets. */
.grid-map.is-geo:before {
  content: none;
}
.grid-map.is-geo {
  background: #e8ece4;
}
.loading-limit {
  position: absolute;
  right: 0;
  top: 22%;
  color: var(--red);
  font-size: 9px;
  background: #fff9;
}
.loading-bar {
  fill: #f59e0b;
}
.loading-bar.critical {
  fill: var(--red);
}
.loading-threshold {
  stroke: var(--red);
  stroke-width: 1.2;
  stroke-dasharray: 4 3;
}
.axis-label {
  fill: #6e7d73;
  font:
    600 9px Inter,
    sans-serif;
}
.axis-limit-label {
  fill: #d64d41;
  font:
    700 9px Inter,
    sans-serif;
}
.axis-warning-label {
  fill: #c77c00;
  font:
    700 9px Inter,
    sans-serif;
}
.health-detail {
  font-size: 10px !important;
  color: #a55a28 !important;
  font-weight: 750;
}
.action-metrics {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 7px;
  margin: 0 0 12px;
}
.action-metrics span {
  padding: 7px 8px;
  background: #ffffff0d;
  border-radius: 6px;
}
.action-metrics small,
.action-metrics b {
  display: block;
}
.action-metrics small {
  font-size: 9px;
  color: #b9d1c4;
}
.action-metrics b {
  font-size: 10px;
  color: #fff;
  margin-top: 2px;
}
.real-line {
  stroke-width: 3;
  filter: drop-shadow(0 1px 1px #173e3744);
}
.loading-line {
  fill: none;
  stroke: var(--orange);
  stroke-width: 2.5;
}
.grid-map {
  height: 540px;
}
.side-stack {
  grid-template-rows: minmax(0, 1fr) 260px;
}
.violation-card {
  height: 260px;
  overflow: hidden;
}
.issue-list {
  height: 185px;
  overflow-y: auto;
  padding-right: 4px;
}
.timeline-chart {
  height: 150px;
  background: none;
  overflow: visible;
}
@media (max-width: 1000px) {
  .side-stack {
    grid-template-rows: auto;
  }
  .violation-card {
    height: 260px;
  }}
@media (max-width: 650px) {
  .grid-map {
    height: 390px;
  }}
.timeline-hint {
  color: var(--muted);
  font-size: 12px;
  margin: 8px 0 0;
}

.day-outcome {
  display: grid;
  gap: 12px;
  margin-bottom: 18px;
}
.outcome-strip {
  border-radius: 16px;
  padding: 18px 21px;
  border: 0;
}
.outcome-strip .eyebrow {
  margin-bottom: 12px;
}
.outcome-items {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 16px;
}
/* Two lines' worth of room whether the label uses them or not: "Time in critical violation" fits on
   one line and "Time outside operating limits" does not, and without this the values below them sat
   at two different heights across the same row. */
.outcome-item span {
  display: block;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.3;
  min-height: 2.6em;
}
/* Never wrapped: "100.18 MWh" is the widest value any strip shows, and broken over two lines it put
   the unit on a row of its own and pushed that item's footnote out of line with the other six. The
   size is set so that value fits one column at the narrowest the grid lets a column get. */
.outcome-item strong {
  display: block;
  font-size: 26px;
  letter-spacing: -1px;
  margin: 4px 0 2px;
  white-space: nowrap;
}
.outcome-item small {
  display: block;
  font-size: 10px;
  line-height: 1.35;
}
.noop-strip {
  background: linear-gradient(145deg, #3d1a15, #5f241c);
}
/* The same hero-then-metrics split the controller and agent strips use, so the three read as one
   sequence. Where they put a percentage, this one puts the date the whole view is about. */
.noop-strip-head {
  display: grid;
  grid-template-columns: minmax(210px, 250px) minmax(0, 1fr);
  gap: 14px 24px;
  align-items: center;
}
/* The hero rules the whole column - forecast chips above, metrics below - so the divider and the
   metric row both sit where the controller's and the agent's do. */
.noop-hero {
  grid-row: 1 / 3;
  align-self: stretch;
  /* The label sits at the top of the banner, where the controller and agent strips put theirs, and
     the date it introduces hangs at the foot of the column with the forecast chips and the metric
     row beside it - centring the whole group instead left the label floating mid-banner. */
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  border-right: 1px solid #ffffff20;
  padding-right: 22px;
}
.noop-strip .outcome-items {
  grid-column: 2;
  padding-top: 13px;
  border-top: 1px solid #ffffff1f;
}
.noop-hero .eyebrow {
  /* Everything after it is pushed to the foot of the column. */
  margin-bottom: auto;
}
.noop-hero strong {
  display: block;
  font-size: 36px;
  line-height: 1;
  letter-spacing: -1.6px;
  color: #fff;
}
.noop-hero small {
  display: block;
  color: #f0c4bb;
  font-size: 12px;
  font-weight: 700;
  margin-top: 8px;
}
.noop-hero em b {
  color: #ffc9bd;
  letter-spacing: 0.3px;
}
.noop-hero em {
  display: block;
  color: #c49a92;
  font-size: 10px;
  font-style: normal;
  line-height: 1.45;
  margin-top: 8px;
}
@media (max-width: 900px) {
  .noop-strip-head {
    grid-template-columns: 1fr;
  }
  .noop-hero {
    grid-row: auto;
    border-right: 0;
    border-bottom: 1px solid #ffffff20;
    padding: 0 0 16px;
  }
  .noop-strip .outcome-items {
    grid-column: 1;
  }
}
/* The two peaks and the day's shape, at the top right of the banner. Wraps to its own row on narrow
   screens rather than squeezing the sparkline to nothing. */
.forecast-chips {
  display: flex;
  align-items: center;
  /* Kept together at the right, against the sparkline they annotate - spread across the strip, the
     two peaks read as unrelated to the curve rather than as its legend. */
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 12px 18px;
}
.forecast-chip {
  display: grid;
  grid-template-columns: auto auto;
  align-items: center;
  column-gap: 8px;
}
.forecast-glyph {
  grid-row: 1 / 3;
  font-size: 19px;
  font-style: normal;
  line-height: 1;
}
.forecast-chip b {
  font-size: 17px;
  letter-spacing: -0.5px;
  color: #ffd9b0;
}
.forecast-chip small {
  font-size: 10px;
  color: #c49a92;
  white-space: nowrap;
}
.forecast-spark-wrap {
  display: grid;
  gap: 3px;
}
.forecast-spark {
  width: 165px;
  height: 46px;
}
.forecast-spark-legend {
  display: flex;
  gap: 10px;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.3px;
  color: #c49a92;
}
.forecast-spark-legend .is-gen {
  color: #ffc98d;
}
.forecast-spark-legend .is-load {
  color: #9fc7ff;
}
/* The strip is dark, so this is a text link in the legend's own type rather than the white square
   button the plan charts use - a white box on maroon reads as a foreign control. */
/* The same square icon button as `.chart-expand`, in the dark strip's palette rather than the white
   panels' - the label used to spell out "detailed view", which read as the strip's only call to
   action when it is just the one affordance every other chart already carries as a glyph. */
.forecast-expand {
  margin-left: auto;
  border: 1px solid #ffffff2e;
  background: #ffffff14;
  border-radius: 6px;
  width: 23px;
  height: 23px;
  line-height: 1;
  padding: 0;
  color: #f0b8ad;
  font-size: 12px;
  cursor: pointer;
  flex: none;
}
.forecast-expand:hover {
  border-color: #ffffff5c;
  color: #fff;
}
/* Expanded: the chart grows onto its own line inside the chips row, with axes drawn in. The head
   grid is left alone on purpose - collapsing it to one column reflowed the metric row below into
   two cramped rows, and only the chart was meant to change size. */
.forecast-open .forecast-spark-wrap {
  flex: 1 0 100%;
}
.forecast-open .forecast-spark {
  width: 100%;
  height: 200px;
}
.forecast-axis-line {
  stroke: #ffffff1f;
  stroke-width: 1;
}
.forecast-axis-line.is-vertical {
  stroke: #ffffff14;
}
.forecast-axis-label {
  fill: #c49a92;
  font-size: 10px;
  font-weight: 700;
}
.forecast-axis-label.is-y {
  text-anchor: end;
}
.forecast-axis-label.is-x {
  text-anchor: middle;
}
.forecast-spark-area {
  fill: #ffc98d2e;
  stroke: none;
}
.forecast-spark-gen {
  fill: none;
  stroke: #ffc98d;
  stroke-width: 1.4;
}
.forecast-spark-load {
  fill: none;
  stroke: #9fc7ff;
  stroke-width: 1.4;
  stroke-dasharray: 3 2;
}
.noop-strip.is-clean .forecast-chip small {
  color: #a9c2b2;
}
.noop-strip .eyebrow {
  color: #f0b8ad;
}
.noop-weather {
  letter-spacing: 2px;
  font-size: 21px !important;
  line-height: 1.2;
  cursor: help;
}
.noop-strip .outcome-item span {
  color: #e8bdb4;
}
.noop-strip .outcome-item strong {
  color: #ff9d8f;
}
.noop-strip .outcome-item small {
  color: #c49a92;
}
.noop-strip.is-clean {
  background: linear-gradient(145deg, #25352c, #31463a);
}
.noop-strip.is-clean .outcome-item strong {
  color: #cfe6d6;
}
/* The middle rung, and it looks like one: amber sits between the baseline's red and the agent's
   green, so the three strips read top to bottom as a scale rather than as three unrelated cards. */
.controller-strip {
  background: linear-gradient(145deg, #4a2f10, #6b4415);
  display: grid;
  gap: 14px;
}
.controller-strip .eyebrow {
  color: #f5cf9a;
  margin: 0;
}
.controller-rows {
  display: grid;
  gap: 16px;
}
.controller-row {
  display: grid;
  /* The same split the forecast and agent strips use, so all three metric rows start at one x. */
  grid-template-columns: minmax(210px, 250px) minmax(0, 1fr);
  gap: 12px 24px;
  align-items: start;
}
/* Same divider the agent strip draws between its hero and its metrics - the three strips are meant
   to be read against each other, so they are ruled the same way. */
.controller-hero {
  border-right: 1px solid #ffffff20;
  padding-right: 22px;
}
.controller-hero strong {
  display: block;
  font-size: 31px;
  letter-spacing: -1.3px;
  margin: 3px 0 1px;
  color: #ffc86b;
}
.controller-hero strong.positive {
  color: #8fe0ac;
}
.controller-hero strong.danger {
  color: #ff9d8f;
}
.controller-hero small {
  display: block;
  font-size: 10px;
  line-height: 1.35;
  color: #d9b489;
}
.controller-name {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  font-size: 13px;
  font-weight: 750;
  color: #fff3e2;
}
.controller-strip .outcome-item span {
  color: #e2c39c;
}
.controller-strip .outcome-item strong {
  color: #ffd9a0;
}
.controller-strip .outcome-item small {
  color: #c4a37e;
}
@media (max-width: 900px) {
  .controller-row {
    grid-template-columns: 1fr;
  }
  .controller-hero {
    border-right: 0;
    border-bottom: 1px solid #ffffff20;
    padding: 0 0 14px;
  }
}
.agent-strip {
  background: linear-gradient(145deg, #173f38, #1f5145);
  display: block;
  padding: 0;
  overflow: hidden;
}
.agent-strip-head {
  display: grid;
  grid-template-columns: minmax(210px, 250px) minmax(0, 1fr);
  gap: 24px;
  align-items: center;
  padding: 18px 21px;
}
.agent-strip-plan {
  background: var(--panel);
  border-radius: 13px;
  margin: 0 7px 7px;
  padding: 17px 19px 15px;
}
.agent-strip .eyebrow {
  color: #c8ddd3;
}
.agent-strip .eyebrow span {
  color: #9fd8bb;
  font-weight: 700;
}
.agent-hero {
  border-right: 1px solid #ffffff20;
  padding-right: 22px;
}
/* Whose plan this is, said once and said clearly, right under the "DAY-AHEAD PLAN" label. */
.agent-hero-name {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 3px 0 9px;
  font-size: 17px;
  font-weight: 800;
  letter-spacing: -0.4px;
  color: #ffffff;
}
.agent-hero-name i {
  font-style: normal;
  font-size: 14px;
  color: var(--lime);
}
.agent-hero strong {
  display: block;
  font-size: 52px;
  line-height: 1;
  letter-spacing: -2.5px;
  color: var(--lime);
}
.agent-hero strong.partial {
  color: #ffd66e;
}
.agent-hero strong.danger {
  color: #ff9d8f;
}
.agent-hero small {
  display: block;
  color: #dcefe3;
  font-size: 12px;
  font-weight: 700;
  margin-top: 8px;
}
.agent-hero em {
  display: block;
  color: #9fb8ac;
  font-size: 10px;
  font-style: normal;
  line-height: 1.45;
  margin-top: 8px;
}
.agent-strip .outcome-item span {
  color: #c8ddd3;
}
.agent-strip .outcome-item strong {
  color: #fff;
}
.agent-strip .outcome-item small {
  color: #9fb8ac;
}
@media (max-width: 900px) {
  .agent-strip-head {
    grid-template-columns: 1fr;
  }
  .agent-hero {
    border-right: 0;
    border-bottom: 1px solid #ffffff20;
    padding: 0 0 16px;
  }}
.compute-status,
.agent-progress {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 10px;
  font-weight: 750;
}
.compute-status {
  color: var(--green);
  white-space: nowrap;
}
.agent-progress {
  color: #d5ebdd;
  margin: 0 0 10px;
}
.compute-status::before,
.agent-progress::before,
.game-standings li.is-waiting b::before {
  content: '';
  width: 10px;
  height: 10px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: mf-spin 0.65s linear infinite;
}
@keyframes mf-spin {
  to {
    transform: rotate(360deg);
  }
}
.health-reason {
  font-size: 10px !important;
  color: #718078 !important;
  margin-top: 5px !important;
}
.operator-action:disabled {
  background: #dfe7df;
  color: #829087;
  cursor: not-allowed;
}
[hidden] {
  display: none !important;
}
.plot-series-controls {
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
  margin: 11px 0 4px;
  font-size: 10px;
  color: var(--muted);
  font-weight: 700;
}
.plot-series-controls > span {
  text-transform: uppercase;
  letter-spacing: 0.8px;
  font-size: 9px;
}
.plot-series-controls label {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: #f0f5f0;
  border: 1px solid #dce7dc;
  border-radius: 6px;
  padding: 5px 7px;
  cursor: pointer;
}
.plot-series-controls input {
  accent-color: var(--green);
  margin: 0;
}
.plot-series-controls input:disabled {
  cursor: not-allowed;
}
@media (max-width: 650px) {
  .plot-series-controls {
    align-items: flex-start;
  }}
.operator-view {
  display: none;
  padding: 24px;
  margin-bottom: 18px;
}
.operator-mode .operator-view {
  display: block;
}
/* The operator view is a playthrough of one day: the year-wide generalisation strip is the one
   section that is not about the day in front of you, so it stays out of it. */
.operator-mode .generalisation {
  display: none;
}
.operator-header {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  align-items: flex-start;
}
.operator-header h2 {
  font-size: 22px;
  letter-spacing: -0.8px;
  margin: 0 0 6px;
}
.operator-header p:last-child {
  color: var(--muted);
  font-size: 12px;
  margin: 0;
}
.operator-action {
  width: 100%;
  border: 0;
  border-radius: 7px;
  background: var(--ink);
  color: var(--lime);
  font: 800 11px inherit;
  padding: 9px;
  cursor: pointer;
}
.recommended .operator-action {
  background: var(--green);
  color: #fff;
}
@media (max-width: 900px) {
  .operator-header {
    display: block;
  }}

/* One colour swatch per policy, shared by the series legend and the charts, so the same policy
   always reads as the same colour wherever it appears. */
.series-swatch {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 2px;
  margin-right: 6px;
  vertical-align: middle;
}
.plot-series-controls label:has(input:disabled) {
  opacity: 0.5;
}
#agentComputeStatus {
  margin: 6px 0 0;
}
/* uPlot's live legend is the hover readout, and it sits below the canvas - the fixed height plus
   overflow:hidden on .mini-chart clipped it away on the two voltage charts (the loading chart had no
   overflow rule, which is why only it showed one). */

.u-legend {
  font-size: 11px;
}

/* --- Action detail panel (agent card) --------------------------------------------------------- */
/* "Explain" used to be an underlined link under the primary button, which read as a footnote - it is
   the second thing anybody wants here, so it is a button of the same weight sitting beside it. */
.agent-actions {
  display: grid;
  gap: 8px;
}
/* One pair of buttons, one geometry: same box, same centred label, differing only in weight of
   colour. Understanding the action is the filled control (`.primary-button`), dispatching it the
   outlined one - they were the other way round, with the filled label pushed to the edges, which
   read as two unrelated controls. Only the two departures from the base skins live here. */
.agent-actions .primary-button {
  justify-content: center;
  gap: 7px;
  white-space: nowrap;
}
.agent-actions .secondary-button span {
  font-size: 17px;
}
.secondary-button {
  width: 100%;
  text-align: center;
  border: 1px solid #b7e46d70;
  border-radius: 8px;
  background: #ffffff14;
  color: #e4f7c6;
  padding: 11px 13px;
  font-size: 12px;
  font-weight: 800;
  cursor: pointer;
  white-space: nowrap;
}
.secondary-button:hover {
  border-color: var(--lime);
  background: #b7e46d24;
  color: #fff;
}
/* --- The shared action explanation ------------------------------------------------------------- */
/* A dialog rather than an inline panel: the same explanation is opened from the agent card in the
   detailed day view and from an option card in the operator game, and neither has room to grow a
   full breakdown in place. Dark green, like the agent card it grew out of, so the per-injection rows
   below need no second palette. */
.explain-modal {
  width: min(720px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  overflow: auto;
  border: 0;
  border-radius: 16px;
  padding: 20px 22px 22px;
  background: linear-gradient(155deg, #173f38, #1c4a3f);
  color: #eaf5ee;
}
.explain-modal::backdrop {
  background: #0b1f1ba6;
}
.explain-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
}
.explain-head .eyebrow {
  color: #9fc9b2;
  margin-bottom: 4px;
}
.explain-head h3 {
  margin: 0;
  font-size: 19px;
  letter-spacing: -0.5px;
}
.explain-lede {
  margin: 6px 0 0;
  font-size: 11px;
  line-height: 1.55;
  color: #b9d5c6;
  max-width: 62ch;
}
.explain-close {
  border: 1px solid #ffffff2b;
  background: #ffffff14;
  color: #dff1e6;
  border-radius: 8px;
  width: 26px;
  height: 26px;
  font-size: 12px;
  cursor: pointer;
  flex: none;
}
.explain-close:hover {
  background: #ffffff2b;
}
.explain-refs {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 16px 0 4px;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: #9fc9b2;
}
.explain-refs > div {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
.explain-ref {
  border: 1px solid #ffffff2b;
  background: #ffffff10;
  color: #dff1e6;
  border-radius: 20px;
  padding: 5px 11px;
  font: 700 10px inherit;
  letter-spacing: 0.2px;
  text-transform: none;
  cursor: pointer;
}
.explain-ref:hover {
  background: #ffffff24;
}
.explain-ref.active {
  background: var(--lime);
  border-color: var(--lime);
  color: #16332b;
}
.explain-block {
  padding: 15px 0;
  border-top: 1px solid #ffffff1a;
}
.explain-block-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.explain-block-head h4 {
  margin: 0;
  font-size: 12px;
  letter-spacing: 0.2px;
  color: #fff;
}
.explain-block-head b {
  font-size: 15px;
  color: var(--lime);
  white-space: nowrap;
}
.explain-note {
  margin: 5px 0 0;
  font-size: 10.5px;
  line-height: 1.55;
  color: #a9c7ba;
  max-width: 72ch;
}
.explain-versus {
  margin: 9px 0 0;
  font-size: 11px;
  color: #cde3d6;
}
.explain-versus b {
  color: #fff;
}

/* The tap changer as a ladder of real positions: ticks are where it *can* stand, pins are where it
   stood, where it is going, and where the comparison would have put it. */
.tap-ladder {
  position: relative;
  height: 52px;
  /* Headroom for a lifted label when two pins land close together. */
  margin: 32px 6px 0;
}
.tap-rail {
  position: absolute;
  top: 21px;
  left: 0;
  right: 0;
  height: 3px;
  border-radius: 3px;
  background: #ffffff26;
}
.tap-tick {
  position: absolute;
  top: 16px;
  width: 1px;
  height: 13px;
  background: #ffffff33;
}
.tap-tick.is-neutral {
  top: 13px;
  height: 19px;
  width: 2px;
  background: #ffffff70;
}
.tap-pin {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  text-align: center;
  white-space: nowrap;
}
.tap-pin b {
  display: block;
  font-size: 11px;
  line-height: 1;
}
.tap-pin small {
  display: block;
  font-size: 8px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  margin-top: 2px;
}
.tap-pin::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 30px;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  transform: translateX(-50%);
}
.tap-pin.is-before {
  color: #9ab5a8;
}
.tap-pin.is-before::after {
  background: #16332b;
  border: 2px solid #9ab5a8;
}
/* Only the label moves - the dot has to stay on the position it marks. */
.tap-pin.is-lifted b,
.tap-pin.is-lifted small {
  transform: translateY(-15px);
}
.tap-pin.is-after {
  color: var(--lime);
  font-weight: 800;
}
/* Nothing moved: one pin says the changer was held, instead of "before" and "after" printed on top
   of each other. "Held" rather than "unchanged" - unchanged reads as a claim about the comparison,
   which is a different policy's call, not this action's own history. */
.tap-pin.is-held small {
  color: #9ab5a8;
}
.tap-pin.is-after::after {
  background: var(--lime);
  box-shadow: 0 0 0 4px #b7e46d2e;
}
/* The comparison sits below the rail, so it can never be mistaken for the action itself. */
.tap-pin.is-reference {
  top: 34px;
  color: #f0b8ad;
}
.tap-pin.is-reference::after {
  top: -12px;
  width: 9px;
  height: 9px;
  border-radius: 2px;
  background: #f0b8ad;
}

/* The ladder's end labels had no rule at all, so three spans ran together as "-15neutral 015". */
.tap-ends {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: #9fc9b2;
}

/* One bar per power setpoint: the track is the envelope, the fill is the action. */
.power-bar {
  position: relative;
  height: 22px;
  margin: 16px 0 0;
  border-radius: 6px;
  background: #ffffff14;
  border: 1px solid #ffffff1f;
}
.power-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  background: linear-gradient(180deg, #b7e46d, #8dc94b);
  border-radius: 5px;
}
.power-pin {
  position: absolute;
  top: -4px;
  bottom: -4px;
  width: 2px;
  transform: translateX(-50%);
}
.power-pin.is-before {
  background: #eaf5ee;
}
.power-pin.is-reference {
  background: #f0b8ad;
  width: 3px;
}
/* Before and the comparison at the same value: one striped pin, both colours. */
.power-pin.is-both {
  width: 3px;
  background: repeating-linear-gradient(180deg, #eaf5ee 0 4px, #f0b8ad 4px 8px);
}
.power-ends {
  display: flex;
  justify-content: space-between;
  margin-top: 5px;
  font-size: 9px;
  font-weight: 700;
  color: #9fc9b2;
}
.explain-sgens {
  border-top: 1px solid #ffffff1a;
  padding-top: 12px;
}
/* Explain and Preview share a row, dispatch is the one full-width action below them - it is the only
   button of the three after which the position on screen is actually gone. */
.game-option-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 7px;
  /* Bottom-aligned within the card, so a longer description above cannot push this card's buttons out
     of line with its neighbours'. */
  margin-top: auto;
}
.game-option-actions .operator-action { grid-column: 1 / -1; }
/* Option cards are light, so the same button needs the light-ground palette. */
.game-option-actions .secondary-button {
  border-color: #bcd6c1;
  background: #fff;
  color: var(--green);
  padding: 9px 11px;
  font-size: 11px;
}
.game-option-actions .secondary-button:hover {
  border-color: var(--green);
  background: #f4faf5;
  color: var(--green);
}
/* The card currently painted onto the map beside it - so the eye can find which one without rereading
   every button's label. */
.preview-button.is-active {
  border-color: var(--green);
  background: var(--green);
  color: #fff;
}
.preview-button.is-active:hover { color: #fff; }
.sgen-toggle {
  margin: 8px 0 0;
  color: var(--lime);
  text-align: left;
  text-decoration: underline;
}
.sgen-list {
  max-height: 260px;
  overflow: auto;
  margin-top: 8px;
  display: grid;
  gap: 5px;
}
.sgen-row {
  display: grid;
  grid-template-columns: auto 1fr 1fr auto;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  border: 1px solid #ffffff18;
  background: #ffffff0c;
  border-radius: 8px;
  padding: 7px 8px;
  color: #e6f2ea;
  font: inherit;
  font-size: 10px;
  cursor: pointer;
}
.sgen-row:hover {
  background: #ffffff1c;
  border-color: var(--lime);
}
.sgen-row.changed {
  border-left: 3px solid var(--lime);
}
.sgen-id {
  font-weight: 800;
  font-size: 11px;
}
.sgen-id small {
  display: block;
  color: #a9c7ba;
  font-size: 9px;
  font-weight: 600;
}
.sgen-values small {
  color: #a9c7ba;
  font-size: 9px;
}
.sgen-values b {
  display: block;
  font-size: 10px;
}
.sgen-values i {
  font-style: normal;
  color: var(--lime);
  font-size: 9px;
}
.sgen-curtail {
  font-weight: 800;
  font-size: 11px;
  text-align: right;
}
.sgen-curtail small {
  display: block;
  color: #a9c7ba;
  font-size: 8px;
  font-weight: 600;
}

/* --- Clickable violation list ----------------------------------------------------------------- */
.issue-item {
  transition: background 0.15s;
}
.issue-item:hover {
  background: #f4f8f4;
}
.issue-item.is-focused {
  background: #eef6ef;
  box-shadow: inset 3px 0 0 var(--green);
}
.operator-action.ghost {
  margin-top: 12px;
  background: #fff;
  color: var(--green);
  border: 1px solid #cfe4d2;
}
.operator-option.chosen .operator-action {
  background: var(--green);
  color: #fff;
}
/* Keep outlines a constant on-screen width when the map is zoomed to a focused element - without
   this a 4-unit stroke renders 4x thicker at 4x zoom and swallows the marker it outlines. */
.real-node,
.real-line,
.generator-marker,
.load-marker,
.transformer-marker {
  vector-effect: non-scaling-stroke;
}
.generalisation {
  padding: 21px 23px 18px;
  margin-bottom: 18px;
}
.generalisation-header {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  align-items: flex-start;
}
.generalisation-header h2 {
  font-size: 19px;
  letter-spacing: -0.7px;
  margin: 0 0 6px;
}
.generalisation-header p:last-child {
  color: var(--muted);
  font-size: 12px;
  margin: 0;
  max-width: 70ch;
}
.day-bars {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
  gap: 10px;
  margin: 20px 0 12px;
}
.day-bar {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  border: 1px solid var(--line);
  border-radius: 11px;
  background: #fff;
  padding: 12px 8px 10px;
  font: inherit;
  cursor: pointer;
  transition: 0.15s;
}
.day-bar:hover:not(:disabled) {
  border-color: #bcd6c1;
  background: #f6faf6;
}
.day-bar.is-current {
  border-color: var(--green);
  box-shadow: 0 0 0 2px #17845d22;
}
.day-bar.is-pending {
  cursor: default;
  opacity: 0.65;
}
.day-bar.is-failed {
  border-color: #f0c4bc;
  background: #fff7f5;
}
.day-bar-track {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 6px;
  height: 92px;
  width: 100%;
  border-bottom: 1px solid var(--line);
}
/* min-height keeps a zero-valued bar visible as a stub on the axis: an agent that removes every
   violation otherwise draws nothing at all, which reads as a rendering failure rather than as the
   result it actually is. */
.day-bar-track .bar {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  width: 14px;
  min-height: 3px;
  border-radius: 3px 3px 0 0;
  overflow: hidden;
}
.day-bar-track .bar b {
  display: block;
  width: 100%;
}
/* Colour says which policy the bar belongs to, not how it did: with three columns per day the
   outcome is already legible from the heights standing next to each other. The solid inner segment
   is the critical part of each policy's own total. */
.day-bar-track .bar.noop {
  background: #f6b0a6;
}
.day-bar-track .bar.noop b {
  background: var(--red);
}
.day-bar-track .bar.controller {
  background: #f7d79a;
}
.day-bar-track .bar.controller b {
  background: #d98c14;
}
.day-bar-track .bar.agent {
  background: #9fdcbb;
}
.day-bar-track .bar.agent b {
  background: var(--green);
}
/* A column whose rollout has not finished yet: held open at full height as a dashed outline, so the
   day does not silently redraw one bar wider once the last policy lands. */
.day-bar-track .bar.is-pending {
  align-self: stretch;
  background: none;
  border: 1px dashed var(--line);
  border-bottom: 0;
  border-radius: 3px 3px 0 0;
}
.day-bar-value {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: 3px;
  font-size: 10px;
  font-weight: 750;
  color: var(--red);
}
.day-bar-value em {
  font-style: normal;
  color: var(--muted);
  font-size: 10px;
}
.day-bar-label {
  font-size: 10px;
  font-weight: 700;
  color: var(--muted);
}
.day-bar.is-current .day-bar-label {
  color: var(--green);
}
.generalisation-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  font-size: 10px;
  font-weight: 700;
  color: var(--muted);
  margin: 0 0 10px;
}
.generalisation-hint {
  color: #9aa79f;
  font-weight: 600;
}
.generalisation-totals {
  font-size: 12px;
  color: var(--muted);
  line-height: 1.6;
  margin: 0;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}
.generalisation-totals b {
  color: var(--ink);
}
@media (max-width: 650px) {
  .generalisation-header {
    display: block;
  }
  .day-bars {
    grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  }}

.plan-computing {
  display: block;
  margin: 0;
  padding: 12px 16px;
  border: 1px dashed #bcd6c1;
  border-radius: 12px;
  background: #f3f9f4;
  color: var(--green);
  font-size: 12px;
  font-weight: 750;
}
.plan-computing-text {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.plan-computing-text::before {
  content: '';
  width: 12px;
  height: 12px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: mf-spin 0.65s linear infinite;
  flex: none;
}
.plan-computing-rows {
  margin-top: 9px;
  display: grid;
  gap: 7px;
}
.plan-computing-row {
  display: grid;
  grid-template-columns: minmax(90px, 26%) 1fr auto;
  align-items: center;
  gap: 10px;
  font-size: 11px;
  font-weight: 700;
}
.plan-computing-row-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.plan-computing-row-eta {
  color: var(--muted);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  min-width: 5ch;
  text-align: right;
}
.plan-computing-row.is-done .plan-computing-row-eta::before {
  content: '✓ ';
  color: var(--green);
}
.operator-mode .agent-strip-plan {
  display: none;
}
.plan-header {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  align-items: flex-start;
  flex-wrap: wrap;
}
.plan-header h3 {
  font-size: 15px;
  letter-spacing: -0.4px;
  margin: 0 0 5px;
}
.plan-header .eyebrow span {
  color: var(--green);
}
.plan-header p:last-child {
  color: var(--muted);
  font-size: 12px;
  margin: 0;
  max-width: 70ch;
}
.plan-downloads {
  text-align: right;
}
.plan-download-label {
  display: block;
  font-size: 9px;
  letter-spacing: 1.1px;
  font-weight: 800;
  color: #738078;
  margin-bottom: 7px;
}
.plan-downloads div {
  display: flex;
  gap: 8px;
}
.plan-download {
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 9px 13px;
  background: #fff;
  color: var(--green);
  font-size: 12px;
  font-weight: 750;
  text-decoration: none;
}
.plan-download:hover {
  border-color: #bcd6c1;
  background: #f4faf5;
}
.plan-charts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 16px;
  margin: 20px 0 4px;
}
.plan-charts article {
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 13px 14px 10px;
  min-width: 0;
}
.plan-chart-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
}
.plan-chart-head h4 {
  font-size: 12px;
  letter-spacing: -0.2px;
  margin: 0;
}
.plan-chart-head span {
  flex: 1;
  min-width: 0;
  font-size: 10px;
  font-weight: 700;
  color: var(--muted);
  text-align: right;
}
.plan-chart {
  height: auto;
  margin-top: 8px;
  overflow: visible;
}
.plan-note {
  color: var(--muted);
  font-size: 11px;
  line-height: 1.6;
  margin: 12px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}
@media (max-width: 650px) {
  .plan-header {
    display: block;
  }
  .plan-downloads {
    text-align: left;
    margin-top: 14px;
  }}

/* Reserved-space loading state: a section that is waiting for data keeps its real layout and
   shimmers where the numbers will be, so nothing below it is pushed down when they arrive. */
@keyframes mf-shimmer {
  0% {
    background-position: -260px 0;
  }
  100% {
    background-position: 260px 0;
  }
}
.is-loading .outcome-item strong,
.is-loading .outcome-item small,
.is-loading .agent-hero strong,
.is-loading .agent-hero small,
.is-loading .agent-hero em,
.is-loading .generalisation-totals {
  color: transparent !important;
  border-radius: 5px;
  background-image: linear-gradient(90deg, #ffffff10 20%, #ffffff2b 50%, #ffffff10 80%);
  background-size: 260px 100%;
  animation: mf-shimmer 1.15s linear infinite;
}
.is-loading .outcome-item strong {
  width: 62%;
}
.is-loading .outcome-item small {
  width: 86%;
}
.is-loading .agent-hero strong {
  width: 70%;
}
.is-loading .agent-hero small {
  width: 88%;
}
.is-loading .agent-hero em {
  width: 94%;
}
.generalisation.is-loading .generalisation-totals {
  background-image: linear-gradient(90deg, #1823230d 20%, #1823231f 50%, #1823230d 80%);
  width: 70%;
  display: block;
}
.day-bar.is-pending .day-bar-track {
  background-image: linear-gradient(90deg, #1823230a 20%, #1823231c 50%, #1823230a 80%);
  background-size: 260px 100%;
  animation: mf-shimmer 1.15s linear infinite;
  border-radius: 5px 5px 0 0;
}
.day-bar.is-pending .day-bar-value {
  color: #aab4ad;
}

/* "At <time> · showing <policy>": the KPI row, map and violation list below are one instant of one
   policy, which their numbers alone never said. */

.worst-moment {
  width: auto !important;
  padding: 0 10px;
  font-size: 11px;
  font-weight: 750;
  color: var(--red) !important;
  border-color: #f0c4bc !important;
  white-space: nowrap;
}
.worst-moment:hover {
  background: #fff6f4 !important;
}
.plan-link {
  color: #cfe9d8;
  font-size: 11px;
  text-align: center;
  /* Sits directly above the agent-action buttons; without this it reads as their caption. */
  margin: 12px 0;
  padding: 6px 0;
}
.plan-link b {
  color: var(--lime);
}
.timeline-panel {
  margin-top: 0;
  margin-bottom: 18px;
}
.action-preview .impact.is-quiet {
  color: #9fb8ac;
  font-weight: 700;
}

.is-loading .agent-strip-plan {
  opacity: 0.55;
}
.is-loading .plan-chart {
  min-height: 150px;
}

.amber-text {
  color: #b07d10;
}
.swatch-noop {
  background: #f6b0a6;
}
.swatch-agent {
  background: var(--green);
}
.swatch-controller {
  background: #f0c14e;
}
.swatch-split {
  background: linear-gradient(180deg, #c9cfcb 50%, #4a5450 50%);
}

/* The operator view stays listed so its place in the product is visible, but it is not
   reachable yet. A disabled button dispatches no click, so the view-switcher handler needs
   no special case. */

/* Legend entries are layer toggles: a 1.5k-bus feeder covered in load squares and injection
   triangles hides the buses underneath, so each can be switched off to see the rest. */
.map-legend button {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--line);
  background: #fff;
  border-radius: 7px;
  padding: 5px 9px;
  font: inherit;
  font-size: 11px;
  font-weight: 650;
  color: var(--muted);
  cursor: pointer;
  transition: 0.12s;
}
.map-legend button:hover {
  border-color: #bcd6c1;
  background: #f6faf6;
}
.map-legend button.is-off {
  opacity: 0.45;
  background: #f2f5f2;
  text-decoration: line-through;
}
/* The action layer with no policy on the map: the entry stays where it is so it is still findable,
   but says there is nothing to draw by refusing the click rather than by drawing an empty overlay. */
.map-legend button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  text-decoration: none;
}
.map-legend button:disabled:hover {
  border-color: var(--line);
  background: #fff;
}
.map-legend button i {
  margin-right: 6px;
}
.legend-trafo {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 2px solid #7b8f85;
  display: inline-block;
}
.map-legend-key {
  display: inline-flex;
  align-items: center;
  color: #9aa79f;
  font-size: 10px;
  font-weight: 600;
}
.plan-figure {
  display: flex;
  align-items: baseline;
  gap: 7px;
  margin: 10px 0 0;
  padding-top: 9px;
  border-top: 1px solid var(--line);
}
.plan-figure b {
  font-size: 19px;
  letter-spacing: -0.6px;
  color: var(--ink);
}
.plan-figure small {
  font-size: 10px;
  color: var(--muted);
  font-weight: 650;
}


/* Policy selector, promoted out of the map card. It reads as a sentence - "At 10:45 the grid below
   is showing [ NoOp baseline | Cost-minimizing agent ]" - so it states where you are and changes it
   in the same control, directly above everything it governs. */
.live-context {
  display: flex;
  align-items: center;
  gap: 11px;
  flex-wrap: wrap;
  margin: 0 0 12px;
}
.live-context-label {
  font-size: 12px;
  color: var(--muted);
  font-weight: 600;
}
.live-context-label b {
  color: var(--ink);
  font-weight: 750;
}
.live-context-hint {
  font-size: 11px;
  color: #9aa79f;
  font-weight: 600;
}
.policy-switch {
  display: inline-flex;
  background: #e8eee8;
  padding: 3px;
  border-radius: 9px;
  gap: 2px;
}
.policy-switch button {
  border: 0;
  background: transparent;
  color: #65736a;
  border-radius: 7px;
  padding: 7px 12px;
  font: inherit;
  font-size: 12px;
  font-weight: 750;
  cursor: pointer;
  white-space: nowrap;
}
.policy-switch button:hover:not(:disabled) {
  color: var(--ink);
}
.policy-switch button:disabled {
  opacity: 0.45;
  cursor: default;
}
.policy-switch button.active {
  background: #fff;
  color: var(--ink);
  box-shadow: 0 1px 3px #193d2d1a;
}
.policy-switch button#viewNoop.active {
  color: #b64538;
}
.policy-switch button.active:not(#viewNoop) {
  color: var(--green);
}
.policy-switch button.is-failed {
  color: var(--red);
  opacity: 1;
  cursor: pointer;
}
.map-policy-badge {
  display: inline-flex;
  align-items: center;
  border-radius: 7px;
  background: #fdece9;
  color: #b64538;
  padding: 7px 10px;
  font-size: 11px;
  font-weight: 750;
}
.map-policy-badge.is-agent {
  background: #e7f6eb;
  color: #22734c;
}

/* Enlarge one chart in place: the card takes the whole row and the plot grows with it. In place
   rather than in a modal, so an expanded chart stays next to the rest of the plan. */
.chart-expand {
  margin-left: 8px;
  border: 1px solid var(--line);
  background: #fff;
  border-radius: 6px;
  width: 23px;
  height: 23px;
  line-height: 1;
  padding: 0;
  color: var(--muted);
  font-size: 12px;
  cursor: pointer;
  flex: none;
}
.chart-expand:hover {
  border-color: #bcd6c1;
  background: #f4faf5;
  color: var(--green);
}
.plan-charts article.is-expanded {
  grid-column: 1/-1;
}
.plan-charts article.is-expanded .chart-expand {
  border-color: #bcd6c1;
  color: var(--green);
}

/* The per-policy checkboxes are the selection the charts, scorecards and the operator view all read
   (see plottedSeries in app.js), but nothing exposes them for editing any more - every policy is
   shown - so the legend itself stays out of the page. */
.plot-series-controls {
  display: none;
}

/* ---- Which section belongs to which tab -------------------------------------------------------
   Stated once and positively: a section a tab does not own is display:none, and each tab switches
   on exactly what it shows. The rules used to be the other way round - each new view had to
   remember to hide every section it did not want - which is how a view could inherit a panel nobody
   meant it to have.

   "Day-ahead plan" is the executive summary of the whole day (baseline -> rule-based control ->
   agent plan); "Detailed day view" is that same day inspected instant by instant. The
   generalisation strip is in neither list: it is deliberately shown in both, unchanged, so the
   spread of days across the year reads the same wherever you are. */
.day-outcome,
.timeline-panel,
.live-context,
.kpi-row,
.workspace {
  display: none;
}
.plan-mode .day-outcome {
  display: grid;
}
.detail-mode .timeline-panel {
  display: block;
}
.detail-mode .live-context {
  display: flex;
}
.detail-mode .kpi-row,
.detail-mode .workspace {
  display: grid;
}

/* Notes and arrows are positioned from their live targets in app.js. */
.guide-overlay { position: fixed; inset: 0; z-index: 100; color: var(--ink); }
.guide-overlay[hidden] { display: none; }
.guide-backdrop { position: absolute; inset: 0; background: rgb(24 35 29 / 0.42); backdrop-filter: blur(1px); }
.guide-heading, .guide-close, .guide-note { position: fixed; z-index: 2; background: #fff; border: 1px solid var(--line); box-shadow: 0 8px 28px #173e3730; }
.guide-heading { top: 18px; left: 50%; width: min(390px, calc(100vw - 32px)); padding: 13px 18px; border-radius: 13px; transform: translateX(-50%); text-align: center; }
.guide-heading .eyebrow { margin-bottom: 4px; }
.guide-heading h2 { margin: 0 0 4px; font-size: 18px; letter-spacing: -0.5px; }
.guide-heading p:last-child { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.35; }
.guide-close { top: 142px; left: 50%; border-radius: 8px; transform: translateX(-50%); padding: 8px 12px; color: #fff; background: var(--navy); border-color: var(--navy); font: 750 12px inherit; cursor: pointer; }
.guide-note { width: min(270px, calc(100vw - 32px)); padding: 13px 14px; border-radius: 12px; font-size: 12px; line-height: 1.42; }
.guide-note strong { display: block; margin-bottom: 4px; color: var(--green); font-size: 13px; }
.guide-note p { margin: 0; color: #425248; }
.guide-connector { position: fixed; z-index: 1; height: 2px; background: var(--lime); transform-origin: left center; box-shadow: 0 0 0 2px #173e3730; }
.guide-connector::after { content: ''; position: absolute; right: -1px; top: -5px; border-top: 6px solid transparent; border-bottom: 6px solid transparent; border-left: 9px solid var(--lime); }
/* Targets stay in the page layer. Raising them above .guide-overlay also raised them above their
   guide notes, so wide targets such as the operator map and alert hid the callout that explained
   them. The guide itself already sits above the dimmed page, and its notes/connectors must win that
   stacking order. */
.guide-target { position: relative; z-index: auto !important; outline: 3px solid var(--lime); outline-offset: 4px; border-radius: 5px; }
@media (max-width: 900px) {
  .guide-overlay { overflow: auto; padding: 185px 16px 22px; }
  .guide-heading, .guide-close { position: absolute; }
  .guide-notes { position: relative; z-index: 2; display: grid; gap: 10px; }
  .guide-note { position: static !important; width: 100%; }
  .guide-connectors { display: none; }
  .guide-target { outline: 0; }
}
/* The outcome charts are collapsed by default: the plan is what the agent does, and six charts open
   at once buried it. */
.plan-collapse {
  margin-top: 14px;
  border-top: 1px solid var(--line);
  padding-top: 12px;
}
.plan-collapse > summary {
  display: flex;
  align-items: baseline;
  gap: 9px;
  cursor: pointer;
  list-style: none;
  font-size: 13px;
  font-weight: 750;
  color: var(--ink);
  padding: 4px 0;
}
.plan-collapse > summary::-webkit-details-marker {
  display: none;
}
.plan-collapse > summary::before {
  content: '▸';
  color: var(--green);
  font-size: 11px;
  transition: transform 0.15s;
}
.plan-collapse[open] > summary::before {
  content: '▾';
}
.plan-collapse > summary:hover {
  color: var(--green);
}
.plan-collapse > summary span {
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
}
.plan-collapse .plan-charts {
  margin-top: 14px;
}

/* Element inspector: clicking a bus or line reports its whole day, not one instant. */
.element-card {
  width: 272px;
  text-align: left;
}
.element-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 8px;
}
.element-head strong {
  display: block;
  font-size: 13px;
}
.element-head small {
  display: block;
  font-size: 10px;
  font-weight: 700;
  margin-top: 2px;
}
.element-head small.status-normal {
  color: var(--green);
}
.element-head small.status-warning {
  color: #b07d10;
}
.element-head small.status-critical {
  color: var(--red);
}
.element-close {
  border: 0;
  background: transparent;
  color: var(--muted);
  font-size: 16px;
  line-height: 1;
  padding: 0 2px;
  cursor: pointer;
}
.element-close:hover {
  color: var(--ink);
}
.element-spark {
  display: block;
  width: 100%;
  height: 58px;
  margin: 9px 0 4px;
  overflow: visible;
}
.element-spark path {
  fill: none;
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
}
.spark-noop {
  stroke: var(--red);
}
.spark-agent {
  stroke: var(--green);
}
.spark-limit {
  stroke: #c3ccc6;
  stroke-width: 1;
  stroke-dasharray: 3 3;
  vector-effect: non-scaling-stroke;
}
.spark-now {
  stroke: #17845d;
  stroke-width: 1;
  stroke-dasharray: 3 2;
  vector-effect: non-scaling-stroke;
}
.element-stats {
  width: 100%;
  border-collapse: collapse;
  margin-top: 6px;
}
.element-stats th {
  text-align: left;
  font-size: 10px;
  font-weight: 650;
  color: var(--muted);
  padding: 2px 0;
}
.element-stats td {
  text-align: right;
  font-size: 10px;
  font-weight: 750;
  padding: 2px 0 2px 8px;
  white-space: nowrap;
}
.element-stats tr:first-child td {
  font-size: 9px;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.4px;
}
.element-stats .agent-col {
  color: var(--green);
}
.element-foot {
  font-size: 9px;
  color: #9aa79f;
  margin: 7px 0 0;
  line-height: 1.4;
}
.map-callout:has(.element-card) {
  padding: 12px 13px;
  max-width: 300px;
}
.agent-hero .hero-second {
  color: #cfe9d8;
  font-size: 11px;
  font-weight: 650;
  margin-top: 5px;
}
/* The controller rung carries the same qualifying line, in its own palette - it is the rung the
   split matters most on, since a rule-based scheme routinely settles the critical violations and
   leaves the operational ones behind. */
.controller-hero .hero-second {
  color: #f5cf9a;
  font-size: 10px;
  font-weight: 650;
  margin-top: 5px;
}
.agent-hero .hero-second.is-partial,
.controller-hero .hero-second.is-partial {
  color: #ffd66e;
}

/* Boot screen: shown until the first day exists, and doubling as the opening "why" screen.
   Reopened later from the topbar, where `is-revisit` drops the progress block and the enter button
   in favour of a close cross. Scrolls rather than centres, because nine cards plus the lede do not
   fit a laptop viewport and a centred flex child clips off the top when it overflows. */
.boot {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  overflow-y: auto;
  background: var(--paper);
  transition: opacity 0.45s ease;
}
.boot[hidden] {
  display: none;
}
.boot.is-done {
  opacity: 0;
  pointer-events: none;
}
.boot-inner {
  position: relative;
  text-align: center;
  max-width: 1180px;
  margin: auto;
  padding: 44px 32px 56px;
}
.boot-inner img {
  height: 110px;
  width: auto;
  display: block;
  margin: 0 auto;
}
.boot-name {
  font-weight: 780;
  font-size: 40px;
  letter-spacing: -1.1px;
  color: var(--ink);
  margin: -12px 0 26px;
}

/* --- Opening screen: the case for any of this --- */

.splash-lede {
  margin: 0 0 34px;
}
.splash-line {
  max-width: 720px;
  margin: 0 auto 26px;
  font-size: 15px;
  line-height: 1.6;
  color: var(--muted);
}
.splash-compare {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px 46px;
  text-align: left;
}
.cmp-head {
  display: flex;
  align-items: baseline;
  gap: 9px;
  flex-wrap: wrap;
  margin: 0 0 2px;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 1.1px;
  text-transform: uppercase;
}
.cmp-head span {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.3px;
  text-transform: none;
  color: var(--muted);
}
.cmp-cell {
  border-left: 3px solid;
  padding: 2px 0 2px 15px;
}
.cmp-cell b {
  display: block;
  font-size: 14.5px;
  font-weight: 780;
  line-height: 1.35;
}
.cmp-cell span {
  display: block;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
  margin-top: 3px;
}
.splash-compare .is-problem {
  color: var(--red);
  border-color: var(--red);
}
.splash-compare .is-answer {
  color: var(--green);
  border-color: var(--green);
}
/* The two sides are a pair, so they face each other across the page's centre line rather than both
   running left: the problem column is mirrored, text and rule on its inner edge. Aligned the same
   way, "Today" sat at the far left margin while "With maze-flex" sat near the middle, and the block
   read as two unrelated lists that happened to be side by side. */
.splash-compare .is-problem {
  text-align: right;
}
.cmp-head.is-problem {
  justify-content: flex-end;
}
.cmp-cell.is-problem {
  border-left: 0;
  border-right: 3px solid var(--red);
  padding: 2px 15px 2px 0;
}
.splash-lede h1 {
  font-size: 34px;
  line-height: 1.14;
  letter-spacing: -1.2px;
  font-weight: 800;
  color: var(--navy);
  margin: 0 0 12px;
}
.splash-groups {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
  text-align: left;
}
.splash-group {
  display: flex;
  flex-direction: column;
  gap: 9px;
}
/* The group heading carries the only colour in the column, so the three claims read apart at a
   glance without every card having to be tinted. */
.splash-group h2 {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  margin: 0 0 2px;
  color: var(--muted);
}
.splash-group h2::before {
  content: '';
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: none;
  background: currentColor;
}
.splash-group.is-critical h2 {
  color: var(--red);
}
.splash-group.is-warning h2 {
  color: var(--orange);
}
.splash-group.is-curtail h2 {
  color: var(--green);
}
.news-card {
  display: block;
  flex: 1;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  padding: 14px 15px 12px;
  text-decoration: none;
  color: inherit;
  transition:
    border-color 0.16s ease,
    transform 0.16s ease,
    box-shadow 0.16s ease;
}
.news-card:hover {
  border-color: #bcd6c1;
  transform: translateY(-2px);
  box-shadow: 0 8px 22px rgba(23, 62, 55, 0.09);
}
.news-figure {
  display: block;
  font-size: 25px;
  font-weight: 800;
  letter-spacing: -1px;
  line-height: 1.1;
  color: var(--navy);
  font-variant-numeric: tabular-nums;
}
.news-head {
  display: block;
  font-size: 12.5px;
  line-height: 1.48;
  color: var(--ink);
  margin-top: 6px;
}
.news-head em {
  font-style: normal;
  font-weight: 750;
  color: var(--green);
}
/* The outlet and date are the card's credibility, but not its content - kept quiet, with the
   external-link arrow attached so it is obvious the card leaves the page. */
.news-meta {
  display: block;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 9px;
}
.news-meta::after {
  content: ' ↗';
}
.splash-kicker {
  max-width: 700px;
  margin: 30px auto 0;
  font-size: 14px;
  line-height: 1.6;
  color: var(--muted);
}
.splash-kicker b {
  color: var(--ink);
}
.splash-foot {
  margin-top: 26px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}
.splash-credit {
  margin: 22px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
}
.splash-credit a {
  color: var(--ink);
  font-weight: 600;
}
.splash-enter {
  border: 0;
  border-radius: 10px;
  background: var(--green);
  color: #fff;
  padding: 13px 30px;
  font: 780 14px inherit;
  cursor: pointer;
  transition:
    opacity 0.2s ease,
    background 0.16s ease;
}
.splash-enter:hover:not(:disabled) {
  background: #126d4d;
}
/* Disabled rather than hidden: the button being visible-but-waiting is what tells you the wait has
   an end, which a bare progress bar never did. */
.splash-enter:disabled {
  opacity: 0.4;
  cursor: default;
}
.splash-close {
  position: absolute;
  top: 20px;
  right: 24px;
  width: 34px;
  height: 34px;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--panel);
  color: var(--muted);
  font-size: 14px;
  cursor: pointer;
}
.splash-close:hover {
  border-color: #bcd6c1;
  color: var(--ink);
}
/* A reload inside the same browser session: the argument has been read, so only the loading cover
   is left - logo, status line and progress bar - and it lifts itself the moment the day lands. The
   retry button deliberately survives, because a reload can still fail to compute its day. */
.boot.is-quiet .splash-lede,
.boot.is-quiet .splash-groups,
.boot.is-quiet .splash-kicker,
.boot.is-quiet .splash-credit,
.boot.is-quiet .splash-enter {
  display: none;
}
.boot.is-quiet .splash-foot {
  margin-top: 8px;
}

/* Revisited from the topbar there is nothing to wait for: the day is already loaded. */
.boot.is-revisit .boot-progress,
.boot.is-revisit .splash-enter,
.boot.is-revisit .boot-retry {
  display: none;
}
.boot.is-ready .boot-status::before {
  animation: none;
  border-right-color: currentColor;
}
@media (max-width: 1000px) {
  .splash-groups,
  .splash-compare {
    grid-template-columns: 1fr;
  }
  .cmp-head {
    display: none;
  }
  /* Stacked, there is no centre line to mirror across: both sides run left again. */
  .splash-compare .is-problem {
    text-align: left;
  }
  .cmp-cell.is-problem {
    border-left: 3px solid var(--red);
    border-right: 0;
    padding: 2px 0 2px 15px;
  }
  .cmp-cell::before {
    content: attr(data-label);
    display: block;
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 1px;
    text-transform: uppercase;
    margin-bottom: 3px;
  }
  .splash-lede h1 {
    font-size: 27px;
  }
}
.boot-status {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-size: 14px;
  font-weight: 750;
  color: var(--green);
  margin: 0;
}
.boot-status::before {
  content: '';
  width: 13px;
  height: 13px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: mf-spin 0.65s linear infinite;
  flex: none;
}
.boot.is-failed .boot-status {
  color: var(--red);
}
.boot.is-failed .boot-status::before {
  animation: none;
  border-right-color: currentColor;
  opacity: 0.35;
}
.boot-note {
  font-size: 11px;
  color: var(--muted);
  line-height: 1.6;
  margin: 14px 0 0;
}
.boot-retry {
  margin-top: 20px;
  border: 1px solid var(--line);
  background: #fff;
  border-radius: 8px;
  padding: 9px 16px;
  font: 750 12px inherit;
  color: var(--green);
  cursor: pointer;
}
.boot-retry:hover {
  border-color: #bcd6c1;
  background: #f4faf5;
}

/* Progress for work that runs server-side for tens of seconds: an indeterminate spinner cannot tell
   "still going" from "hung", and a day rollout is long enough for that to matter. */
.progress-track {
  position: relative;
  height: 5px;
  border-radius: 99px;
  background: #dfe7e0;
  overflow: hidden;
}
.progress-track span {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  border-radius: inherit;
  background: linear-gradient(90deg, #3fae7f, var(--green));
  transition: width 0.25s ease;
}
.boot-inner .progress-track {
  width: 260px;
  margin: 16px auto 0;
}
.plan-computing .progress-track {
  margin-top: 9px;
}
.progress-track.is-idle span {
  transition: none;
}
.progress-track.is-indeterminate span {
  background: linear-gradient(90deg, transparent, var(--green), transparent);
  background-size: 40% 100%;
  background-repeat: no-repeat;
  animation: mf-sweep 1.15s ease-in-out infinite;
  transition: none;
}
@keyframes mf-sweep {
  0% {
    background-position: -40% 0;
  }
  100% {
    background-position: 140% 0;
  }
}

/* Operator game: the day stops when the grid needs a decision, and every choice is priced. */
.operator-restart {
  border: 1px solid var(--line);
  background: #fff;
  border-radius: 8px;
  padding: 9px 13px;
  font: 750 12px inherit;
  color: var(--green);
  cursor: pointer;
  white-space: nowrap;
}
.operator-restart:hover { border-color: #bcd6c1; background: #f4faf5; }
.game-clock { display: flex; align-items: center; gap: 14px; margin: 20px 0 14px; }
.game-time { font-size: 24px; font-weight: 780; letter-spacing: -1px; min-width: 76px; }
.game-clock .progress-track { flex: 1; }
.game-decisions { font-size: 11px; font-weight: 700; color: var(--muted); white-space: nowrap; }
/* The day is a known handful of decisions, and saying which one this is tells anyone watching how long
   the demo has left to run. */
.game-handovers {
  font: 750 11px inherit; white-space: nowrap;
  border: 1px solid #cfe3d4; border-radius: 20px; padding: 4px 10px;
  background: #f4faf5; color: var(--green);
}
/* Said once per card, under the outcome it qualifies: the figures above are the first step only, and a
   handed-over policy keeps deciding after it. */
.game-option-mode { font-size: 10px; font-weight: 700; color: var(--muted); margin: -6px 0 12px; }
/* Sits between the timeline and the option cards, so it needs air above it as well as below: it no
   longer follows the clock at the top of the view but a full-width chart panel. */
.game-alert {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; margin: 18px 0 12px;
  background: #fff5ee; border: 1px solid #f6d9cc; border-radius: 10px;
}
.game-alert strong, .game-alert small { display: block; }
.game-alert strong { font-size: 12px; }
.game-alert small { font-size: 10px; color: var(--muted); margin-top: 2px; }
.game-options { display: grid; grid-template-columns: repeat(auto-fit, minmax(215px, 1fr)); gap: 12px; }
/* A column with the buttons pinned to the bottom, so cards whose text wraps to a different number of
   lines still line their buttons up with each other - the grid already stretches every card to the
   tallest one, which on its own just left the short cards with their buttons floating mid-card. */
.game-option { border: 1px solid var(--line); border-radius: 12px; padding: 14px; background: #fff; display: flex; flex-direction: column; }
.game-option.is-clear { border-color: #bddfc4; background: #f4faf5; }
/* The policy already running the grid. Marked as the incumbent rather than as one more thing to
   dispatch: after a hand-over the real question at a stop is whether to let it carry on, and the card
   that answers "yes" should be the one the eye lands on first. */
.game-option.is-incumbent { border-color: var(--green); box-shadow: inset 3px 0 0 var(--green); }
.game-option.is-incumbent .eyebrow { color: var(--green); }
.game-option h3 { font-size: 14px; letter-spacing: -.3px; margin: 0 0 9px; }
.game-outcome { display: flex; flex-direction: column; gap: 3px; margin-bottom: 12px; font-size: 11px; color: var(--muted); }
.game-outcome b { font-size: 12px; }
/* All three limits are listed in a fixed order; which one is in trouble is said in colour alone
   (.outcome-clear / -operational / -critical), so the rows themselves stay uniform. */
.outcome-reading { opacity: 0.85; }
.outcome-clear { color: var(--green); }
.outcome-operational { color: #b07d10; }
.outcome-critical { color: var(--red); }
/* The operator's own action sits below four ready-made options, and as a hairline rule with a small
   label it read as a footnote to them - so the one choice the game is actually about was the easiest
   one to miss. Carded and accented to sit level with the option cards, without shouting over them. */
.game-custom {
  margin-top: 16px;
  border: 1px solid #cfe3d4;
  border-left: 3px solid var(--green);
  border-radius: 12px;
  background: #f7fbf8;
  padding: 4px 14px;
}
.game-custom > summary { display: flex; align-items: baseline; gap: 9px; cursor: pointer; list-style: none; font-size: 14px; font-weight: 800; letter-spacing: -.2px; padding: 10px 0; }
.game-custom > summary:hover { color: var(--green); }
.game-custom > summary::-webkit-details-marker { display: none; }
.game-custom > summary::before {
  content: "▸";
  display: grid; place-items: center;
  width: 18px; height: 18px; flex: none;
  border-radius: 50%; background: var(--green); color: #fff;
  font-size: 9px; align-self: center;
}
.game-custom[open] > summary::before { content: "▾"; }
.game-custom > summary span { font-size: 11px; font-weight: 600; color: var(--muted); }
.game-custom-body { padding: 4px 0 10px; }
.game-seed { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; font-size: 11px; color: var(--muted); margin: 0 0 12px; }
.game-seed-chip { border: 1px solid var(--line); background: #fff; border-radius: 20px; padding: 4px 10px; font: 700 10px inherit; color: var(--green); cursor: pointer; }
.game-seed-chip:hover { border-color: #bcd6c1; background: #f4faf5; }
.game-slider { display: grid; grid-template-columns: 150px 1fr 60px; align-items: center; gap: 12px; margin-bottom: 9px; font-size: 11px; font-weight: 700; color: var(--muted); }
.game-slider input { accent-color: var(--green); }
.game-slider b { text-align: right; font-size: 12px; color: var(--ink); }
.game-custom-note { font-size: 11px; color: var(--muted); margin: 10px 0 0; display: flex; gap: 8px; flex-wrap: wrap; align-items: baseline; }
.game-score { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line); }
.game-score-main span, .game-score-main small { display: block; font-size: 10px; font-weight: 700; color: var(--muted); }
.game-score-main strong { display: block; font-size: 34px; letter-spacing: -1.5px; color: var(--green); }
.game-score-detail { display: flex; gap: 18px; flex-wrap: wrap; }
.game-score-detail small { display: block; font-size: 10px; color: var(--muted); font-weight: 700; }
.game-score-detail b { font-size: 13px; color: var(--red); }
.game-score-foot { flex-basis: 100%; font-size: 11px; color: var(--muted); margin: 4px 0 0; }
/* The one red thing in this column: a caveat on the score, not on the grid, so it is tinted rather
   than alarmed - the same red as a violation, at the weight of a footnote. */
.game-cutoff {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 12px 14px; margin-top: 16px;
  background: #fdf1f0; border: 1px solid #f2c9c4; border-left: 3px solid var(--red); border-radius: 10px;
}
.game-cutoff .critical-dot { flex: none; margin-top: 4px; }
.game-cutoff strong { display: block; font-size: 12px; color: var(--red); }
.game-cutoff small { display: block; font-size: 11px; color: var(--muted); margin-top: 3px; line-height: 1.5; }
/* Folded away by default: the running score is glanceable, and the arithmetic behind it is what
   someone asks for once - usually the moment they disagree with their number. */
.game-score-how { flex-basis: 100%; margin: 6px 0 0; font-size: 11px; color: var(--muted); }
.game-score-how summary { cursor: pointer; font-weight: 700; color: var(--ink); }
.game-score-how p { margin: 8px 0 0; line-height: 1.55; }
.game-score-how b { color: var(--ink); }
.game-score-table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 11px; }
.game-score-table th { text-align: left; font-weight: 700; color: var(--ink); }
.game-score-table th, .game-score-table td { padding: 5px 8px 5px 0; border-top: 1px solid var(--line); }
.game-score-table td:last-child { text-align: right; }
.game-score-table tbody b { color: var(--red); }
.game-score-table tfoot b { color: var(--green); font-size: 13px; }
.game-result { margin-top: 16px; padding: 16px; border: 1px solid #bddfc4; border-radius: 12px; background: #f4faf5; }
.game-standings { list-style: none; margin: 6px 0 0; padding: 0; }
.game-standings li { display: flex; justify-content: space-between; padding: 7px 0; border-top: 1px solid #e0ece2; font-size: 12px; font-weight: 700; }
.game-standings li.is-you { color: var(--green); font-weight: 800; }
/* Named but not yet scored: present enough to show who is in the comparison, quiet enough not to be
   mistaken for a result. */
.game-standings li.is-waiting { color: var(--muted); font-weight: 700; }
.game-standings li.is-waiting b { display: flex; align-items: center; gap: 6px; }
.game-standings li.is-waiting b small { font-size: 10px; font-weight: 700; }
.game-standings b { font-size: 14px; }
@media (max-width: 650px) { .game-slider { grid-template-columns: 1fr; gap: 4px; }}
.game-slider span small { display: block; font-size: 9px; font-weight: 600; color: #9aa79f; }
/* Map on the left, suggested actions on the right - the same two-column row as before, but the right
   column is now the decision itself (alert + option cards) rather than a second, static grid summary. */
.game-situation { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(270px, 1fr); gap: 16px; margin-bottom: 16px; align-items: stretch; }
/* Grid stretches both columns to the row's height already; what is missing is something inside the
   shorter one to actually fill that height with, rather than leaving blank space below it while the
   taller column's content runs on. The map stays its own fixed size (.game-map-slot .grid-map); the
   violations card is what grows, so a short stack (map + collapsed toggle) still ends flush with
   however tall the option cards beside it happen to be. */
.game-map-col { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.game-actions-col { min-width: 0; }
.game-map-wrap { flex: 0 0 auto; border: 1px solid var(--line); border-radius: 12px; padding: 13px 14px; background: #edf4ed; }
/* The real network card is moved in here while the operator view is open: it brings its own panel
   chrome, so the wrapper stops drawing a second frame around it. */
.game-map-wrap:has(.grid-card) { border: 0; padding: 0; background: none; }
.game-map-slot { margin-top: 8px; }
.game-map-slot .grid-card { padding: 15px; }
/* Full height, as in the detailed day view: this is the map an operator is deciding from, and a
   two-thirds-size copy of it made the violations it is meant to show unfindable. */
.game-map-slot .grid-map { height: 425px; }
/* Shown only while a card's Preview is active, next to the "Previewing: <option>" title it replaces
   .policy-map-title's default with - see renderGameSituation. */
.game-map-clear {
  border: 1px solid var(--line); border-radius: 20px; background: #fff;
  padding: 3px 10px; font: 700 10px inherit; color: var(--muted); cursor: pointer; white-space: nowrap;
}
.game-map-clear:hover { border-color: #bcd6c1; background: #f4faf5; color: var(--green); }
/* The map defaults to "unless you act" rather than "right now" - see the Default map decision. Tinted
   like the alert it agrees with, so the map and the sentence above the cards read as one claim. */
.policy-map-title.is-warning #gameMapTitle { color: #a8501f; }
.policy-map-title.is-preview #gameMapTitle { color: var(--green); }

/* The violation ledger, ported from the detailed day view but collapsed by default: the map and the
   alert above it already say what needs attention, and the exhaustive per-element list underneath the
   map is now something an operator opens rather than a fixed-height column beside it. */
.game-violations {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line); border-radius: 12px; padding: 13px 14px; background: #fff;
}
.game-outlook { font-size: 11px; line-height: 1.65; color: var(--muted); margin: 0 0 11px; }
.game-outlook b { color: var(--ink); }
.game-violations-toggle {
  display: flex; align-items: center; gap: 9px; width: 100%;
  border: 0; background: none; padding: 0; margin: 0;
  font: 800 12px inherit; letter-spacing: -.2px; color: var(--ink); cursor: pointer; text-align: left;
}
.game-violations-toggle .count-badge { width: 22px; height: 22px; font-size: 11px; margin-left: auto; }
.game-violations-caret { font-size: 10px; color: var(--muted); transition: transform .15s ease; }
.game-violations-toggle[aria-expanded="true"] .game-violations-caret { transform: rotate(90deg); }
.game-violations .issue-list { margin-top: 12px; max-height: 320px; overflow-y: auto; }
@media (max-width: 900px) { .game-situation { grid-template-columns: 1fr; }}

/* Only the operator's live violation markers pulse; the detailed view stays a quiet inspection tool. */
.operator-mode #networkMap .real-node.warning,
.operator-mode #networkMap .real-node.critical,
.operator-mode #networkMap .real-line.warning,
.operator-mode #networkMap .real-line.critical { animation: map-violation-pulse 1.25s ease-in-out infinite; }
@keyframes map-violation-pulse { 50% { stroke-width: 7; filter: drop-shadow(0 0 5px #d64d3d); } }

/* Custom controls are a focused, temporary configuration task rather than an expanding section
   underneath the decision cards. */
dialog.game-custom { width: min(560px, calc(100vw - 28px)); max-height: calc(100vh - 28px); margin: auto; padding: 0; border: 0; border-radius: 14px; background: #fff; box-shadow: 0 20px 70px #10291d55; }
dialog.game-custom::backdrop { background: #10291d80; }
dialog.game-custom .game-custom-body { padding: 18px; }
.custom-modal-head { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; margin-bottom: 12px; }
.custom-modal-head .eyebrow { margin: 0 0 3px; }
.custom-modal-head h3 { margin: 0; font-size: 18px; letter-spacing: -.35px; }
.custom-modal-close { width: 28px; height: 28px; border: 1px solid var(--line); border-radius: 7px; background: #fff; color: var(--muted); font: 700 18px/1 inherit; cursor: pointer; }
.custom-preview-label { margin: 14px 0 4px; font-size: 10px; font-weight: 800; letter-spacing: .7px; color: var(--muted); text-transform: uppercase; }
dialog.game-custom [data-custom-commit] { margin-top: 12px; }

/* "What is in this grid" - one chip in the topbar, so the question is answerable from every view.
   line-height is pinned rather than left at "normal": the emoji glyphs used as icons (⚡, ⓘ) pull in
   different fallback-font line metrics, which otherwise made these same-class buttons render at
   different heights. */
.run-controls .grid-stats-button {
  border: 0; background: #e6efe4; border-radius: 7px;
  height: 32px; padding: 0 10px; margin-right: 2px;
  display: inline-flex; align-items: center;
  font: 750 11px inherit; line-height: normal; color: var(--green); cursor: pointer;
}
.run-controls .grid-stats-button:hover { background: #d6e8d8; color: #106642; }
.run-controls #computeDay { height: 32px; padding-top: 0; padding-bottom: 0; }
.grid-stats-panel {
  position: absolute; top: 58px; z-index: 40; width: 340px;
  padding: 14px 16px; background: #fff; border: 1px solid var(--line);
  border-radius: 12px; box-shadow: 0 10px 28px #1a3a2a1f;
}
.grid-stats-title { font-size: 10px; letter-spacing: 1.1px; font-weight: 800; color: #738078; margin: 0 0 8px; text-transform: uppercase; }
.grid-stats-table { width: 100%; border-collapse: collapse; }
.grid-stats-table + .grid-stats-table { margin-top: 10px; border-top: 1px solid var(--line); }
.grid-stats-table th { text-align: left; font-size: 11px; font-weight: 650; color: var(--muted); padding: 4px 0; }
.grid-stats-table td { text-align: right; font-size: 11px; font-weight: 750; padding: 4px 0; white-space: nowrap; }
.grid-stats-table td:last-child { text-align: right; font-weight: 600; color: #9aa79f; padding-left: 10px; font-size: 10px; }
.grid-stats-empty { font-size: 11px; color: var(--muted); margin: 0; }
.scenario { position: relative; }

/* --- Operator view: the day so far ------------------------------------------------------------ */
/* Two small charts of the playthrough's own trace. The map answers "what is wrong now"; these
   answer "where is this heading, and did my last decision hold". */
.game-timeline {
  /* Sits between the clock and the map row now, clear of both. */
  margin: 4px 0 18px;
  padding: 15px 17px 13px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: #fff;
}
.game-timeline-head {
  display: flex;
  justify-content: space-between;
  align-items: start;
  gap: 14px;
  flex-wrap: wrap;
}
.game-timeline-head h3 {
  margin: 2px 0 0;
  font-size: 14px;
  letter-spacing: -0.3px;
}
.game-timeline-head h3 b {
  color: var(--green);
}
.game-timeline-key {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 13px;
  font-size: 10px;
  font-weight: 650;
  color: var(--muted);
}
.key-line {
  display: inline-block;
  width: 13px;
  height: 0;
  margin-right: 5px;
  vertical-align: middle;
  border-top: 2px solid var(--muted);
}
.key-vmax { border-color: #e85445; }
.key-vmin { border-color: #4f86d6; }
.key-load { border-color: #f59e0b; }
.key-limit { border-top-style: dashed; border-color: #9aa79f; }
.game-timeline-charts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
  margin-top: 12px;
}
@media (max-width: 900px) {
  .game-timeline-charts { grid-template-columns: 1fr; }
}
.trace-figure {
  margin: 0;
}
.trace-figure.is-expanded {
  grid-column: 1/-1;
}
.trace-figure.is-expanded .chart-expand {
  border-color: #bcd6c1;
  color: var(--green);
}
.trace-figure.is-expanded .trace-chart {
  height: 260px;
}
.trace-figure figcaption {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  font-size: 10px;
  font-weight: 650;
  color: var(--muted);
  margin-bottom: 6px;
}
.trace-figure figcaption b {
  font-size: 12px;
  font-weight: 800;
  color: var(--ink);
  white-space: nowrap;
}
.trace-figcaption-end {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex: none;
}
/* The value scale sits beside the plot rather than on it, so the curves keep the full width. */
.trace-plot {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 7px;
  align-items: stretch;
}
.trace-chart-wrap {
  position: relative;
  min-width: 0;
}
.trace-chart {
  width: 100%;
  height: 96px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #f7faf7;
}
/* One p.u. value per labeled limit line, e.g. the 0.95/1.05 operational bounds - sat right on the
   dashed rule they belong to, not off in the end-of-axis readout where the connection to the line
   underneath it is lost. */
.trace-limit-label {
  position: absolute;
  left: 4px;
  transform: translateY(-50%);
  font-size: 8px;
  font-weight: 700;
  color: #9aa79f;
  background: rgba(247, 250, 247, 0.85);
  padding: 0 3px;
  border-radius: 3px;
  pointer-events: none;
  white-space: nowrap;
}
.trace-ends {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  font-size: 9px;
  font-weight: 700;
  color: #9aa79f;
  white-space: nowrap;
}
.trace-hours {
  display: flex;
  justify-content: space-between;
  font-size: 9px;
  font-weight: 650;
  color: #9aa79f;
  margin-top: 3px;
  /* The scale ends sit outside the plot, so the hour labels have to clear the same gutter to stay
     under the point of the day they mark. */
  padding-right: 52px;
}
/* preserveAspectRatio: none stretches the viewBox, so every stroke here is kept true size. */
.trace-chart path,
.trace-chart line {
  vector-effect: non-scaling-stroke;
}
.trace-chart path {
  fill: none;
  stroke-width: 1.6;
  stroke-linejoin: round;
}
.trace-vmax { stroke: #e85445; }
.trace-vmin { stroke: #4f86d6; }
.trace-load { stroke: #f59e0b; }
.trace-grid { stroke: #dfe8df; stroke-width: 1; }
.trace-limit { stroke: #9aa79f; stroke-width: 1; stroke-dasharray: 4 3; }
/* Where the playthrough has got to: the curves stop here, and this says so rather than leaving the
   empty right-hand side reading as missing data. */
.trace-now { stroke: var(--green); stroke-width: 1.4; stroke-dasharray: 3 2; }

/* Toasts. Bottom-right rather than over the panel: the notice a player gets here is usually "the demo
   is busy", which they read while still looking at the page that refused them. */
.toast-stack {
  position: fixed; right: 18px; bottom: 18px; z-index: 90;
  display: flex; flex-direction: column-reverse; gap: 9px;
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex; align-items: flex-start; gap: 10px;
  max-width: 320px; padding: 11px 13px;
  background: #fff; border: 1px solid var(--line); border-left: 3px solid var(--muted);
  border-radius: 10px; box-shadow: 0 6px 20px rgba(16, 24, 40, .14);
  animation: toast-in .18s ease-out;
}
.toast.is-busy { border-left-color: var(--red); }
.toast strong { display: block; font-size: 12px; }
.toast small { display: block; font-size: 10.5px; color: var(--muted); margin-top: 3px; line-height: 1.5; }
.toast-close {
  flex: none; margin: -2px -4px 0 auto; padding: 0 4px;
  background: none; border: 0; color: var(--muted); font-size: 14px; line-height: 1.2; cursor: pointer;
}
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* The narrow-viewport notice. Sits above .boot (z-index 50) so it is the first thing a phone
   visitor sees, and takes itself out of the flow entirely once dismissed. */
.mobile-notice {
  position: fixed;
  inset: 0;
  z-index: 65;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--paper);
}
.mobile-notice[hidden] {
  display: none;
}
.mobile-notice-card {
  width: 100%;
  max-width: 380px;
  text-align: center;
}
.mobile-notice-card img {
  height: 56px;
  width: auto;
  margin: 0 auto;
  display: block;
}
.mobile-notice-card h1 {
  margin: 24px 0 0;
  font-size: 24px;
  font-weight: 800;
  letter-spacing: -0.01em;
  color: var(--navy);
}
.mobile-notice-card p {
  margin: 12px 0 0;
  font-size: 14px;
  line-height: 1.6;
  color: var(--muted);
}
.mobile-notice-card button {
  margin-top: 28px;
  padding: 12px 32px;
  border: 0;
  border-radius: 12px;
  background: var(--green);
  color: #fff;
  font-size: 16px;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.2s ease;
}
.mobile-notice-card button:hover {
  background: var(--navy);
}
