/* SPDX-License-Identifier: Apache-2.0 */
/* Accessibility overrides layered on top of the generated styles and
   style.css (loaded after both in the head define). Manual-edit file:
   the dev server only regenerates static/gen/. */

/* Skip link: visually hidden until it receives keyboard focus. Main content
   comes first in DOM order, so the link jumps forward to the navigation. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0.5rem;
  z-index: 200;
}
.skip-link:focus-visible {
  left: 0.5rem;
  padding: 0.4rem 0.8rem;
  border-radius: var(--radius-field);
  background: var(--color-primary);
  color: var(--color-primary-content);
  font-size: 0.85rem;
}

/* The theme toggle checkbox stays in the tab order. daisyui's swap hides
   the input (visibility: hidden), which also removes it from keyboard
   focus; keep it visible but transparent, stretched over the button so its
   focus ring wraps the control */
.swap input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  visibility: visible;
  cursor: pointer;
}

/* Muted text contrast (WCAG 1.4.3, the light theme is the strict case):
   daisyui stat-title/stat-desc use 55-60% content color, which lands below
   the 4.5:1 AA ratio on light. Table headers are exempt: they use the brand
   secondary color, see the .table thead th rule in style.css */
.stat-title,
.stat-desc {
  color: color-mix(in oklab, var(--color-base-content) 75%, transparent) !important;
}

/* daisyui .label mixes currentcolor at 60%, also below AA on light (the
   containers page filter/show-stopped labels) */
.label {
  color: color-mix(in oklab, var(--color-base-content) 75%, transparent);
}

/* Row actions and hover-revealed links are dimmed/hidden until the row is
   hovered; keep them fully visible for keyboard focus too */
[class*="group-hover:opacity-100"]:focus-within,
a[class*="group-hover:opacity-100"]:focus-visible,
tr:focus-within [class*="group-hover:opacity-100"] {
  opacity: 1;
}

/* Touch-only devices have no hover at all - tailwind additionally gates its
   group-hover variants behind @media (hover: hover) - so hover-revealed row
   links (apps/audit/containers ↗) would never appear. Show them always,
   undoing the hidden state's -translate-x-1 shift as well */
@media (hover: none) {
  [class*="group-hover:opacity-100"] {
    opacity: 1;
    translate: 0 0;
  }
}

/* ---- Optical alignment --------------------------------------------------
   Mixed font sizes in a vertically centered flex row: centering the boxes
   leaves the smaller text's glyphs sitting visibly high (its cap band ends
   up top-aligned with the heading's). Line-height changes cannot fix this -
   under box centering the baseline position depends only on the font size -
   so the small caption next to a heading is nudged down to optically center
   its glyph band within the heading's (a skipped_apps details caption
   too, next to the runs count). */
:is(h1, h2, h3) ~ :is(span, details).text-xs {
  position: relative;
  top: 0.11em;
}

/* The ⌘K hint: glyphs without descenders look high in the centered kbd
   pill; bias the content box down to optically center them */
.kbd {
  padding-top: 2px;
}

/* ---- Layout stability during page load ----------------------------------
   The sidebar (drawer-side) is the last element in the DOM, keeping the
   main content first for keyboard and reader order. On large pages the
   browser paints the streamed content before the sidebar arrives; daisyui
   sizes the sidebar grid column by content (grid-auto-columns:
   max-content), so the column is 0 wide until the sidebar parses in and
   the whole page then shifts right - visible as a flicker on refresh and
   when navigating to content-heavy pages. Reserve the docked sidebar
   column up front and pre-paint its surface and border. */
@media (min-width: 64rem) {
  .drawer.lg\:drawer-open {
    grid-template-columns: 15rem auto;
    background: linear-gradient(
      to right,
      var(--color-base-100) calc(15rem - 1px),
      var(--color-base-300) calc(15rem - 1px),
      var(--color-base-300) 15rem,
      transparent 15rem
    );
  }
  /* Once the sidebar has parsed it sizes (and paints) its own column, and
     the column must track the side's width so the native resize below can
     hand freed space to the content. The pre-paint gradient would otherwise
     leave a stray border line when the sidebar is resized narrower. */
  .drawer.lg\:drawer-open:has(.drawer-side) {
    grid-template-columns: max-content auto;
    background: none;
  }
}

