/* ══ Site carousel (carousel_ui.r, .ndx-carousel-root) ══════════════════════
   All STATIC carousel styling lives here; head.r links this file after the
   base stylesheet with a content-hash cache buster. The only CSS the module
   emits inline is what varies per render: iframe heights and the grid's
   grid-template-columns (a literal integer - engines that don't substitute
   vars inside repeat() drop the property and collapse to one column). */

/* Full-bleed always: no card chrome, and the shell's container padding zeroed
   so the windows run edge to edge. */
.container-fluid:has(.ndx-carousel-root) {
  padding-left: 0 !important;
  padding-right: 0 !important;
  padding-top: .25rem !important;
}

/* ══ Top control strip (.ndx-bar) ═══════════════════════════════════════════
   Reads left to right as three zones — OPEN (controls) │ MOVE (transport,
   live) │ SEE (lens, status) — separated by hairline rules rather than by
   nothing at all, which is what made a row of equal-weight ghost icons hard
   to parse at a glance. The strip carries its own bottom rule so it reads as
   chrome sitting above the wall, not as buttons floating on it.
   Its total height is the CHROME constant in carousel_ui.r (148px) — change
   the padding here and change that number. */
.ndx-bar {
  padding: 0 .15rem .3rem;
  margin-bottom: .3rem;
  border-bottom: 1px solid var(--bs-border-color);
}

.ndx-bar-sep {
  flex: 0 0 auto;
  width: 1px;
  height: 22px;
  background: var(--bs-border-color);
  margin: 0 .1rem;
}

/* Transport cluster: prev / play / next are one decision, so they get one
   hairline shell with dividers between, instead of three loose icons at equal
   weight. The buttons inside keep the .ndx-menu-bar ghost treatment. */
.ndx-bar-seg {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  border: 1px solid var(--bs-border-color);
  border-radius: 7px;
  background: var(--bs-tertiary-bg);
  padding: 1px;
}
/* SPECIFICITY NOTE — read before touching any rule in this block.
   home.scss styles this bar with `.ndx-menu-bar .btn:not(.offcanvas .btn)`,
   which is (0,4,0): `:not()` contributes the specificity of its argument, and
   `.offcanvas .btn` is two classes. It sets `border: 0 !important` and
   `opacity: .55`. So every rule here that has to win carries a
   `.ndx-carousel-root` prefix to reach (0,5,0) — this stylesheet loads after
   home.css, so an equal-specificity rule would tie and win too, but (0,5,0)
   leaves no doubt. Drop the prefix and the button chrome silently vanishes. */
.ndx-carousel-root .ndx-ctl-wrap.ndx-menu-bar .ndx-bar-seg .btn {
  padding: .12rem .45rem !important;
  border-radius: 0 !important;
}
.ndx-carousel-root .ndx-ctl-wrap.ndx-menu-bar .ndx-bar-seg .btn + .btn {
  border-left: 1px solid var(--bs-border-color) !important;
}
/* Play is the cluster's primary — a touch more present than its neighbours. */
.ndx-carousel-root .ndx-ctl-wrap.ndx-menu-bar .ndx-bar-seg .btn.ndx-bar-play {
  opacity: .85;
}
.ndx-carousel-root .ndx-ctl-wrap.ndx-menu-bar .ndx-bar-seg .btn.ndx-bar-play:hover {
  opacity: 1;
}

/* Live / All toggle: its own pill, outside the transport shell, because it
   filters rather than moves. */
.ndx-carousel-root .ndx-ctl-wrap.ndx-menu-bar .btn.ndx-bar-pill {
  border: 1px solid var(--bs-border-color) !important;
  border-radius: 7px !important;
  background: var(--bs-tertiary-bg) !important;
  padding: .18rem .55rem !important;
}

/* Shiny's actionButton emits `<i>` immediately followed by the label text, so
   without this the bar reads "▶Play" and "⚡Live" with no breathing space. */
.ndx-carousel-root .ndx-bar-play .fas,
.ndx-carousel-root .ndx-bar-play .far,
.ndx-carousel-root .ndx-bar-pill .fas,
.ndx-carousel-root .ndx-bar-pill .far { margin-right: .32rem; }

/* An engaged toggle says so through its icon — module.r swaps in
   icon(..., class = "ndx-on") because updateActionButton can rewrite the icon
   but not the button's class, and these buttons must stay static (re-rendering
   an actionButton resets its click count). */
.ndx-carousel-root .ndx-ctl-wrap.ndx-menu-bar .btn .ndx-on {
  color: var(--ndx-accent) !important;
}
.ndx-carousel-root .ndx-ctl-wrap.ndx-menu-bar .btn.ndx-bar-pill .ndx-on {
  color: var(--ndx-warn) !important;
}
.ndx-carousel-root .ndx-ctl-wrap.ndx-menu-bar .btn:has(.ndx-on) {
  opacity: 1 !important;
}

