/* ---- Tokens ---- */

:root {
  /* Active tokens default to the light palette. */
  color-scheme: light;
  --pliego-primary: #1D3341;
  --pliego-fg: #1a1a1a;
  --pliego-bg: #ffffff;
  --pliego-surface: #f5f5f5;
  --pliego-muted: #5a5a5a;
  --pliego-border: #e0e0e0;
  /* Muted text on a border-colored fill (.badge--muted). Light reuses
     --pliego-muted: #5a5a5a on #e0e0e0 is 5.23:1. Dark cannot: #8b949e on
     #30363d is 3.96:1 at 12.8px normal, short of 4.5:1. The dark value is
     --pliego-dark-muted-on-border. */
  --pliego-muted-on-border: var(--pliego-muted);
  --pliego-error: #b00020;
  /* --pliego-warn/--pliego-success: darkened from #9c5d0a/#1b8a3d. Both are read as small text
     directly (.live-region__state) and as text sitting on their own translucent .16-alpha tint
     (.badge--warn/.badge--ok): because
     the tint's own hue is the token's hue, MORE tint pulls the composite toward the text colour
     itself and LOWERS contrast, so the badge role is satisfied by darkening the token rather than
     touching the shared .16 alpha. The old #1b8a3d measured only 4.06:1 against --pliego-surface
     even with zero tint (already short of 4.5:1 as plain text, before any badge is involved);
     #167032 measures 4.52:1 tinted at .16 against --pliego-surface (the harder of the two
     backgrounds a badge sits on) and 6.18:1 against white as a plain-text/background-under-white
     colour. #9c5d0a already passed as plain text (4.84:1) - only the tinted-badge role failed
     (3.93:1); #8d5409 measured 4.50:1 tinted at .16 against --pliego-surface - a value that sits
     exactly on the 4.5:1 boundary is a defect of the same shape as the one this token was being
     fixed for, not a pass: a checker computing the ratio independently can round it under the
     line. #8a5209 measures 4.66:1 tinted at .16 against --pliego-surface, real margin rather than
     a boundary value. Dark-mode --pliego-dark-success/--pliego-dark-warn are unchanged: the same
     .16-alpha tint composited with the dark-mode text colour already clears 4.5:1 on both dark
     backgrounds (4.92:1/6.88:1 worst case) without adjustment. */
  --pliego-warn: #8a5209;
  --pliego-success: #167032;

  /* --pliego-primary-hover/--pliego-link/--pliego-accent: one value, because every use of it
     pairs against white either way round (a resting-state link on the white/near-white page,
     or white text set on it as a button/hover background) - darkening it once satisfies both
     directions at the same time. The former #2B84A2 measured 4.26:1 against white, short of the
     4.5:1 AA text minimum; #25778F measures 5.10:1 against white and 4.68:1 against
     --pliego-surface (#f5f5f5), the next-lightest background a link actually sits on
     (.form-card, etc). Rejected: a per-component override (e.g. only
     .breadcrumb__item or only .btn--primary) - the failing value is the token itself, used by
     both a text role and a background-under-white-text role, so only fixing the token closes
     every use at once instead of chasing each caller. */
  --pliego-primary-hover: #25778F;
  --pliego-link: #25778F;
  --pliego-accent: #25778F;

  /* Muted text on the sidebar navy (#1D3341, theme-independent): 5.1:1, AA for the small
     role/version lines in the sidebar footer. The old rgba(.3)/.45 grays sat at 2.6-3.9:1. */
  --pliego-on-navy-muted: rgba(255, 255, 255, .55);

  /* --pliego-primary-text/--pliego-danger-bg: split roles of --pliego-primary/--pliego-error the
     same way --pliego-link/--pliego-accent split from --pliego-primary-hover in the dark palette
     below - one token cannot pass both "read as text on the page background" and "read as the
     background under white text" once light and dark diverge. In light both roles already share
     one passing value (--pliego-primary is #1D3341 navy, huge margin either way; --pliego-error is
     #b00020, 7.33:1 either way), so both new tokens simply alias the existing one here. Dark is
     where they diverge - see the dark palette comment. */
  --pliego-primary-text: var(--pliego-primary);
  --pliego-danger-bg: var(--pliego-error);

  /* Dark palette values - single source of truth (aligned to the eppsc dark theme: soft,
     slightly blue-tinted, comfortable contrast), referenced by the dark selectors below. */
  --pliego-dark-primary: #4a7a9a;
  --pliego-dark-fg: #e6edf3;
  --pliego-dark-bg: #0f1419;
  --pliego-dark-surface: #1a2129;
  --pliego-dark-muted: #8b949e;
  --pliego-dark-border: #30363d;
  /* #9ca4ad on #30363d is 4.84:1. Lightening --pliego-dark-muted itself would
     restyle every muted line; darkening the badge fill until #8b949e passes
     (#181e25) sinks the chip into the page (#0f1419) and the surface (#1a2129). */
  --pliego-dark-muted-on-border: #9ca4ad;
  --pliego-dark-error: #ef5350;
  --pliego-dark-warn: #f0a64d;
  --pliego-dark-success: #4caf50;

  /* Unlike the light palette above, --pliego-dark-link cannot share one value with
     --pliego-dark-primary-hover/--pliego-dark-accent: dark-link pairs against the dark page
     background (near-black, #0f1419/#1a2129), which needs a LIGHT enough colour, while
     primary-hover/accent pair against white text set on them as a background, which needs a
     DARK enough colour - the same value cannot sit on both sides of 4.5:1 against a near-black
     and against white at once (it would need luminance <=0.183 for the white pairing and
     >=0.205 for the near-black pairing, which do not overlap). The former shared #5b9bb8
     measured 6.02:1 against --pliego-dark-bg and 5.28:1 against --pliego-dark-surface (both
     already AA, left as --pliego-dark-link, unchanged) but only 3.08:1 for white text set on it
     as a background (.btn--primary, .btn--primary:hover, .btn:hover) - short of 4.5:1.
     #43738C measures 5.16:1 against white, so primary-hover/accent split off to it. Rejected:
     opacity or a lighter/darker overlay on the shared colour per component - the two roles need
     opposite luminance directions from the same starting value, which no single filter expresses;
     they are two different tokens now, not two renderings of one. */
  --pliego-dark-primary-hover: #43738C;
  --pliego-dark-link: #5b9bb8;
  --pliego-dark-accent: #43738C;

  /* --pliego-dark-primary-text: --pliego-dark-primary (#4a7a9a) already passes as the background
     under white text (.btn, .pager__btn--current, 4.62:1) but fails read as text on the dark
     page (3.51:1 against --pliego-dark-surface, the
     live .topbar-tenant__switch/.badge--info pairing) - the same two-role conflict as dark-link
     vs dark-accent above, so text-role readers get their own token instead of a third hardcoded
     colour. --pliego-dark-link already exists for exactly "light enough to read on near-black"
     (6.02:1/5.28:1, see above); reused rather than restated. */
  --pliego-dark-primary-text: var(--pliego-dark-link);

  /* --pliego-dark-danger-bg: --pliego-dark-error (#ef5350) is read as small text
     (.required-star, badge--bad's own tint) at 4.66:1/5.31:1 against
     the two dark backgrounds - fine - but the same value used as the background under white text
     (.btn--danger "Cerrar sesión", .topbar-bell__badge, .icon-btn--danger:hover) measures only
     3.49:1, short of 4.5:1: #ef5350 is a bright, light-leaning red chosen to read as text on
     near-black, not to sit under white. Darkening it to pass the white-text pairing would in turn
     fail the text-on-near-black pairing (checked: darkening enough for white-on-it >=4.5:1 drops
     text-on-surface to 3.60:1) - another instance of one value unable to serve both roles, so a
     second, background-only value is introduced rather than compromising either reading. #e71915
     measures 4.62:1 for white text set on it. */
  --pliego-dark-danger-bg: #e71915;
}