/* Sidebar nav icons: primary-colored at rest; on the active item they
   inherit the pill's content color (primary green sits too dark on the
   active background) */
.menu li > a.menu-active svg {
  color: inherit;
}

/* Dark mode: daisyui's active pill is the neutral surface (#22312a), which
   is nearly invisible against the base-100 sidebar (#17221a). Route the
   pill through a primary-tinted lifted surface instead; light mode keeps
   the daisyui neutral (a dark pill on white, already high contrast). The
   guarded pair covers both an explicit data-theme choice and the
   system-preference default (no data-theme attribute). */
html[data-theme="openrun-dark"] #main-nav .menu {
  --menu-active-bg: color-mix(in oklab, var(--color-primary) 24%, var(--color-base-100));
  --menu-active-fg: var(--color-base-content);
}
@media (prefers-color-scheme: dark) {
  html:not([data-theme="openrun-light"]) #main-nav .menu {
    --menu-active-bg: color-mix(in oklab, var(--color-primary) 24%, var(--color-base-100));
    --menu-active-fg: var(--color-base-content);
  }
}

/* ---- <secret-input> component -----------------------------------------
   The custom element renders its light DOM from console.js, which loads in
   <head> before the body parses, so elements upgrade during parsing and
   pages never flash un-upgraded content. The :not(:defined) guard keeps an
   element invisible (while holding an input-sized layout box) in the
   unlikely case the script is delayed or fails. */
secret-input {
  display: block;
  min-width: 0;
}
secret-input:not(:defined) {
  visibility: hidden;
  min-height: calc(var(--size-field, 0.25rem) * 10); /* daisyui input height */
}

/* ---- Disabled controls must look disabled -------------------------------
   Row-action buttons carry text-error/text-primary color utilities and
   restore/save buttons use colored variants; the Tailwind utility wins over
   daisyui's :disabled color, so a disabled delete still rendered full red.
   Mute the text of anything disabled (loaded last, wins ties). */
.btn:disabled,
.btn[disabled],
.btn.btn-disabled,
a.btn-disabled {
  color: color-mix(in oklab, var(--color-base-content) 35%, transparent) !important;
}
/* Dropdown menu entries (app detail Actions) are NOT .btn: a disabled
   Delete kept its text-error (bright red) and the entry icons their
   text-primary. Inherit the li.menu-disabled muted color instead. */
.menu li.menu-disabled :is(.text-error, .text-primary) {
  color: inherit !important;
}
/* Mobile sign-in strip: hidden for the session once dismissed via its ✕
   button (sessionStorage flag stamped on <html> by the pre-paint head
   snippet, so no page in the session flashes the strip) */
html[data-signin-dismissed] #signin-strip {
  display: none;
}

/* Mobile page header: the hamburger is a 3.5rem btn-xl square around a
   1.75rem icon, so the icon sat visually inset from the content edge; the
   negative margin swallows the button's left padding so the icon lines up
   flush with the page content (.mobile-brand — the logo + hamburger stack —
   is lg:hidden, desktop unaffected) */
.page-header .mobile-brand {
  margin-left: -0.875rem;
}

/* The drawer button pulls up so its icon centers on the header's second
   text row (page-desc, or breadcrumbs on detail pages) instead of
   hanging below it — only the button's transparent top padding overlaps
   the logo's box, the visible icon still clears the logo image. */
.page-header .mobile-brand button {
  margin-top: -0.8125rem;
}

/* The header title block must share the hamburger's line: flex line-breaking
   uses the block's max-content width (its one-line page-desc), which pushed
   the whole block below the hamburger on mobile. flex-basis 0 keeps it on
   the first line and the desc wraps inside instead. */
.page-header > div:has(.page-title) {
  flex: 1 1 0%;
  min-width: 0;
}

/* Below lg (where the logo+hamburger stack renders) the title block pins
   to the top like the stack and nudges down 0.25rem so the h1's line
   center sits on the 3rem logo's center — letting the title block
   re-center against the taller stack floats the logo off the title line
   whenever the page-desc is short. */
@media (max-width: 63.9375rem) {
  .page-header > div:has(.page-title) {
    align-self: flex-start;
    margin-top: 0.25rem;
  }
}

/* Below sm the header actions get their own full-width line (basis 100%
   forces the wrap) instead of sharing line one — otherwise they'd squeeze
   the flex-basis-0 title block into a sliver. Actions right-align and wrap;
   the search box (w-full under sm) takes a full row by itself. */