/* ── Status line ──
   WHERE (mode icon + subject) / WHAT (lens) / position / live / play state,
   with the pager pinned to the far end. Every colour is a theme token: the
   literals this replaced were cool-toned against a warm-charcoal palette and
   went invisible under the light theme. */
.ndx-cap-subject {
  font-weight: 600;
  font-size: .82rem;
  font-family: var(--bs-font-monospace, ui-monospace, monospace);
  color: var(--bs-emphasis-color);
}
.ndx-cap-mode { color: var(--ndx-muted); margin-right: .35rem; }
.ndx-cap-badge {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .1rem .45rem;
  border-radius: 5px;
  font-size: .6rem;
  letter-spacing: .02em;
  background: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
  border: 1px solid var(--bs-border-color);
}
.ndx-cap-badge-live { color: var(--ndx-ok); border-color: var(--ndx-ok); }
.ndx-cap-pos {
  font-size: .7rem;
  color: var(--bs-tertiary-color);
  font-variant-numeric: tabular-nums;
}
.ndx-cap-live {
  font-size: .7rem;
  color: var(--ndx-ok);
  display: inline-flex;
  align-items: center;
  gap: .3rem;
}
/* Paused is a resting state, not a warning — amber stays reserved for the
   fleet actions, which are the only thing on this page that can break something. */
.ndx-cap-state { font-size: .7rem; color: var(--bs-tertiary-color); }
.ndx-cap-auto { color: var(--ndx-accent); }

.ndx-cap-pager {
  gap: .1rem;
  border: 1px solid var(--bs-border-color);
  border-radius: 7px;
  background: var(--bs-tertiary-bg);
  padding: 1px;
}
.ndx-carousel-root .ndx-ctl-wrap.ndx-menu-bar .ndx-cap-pager .btn {
  padding: .05rem .4rem !important;
}
.ndx-cap-pageno {
  font-size: .65rem;
  color: var(--bs-secondary-color);
  font-variant-numeric: tabular-nums;
  padding: 0 .15rem;
}

/* ══ Controls offcanvas ═════════════════════════════════════════════════════
   One idiom, two levels: a section (icon + eyebrow + optional note) holding
   labelled fields, with same-kind fields sharing a row. The panel used to be a
   flat run of eyebrow labels at equal weight, one control per row, so setting
   tile height looked exactly like committing every repo on the platform. */
/* ONE divider vocabulary inside the sheet. The column-rule already separates
   concerns side to side; a per-section top hairline added a second, weaker
   rule that only appeared where two sections happened to land in the same
   column - so the same relationship was drawn two different ways depending on
   how the flow packed. Sections are separated by space alone; the section
   header's own weight is what starts a new block. */
.ndx-sec {
  padding: 0 0 1.15rem;
  border-top: 0;
  break-inside: avoid;
}
.ndx-sec:first-of-type { padding-top: 0; }

.ndx-sec-hd {
  display: flex;
  align-items: center;
  gap: .45rem;
  font-size: .66rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--bs-secondary-color);
  margin-bottom: .55rem;
}
.ndx-sec-hd i { color: var(--ndx-accent); font-size: .72rem; opacity: .85; }

.ndx-sec-note {
  font-size: .66rem;
  line-height: 1.4;
  color: var(--bs-tertiary-color);
  margin: -.2rem 0 .45rem;
}

/* Fleet actions — the one section that changes repos and the running platform
   rather than what is on screen, so it wears the reserved amber and is drawn
   as an enclosed region rather than just another band. A hairline left rule
   was tried first and read as an accident at this contrast; a bordered, faintly
   tinted card is unmistakably "this one is different". */
/* The only enclosed section, because it is the only one that changes repos and
   the running platform rather than what is on screen. Now that no other
   section draws a border, the card alone carries that weight - so the tint can
   come down and the border can soften without losing the distinction. Padded
   to sit on the same rhythm as its unbordered neighbours rather than towering
   over them. */
.ndx-sec-danger {
  border: 1px solid rgba(224, 165, 63, .22);
  border-radius: 8px;
  background: rgba(224, 165, 63, .03);
  padding: .7rem .75rem .75rem;
  margin-top: -.15rem;
}
.ndx-sec-danger .ndx-sec-hd,
.ndx-sec-danger .ndx-sec-hd i { color: var(--ndx-warn); }
.ndx-sec-danger .ndx-sec-note { color: var(--ndx-warn); opacity: .85; }

/* Field: micro-label over its control, every control full-width. The old panel
   mixed 110px / 150px / 100% widths, which read as three different kinds of
   control when they are all just selects.
   Flex column with the control pushed to the bottom, so two fields sharing a
   row line their CONTROLS up even when one label wraps and the other doesn't. */