/* Dark theme token mapping. app.js resolves the light/dark/system preference and writes the
   effective value to data-theme, so this drives explicit dark *and* system->dark. The @media
   block only takes effect before app.js runs (or with JS disabled), giving the right palette
   from the OS preference with no flash. Only the mapping is restated; values live once above. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --pliego-primary: var(--pliego-dark-primary);
  --pliego-primary-hover: var(--pliego-dark-primary-hover);
  --pliego-fg: var(--pliego-dark-fg);
  --pliego-bg: var(--pliego-dark-bg);
  --pliego-surface: var(--pliego-dark-surface);
  --pliego-muted: var(--pliego-dark-muted);
  --pliego-border: var(--pliego-dark-border);
  --pliego-muted-on-border: var(--pliego-dark-muted-on-border);
  --pliego-error: var(--pliego-dark-error);
  --pliego-warn: var(--pliego-dark-warn);
  --pliego-link: var(--pliego-dark-link);
  --pliego-success: var(--pliego-dark-success);
  --pliego-accent: var(--pliego-dark-accent);
  --pliego-primary-text: var(--pliego-dark-primary-text);
  --pliego-danger-bg: var(--pliego-dark-danger-bg);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    color-scheme: dark;
    --pliego-primary: var(--pliego-dark-primary);
    --pliego-primary-hover: var(--pliego-dark-primary-hover);
    --pliego-fg: var(--pliego-dark-fg);
    --pliego-bg: var(--pliego-dark-bg);
    --pliego-surface: var(--pliego-dark-surface);
    --pliego-muted: var(--pliego-dark-muted);
    --pliego-border: var(--pliego-dark-border);
    --pliego-muted-on-border: var(--pliego-dark-muted-on-border);
    --pliego-error: var(--pliego-dark-error);
    --pliego-warn: var(--pliego-dark-warn);
    --pliego-link: var(--pliego-dark-link);
    --pliego-success: var(--pliego-dark-success);
    --pliego-accent: var(--pliego-dark-accent);
    --pliego-primary-text: var(--pliego-dark-primary-text);
    --pliego-danger-bg: var(--pliego-dark-danger-bg);
  }
}

/* ---- Base ---- */

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  color: var(--pliego-fg);
  background: var(--pliego-bg);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

.live-region__state { color: var(--pliego-muted); font-size: .875rem; }
.live-region[data-live-state="connected"] .live-region__state { color: var(--pliego-success); }
.live-region[data-live-state="stale"] .live-region__state { color: var(--pliego-warn); font-weight: 600; }

/* Consistent headings so content never reads as loose text floating on the page. */
h1 { font-size: 1.5rem; font-weight: 700; color: var(--pliego-fg); margin: 0 0 1.25rem; line-height: 1.2; }
h2 { font-size: 1.15rem; font-weight: 600; color: var(--pliego-fg); margin: 1.75rem 0 .75rem; line-height: 1.25; }
h3 { font-size: 1rem; font-weight: 600; color: var(--pliego-fg); margin: 1.25rem 0 .5rem; }
/* Standalone explanatory paragraphs read as muted intro text under the heading, not body noise. */
.page-content > p { color: var(--pliego-muted); max-width: 70ch; }

a { color: var(--pliego-link); }

/* Adjacent buttons never touch. Containers that already use flex gap opt out. */
.btn + .btn { margin-left: .5rem; }
:where(.row-actions, .page-header__actions, .pager__nav, .bulk-bar, .export-menu, .filters, .topbar-actions) .btn + .btn { margin-left: 0; }
td form + form, td form + .btn, td .btn + form, td a.btn + form, td form + a.btn { margin-left: .35rem; }

:focus-visible { outline: 3px solid var(--pliego-primary); outline-offset: 2px; }

/* Every shell's skip link: off-screen until focused, then the first
   keyboard stop, so Tab jumps straight past the sidebar/topbar to <main>. */