@media (max-width: 39.9375rem) {
  .page-header .page-actions {
    flex-basis: 100%;
    flex-wrap: wrap;
    justify-content: flex-end;
  }

  /* Tighter tab padding and a slightly smaller face so three-way tab
     groups (apps: All / Pending promotion / Needs approval) hold one
     390px line with headroom for two-digit count badges. */
  .tabs .tab {
    --tab-p: 0.5rem;
    font-size: 0.8125rem;
  }
}

/* Keep the wide sidebar brand legible without crowding the 15rem drawer. */
.sidebar-logo {
  height: 3.75rem;
}

@media (min-width: 48rem) {
  .sidebar-logo {
    height: 5rem;
  }
}

/* ---- Page and swap transitions: NONE (2026-09-10) -------------------------
   Navigations and htmx swaps cut instantly. The console used to run view
   transitions (cross-document via @view-transition, same-document via
   hx-swap transition:true): first a sequenced fade-through, then a 150ms
   plus-lighter crossfade. Both read as flicker: the fade-through blinked
   the content region and an unchanged page title, and a crossfade
   double-exposes everything that differs (shifted rows ghost, two pages
   blend) for its whole duration. There is no animated transition that is
   free of such artifacts, so there is none. The layout head keeps
   `<link rel="expect" blocking="render">` so a navigation paints the new
   page in one complete frame (no partially rendered page), and Chrome's
   paint holding shows the old page until then. Do not add
   view-transition-name or transition:true back without re-reading this.

   Chrome elements that host popups (a daisyUI dropdown in the page header
   or the page actions cluster) are lifted above page content so a menu is
   never painted under the cards that follow it in the DOM; the lift stays
   below the fixed overlays (.nav-progress z-index:100, .skip-link
   z-index:200) */
.page-header,
.page-actions {
  position: relative;
  z-index: 20;
}

/* ---- Resizable panels ----------------------------------------------------
   Native CSS resize (no script): the browser renders a corner grip. The
   sidebar is resizable where the drawer is static (lg+); sizes are not
   persisted across navigations - that is inherent to the CSS-only approach */
@media (min-width: 64rem) {
  /* The resize must live on .drawer-side: the drawer grid column (the
     max-content template above) tracks the side's definite width, and with
     overflow hidden the grid item's automatic min-size floor is dropped so
     the column actually follows - resizing the aside alone would leave the
     column, and a dead gap, behind */
  .drawer-side {
    width: 15rem; /* starting size; the browser resize writes inline width */
    resize: horizontal;
    overflow: hidden;
    min-width: 11rem;
    max-width: 24rem;
  }
  aside.console-sidebar {
    width: 100%;
    min-width: 0;
  }
}

/* ---- AXE AA color contrast (audited with axe-core, both themes) --------
   The brand light green (--color-primary #00c200) is a FILL color: as text
   on light surfaces it measures ~2.3:1. The brand dark green
   (--color-secondary #007700, the design layer's table header color)
   measures 2.8:1 on the dark card background. --or-accent-text is the
   readable brand green for text in the active theme: dark green on light
   surfaces, bright green on dark. Buttons and badges with a primary FILL
   keep their designed colors (their content color is dark and passes). */
:root {
  --or-accent-text: #007700; /* light default, 5.7:1 on white */
  --or-error-text: #c02827; /* soft error badge text, 4.9:1 (brand error #d3302f is 4.46) */
  /* inactive tabs: daisyui dims them below 4.5:1 in both themes */
  --or-tab-dim: color-mix(in srgb, var(--color-base-content) 80%, transparent);
}
:root[data-theme="openrun-dark"] {
  --or-accent-text: var(--color-primary); /* 6.8:1 on the dark card */
  --or-error-text: var(--color-error);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="openrun-light"]) {
    --or-accent-text: var(--color-primary);
    --or-error-text: var(--color-error);
  }
}
.text-primary {
  color: var(--or-accent-text);
}
.badge-soft.badge-primary,
.badge-soft.badge-secondary, /* RBAC grant role badges: brand green 2.8:1 on dark */
.btn-soft.btn-primary {
  color: var(--or-accent-text);
}
.badge-soft.badge-error,
.alert-soft.alert-error {
  color: var(--or-error-text);
}
.table thead th {
  color: var(--or-accent-text);
}
.tabs .tab:not(.tab-active) {
  color: var(--or-tab-dim);
}