.ndx-fld {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
/* In an even pair the two controls should sit on one line even if one label
   wraps to two - so the controls are bottom-aligned. In a split row the right
   side is a tall wrapping checkbox group, and bottom-aligning would drag the
   left label halfway down the row, so those align at the top instead. */
.ndx-fld-pair > .ndx-fld { justify-content: flex-end; }
.ndx-fld-split > .ndx-fld { justify-content: flex-start; }
.ndx-fld + .ndx-fld,
.ndx-fld-pair + .ndx-fld-pair,
.ndx-fld-pair + .ndx-fld,
.ndx-fld-split + .ndx-fld,
.ndx-fld + .ndx-fld-split,
.ndx-fld + .ndx-fld-pair { margin-top: .5rem; }
.ndx-fld-lbl {
  display: block;
  font-size: .66rem;
  line-height: 1.3;
  color: var(--bs-tertiary-color);
  margin-bottom: .2rem;
}
.ndx-fld .form-group,
.ndx-fld .shiny-input-container { margin-bottom: 0 !important; width: 100% !important; }
.ndx-fld select { width: 100% !important; }
/* selectInput(label = NULL) still emits an EMPTY <label class="control-label
   shiny-label-null">, and .control-label is display:block with its own line
   box - which is exactly why paired fields used to sit at different heights,
   a button beside a select being the worst case. */
.ndx-ctl-wrap .offcanvas .shiny-label-null { display: none !important; }
/* Field-level button (the view toggle) sized like the selects beside it. */
.ndx-fld-btn {
  width: 100%;
  min-height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .35rem;
  font-size: .78rem;
}

/* Same-kind controls share a row: four stacked full-width selects made a short
   panel scroll for no reason. `split` is the uneven variant — a narrow select
   beside a wrapping checkbox group. */
.ndx-fld-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .5rem;
}
.ndx-fld-split {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: .6rem;
}
@media (max-width: 420px) {
  .ndx-fld-pair, .ndx-fld-split { grid-template-columns: 1fr; }
}

/* ── One control height across the sheet ──
   Selects, text inputs and field-level buttons all sit on a 32px box. Mixed
   heights are what made rows look hand-placed: a select beside a button beside
   an input each landed on its own baseline, so no two columns lined up even
   when their labels did. One number, applied once. */
.ndx-ctl-wrap .offcanvas .ndx-fld > .form-control,
.ndx-ctl-wrap .offcanvas .ndx-fld > .shiny-input-container > .form-control,
.ndx-ctl-wrap .offcanvas .ndx-fld select,
.ndx-ctl-wrap .offcanvas .ndx-fld .btn {
  min-height: 32px;
  height: 32px;
  padding-top: 0;
  padding-bottom: 0;
  font-size: .78rem;
}
/* Shiny wraps every input in .form-group/.shiny-input-container with a bottom
   margin; inside a field the label above already provides the spacing. */
.ndx-ctl-wrap .offcanvas .ndx-fld .form-group,
.ndx-ctl-wrap .offcanvas .ndx-fld .shiny-input-container { margin-bottom: 0 !important; }

/* Live read-outs under a control (grid shape, what a fleet action resolves to) */
.ndx-hint {
  font-size: .62rem;
  line-height: 1.35;
  color: var(--bs-tertiary-color);
  margin-top: .3rem;
}
.ndx-hint-warn { color: var(--ndx-warn); margin-top: .25rem; }
.ndx-warn-ico { color: var(--ndx-warn); }

/* Chip rows — jump-to-site and open-in-new-tab. One shape for both, since both
   are "pick a thing" targets. Quest-sized (≥32px, gap) so a laser-pointer
   scroll-drag doesn't graze a click. */
.ndx-chips {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
}
.ndx-chip {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  min-height: 32px;
  padding: .25rem .65rem;
  font-size: .7rem;
  line-height: 1.2;
  border-radius: 7px;
  border: 1px solid var(--bs-border-color);
  background: var(--bs-body-bg);
  color: var(--bs-secondary-color);
  text-decoration: none;
  transition: border-color .12s ease, color .12s ease;
}
.ndx-chip:hover {
  color: var(--bs-emphasis-color);
  border-color: var(--ndx-accent);
  text-decoration: none;
}
.ndx-chip-on {
  border-color: var(--ndx-accent);
  color: var(--bs-emphasis-color);
  background: var(--bs-primary-bg-subtle);
}
.ndx-chip-dot { color: var(--ndx-ok); }

/* selectize theme (forced past selectize.css specificity; shared-theme tokens
   so the control flips with the light/dark toggle) */
