/* 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;
}

/* 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. */
:is(h1, h2, h3) ~ span.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;
  }
}

/* ---- View transitions ----------------------------------------------------
   Same-origin page navigations fade via view transitions instead of
   flashing a repaint (audit -> containers etc.). The model is
   ANIMATE-NOTHING-BY-DEFAULT: the root snapshot (all chrome - sidebar,
   logo, page background) is pinned static, and only the named content
   region (main) and the header text pair fade. Anything new added to a
   page is static in transitions unless it is explicitly named. Reduced
   motion opts out (the reduced-motion block above also zeroes animation
   durations). Chrome 126+; other browsers ignore all of this and keep
   plain navigation */
@media (prefers-reduced-motion: no-preference) {
  /* The custom animations below are for CROSS-DOCUMENT navigations only,
     so they are gated on the cross-doc active type: htmx partial swaps with
     transition:true start SAME-document view transitions against this very
     stylesheet, and ungated they made every chip/tab swap dip the whole
     page - sidebar logo and header included - to the background and back
     (the root fade-through + title rise/settle read as chrome flicker).
     Same-document transitions carry no types, match nothing here, and fall
     back to the UA default plus-lighter crossfade: unchanged pixels stay
     EXACTLY static (opacities sum to 1) while the swapped content
     crossfades - which is the wanted partial-swap look. Browsers that
     ignore the types descriptor simply keep default crossfades */
  @view-transition {
    navigation: auto;
    types: cross-doc;
  }
  /* THE ROOT IS PINNED: chrome is static because it is never animated,
     not because each piece is individually exempted. Everything without a
     view-transition-name below - the sidebar INCLUDING the logo above
     #main-nav, the drawer chrome, the page background - lives in the root
     snapshot, which snaps straight to the new state on every transition.
     Only the NEW snapshot shows: snapshots composite with plus-lighter,
     so keeping both visible double-exposes anything that differs (the
     sidebar menu-active highlight glowed on two nav entries at once).
     Before this, chrome was pinned piecemeal (console-sidebar on
     #main-nav, console-header on .page-header) while root faded - and
     every unnamed piece of chrome, like the logo, flickered with the root
     fade */
  ::view-transition-old(root) {
    display: none;
  }
  ::view-transition-new(root) {
    animation: none;
  }
  /* The page content region is the ONLY thing that fades on navigation.
     Fade-through: the old content fades fully out to the page background
     before the new one fades in. A plain crossfade double-exposes two
     dissimilar pages (audit's dense table over config's cards) which
     reads as a bright flash; sequencing through the solid background
     never overlaps them. The group box does not tween (transparent box,
     a size snap is invisible) */
  main {
    view-transition-name: console-content;
  }
  ::view-transition-group(console-content) {
    animation: none;
  }
  html:active-view-transition-type(cross-doc)::view-transition-old(
      console-content) {
    animation: vt-page-out 90ms ease-out both;
  }
  html:active-view-transition-type(cross-doc)::view-transition-new(
      console-content) {
    animation: vt-page-in 150ms ease-out 60ms both;
  }

  /* title and description animate as a pair: both carry the same
     view-transition-class (Chrome 125+), so the rise/settle rules are
     written once against *.console-pair. Names must still be unique */
  .page-title {
    view-transition-name: console-title;
    view-transition-class: console-pair;
  }
  .page-desc {
    view-transition-name: console-desc;
    view-transition-class: console-pair;
  }
  /* header right-side cluster (search box, page action buttons) fades with
     the same sequenced no-scale treatment */
  .page-actions {
    view-transition-name: console-actions;
    view-transition-class: console-pair;
  }

  /* THE STACKING INVARIANT — read before adding a view-transition-name.
     A view-transition-name creates a stacking context on the element even
     when no transition is running. An element in its own stacking context at
     z-index:auto is painted at the level of normal page content, so any
     popup opened from inside it (a daisyUI dropdown, a menu) is painted
     UNDER the content that follows it in the DOM - the menu opens but is
     hidden behind later cards. This bit .page-header (app detail Actions)
     and .page-actions (builder Publish) separately. `main` is named too
     (console-content), so every popup host inside it competes within
     main's own stacking context - the lift below resolves order there.

     Fix, in ONE place: every console chrome element that carries a
     view-transition-name is lifted above page content here. When you add a
     new view-transition-name to a chrome element that can host a popup, add
     its selector to this list - do not scatter per-element z-index. Chrome
     legitimately sits above page content, so an always-on lift is correct;
     it stays below the fixed overlays (.nav-progress z-index:100,
     .skip-link z-index:200). The names are only set for non-reduced-motion,
     so this rule lives in the same block. */
  .page-header,
  .page-actions {
    position: relative;
    z-index: 20;
  }
  /* No geometry tween and no snapshot scaling: the group box snaps (the
     titles are left-anchored transparent text, a width snap is invisible)
     and old/new keep their natural size, so differing title widths
     (Containers vs Configuration) never stretch the text. The fades run in
     sequence - old fully out, new landing just after - so two readable
     texts never overlap */
  html:active-view-transition-type(cross-doc)::view-transition-group(
      *.console-pair) {
    animation: none;
  }
  html:active-view-transition-type(cross-doc)::view-transition-old(
      *.console-pair),
  html:active-view-transition-type(cross-doc)::view-transition-new(
      *.console-pair) {
    width: auto;
    height: auto;
  }
  html:active-view-transition-type(cross-doc)::view-transition-old(
      *.console-pair) {
    animation: vt-title-out 90ms ease-out both;
  }
  html:active-view-transition-type(cross-doc)::view-transition-new(
      *.console-pair) {
    animation: vt-title-in 160ms cubic-bezier(0.2, 0.8, 0.2, 1) 70ms both;
  }
}