/* daisy tooltips (rendered by the templates as class="tooltip" +
   data-tip, no JS involved): cap the bubble width and wrap - long
   identifiers would otherwise render one enormous single line */
.tooltip:before {
  max-width: 18rem;
  white-space: normal;
}
/* On narrow screens an 18rem bubble is wider than the space beside its
   anchor: table badges (tooltip-left mid-screen) and row actions ran past
   the viewport edge. Cap tighter so the bubble wraps and stays on screen;
   dropdown menus sit hard against the screen edge and need the narrowest
   cap (the dropdown itself is ~15rem of the 390px viewport). */
@media (max-width: 47.9375rem) {
  .tooltip:before {
    max-width: 10rem;
  }
  .dropdown-content .tooltip:before {
    max-width: 8rem;
  }
}

/* Disabled buttons keep hover so their "requires <perm>" tooltip shows:
   daisyui sets pointer-events:none on disabled buttons, which suppressed
   the permission tooltips entirely (native titles included, a long
   standing gap). A disabled button still cannot be activated - the
   disabled attribute blocks click dispatch - only hover is restored.
   Disabled anchors must omit href in their markup; this rule restores
   pointer events and does not itself prevent anchor navigation. */
button.tooltip:disabled,
a.tooltip.btn-disabled {
  pointer-events: auto;
  cursor: not-allowed;
}

/* Sidebar tooltips must stay INSIDE the sidebar clip box (.drawer-side has
   overflow:hidden for the resize support): nav items and footer controls
   use top/bottom placement with a bubble narrower than the sidebar. Nav
   labels are self-explanatory, so the bubbles (which overlap the next
   item) appear only after a hover-intent delay - the delay lives on the
   :hover state only, hiding stays immediate */
.drawer-side .tooltip:before {
  max-width: 11rem;
}
.drawer-side .tooltip:hover:before,
.drawer-side .tooltip:hover:after {
  transition: opacity 150ms ease-out 700ms;
}

/* ---- Mobile layout: let scrollers actually scroll ----------------------
   Grid/flex items default to min-width:auto, so a wide table propagates
   its min-content width through .card / .drawer-content up to the page
   (the body scrolls sideways) instead of scrolling inside its own
   .overflow-x-auto container. Zero the floor on the standard containers.
   And daisyui keeps .label-text-alt on one line - long field helper text
   must wrap. */
.drawer-content,
.card,
.card-body {
  min-width: 0;
}
.label {
  min-width: 0;
}
.label-text-alt {
  white-space: normal;
  min-width: 0;
}

/* Hidden tooltip bubbles must not widen the page. daisyui keeps the
   bubble in layout (position:absolute, opacity:0), so a bubble near the
   right viewport edge adds horizontal page scroll on mobile even though
   nothing is visible. While hidden, park it as position:fixed - fixed
   boxes never contribute to scrollable overflow - and let the show
   states (mirroring daisyui's) restore the normal absolute placement.
   The opacity transitions (and the sidebar show delay) are unaffected. */
.tooltip:not(:hover):not(.tooltip-open):not(:has(:focus-visible)) > .tooltip-content,
.tooltip[data-tip]:not(:hover):not(.tooltip-open):not(:has(:focus-visible)):not([hx-disable][disabled])::before,
.tooltip:not(:hover):not(.tooltip-open):not(:has(:focus-visible))::after {
  position: fixed;
}

/* In-flight spinner on tooltip buttons. The spinner (style.css
   button[hx-disable][disabled]::before) and the daisyui tooltip bubble
   share the button's ::before: only content/size/border came from the
   spinner rule, the bubble's absolute placement, padding, background and
   hover-only opacity still applied, so the spinner appeared OUTSIDE the
   button at the bubble position (below it for tooltip-bottom) and only
   while the pointer stayed over the button (the hover restore above).
   While the request runs, reset the bubble box so the spinner is a plain
   inline flex item before the label (the .btn gap spaces it), always
   visible, and hide the bubble's arrow (::after). The parking rule above
   excludes the in-flight state for the same reason. */
button.tooltip[hx-disable][disabled]::before {
  position: static;
  inset: auto;
  transform: none;
  opacity: 1;
  color: inherit;
  background: none;
  padding: 0;
  max-width: none;
  z-index: auto;
  transition: none;
}
button.tooltip[hx-disable][disabled]::after {
  display: none;
}