.ndx-site-picker .selectize-control { width: 100% !important; }
.ndx-site-picker .selectize-control.multi .selectize-input {
  display: flex !important;
  flex-wrap: wrap !important;
  align-items: center;
  gap: 8px !important;
  padding: 8px 10px !important;
  background: var(--bs-body-bg) !important;
  border: 1px solid var(--bs-border-color) !important;
  color: var(--bs-body-color) !important;
  box-shadow: none !important;
}
.ndx-site-picker .selectize-control.multi .selectize-input.focus {
  border-color: var(--ndx-accent) !important;
}
.ndx-site-picker .selectize-control.multi .selectize-input > .item {
  background: var(--bs-secondary-bg) !important;
  color: var(--bs-body-color) !important;
  border: 1px solid var(--bs-border-color) !important;
  border-radius: 6px !important;
  margin: 0 !important;
  padding: 4px 10px !important;
  font-size: .75rem;
}
.ndx-site-picker .selectize-control.multi .selectize-input > .item .remove {
  border-left-color: var(--bs-border-color) !important;
  color: var(--ndx-muted) !important;
}
.ndx-site-picker .selectize-input > input {
  color: var(--bs-body-color) !important;
  background: transparent !important;
}
.ndx-site-picker .selectize-dropdown {
  background: var(--bs-secondary-bg) !important;
  color: var(--bs-body-color) !important;
  border-color: var(--bs-border-color) !important;
}
.ndx-site-picker .selectize-dropdown .option { color: var(--bs-body-color) !important; }
.ndx-site-picker .selectize-dropdown .active {
  background: var(--bs-tertiary-bg) !important;
  color: var(--bs-body-color) !important;
}

/* "Play cycles through" lens checkboxes - Quest-sized targets, site accent.
   Covers both Bootstrap markup generations (.form-check and .checkbox).
   Two-up so the four lenses cost two short rows beside the pace select rather
   than four full-width ones. */
.ndx-cycle .shiny-options-group {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 .4rem;
}
.ndx-cycle .form-group,
.ndx-cycle .shiny-input-container { margin-bottom: 0 !important; }
.ndx-cycle label {
  min-height: 30px;
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  margin-right: 0;
  font-size: .72rem;
  color: var(--bs-body-color);
}
.ndx-cycle input[type=checkbox] {
  width: 1.05rem;
  height: 1.05rem;
  accent-color: var(--ndx-accent);
}

/* pace / lens / height selects */
.ndx-carousel-root select {
  background: var(--bs-body-bg) !important;
  color: var(--bs-body-color) !important;
  border-color: var(--bs-border-color) !important;
}

/* grid mode: columns come inline per render from the "per row" control.
   Phone-width safety only; every wider viewport honours the choice. */
.ndx-grid { display: grid; gap: .6rem; }
@media (max-width: 560px) {
  .ndx-grid { grid-template-columns: 1fr !important; }
}

/* ── Grid tile tools ──
   ONE flush bar pinned to the tile's top-right corner, not four floating
   chips. The embedded page owns the top-left (its brand) and puts its own
   controls top-right, so our tools have to look deliberately like a toolbar
   for THIS tile rather than debris scattered over the site's nav.

   - flush: `top:0; right:0`, no inset, and the outer corners follow the
     tile's own radius so the bar sits into the corner rather than near it.
   - middle: the bar is `align-items:center` at the height of the strip it
     overlays, so the icons line up with the embedded header's own row.
   - joined: buttons are `position:static` inside the flex bar and share
     borders (`+ *` collapses the doubled edge), so the group reads as one
     segmented control - the same idiom as the transport cluster in the bar.

   Slot offsets (right:6/40/74/108) are GONE - the flex bar orders them, so
   adding or removing a tool no longer means renumbering every rule. */
/* Each grid cell is now a uiOutput slot with the tile inside it (per-tile
   rendering - see CONTEXT.md), so the SLOT is the grid child. min-width:0
   because a grid item defaults to min-width:auto, which refuses to shrink
   below its content and would let a wide tile push the track past
   minmax(0,1fr) - the same class of bug as the missing iframe width:100%
   that once left tiles at their ~300px intrinsic box. */
.ndx-tile-slot { min-width: 0; }
.ndx-tile { position: relative; }

/* THE WINDOW ITSELF. Only the height is written inline per render (it is the
   one thing that genuinely varies); width, border, radius and backdrop live
   here on theme tokens.

   width:100% is load-bearing and easy to lose: an iframe with no width falls
   back to its ~300px intrinsic box, which leaves the tile mostly empty and
   strands the absolutely-positioned tools bar out in the blank space to its
   right. If tiles ever render narrow with white space beside them, this rule
   is what went missing. */
.ndx-tile iframe {
  width: 100%;
  display: block;
  min-height: 160px;
  border: 1px solid var(--bs-border-color);
  border-radius: 6px;
  background: var(--bs-body-bg);
}
/* Single view: one big window, so it gets a taller floor and a softer corner. */
.ndx-tile-solo iframe {
  min-height: 280px;
  border-radius: 8px;
}


