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

/* ---- <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 (the button is lg:hidden, desktop unaffected) */
.page-header > button[aria-controls='main-nav'] {
  margin-left: -0.875rem;
}

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

@media (min-width: 48rem) {
  .sidebar-logo {
    height: 6.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 {
  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 (upgraded from title attributes in console.js): 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;
}

/* ---- Boosted form posts ------------------------------------------------
   The full-page operation forms (app/sync/binding/service/config/builder
   create+update) are hx-boosted: 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;
}