/* ---- Op-form posts -------------------------------------------------------
   The full-page operation forms (app/sync/binding/service/config/builder
   create+update) post via hx-post with a narrow target (the op_form
   block): htmx adds .htmx-request to the form while its request is in
   flight. CSS dims the submit buttons and blocks pointer clicks; it does
   not disable keyboard submission. The clicked button (marked .btn-inflight
   by console.js from the submit event's submitter - CSS alone cannot tell
   Validate from Create) shows the shared inline spinner (btn-spin
   keyframes in style.css). */
form.htmx-request button[type="submit"] {
  pointer-events: none;
  opacity: 0.75;
}
.btn-inflight::before {
  content: "";
  display: inline-block;
  width: 0.7em;
  height: 0.7em;
  flex: none;
  border-radius: 9999px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  animation: btn-spin 0.5s linear infinite;
}

/* ---- Badges: grow with wrapped text -------------------------------------
   daisyui pins the badge pill to a one-line height (height: var(--size)),
   so a label that wraps in a narrow spot - e.g. the openrun-developer role
   badge in the RBAC grants table - renders OUTSIDE the pill. Let the pill
   grow with its content instead; single-line badges keep the exact daisyui
   height through min-height. */
.badge {
  height: auto;
  min-height: var(--size);
}

/* ---- Builder chat: tool-call chips --------------------------------------
   Agent tool titles are raw shell commands that can run hundreds of
   characters; cap the chip with an ellipsis (the full command stays
   readable via the native title= tooltip - unbounded text keeps native
   titles by convention). Used by the server-rendered transcript and the
   live chips appended by builderchat.js. */
builder-chat .bc-tool-title {
  display: inline-block;
  max-width: 26rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}


/* ---- Overview bento grid ------------------------------------------------
   Tile and chart styling for the overview home page (/ and /overview).
   Console-owned layer: the pro analytics chart CSS (.an-*) lives in the
   console-pro repo and is not shared. Bars are single-hue magnitude marks
   in the readable brand green (--or-accent-text); status dots use the
   theme status tokens; folded tails keep the de-emphasis gray. Tiles get
   NO hover transform: a transform would create a stacking context and
   push the dot tooltips under later tiles (see THE STACKING INVARIANT
   note above the popup-host lift rule). */
.ov-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* dense packing: tiles the user cannot read are omitted, the remaining
     ones close the gaps */
  grid-auto-flow: dense;
  gap: 1rem;
}
.ov-tile {
  position: relative;
  min-width: 0;
  background: var(--color-base-100);
  border: 1px solid var(--color-base-300);
  border-radius: var(--radius-box);
  padding: 1rem;
}
/* Tiles are positioned siblings painting in DOM order, so a dot tooltip
   bubble reaching into a LATER tile's area was painted under it (wide
   bubbles for long sync repo paths). Lift the interacted tile above its
   siblings; hover-only would miss keyboard focus on the dot links */
.ov-tile:hover,
.ov-tile:focus-within {
  z-index: 10;
}
/* THE STACKING INVARIANT, sibling flavor, generalized: a tooltip bubble
   from a host in static flow paints UNDER any later positioned sibling at
   z auto - daisyui .card is position:relative, so a tooltip-bottom bubble
   on the containers filter chips disappeared behind the list card below.
   daisyui .tooltip is position:relative, so lifting the host while its
   bubble shows puts the bubble above later siblings; transient, so it
   cannot mask dropdown/menu layers at rest */
.tooltip:hover,
.tooltip.tooltip-open,
.tooltip:has(:focus-visible) {
  z-index: 30;
}
/* Bottom bubble anchored to the host's LEFT edge instead of centered: for
   tooltip hosts near the content's left edge (containers page's Running
   chip), whose centered bubble would reach under the sidebar (DOM-last,
   paints above everything in main). Overrides daisyui's left:50% +
   translateX(--tt-trans) centering; the arrow stays centered on the host */
.tooltip-anchor-start.tooltip-bottom[data-tip]::before {
  left: 0;
  --tt-trans: 0%;
}
/* The same, only while the sidebar is docked (lg:drawer-open): the page
   title's docs link. On phones the sidebar is a hidden drawer and a long
   title pushes the link to the right edge, where a left-anchored bubble
   would run past the viewport, so it stays centered there */