.ndx-tile-tools {
  position: absolute;
  /* Three zones, two of them already taken, so this sits in the third.
     Row 1 far-right is the page's own controls (fullscreen, theme, and an
     avatar when signed in) — the original position, which sat on top of them.
     Row 2 far-right is the operator bar's action end, which is where exec puts
     Deploy and its Save-ok badge — the 40px position, which covered those.
     Neither right-hand zone is reliably free, so the bar moves onto row 1 and
     to the LEFT of the page's control cluster: same row every lens, beside
     those controls rather than over them.

     104px is that cluster at its widest — three ~28px buttons plus gaps and
     the 8px edge. Pages with only two controls simply leave a gap, which is
     harmless; a page that ever adds a fourth would need this number raised. */
  top: 6px;
  right: 104px;
  display: flex;
  align-items: center;
  gap: 0;
  /* Numbers match .ndx-bar-seg in css/home.scss exactly (1px padding, 7px
     radius, tertiary surface) rather than approximating it. This cluster was
     the same idea drawn five ways differently - 5px radius, 0 1px padding, a
     secondary surface - which is why it read as bulkier and slightly off
     beside every other segmented group in the estate. Overlay behaviour
     (absolute, opacity .5 at rest) stays local; only the shape vocabulary is
     shared, because that is the part a reader compares across tiles. */
  padding: 1px;
  border-radius: 7px;
  background: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
  /* Recessive at rest so a wall of tiles is windows, not chrome; solid the
     moment you are actually working in that tile. */
  opacity: .5;
  transition: opacity .12s ease;
  z-index: 2;
}
.ndx-tile:hover .ndx-tile-tools,
.ndx-tile-tools:focus-within { opacity: 1; }

/* SPECIFICITY: these are Shiny actionButtons (they arrive as `btn btn-default
   action-button`) and one <a>. Bootstrap's `.btn` sets padding .375/.75rem and
   font-size 1rem, and `a` sets the link colour — both tie with a bare
   `.ndx-tile-close` at (0,1,0), so whichever sheet loads last wins and the
   buttons render full size if anything reorders. Prefixing with
   `.ndx-carousel-root` reaches (0,2,0) and settles it — the same rule this
   stylesheet already follows for the top bar. */
.ndx-carousel-root .ndx-tile-open,
.ndx-carousel-root .ndx-tile-refresh,
.ndx-carousel-root .ndx-tile-cycle,
.ndx-carousel-root .ndx-tile-close {
  position: static;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  color: var(--ndx-muted);
  border: 0;
  border-radius: 0;
  font-size: .62rem;
  line-height: 1;
  padding: 0;
  min-width: 0;
  box-shadow: none;
  text-decoration: none;
}
/* Hairline between adjacent tools only - no outer edges, so the group reads
   as one control. */
.ndx-carousel-root .ndx-tile-tools > * + * { border-left: 1px solid var(--bs-border-color); }
/* Inner corners, same as .ndx-bar-seg's first/last child rule: without them the
   square button corners sit inside a 7px container and leave a visible sliver
   of background at each end, which is the "uneven" edge on a 22px control. */
.ndx-carousel-root .ndx-tile-tools > *:first-child {
  border-top-left-radius: 6px; border-bottom-left-radius: 6px;
}
.ndx-carousel-root .ndx-tile-tools > *:last-child {
  border-top-right-radius: 6px; border-bottom-right-radius: 6px;
}
/* Shiny puts a margin on the icon when a label follows; these have no label. */
.ndx-carousel-root .ndx-tile-tools .fa,
.ndx-carousel-root .ndx-tile-tools .fas { margin: 0; }

.ndx-carousel-root .ndx-tile-open:hover,
.ndx-carousel-root .ndx-tile-cycle:hover,
.ndx-carousel-root .ndx-tile-refresh:hover { color: var(--bs-body-color); }
.ndx-carousel-root .ndx-tile-close:hover { color: var(--ndx-warn); }

/* Domain label: the left end of the same bar, sharing its baseline. */
.ndx-tile-dom {
  position: static;
  max-width: 46%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--bs-font-monospace, ui-monospace, monospace);
  font-size: .6rem;
  line-height: 1;
  padding: 0 .5rem;
  background: transparent;
  color: var(--ndx-muted);
  border: 0;
  opacity: .8;
  transition: opacity .12s ease;
  pointer-events: none;
}
.ndx-tile:hover .ndx-tile-dom { opacity: 1; }

.ndx-tile-badge {
  position: absolute;
  bottom: 6px;
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  background: var(--bs-secondary-bg);
  color: var(--ndx-warn);
  border: 1px solid var(--bs-border-color);
  border-radius: 5px;
  padding: 1px 8px;
  font-size: .58rem;
  line-height: 1.6;
  pointer-events: none;   /* never intercept a click meant for the site */
  z-index: 2;
}

/* ── Controls panel: a bottom sheet ─────────────────────────────────────────
   It rises from the bottom edge rather than sliding in from the right. Two
   reasons: a side drawer covered the right-hand column of tiles — the part you
   are usually adjusting the layout in order to see — and a workbench drawer
   under the work is the shape an IDE already trains you to expect.

   The shape change means the content is now WIDE and SHORT rather than narrow
   and tall, so the sections flow into columns (below) instead of stacking into
   a scroll. Height is capped, not fixed: the panel is as tall as it needs to
   be, up to a limit, so a short panel does not leave a band of empty sheet.

   svh with a vh fallback declared first — on iOS, vh counts the area behind the
   browser chrome, so a vh-sized sheet has its last row under the toolbar. */