.skip-link {
  position: absolute;
  top: -3rem;
  left: 0;
  z-index: 2000;
  padding: .6rem 1rem;
  background: var(--pliego-primary);
  color: #fff;
  text-decoration: none;
  border-radius: 0 0 8px 0;
  transition: top .15s;
}
.skip-link:focus { top: 0; }

/* ---- Layout: Sidebar + Topbar ---- */
.layout-wrapper {
  display: flex;
  min-height: 100vh;
}

/* Sidebar - fixed 250px */
.sidebar {
  position: fixed;
  top: 0;
  left: 0;
  width: 250px;
  height: 100vh;
  background: #1D3341;
  color: #fff;
  display: flex;
  flex-direction: column;
  z-index: 500;
  overflow: hidden;
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: 1rem 1.25rem;
  font-size: 1.1rem;
  font-weight: 700;
  color: #fff;
  text-decoration: none;
  border-bottom: 1px solid rgba(255,255,255,.08);
  flex-shrink: 0;
}
.sidebar-brand:hover { color: #fff; }
.sidebar-brand-logo { height: 52px; width: auto; max-width: 100%; display: block; }

.sidebar-nav {
  flex: 1;
  overflow-y: auto;
  padding: .5rem 0;
}
.sidebar-nav::-webkit-scrollbar { width: 4px; }
.sidebar-nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,.2); border-radius: 2px; }

.sidebar-section {
  padding: .75rem 1.25rem .25rem;
  font-size: .7rem;
  text-transform: uppercase;
  letter-spacing: .08em;
  /* --pliego-on-navy-muted (.55, :root), not rgba(255,255,255,.4): that alpha composites to
     #77858d on the sidebar's own #1D3341, 3.44:1 - the exact class of defect the token exists to
     close (DECISIONS.md 246). */
  color: var(--pliego-on-navy-muted);
  font-weight: 600;
}

.sidebar-nav-link {
  display: flex;
  align-items: center;
  gap: .65rem;
  padding: .55rem 1.25rem;
  color: rgba(255,255,255,.75);
  text-decoration: none;
  font-size: .875rem;
  border-left: 3px solid transparent;
  transition: background .15s, color .15s;
}
.sidebar-nav-link:hover {
  background: rgba(255,255,255,.08);
  color: #fff;
}
.sidebar-nav-link--active {
  background: rgba(43,132,162,.2);
  /* Not #2B84A2 (the pre-246 accent literal): as a UI-component boundary it needs 3:1 against
     the surface it borders, which here is this rule's own translucent background composited over
     the theme-independent sidebar navy - #204354. #2B84A2 measures only 2.47:1 against that
     composite (3.07:1 against plain navy alone is not the adjacent colour); --pliego-dark-link
     (#5b9bb8), already established above as "light enough to read on navy/near-black", measures
     3.43:1 against the same composite and 4.26:1 against plain navy - reused rather than a third
     navy-adjacent token. */
  border-left-color: var(--pliego-dark-link);
  color: #fff;
}
.sidebar-nav-link i { font-size: 1.1rem; width: 1.25rem; text-align: center; }