@media (min-width: 64rem) {
  .tooltip-anchor-start-lg.tooltip-bottom[data-tip]::before {
    left: 0;
    --tt-trans: 0%;
  }
}
/* From the tablet width up: (1) table scroll boxes stop clipping bubbles.
   .overflow-x-auto makes BOTH axes clip (overflow-y computes to auto), so
   a left/right bubble taller than its row was cut at the box's top or
   bottom edge on first/last rows and in short tables (2-5 line tips lost
   up to a quarter of their height), and top bubbles on the first row lost
   their top. The wrappers exist for PHONE widths, where a wide table has
   to scroll inside its card; no table needs to scroll from 48rem up
   (measured over every page), so the box becomes x-clip (still no spill
   past the card) and y-visible. Only wrappers marked .ox-fit: console.js
   (fitTableWrappers) sets the class on a table wrapper whose table fits
   its width and removes it when the table is wider - that table keeps
   the scroll box, a clipped one would lose its last columns with no way
   to reach them. Code and log panes are never marked. (2) page header controls anchor their bottom
   bubble to the host's RIGHT edge: page-actions sit at the right edge of
   main, where a centered bubble (wider than its button) ran past the
   viewport */
@media (min-width: 48rem) {
  main .overflow-x-auto.ox-fit {
    overflow-x: clip;
    overflow-y: visible;
  }
  .page-actions .tooltip.tooltip-bottom[data-tip]::before {
    left: auto;
    right: 0;
    --tt-trans: 0%;
  }
}
/* Belt for the dot bubbles: the tips are tail-truncated server-side, the
   cap keeps any remaining long tip from running off the tile */
.ov-viz .tooltip:before {
  max-width: 18rem;
}
/* Brand hairline riding each tile's top edge, covering half the tile
   width */
.ov-tile::after {
  content: "";
  position: absolute;
  top: -1px;
  left: 0.9rem;
  width: 50%;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(
    90deg,
    var(--color-secondary),
    color-mix(in oklch, var(--color-secondary) 5%, transparent)
  );
  opacity: 0.55;
}
/* The server identity minis: a row of their own across the grid, the
   tiles sharing the width equally however many there are */
.ov-minis {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 1rem;
}
/* The Remote API mini lists three named surfaces with their state: one
   track truncated "Management MCP", it takes two */
.ov-mini-wide {
  grid-column: span 2;
}
/* The containers type filter is six joined buttons: at the default
   padding the row is a few px wider than a phone */
@media (max-width: 39.9375rem) {
  [aria-label="Filter containers by type"] > .btn {
    padding-inline: 0.5rem;
  }
}
/* On a phone the minis pair up: an odd one out takes the full row instead
   of sitting beside an empty cell */
@media (max-width: 39.9375rem) {
  .ov-minis > :last-child:nth-child(odd) {
    grid-column: 1 / -1;
  }
}
/* Hero apps tile: the page's one oversized element */
.ov-apps {
  grid-column: span 2;
  grid-row: span 2;
}
.ov-syncs,
.ov-repl {
  grid-column: span 2;
  grid-row: span 2;
}
.ov-containers,
.ov-services {
  grid-column: span 2;
}
.ov-activity {
  grid-column: 1 / -1;
}
/* Two columns under lg (the sidebar breakpoint): area tiles full width,
   the server minis pair up */
@media (max-width: 1023.98px) {
  .ov-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .ov-apps,
  .ov-syncs,
  .ov-repl {
    grid-row: auto;
  }
}

/* Numerals: the display face, tabular so a grid re-render (Refresh
   button, lazy tile swap-ins) never shifts layout. The hero number uses
   the readable brand green */
.ov-num {
  font-family: "Space Grotesk", var(--font-sans);
  font-size: clamp(2.5rem, 5vw, 3.25rem);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--or-accent-text);
}
.ov-num-sm {
  font-family: "Space Grotesk", var(--font-sans);
  font-size: 1.75rem;
  font-weight: 700;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.ov-mini-num {
  font-family: "Space Grotesk", var(--font-sans);
  font-size: 1.4rem;
  /* Grotesk ships 500/700; 600 resolved to 700 anyway - say what renders */
  font-weight: 700;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
}
.ov-label {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-base-content) 70%, transparent);
}