.ndx-ctl-wrap .offcanvas.offcanvas-bottom {
  height: auto;
  max-height: 82vh;
  max-height: 82svh;
  background: var(--bs-secondary-bg);
  color: var(--bs-body-color);
  border-top: 1px solid var(--bs-border-color);
  border-left: 0;
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -12px 40px rgba(0, 0, 0, .38);
}

/* Grab handle: the affordance that says "this is a sheet you can dismiss".
   Drawn on the header, so it costs no extra element. */
.ndx-ctl-wrap .offcanvas.offcanvas-bottom .offcanvas-header::before {
  content: "";
  position: absolute;
  top: 6px;
  left: 50%;
  transform: translateX(-50%);
  width: 36px;
  height: 4px;
  border-radius: 2px;
  background: var(--bs-border-color);
}
.ndx-ctl-wrap .offcanvas.offcanvas-bottom .offcanvas-header { position: relative; }

/* Sections flow into columns on a wide sheet. `columns` (not grid) because the
   sections are different heights and should pack, not line up in rows with
   ragged gaps; break-inside keeps a section whole rather than splitting its
   fields across a column boundary. */
.ndx-ctl-wrap .offcanvas.offcanvas-bottom .offcanvas-body {
  columns: 1;
  column-gap: 1.75rem;
  /* A hairline between columns, so the sheet reads as distinct concerns side
     by side rather than one wide field of controls. Draws only where there are
     two or more columns, so it costs nothing on a phone. */
  column-rule: 1px solid var(--bs-border-color);
}
.ndx-ctl-wrap .offcanvas.offcanvas-bottom .offcanvas-body > .ndx-sec {
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  page-break-inside: avoid;
}
/* Sites spans the full sheet above the columns. Two reasons: it is the primary
   control and earns the width, and CSS multi-column is a fragmentation context
   — the selectize dropdown is absolutely positioned and can be clipped by the
   column box it lives in. Spanning takes it out of that context entirely. */
.ndx-ctl-wrap .offcanvas.offcanvas-bottom .offcanvas-body > .ndx-sec-wide {
  column-span: all;
  border-top: 0;
  padding-top: .25rem;
  /* Spanning elements break the column run, so this padding is the gap between
     the picker and the columns beneath it - keep it small or the sheet opens
     with a band of nothing under the chips. */
  padding-bottom: .45rem;
  margin-bottom: .55rem;
  border-bottom: 1px solid var(--bs-border-color);
}

/* The first section has no top rule when it is genuinely first; in columns
   every column starts a new run, so restore the rule for all of them. */
.ndx-ctl-wrap .offcanvas.offcanvas-bottom .offcanvas-body > .ndx-sec:first-of-type {
  border-top: 1px solid var(--bs-border-color);
  padding-top: .55rem;
}
@media (min-width: 820px) {
  .ndx-ctl-wrap .offcanvas.offcanvas-bottom .offcanvas-body { columns: 2; }
}
@media (min-width: 1200px) {
  .ndx-ctl-wrap .offcanvas.offcanvas-bottom .offcanvas-body { columns: 3; }
}
@media (min-width: 1700px) {
  .ndx-ctl-wrap .offcanvas.offcanvas-bottom .offcanvas-body { columns: 4; }
}
.ndx-ctl-wrap .offcanvas-header {
  /* No bottom rule: the sheet's own top edge + shadow already say "this is a
     separate surface", and a second rule 40px below it read as a stripe. */
  border-bottom: 0;
  padding-top: .85rem;
  padding-bottom: .35rem;
}
.ndx-ctl-wrap .offcanvas-title { font-size: .95rem; }
.ndx-ctl-wrap .offcanvas-body { padding-top: .35rem; }
/* Selects inside the panel: one compact size, so a column of them reads as a
   column rather than as a stack of differently-weighted boxes. */
.ndx-ctl-wrap .offcanvas select.form-control,
.ndx-ctl-wrap .offcanvas select.form-select {
  padding: .22rem 1.4rem .22rem .5rem;
  font-size: .78rem;
  height: auto;
  min-height: 32px;
}

/* ── Fleet actions (offcanvas) ──
   The enclosing .ndx-sec-danger card already says "this one is different", so
   the body just needs to sit inside it. */
.ndx-bulk .form-group,
.ndx-bulk .shiny-input-container { margin-bottom: 0; }
.ndx-bulk-run { margin-top: .5rem; }
/* Quest comfort: the dry-run tick is the smallest target in the panel.
   Both selectors because Shiny's checkboxInput wraps in .checkbox on the
   Bootstrap 3 shim and .form-check under Bootstrap 5. */
.ndx-bulk .checkbox label,
.ndx-bulk .form-check label {
  min-height: 32px;
  display: flex;
  align-items: center;
  gap: .4rem;
  font-size: .72rem;
  margin-bottom: 0;
}
.ndx-bulk input[type=checkbox] { width: 16px; height: 16px; }
.ndx-bulk .btn { min-height: 32px; }
.ndx-bulk .btn .fas { margin-right: .32rem; }