.sidebar-footer {
  padding: .75rem 1.25rem;
  border-top: 1px solid rgba(255,255,255,.08);
  font-size: .8rem;
  flex-shrink: 0;
}
.sidebar-user { display: flex; align-items: center; gap: .6rem; }
.sidebar-user-avatar { font-size: 1.6rem; color: rgba(255,255,255,.6); line-height: 1; flex-shrink: 0; }
.sidebar-user-id { min-width: 0; }
.sidebar-user-name { font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sidebar-user-role { color: var(--pliego-on-navy-muted); font-size: .72rem; text-transform: capitalize; }
.sidebar-footer-meta { color: var(--pliego-on-navy-muted); font-size: .7rem; margin-top: .5rem; font-variant-numeric: tabular-nums; }
/* No opacity: it silently multiplies the inherited --pliego-muted's own already-AA foreground
   against whatever the footer's background happens to be, rather than stating a colour that is
   checked against it. The .7 opacity computed to #8c8c8c on white, 3.36:1, and to #666e76 on
   the dark page background, 3.58:1 - both short of 4.5:1 - while the plain inherited
   --pliego-muted the rest of .app-footer already uses measures 6.90:1 on white and 6.02:1 on
   --pliego-dark-bg, comfortably AA on the one background this shared footer actually sits on in
   every shell (.layout-wrapper/.main-wrapper carry no background of their own). */
.footer-version { font-variant-numeric: tabular-nums; }

/* Main content area - offset by sidebar */
.main-wrapper {
  margin-left: 250px;
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  /* Flex item of .layout-wrapper: without this, a wide child's min-content width propagates up
     and the wrapper itself overflows the viewport instead of letting .table-scroll clip. */
  min-width: 0;
}

/* Topbar - sticky white bar. No z-index of its own: the hamburger inside carries the shell's
   topmost layer (see .hamburger) and the drawer (.sidebar, 500) and its overlay (499) must paint
   above the bar when the mobile nav is open. Sticky still paints above static page content. */
.topbar {
  position: sticky;
  top: 0;
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .6rem 1.5rem;
  background: var(--pliego-bg);
  border-bottom: 1px solid var(--pliego-border);
  min-height: 52px;
}
[data-theme="dark"] .topbar { background: var(--pliego-surface); }

/* The public shell's topbar brand: the authenticated topbar has no equivalent
   because its sidebar already carries the logo; a page's own <h1> is the
   on-screen title everywhere, so no per-page title element exists here. */
.topbar-brand {
  font-size: 1rem;
  font-weight: 700;
  color: var(--pliego-fg);
  text-decoration: none;
}

/* Active tenant, backoffice shell only (a tenant-bound page's operating
   scope, STANDARDS.md sec 17). */
.topbar-tenant {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  font-size: .8rem;
  color: var(--pliego-muted);
}
.topbar-tenant__switch {
  font-size: .75rem;
  /* --pliego-primary-text, not --pliego-primary: this is small (.75rem) text on the topbar
     background, not a button background; --pliego-dark-primary reads at only 3.51:1 there (see
     the token's :root comment). */
  color: var(--pliego-primary-text);
}

/* The action cluster may shrink and wrap internally; flex-shrink: 0 here is what pushed the
   document to 393-454px on every narrow page (the bar's min-content exceeded the viewport). */
.topbar-actions {
  display: flex;
  align-items: center;
  gap: .5rem;
  row-gap: .35rem;
  flex-wrap: wrap;
  justify-content: flex-end;
  min-width: 0;
  margin-left: auto;
}

.topbar-actions .theme-toggle {
  margin-left: 0;
  color: var(--pliego-fg);
  border: 1px solid var(--pliego-border);
}
.topbar-actions .btn--ghost { color: var(--pliego-fg); }
.topbar-cross { display: inline-flex; align-items: center; gap: .35rem; white-space: nowrap; }
/* Topbar search: the shared cross-surface search control. Collapses to the icon button alone on
   narrow screens, where the topbar has no room for the field. */
.topbar-search { display: flex; align-items: center; gap: .25rem; min-width: 0; }
.topbar-search__input { width: 190px; min-width: 0; max-width: 100%; height: 34px; padding: 0 .6rem; border: 1px solid var(--pliego-border); border-radius: 8px; background: var(--pliego-surface); color: var(--pliego-fg); font-size: .85rem; }
.topbar-search__input:focus { outline: 2px solid var(--pliego-primary); outline-offset: 1px; }
.topbar-search__btn { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; padding: 0; border: 1px solid var(--pliego-border); border-radius: 8px; background: transparent; color: var(--pliego-fg); cursor: pointer; }
.topbar-search__btn:hover { background: var(--pliego-bg); }
@media (max-width: 900px) { .topbar-search__input { display: none; } }
.topbar-bell { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 8px; color: var(--pliego-fg); text-decoration: none; font-size: 1.1rem; }
.topbar-bell:hover { background: var(--pliego-bg); }
.topbar-bell__badge { position: absolute; top: 3px; right: 3px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px; background: var(--pliego-danger-bg); color: #fff; font-size: .66rem; font-weight: 700; line-height: 16px; text-align: center; box-sizing: border-box; }

/* User menu dropdown (no-JS <details>; app.js closes it on outside click). */
.user-menu { position: relative; }
.user-menu__btn { list-style: none; display: inline-flex; align-items: center; gap: .5rem; cursor: pointer; padding: .25rem .5rem; border-radius: 8px; color: var(--pliego-fg); }
.user-menu__btn::-webkit-details-marker { display: none; }
.user-menu__btn:hover { background: var(--pliego-surface); }
.user-menu__name { font-size: .88rem; font-weight: 600; max-width: 12rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-menu__role { font-size: .72rem; color: var(--pliego-muted); }
.user-menu__pop { position: absolute; right: 0; top: calc(100% + .4rem); min-width: 14rem; display: flex; flex-direction: column; background: var(--pliego-bg); border: 1px solid var(--pliego-border); border-radius: 10px; box-shadow: 0 6px 24px rgba(0,0,0,.18); padding: .35rem; z-index: 600; }
.user-menu__pop a, .user-menu__pop button { display: flex; align-items: center; gap: .6rem; padding: .55rem .65rem; border-radius: 6px; background: none; border: none; color: var(--pliego-fg); text-decoration: none; font: inherit; width: 100%; text-align: left; cursor: pointer; }
.user-menu__pop a:hover, .user-menu__pop button:hover { background: var(--pliego-surface); }
.user-menu__pop form { margin: 0; }
.user-menu__pop hr { border: none; border-top: 1px solid var(--pliego-border); margin: .3rem .2rem; }

/* Hamburger - hidden on desktop. On mobile it is a fixed control outside the topbar: the sticky
   bar always creates a stacking context, so a button inside it could never sit above the open
   drawer (.sidebar, z 500). Fixed at the bar's top-left corner with the bar's left padding
   reserving its place in the flow, z 501 keeps it clickable above the drawer (500) and its
   overlay (499) - the toggle is always reachable. */
.hamburger {
  display: none;
  position: fixed;
  top: .6rem;
  left: 1.5rem;
  z-index: 501;
  background: none;
  border: none;
  color: var(--pliego-fg);
  font-size: 1.3rem;
  padding: .25rem;
  cursor: pointer;
}

/* Page content */
.page-content {
  flex: 1;
  padding: 1.75rem 2rem;
  /* Flex item of .main-wrapper: same min-width:auto trap as its parent - without it the
     min-content chain defeats .table-scroll and the document overflows at narrow widths. */
  min-width: 0;
}
/* Backoffice footer */
/* In normal document flow, not position: fixed (which used to cover page
   content at short viewport heights). .main-wrapper is already a min-height:
   100vh flex column, so margin-top: auto keeps it pinned to the bottom of
   the viewport on a short page while staying part of the scrollable
   document - never overlapping - on a long one. */
.app-footer {
  margin-top: auto;
  padding: .75rem 1.5rem;
  font-size: .8rem;
  color: var(--pliego-muted);
  border-top: 1px solid var(--pliego-border);
  text-align: center;
}

/* Sidebar overlay on mobile */
.sidebar-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,.4);
  z-index: 499;
}

/* Responsive */
@media (max-width: 768px) {
  /* Closed drawer is visibility:hidden as well as translated: a transform alone still paints when
     the document is horizontally scrolled (the full-page capture then composites the fixed sidebar
     shifted by scrollLeft, leaking a strip of navy over the hamburger). visibility:hidden paints
     nothing at any document width; the delay keeps the slide-down visible for the duration. */
  .sidebar {
    transform: translateX(-100%);
    visibility: hidden;
    transition: transform .25s ease, visibility 0s .25s;
  }
  .sidebar--open {
    transform: translateX(0);
    visibility: visible;
    transition: transform .25s ease, visibility 0s 0s;
  }
  .sidebar-overlay--visible {
    display: block;
  }
  .main-wrapper {
    margin-left: 0;
  }
  .hamburger {
    display: inline-flex;
  }
  /* Reserve the fixed hamburger's place: its width (~1.8rem) plus the bar's gap. */
  .topbar { padding-left: 4.0625rem; }
}

/* Flat data tables: clean rows, uppercase header, subtle row hover. Not a card. */
table { border-collapse: collapse; width: 100%; margin: 1rem 0; font-size: .9rem; }
th, td { text-align: left; padding: 0.55rem 0.8rem; border-bottom: 1px solid var(--pliego-border); vertical-align: middle; }
th { color: var(--pliego-muted); font-size: 0.78rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 2px solid var(--pliego-border); }
tbody tr:hover { background: var(--pliego-surface); }
/* Action column: a tight, right-aligned cell of icon buttons (the .row-actions wrapper). */
td:has(.row-actions) { width: 1%; white-space: nowrap; text-align: right; }

form { margin: 1rem 0; }
label { display: block; margin: 0.85rem 0 0.3rem; font-size: .9rem; font-weight: 500; }
/* accent-color themes the native controls (checkbox, radio, range, progress) in both themes;
   color-scheme on :root covers the rest of the browser chrome (select arrow, date picker,
   scrollbar). Without these, unchecked boxes render as solid white squares on dark forms. */
input, select, textarea { padding: 0.5rem 0.65rem; font: inherit; color: var(--pliego-fg); background: var(--pliego-bg); border: 1px solid var(--pliego-border); border-radius: 6px; accent-color: var(--pliego-primary); }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--pliego-accent); box-shadow: 0 0 0 3px rgba(43,132,162,.15); }
button {
  padding: 0.5rem 0.9rem;
  font: inherit;
  font-weight: 500;
  color: #fff;
  background: var(--pliego-primary);
  border: 0;
  border-radius: 6px;
  cursor: pointer;
}
button:hover { background: var(--pliego-primary-hover); }
button + button { margin-left: .5rem; }
/* List controls: search filters, bulk-action bar, export bar, pager - all consistent toolbars. */
.filters { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-bottom: 1rem; }
.bulk-bar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: 1rem 0; }
.bulk-bar__label { color: var(--pliego-muted); font-size: .9rem; }
.export-menu { display: flex; align-items: center; gap: .5rem; margin: 1rem 0; }
.export-menu__label { color: var(--pliego-muted); font-size: .9rem; }
.pager { display: flex; align-items: center; justify-content: space-between; gap: .75rem; margin: 1rem 0; flex-wrap: wrap; }
.pager__range { color: var(--pliego-muted); font-size: .85rem; }
.pager__nav { display: flex; gap: .35rem; align-items: center; flex-wrap: wrap; }
.pager__btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 2rem; height: 2rem; padding: 0 .5rem;
  border: 1px solid var(--pliego-border); border-radius: 6px;
  background: var(--pliego-surface); color: var(--pliego-fg);
  font-size: .85rem; text-decoration: none; line-height: 1;
  font-variant-numeric: tabular-nums; transition: background .12s, border-color .12s, color .12s;
}
.pager__btn:hover { border-color: var(--pliego-primary); color: var(--pliego-primary); }
.pager__btn--current { background: var(--pliego-primary); border-color: var(--pliego-primary); color: #fff; font-weight: 600; }
.pager__btn--current:hover { color: #fff; }
.pager__btn.is-disabled { opacity: .4; pointer-events: none; }
.pager__gap { color: var(--pliego-muted); padding: 0 .15rem; }
.btn.is-disabled, .btn[aria-disabled="true"] { opacity: .45; pointer-events: none; cursor: default; }

/* Theme toggle button (topbar) */
.theme-toggle {
  background: none;
  border: 1px solid rgba(255,255,255,0.4);
  color: #fff;
  padding: 0.3rem 0.6rem;
  font-size: 0.85rem;
  border-radius: 4px;
  cursor: pointer;
  margin-left: auto;
}
.theme-toggle:hover { background: rgba(255,255,255,0.15); }

/* ---- Components ---- */

/* Buttons - a complete style so anchors and <button>s look identical (an .btn anchor must
   read as a button, never a bare link). Variants override colour. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  padding: .5rem .9rem; font: inherit; font-size: .9rem; font-weight: 500; line-height: 1.2;
  color: #fff; background: var(--pliego-primary); border: 1px solid transparent; border-radius: 6px;
  cursor: pointer; text-decoration: none; white-space: nowrap;
}
.btn:hover { background: var(--pliego-primary-hover); color: #fff; }
.btn--primary { background: var(--pliego-accent); border-color: var(--pliego-accent); color: #fff; }
.btn--primary:hover { background: var(--pliego-primary-hover); }
.btn--ghost { background: none; color: var(--pliego-fg); border: 1px solid var(--pliego-border); }
.btn--ghost:hover { background: var(--pliego-surface); color: var(--pliego-fg); }
.btn--sm { padding: .3rem .6rem; font-size: .82rem; }
.back-link { display: inline-flex; align-items: center; gap: .35rem; margin-bottom: 1rem; }
.back-link i { font-size: .9em; }
/* Not the literal #2B84A2/#236f89 (the pre-246 accent value and an ad hoc darker shade of it):
   both were an unswept fifth copy of the accent colour, at 4.26:1 white-on-background, the exact
   pairing DECISIONS.md 246 already fixed everywhere it read --pliego-accent/--pliego-primary-hover
   directly. Reusing those tokens closes the same gap here instead of restating a third numeric
   copy of the same colour. */
.btn--secondary { background: var(--pliego-accent); border-color: var(--pliego-accent); color: #fff; }
.btn--secondary:hover { background: var(--pliego-primary-hover); color: #fff; }
/* --pliego-danger-bg, not --pliego-error: see the token's own comment in :root - --pliego-error
   is tuned to read as text in dark mode and fails white-on-it there (3.49:1); danger-bg is the
   split background-under-white-text counterpart (4.62:1 in dark, aliases --pliego-error in light
   where one value already serves both roles). */
.btn--danger { background: var(--pliego-danger-bg); border-color: var(--pliego-danger-bg); color: #fff; }
.btn--danger:hover { opacity: .9; }
/* Read-only detail card: a key/value definition list rendered as an aligned two-column grid. */
.detail-card { background: var(--pliego-surface); border: 1px solid var(--pliego-border); border-radius: 10px; padding: 1.25rem 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
.detail-list { display: grid; grid-template-columns: max-content 1fr; gap: .5rem 1.25rem; margin: 0; }
.detail-list dt { font-weight: 600; color: var(--pliego-muted); font-size: .85rem; }
.detail-list dd { margin: 0; }
/* The [hidden] attribute must win over component display rules (e.g. a flex/grid container). */
[hidden] { display: none !important; }

/* App-launcher grid (front Sistemas/Recursos): compact, fully clickable cards in a responsive grid. */
.launch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 1.25rem; }
.launch-card {
  display: flex; align-items: center; gap: .85rem;
  background: var(--pliego-surface); border: 1px solid var(--pliego-border); border-radius: 10px;
  padding: 1rem 1.1rem; text-decoration: none; color: inherit;
  transition: border-color .12s, transform .12s, box-shadow .12s;
}
.launch-card:hover { border-color: var(--pliego-primary); transform: translateY(-2px); box-shadow: 0 6px 18px rgba(0,0,0,.18); }
.launch-card__icon {
  flex-shrink: 0; width: 44px; height: 44px; border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.4rem; background: rgba(43,132,162,.12); color: var(--pliego-primary);
}
.launch-card__body { min-width: 0; display: flex; flex-direction: column; gap: .15rem; }
.launch-card__title { font-weight: 600; color: var(--pliego-fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.launch-card__desc { font-size: .82rem; color: var(--pliego-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* List/detail page header: title on the left, primary action(s) pushed right (eppsc parity). */
.page-header { display: flex; align-items: center; justify-content: space-between; gap: .75rem; margin-bottom: 1rem; flex-wrap: wrap; }
.page-header h1 { margin: 0; font-size: 1.3rem; }
.page-header__actions { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.page-header__titles { display: flex; flex-direction: column; gap: .15rem; }
.page-header__eyebrow { font-size: .68rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--pliego-muted); }

/* Breadcrumb trail (detail/form pages reached from a list). Clickable text + chevron separators. */
.breadcrumb { display: flex; align-items: center; flex-wrap: wrap; gap: .35rem; margin-bottom: .85rem; font-size: .85rem; }
.breadcrumb__item { color: var(--pliego-link); text-decoration: none; }
.breadcrumb__item:hover { text-decoration: underline; }
.breadcrumb__item--current { color: var(--pliego-muted); font-weight: 500; }
.breadcrumb__sep { font-size: .7rem; color: var(--pliego-muted); }

/* Badges and tags */
.badge {
  display: inline-block;
  padding: .15em .55em;
  font-size: .8rem;
  font-weight: 600;
  border-radius: 10px;
  white-space: nowrap;
}
/* Tone backgrounds are translucent so the same rule reads on both light and dark themes;
   the text uses the theme-aware semantic token. --pliego-success/--pliego-warn/--pliego-error are
   each chosen (see :root) so this same .16 alpha clears 4.5:1 against --pliego-surface/--pliego-bg
   in both palettes without a per-theme override - increasing the alpha here would instead LOWER
   contrast, since the tint and the text share the same hue and more tint pulls the composite
   toward the text colour itself. */
.badge--ok { background: rgba(27,138,61,.16); color: var(--pliego-success); }
.badge--warn { background: rgba(184,115,10,.16); color: var(--pliego-warn); }
.badge--bad { background: rgba(176,0,32,.16); color: var(--pliego-error); }
.badge--muted { background: var(--pliego-border); color: var(--pliego-muted-on-border); }

/* Alerts: a bordered, tinted message block (success / error / warning / info). Used for flash
   messages and inline banners. The left accent + soft tint read on both light and dark themes. */
.alert {
  display: flex; align-items: flex-start; gap: .55rem;
  border: 1px solid var(--pliego-border); border-left: 3px solid var(--pliego-muted);
  border-radius: 8px; padding: .75rem 1rem; margin-bottom: 1.25rem;
  font-size: .9rem; line-height: 1.45; background: var(--pliego-surface);
}
.alert i { margin-top: .1rem; flex-shrink: 0; }
.alert--success { border-left-color: var(--pliego-success); background: rgba(27,138,61,.08); }
.alert--success i { color: var(--pliego-success); }
.alert--danger { border-left-color: var(--pliego-error); background: rgba(176,0,32,.08); }
.alert--danger i { color: var(--pliego-error); }
.alert--warning { border-left-color: var(--pliego-warn); background: rgba(184,115,10,.08); }
.alert--warning i { color: var(--pliego-warn); }
.alert--info { border-left-color: var(--pliego-primary); background: rgba(43,132,162,.08); }
.alert--info i { color: var(--pliego-primary); }
/* color: --pliego-primary-text, not --pliego-primary: --pliego-primary is #1D3341 navy in light
   (9.98:1 tinted here, unaffected) but --pliego-dark-primary in dark, which read as text measures
   only 2.92:1 tinted at .16 (that role is what --pliego-primary-text exists to split off - see
   :root). Even with the lighter --pliego-dark-primary-text (#5b9bb8), .16 alpha still falls short
   (4.38:1); the dark override below narrows the tint to .12, which clears it (4.61:1). */
.badge--info { background: rgba(43,132,162,.16); color: var(--pliego-primary-text); }
[data-theme="dark"] .badge--info { background: rgba(43,132,162,.12); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .badge--info { background: rgba(43,132,162,.12); }
}

/* Toast notifications */
#toasts {
  position: fixed;
  top: 1rem;
  right: 1rem;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: .5rem;
  max-width: 380px;
}
@keyframes toast-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toast-out { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }

/* Busy state */
.is-busy { position: relative; pointer-events: none; }
.is-busy::after {
  content: "";
  display: inline-block;
  width: .85rem;
  height: .85rem;
  border: 2px solid #fff;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin .6s linear infinite;
  margin-left: .4rem;
  vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Login page */
.login-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #1D3341;
  padding: 1rem;
}
.login-card {
  /* The login card is always a light surface on the navy page, in any theme. Re-mapping the
     semantic tokens here keeps every inner rule (labels, inputs, icon prefixes) correct
     light-on-white without per-element overrides, and fixes the dark-box / faint-label
     artifacts that appeared when the page theme was dark. */
  --pliego-fg: #1a2535;
  --pliego-bg: #ffffff;
  --pliego-surface: #f1f3f5;
  --pliego-border: #d8dde3;
  --pliego-muted: #6b7280;
  /* The card stays white in dark mode, so its inherited link role must use the light-surface
     value rather than --pliego-dark-link. #25778F measures 5.10:1 against this white card. */
  --pliego-link: #25778F;
  background: #fff;
  color: var(--pliego-fg);
  border-radius: 12px;
  padding: 2rem 2.5rem;
  width: 100%;
  max-width: 400px;
  box-shadow: 0 8px 24px rgba(0,0,0,.2);
}
/* Neutralise the browser's yellow autofill so it doesn't clash with the white card. */
.login-card input:-webkit-autofill,
.login-card input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 30px #fff inset;
  -webkit-text-fill-color: #1a2535;
  caret-color: #1a2535;
}
.login-brand-logo { display: block; height: 56px; width: auto; margin: 0 auto 1rem; }
.login-card h1 {
  color: #1D3341;
  font-size: 1.3rem;
  text-align: center;
  margin: 0 0 .25rem;
}
.login-card label { margin: .75rem 0 .25rem; font-size: .85rem; font-weight: 500; color: var(--pliego-fg); }
.login-card .input-group {
  display: flex;
  align-items: center;
  border: 1px solid var(--pliego-border);
  border-radius: 6px;
  overflow: hidden;
}
.login-card .input-group i {
  padding: .5rem .65rem;
  color: var(--pliego-muted);
  background: var(--pliego-surface);
  font-size: 1rem;
}
.login-card .input-group input {
  flex: 1;
  border: none;
  border-radius: 0;
  padding: .55rem .65rem;
  background: transparent;
}
/* #25778F, not var(--pliego-accent): the card above already forces a permanently light surface
   regardless of the active theme (see its own comment), so a token that switches to the dark
   palette under [data-theme="dark"] would clash with the rest of this always-light card - a
   literal is used here for the same reason --pliego-fg/--pliego-bg/etc are re-forced to light
   values just above, not restated as a fresh colour: #25778F is exactly --pliego-accent's own
   light-palette value (5.10:1 on this card's white background), the pre-246-fix literal (#2B84A2,
   4.26:1) is what this replaces. */
.login-card .input-group:focus-within { border-color: #25778F; }
.login-card button[type="submit"] {
  width: 100%;
  padding: .65rem;
  font-size: 1rem;
  background: #25778F;
  border-radius: 6px;
  margin-top: 1.25rem;
}
/* #236f89 (unchanged): already measured 5.67:1 for white text - this hover shade was never the
   failing pairing, only the resting-state #2B84A2 above was. */
.login-card button[type="submit"]:hover { background: #236f89; }
.login-card .login-footer { text-align: center; margin-top: 1rem; font-size: .85rem; }

/* Iconized row actions (view / edit / delete) - uniform across every list */
.row-actions { display: inline-flex; align-items: center; gap: .25rem; }
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; padding: 0;
  border: 1px solid var(--pliego-border); border-radius: 6px;
  background: var(--pliego-bg); color: var(--pliego-fg);
  cursor: pointer; text-decoration: none; font-size: 1rem; line-height: 1;
  transition: background .15s, color .15s, border-color .15s;
}
.icon-btn:hover { background: var(--pliego-surface); border-color: var(--pliego-primary); color: var(--pliego-primary); }
.icon-btn--danger:hover { background: var(--pliego-danger-bg); border-color: var(--pliego-danger-bg); color: #fff; }

/* KPI / stat cards (netspo style) - used by the dashboard and the Seguridad overview. Left accent
   border, big tabular value, uppercase eyebrow label. */
.stat-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin: 1.25rem 0 1.5rem; }
.stat-card {
  background: var(--pliego-surface); border: 1px solid var(--pliego-border);
  border-left: 4px solid var(--pliego-primary); border-radius: 8px;
  padding: 1.1rem 1.25rem; box-shadow: 0 1px 3px rgba(0,0,0,.08);
  display: flex; flex-direction: column; gap: .35rem;
}
.stat-card__value { font-size: 2rem; font-weight: 800; line-height: 1; color: var(--pliego-fg); letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.stat-card__label { font-size: .7rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--pliego-muted); }

/* Create/edit form cards: a boxed, intentional form (replaces raw fields at page bottom). */
.form-card { max-width: 680px; background: var(--pliego-surface); border: 1px solid var(--pliego-border); border-radius: 10px; padding: 1.25rem 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
.form-card > h2:first-child { margin-top: 0; }

/* Closed form page-kind contract (form_page.templ): label and control are declared as siblings
   inside .form-group, associated by for=/id= rather than nested label > control the way the
   .form-card/.form-field rules above and below expect, so none of those width rules ever reach
   these controls at all. An unstyled <select> sizes to its own widest <option> text instead of
   its container - which is what forced /admin/tenders/new 163px past a 390px viewport
   (award_granularity's own longest option, "Por renglón (adjudicatarios independientes por
   renglón)", had no width to shrink into). Every FormPageVM field shares this one class
   regardless of page, so the fix is here once rather than per field or per route. */
.form-control { width: 100%; }
.required-star { color: var(--pliego-error); margin-left: .15rem; font-weight: 700; }
.ceremony-progress { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin: .5rem 0 1rem; }

/* Help / guidance affordance: a side panel next to forms documents what each field means - so
   every page is self-explanatory. */
.help-aside {
  background: var(--pliego-surface); border: 1px solid var(--pliego-border);
  border-radius: 8px; padding: 1rem 1.1rem; font-size: .85rem; line-height: 1.5;
}
.help-aside__title { display: flex; align-items: center; gap: .4rem; font-weight: 600; font-size: .9rem; margin: 0 0 .6rem; }
.help-aside__title i { color: var(--pliego-primary); }
.help-aside dl { margin: 0; }
.help-aside dt { font-weight: 600; margin-top: .7rem; }
.help-aside dt:first-of-type { margin-top: 0; }
.help-aside dd { margin: .1rem 0 0; color: var(--pliego-muted); }

/* In real (non-inline) forms, fields stack under their label - cleaner than label-beside-field,
   and fixes textareas that rendered next to their label. Inline filter/search forms keep rows. */
.form-card label { display: block; }
.form-card label > input:not([type=checkbox]):not([type=radio]):not([type=color]),
.form-card label > select, .form-card label > textarea { display: block; margin-top: .3rem; width: 100%; max-width: 460px; }
textarea { min-height: 7rem; resize: vertical; width: 100%; max-width: 460px; }

/* Multi-field filter bars (Auditoría, Depuración) */
.filters--grid { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .6rem 1rem; margin-bottom: 1rem; }
.filters--grid label { display: flex; flex-direction: column; gap: .2rem; font-size: .8rem; color: var(--pliego-muted); }
.filters--grid input, .filters--grid select { min-width: 140px; }
.filters__actions { display: flex; gap: .5rem; align-items: flex-end; }

/* Inline input + adjacent action buttons (e.g. password field with show/generate) */
.input-group { display: flex; align-items: stretch; gap: .4rem; }
/* width 0 (not just min-width 0): the input's intrinsic width (~20ch) is otherwise the group's
   min-content floor and forced the whole document wide at 390px (users-new rendered 515px wide).
   flex-grow lets it fill the space its siblings leave. */
.input-group > input { flex: 1 1 auto; width: 0; min-width: 0; }
.text-muted { color: var(--pliego-muted); }

/* ---- Page kinds (ListPage, DetailPage, OverviewPage, TenderSectionNav) ---- */

.table-section__head { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; margin: 1rem 0 .5rem; flex-wrap: wrap; min-width: 0; }
/* Weight and line-height live on the class: a title that repeats the page title is a p, not an h2. */
.table-section__title { margin: 0; font-size: 1.05rem; font-weight: 600; line-height: 1.25; }
.table-section__count { color: var(--pliego-muted); font-size: .85rem; }
/* Tender workspace section navigation (TenderSectionNav): each lifecycle-stage group's own
   heading, a small uppercase muted label rather than a new size/weight/color combination. */
.tender-sections__group-title { margin: 1rem 0 .35rem; font-size: .68rem; text-transform: uppercase; letter-spacing: .04em; color: var(--pliego-muted); }
/* Table shell (TableSection and tableMarkup): horizontal scroll wrapper and explicit empty
   state. The wrapper keeps a wide table inside the page; min-width: 0 lets it shrink inside
   flex/grid parents. overflow-x is auto, so a table that fits is not a scrollport. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; min-width: 0; }
.table-scroll:focus-visible { outline: 3px solid var(--pliego-primary); outline-offset: 2px; }
.table-empty { display: flex; flex-direction: column; align-items: center; gap: .4rem; padding: 1.5rem 1rem; color: var(--pliego-muted); text-align: center; }
.table-empty i { font-size: 1.6rem; }
.table-empty p { margin: 0; }
/* A numeric column (ColumnVM.Numeric, tableColumnHeader/tableCell) reads as numeric: right-
   aligned with tabular figures, so a column of amounts lines up on its digits instead of its
   left edge - structure, not a new color (user ruling A, DECISIONS.md 317). */
.is-numeric { text-align: right; font-variant-numeric: tabular-nums; }
/* A paired field group (FieldGroupVM.Paired, form_page.templ) lays its fields side by side
   instead of stacked, reading as one pair rather than two unrelated questions - the legend
   stays its own full-width line above them (user ruling A, DECISIONS.md 317). */
.form-group-set--paired { display: flex; flex-wrap: wrap; gap: 1rem; }
.form-group-set--paired > legend { flex: 0 0 100%; }
.form-group-set--paired > div { flex: 1 1 200px; min-width: 0; }

/* ---- Print ---- */

/* A printed page keeps its own identity - PageKindHeader's eyebrow, title and badge, sourced
   from each page's own view model (the record's own kind and its tender's number, already
   there for every page, legal surface or not) - and loses the shell chrome around it: the skip
   link, the sidebar and its mobile controls, the topbar and the footer serve only the live
   interactive session a printed page does not have. .main-wrapper's sidebar-width margin is
   reclaimed so the page uses the full sheet once the sidebar it was reserved for is gone. */
@media print {
  .skip-link,
  .sidebar,
  .sidebar-overlay,
  .hamburger,
  .topbar,
  .app-footer {
    display: none;
  }
  .main-wrapper {
    margin-left: 0;
  }
}