/* Magnitude bars (apps by spec): thin marks, rounded data-end, recessive
   track one step off the card surface */
.ov-track {
  height: 12px;
  border-radius: 0 4px 4px 0;
  background: var(--color-base-200);
  overflow: hidden;
}
.ov-bar {
  height: 100%;
  border-radius: 0 4px 4px 0;
  min-width: 2px;
  background: var(--or-accent-text);
}
.ov-bar-other {
  background: #898781;
}

/* Status dots (sync/replication/container rows): status tokens only (no
   series identity). Values always render as text next to the dots */
.ov-dot {
  display: inline-block;
  flex: none;
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 9999px;
}
.ov-dot-ok {
  background: var(--color-success);
}
.ov-dot-err {
  background: var(--color-error);
}
.ov-dot-warn {
  background: var(--color-warning);
}
.ov-dot-off {
  background: color-mix(in srgb, var(--color-base-content) 30%, transparent);
}
/* The activity ticker box shows the header plus ~10 table rows, the rest
   scroll; the header stays pinned while scrolling (it needs an opaque
   surface or rows show through) */
.ov-ticker {
  max-height: 17.5rem;
  overflow-y: auto;
  overflow-x: auto;
}
.ov-ticker thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--color-base-100);
}

/* ---- Sidebar nav group titles -------------------------------------------
   daisyui dims .menu-title below the 4.5:1 AA bar on both console themes;
   the nav group labels (Deploy / Operate) re-anchor at the /70 floor. */
.menu .menu-title {
  color: color-mix(in srgb, var(--color-base-content) 70%, transparent);
}

/* Tiles hosting full-bleed content (tables, stats strips - the analytics
   cards): the content runs to the tile edges, headers carry their own
   padding. Declared after .ov-tile so it wins the padding tie */
.ov-tile.ov-flush {
  padding: 0;
}

/* App detail Config/Compare tabs: numbered export panes and the aligned
   side-by-side diff. Every diff row renders on both panes (spacer rows keep
   the line alignment); removed lines tint the left pane, added lines the
   right, with an inset edge so color is not the only marker */
.diff-code {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  line-height: 1.6;
  padding: 0.4rem 0;
  min-width: max-content;
}
.diff-row {
  display: flex;
  white-space: pre;
}
.diff-ln {
  flex: none;
  width: 2.6rem;
  text-align: right;
  padding-right: 0.75rem;
  color: color-mix(in oklab, var(--color-base-content) 45%, transparent);
  user-select: none;
  font-variant-numeric: tabular-nums;
}
.diff-tx {
  padding-right: 1.25rem;
}
.diff-add {
  background: color-mix(in oklab, var(--color-success) 14%, transparent);
  box-shadow: inset 3px 0 0 var(--color-success);
}
.diff-del {
  background: color-mix(in oklab, var(--color-error) 12%, transparent);
  box-shadow: inset 3px 0 0 var(--color-error);
}

/* Python highlighting for the config/diff panes (Config and Compare tabs):
   the same token mapping as the builder-files viewer theme in
   builderfiles.js, on the console's contrast-safe tokens */
.diff-code .hljs-comment,
.diff-code .hljs-quote {
  color: color-mix(in oklab, var(--color-base-content) 62%, transparent);
  font-style: italic;
}
.diff-code .hljs-keyword,
.diff-code .hljs-meta,
.diff-code .hljs-title,
.diff-code .hljs-name {
  color: var(--or-accent-text);
  /* mono ships 400/500 only; 600 would silently resolve to 500 anyway */
  font-weight: 500;
}
.diff-code .hljs-string {
  color: var(--color-success);
}
.diff-code .hljs-number,
.diff-code .hljs-literal {
  color: var(--color-accent);
}
.diff-code .hljs-built_in {
  color: var(--color-warning);
}

/* Bindings-page card health indicator: the unhealthy chip is a <details>
   whose failure panel expands IN FLOW inside the card header, pushing the
   table down - never a popup (a dropdown opened from one card paints under
   the next positioned sibling card: THE STACKING INVARIANT). Open, the
   details takes a full-width line of the flex-wrap header so the panel gets
   the card's whole width. */
.bh-details > summary {
  list-style: none;
}
.bh-details > summary::-webkit-details-marker {
  display: none;
}
.bh-details[open] {
  flex-basis: 100%;
}