/* Run log. column-reverse pins the scroller to the newest line with no JS -
   this app's CSP blocks injected scripts, so there is no scroll handler to
   reach for. The <pre> keeps its own order; only the scroll anchor flips. */
.ndx-bulk-log {
  display: flex;
  flex-direction: column-reverse;
  overflow: auto;
  max-height: 220px;
  margin-top: .5rem;
  background: var(--ndx-bg, #191714);
  border: 1px solid var(--bs-border-color);
  border-radius: 6px;
  padding: .4rem .5rem;
}
.ndx-bulk-log pre {
  margin: 0;
  font-size: .6rem;
  line-height: 1.35;
  color: var(--bs-secondary-color);
  white-space: pre-wrap;
  word-break: break-word;
}

/* ── Lens picker in the top bar ──
   Shiny wraps selectInput in .form-group.shiny-input-container, and emits an
   empty .shiny-label-null above it for label = NULL - between them the select
   sat both taller than and lower than the buttons beside it. Zeroed and sized
   to the same 30px pill as the transport cluster, so the strip reads as one
   row of controls rather than a row with a form dropped into it. */
/* The picker names its AXIS, not just its value. A bare dropdown reading
   "Exec" says what is selected but never what kind of thing it selects, so the
   control that decides how every site is rendered was the one item in the bar
   you had to already know to recognise. Hidden under md - a phone gets the
   dropdown alone, where bar width is genuinely scarce. */
.ndx-lens-lbl {
  font-size: .68rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--bs-secondary-color);
  white-space: nowrap;
}
.ndx-lens-pick,
.ndx-lens-pick .form-group,
.ndx-lens-pick .shiny-input-container { margin-bottom: 0 !important; }
.ndx-lens-pick .shiny-label-null { display: none !important; }
.ndx-lens-pick select {
  padding: .12rem 1.5rem .12rem .5rem;
  font-size: .78rem;
  line-height: 1.4;
  height: 30px;
  border-radius: 7px;
  background-color: var(--bs-tertiary-bg) !important;
  border-color: var(--bs-border-color) !important;
  color: var(--bs-secondary-color) !important;
}
.ndx-lens-pick select:hover { color: var(--bs-body-color) !important; }

/* ── Height ──
   No flex cascade through the shell (body/main/container) and no relying on
   the parent chain: every window gets an explicit height written inline, so
   nothing depends on an ancestor resolving one. The only page-level rule is
   dropping the footer, which frees the dead space under the frame.

   Hidden at EVERY width, not just desktop: this page is a wall of live windows
   with no reading flow to end, so on a phone the footer was simply a band of
   empty page under the last tile. (exec.ndexr.io keeps its footer on mobile as
   a bottom anchor for iOS, but that page locks its height; this one scrolls
   naturally, so nothing depends on the anchor.) */
body > footer.footer { display: none !important; }

/* ── Mobile ─────────────────────────────────────────────────────────────────
   The page is built for a desktop/Quest wall, but it gets opened on a phone.
   Three things break there, all fixed with layout only - no separate mobile
   markup, so there is one DOM to reason about.

   Heights use svh (smallest viewport height) with a vh fallback declared
   first: on iOS, vh counts the area *behind* the browser chrome, so a tile
   sized in vh is partly unreachable on first paint. Engines without svh keep
   the vh line. */

/* 1. The top bar is flex-nowrap so the desktop row never breaks; on a phone
      that pushed the caption (and with it the pager) off-screen entirely.
      It wraps here - but into TWO rows, not three. Letting the caption take a
      full line of its own left the lens select stranded on a line of its own
      too, with a band of empty bar beside it; the status line is short enough
      to sit next to the lens. Drop the hairline separators (they only divide
      zones that are no longer on one line) and tighten the gap. */
@media (max-width: 700px) {
  .ndx-ctl-wrap {
    flex-wrap: wrap !important;
    row-gap: .3rem;
    gap: .35rem;
  }
  .ndx-bar-sep { display: none; }
  /* Lens + status share row two. min-width:0 lets the caption actually
     truncate instead of forcing a third row. */
  .ndx-ctl-wrap > .flex-grow-1 {
    flex: 1 1 auto !important;
    min-width: 0;
  }
  .ndx-lens-pick select { padding-right: 1.3rem; }
}