@keyframes vt-title-out {
  to {
    opacity: 0;
    transform: translateY(-4px);
  }
}

@keyframes vt-title-in {
  from {
    opacity: 0;
    transform: translateY(5px);
  }
}

@keyframes vt-page-out {
  to {
    opacity: 0;
  }
}

@keyframes vt-page-in {
  from {
    opacity: 0;
  }
}

/* ---- 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,
.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.
   Anchor-based .btn-disabled stays inert: those keep a real href */
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))::before,
.tooltip:not(:hover):not(.tooltip-open):not(:has(:focus-visible))::after {
  position: fixed;
}

/* ---- 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. The submit buttons disable so the operation cannot be
   double-submitted, and 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;
  transition: border-color 150ms ease, box-shadow 150ms ease;
}
.ov-tile:hover {
  border-color: color-mix(in srgb, var(--color-primary) 40%, var(--color-base-300));
  box-shadow: 0 4px 16px color-mix(in srgb, var(--color-primary) 10%, transparent);
}
/* 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%;
}
/* 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; brightens on hover for pop */
.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;
  transition: opacity 150ms ease;
}
.ov-tile:hover::after {
  opacity: 1;
}
/* Hero apps tile: faint brand glow anchored at the number's corner */
.ov-apps {
  grid-column: span 2;
  grid-row: span 2;
  background: radial-gradient(
    120% 90% at 0% 0%,
    color-mix(in srgb, var(--color-primary) 7%, var(--color-base-100)),
    var(--color-base-100) 62%
  );
}
.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);
}
/* Replication heartbeat: expanding ring on the live state dot */
.ov-pulse {
  position: relative;
}
.ov-pulse::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 9999px;
  background: inherit;
  animation: ov-ping 2.2s cubic-bezier(0, 0, 0.2, 1) infinite;
}
@keyframes ov-ping {
  0% {
    transform: scale(1);
    opacity: 0.55;
  }
  70%,
  100% {
    transform: scale(2.1);
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .ov-pulse::after {
    animation: none;
    opacity: 0;
  }
}

/* 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%;
}