@media (max-width: 560px) {
  /* 2. Fit tiles are recomputed for the phone rather than floored. The inline
        height comes from `(100vh - CHROME) / rows` where CHROME (148px)
        describes the DESKTOP bar - on a phone the chrome is roughly twice
        that, and at 6 sites in one column each tile is a 1/6-screen sliver
        anyway. One comfortable height, and you page between them.
        Scoped to .ndx-fit so an explicitly chosen height still wins.
        !important because the height it competes with is written inline. */
  .ndx-tile iframe.ndx-fit {
    height: 62vh !important;
    height: 62svh !important;
    min-height: 0 !important;
  }

  /* 3. Tile tools: the bar keeps its shape, the contents shrink. No slot
        offsets to renumber any more - the flex bar orders them. Buttons stay
        24px, still tappable, and the domain label gives up width first. */
  /* Phone: the bar keeps its position and only gives up the label's width.
     Buttons stay 22px - shrinking them further drops below a tappable target,
     and there is no slot geometry to renumber any more. */
  .ndx-tile-dom { max-width: 30%; padding: 0 .3rem; }

  /* Bottom sheet on a phone: taller (there is no wide dimension to spend, so
     spend the tall one) and single-column, with a tighter section rhythm. */
  .ndx-ctl-wrap .offcanvas.offcanvas-bottom {
    max-height: 88vh;
    max-height: 88svh;
    border-radius: 14px 14px 0 0;
  }
  .ndx-ctl-wrap .offcanvas.offcanvas-bottom .offcanvas-body { columns: 1; }
  .ndx-sec { padding: .4rem 0 .45rem; }
  .ndx-sec-hd { margin-bottom: .3rem; }

  /* Paired fields (.ndx-fld-pair / .ndx-fld-split) are an even 1fr 1fr on
     desktop; at phone width two selects side by side are too narrow to read
     their own values, so they stack. */
  .ndx-fld-pair,
  .ndx-fld-split { grid-template-columns: 1fr !important; }
}

/* ══ Campaign Signals board (signals_view.r; flag NDX_CAMPAIGNS) ═════════════
   Cards on the same bottom-sheet idiom as Controls. High severity gets the
   reserved amber edge - same rule as fleet actions: amber = can hurt you. */
.ndx-sig-filters {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: .5rem;
}
@media (max-width: 700px) {
  .ndx-sig-filters { grid-template-columns: 1fr 1fr; }
}

.ndx-sig-card {
  border: 1px solid var(--bs-border-color);
  border-radius: 8px;
  background: var(--bs-tertiary-bg);
  padding: .55rem .7rem .5rem;
  margin-bottom: .55rem;
}
.ndx-sig-card.ndx-sig-high { border-left: 3px solid var(--ndx-warn); }

.ndx-sig-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .4rem;
  margin-bottom: .3rem;
}
.ndx-sig-id {
  font-family: var(--bs-font-monospace, ui-monospace, monospace);
  font-size: .7rem;
  color: var(--bs-secondary-color);
}
.ndx-sig-route {
  margin-left: auto;
  font-family: var(--bs-font-monospace, ui-monospace, monospace);
  font-size: .66rem;
  color: var(--bs-tertiary-color);
}
.ndx-sig-title { font-size: .82rem; font-weight: 600; color: var(--bs-emphasis-color); }
.ndx-sig-ask   { font-size: .74rem; color: var(--bs-body-color); margin: .15rem 0; }
.ndx-sig-ev {
  font-size: .68rem;
  color: var(--bs-secondary-color);
  font-family: var(--bs-font-monospace, ui-monospace, monospace);
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: 5px;
  padding: .25rem .45rem;
  margin: .2rem 0;
  word-break: break-word;
}
.ndx-sig-ev > span { color: var(--bs-tertiary-color); }
.ndx-sig-meta {
  display: flex;
  flex-wrap: wrap;
  gap: .7rem;
  font-size: .64rem;
  color: var(--bs-tertiary-color);
  font-variant-numeric: tabular-nums;
  margin-bottom: .35rem;
}

.ndx-sig-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .35rem;
}
/* Flatten .btn chrome to chip size (same trick as the tile cycle/X buttons). */
.ndx-carousel-root .ndx-sig-actions .btn.ndx-sig-act,
.ndx-ctl-wrap .offcanvas .btn.ndx-sig-act {
  font-size: .66rem;
  padding: .15rem .5rem;
  min-height: 26px;
  border: 1px solid var(--bs-border-color);
  border-radius: 6px;
  background: var(--bs-body-bg);
  color: var(--bs-secondary-color);
}
.ndx-ctl-wrap .offcanvas .btn.ndx-sig-act:hover {
  color: var(--bs-body-color);
  border-color: var(--ndx-accent);
}
.ndx-sig-merge {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  margin-left: auto;
}
.ndx-sig-merge .form-group,
.ndx-sig-merge .shiny-input-container { margin-bottom: 0 !important; width: 72px; }
.ndx-sig-merge input {
  font-size: .66rem;
  padding: .15rem .35rem;
  min-height: 26px;
}

/* Broadcast attachments: Shiny's fileInput is a full-width bordered block with
   a progress bar; inside the sheet it only needs to read as one more field. */
.ndx-bc-files .form-group,
.ndx-bc-files .shiny-input-container { margin-bottom: 0 !important; }
.ndx-bc-files .input-group { height: 32px; }
.ndx-bc-files .form-control,
.ndx-bc-files .input-group-btn,
.ndx-bc-files .btn {
  height: 32px;
  font-size: .78rem;
  padding-top: 0;
  padding-bottom: 0;
}
.ndx-bc-files .progress { height: 3px; margin-top: .25rem; margin-bottom: 0; }
