@layer base;

/* Design tokens — one file per token type, in tokens/.
 *
 * Imported here rather than through a tokens/index.css barrel: global.css is a
 * <link> in the document head, so every level of @import nesting is one more
 * serial round trip before first paint. Listed flat, the ten files are
 * discovered in a single parse and fetched in parallel.
 *
 * `node ui/scripts/gen-tokens.mjs` reads the directory, not this list, so it
 * cannot fall out of step with what is actually there. */
@import '/common/tokens/palette.css' layer(base);
@import '/common/tokens/colors.css' layer(base);
@import '/common/tokens/typography.css' layer(base);
@import '/common/tokens/spacing.css' layer(base);
@import '/common/tokens/border-radius.css' layer(base);
@import '/common/tokens/border-width.css' layer(base);
@import '/common/tokens/sizing.css' layer(base);
@import '/common/tokens/elevation.css' layer(base);
@import '/common/tokens/motion.css' layer(base);
@import '/common/tokens/z-index.css' layer(base);

/* Nasiko UI Design System fonts — vendored locally (see nasiko_ui Flutter package) */
@font-face {
  font-family: 'Chivo Mono';
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url('fonts/ChivoMono-Regular.ttf') format('truetype');
}
@font-face {
  font-family: 'Chivo Mono';
  font-weight: 500 700; /* 700 synthesized from Medium — no bold TTF shipped */
  font-style: normal;
  font-display: swap;
  src: url('fonts/ChivoMono-Medium.ttf') format('truetype');
}
@font-face {
  font-family: 'Inter';
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  src: url('fonts/Inter-VariableFont_opsz.ttf') format('truetype-variations');
}
@font-face {
  font-family: 'Inter';
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
  src: url('fonts/Inter-Italic-VariableFont_opsz.ttf') format('truetype-variations');
}

@layer base {
/* 1. Design Tokens
 *
 * The tokens themselves live in tokens/ — one file per token type. They are
 * imported at the top of this file, into @layer base, so the cascade is
 * unchanged from when they were declared inline here.
 *
 * tokens/ is the single source of truth and the input to
 * `node ui/scripts/gen-tokens.mjs`, which emits tokens/tokens.json — the
 * machine-readable form a generator reads. Change a value here, regenerate.
 *
 * Theme: light-dark() follows the OS by default; an explicit
 * [data-theme="light"|"dark"] on <html> pins it.
 */
:root {
  color-scheme: light dark;
}

/* Explicit theme pin — wins over the OS preference */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

/* 2. Base Reset */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  scrollbar-gutter: stable;
  -webkit-text-size-adjust: 100%;
  overflow-x: clip;
}

body {
  background-color: var(--shell-bg);
  color: var(--color-text-main);
  font-family: var(--font-sans);
  line-height: 1.5;
  font-size: var(--font-size-base);
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

/* Mobile (<1024px): no rail/card chrome — the content plane is the page
   itself, so page components sit on the white surface directly. Desktop gets
   the floating white card in the @media block below. */
@media (max-width: 1023.98px) {
  body:has(> app-header) > :not(app-header):not(app-loading-bar):not(app-nav-search):not(app-toast):not(div):not(dialog) {
    background: var(--color-bg-surface);
    flex: 1;
  }
}

main {
  width: min(100% - 2 * var(--s-24), var(--page-max-width));
  margin: 0 auto;
  padding: var(--s-16) 0;
}

/* Full-viewport app-shell layout — no centering, panes scroll independently */
main.app-shell {
  width: 100%;
  max-width: none;
  margin: 0;
  padding: 0;
  height: calc(100dvh - var(--app-header-height));
  overflow: hidden;
}

@media (min-width: 1024px) {
  /* NightOwl shell: fixed 52px ink topbar + fixed left icon rail; the page
     content is the white card floating on the ink backdrop. app-header
     renders both bars; body padding carves out their space. */
  body {
    padding-top: var(--shell-topbar-height);
    /* Gutter + rail: the rail is fixed at left:var(--shell-gutter), so the
       content plane starts past both it and the gutter it floats in. */
    padding-left: calc(var(--shell-gutter) + var(--app-sidebar-width));
  }

  body > main, .app-layout > main {
    flex: 1;
    min-width: 0;
    width: auto;
    max-width: var(--page-max-width);
    margin-inline: auto;
  }

  body > :not(app-header):not(app-loading-bar):not(main) {
    flex: 1;
    min-width: 0;
  }

  body > main.app-shell, .app-layout > main.app-shell {
    max-width: none;
    margin: 0;
    height: calc(100dvh - var(--shell-topbar-height));
  }

  main {
    width: min(100% - 2 * var(--s-24), var(--page-max-width));
    margin: 0 auto;
    padding: var(--s-16) 0;
  }

  main.app-shell {
    height: 100dvh;
  }

  /* Content card — the page component floats as the single white r8 card on
     the ink shell (floating chrome like toasts, dialogs, and the kb palette
     are plain divs / dedicated tags and are opted out). */
  body:has(> app-header) > :not(app-header):not(app-loading-bar):not(app-nav-search):not(app-toast):not(main):not(div):not(dialog),
  body:has(> app-header) > main {
    background: var(--color-bg-surface);
    border: none;
    border-radius: var(--r-8);
    /* Even frame on left/right/bottom. Left was 0, which put the card's two
       left corners flush against the rail — the 8px radius cut notches of ink
       out of the edge instead of reading as a floating panel. Top stays 0: the
       card butts the topbar by design. */
    margin: 0 var(--shell-gutter) var(--shell-gutter);
    min-height: calc(100dvh - var(--shell-topbar-height) - var(--s-12));
    box-shadow: 0 8px 16px rgba(100, 116, 139, 0.12);
  }

  /* <main> content pages keep their centered column inside the card */
  body:has(> app-header) > main {
    max-width: none;
    padding: var(--card-content-pad);
  }

  body:has(> app-header) > main > * {
    max-width: var(--page-max-width);
    margin-inline: auto;
  }

  /*
    SPA: the router mounts pages into <main id="outlet">, so the page is a
    grandchild of body, not a child. The rule above centres a <main>'s children
    in a --page-max-width column — correct for an MPA content page, wrong for a
    router-mounted one: it shrinks the whole page to a narrow centred strip
    inside the card and leaves the module-nav gutter empty beside it.

    The outlet is a passthrough. The card geometry above still applies to it,
    but the page inside owns its own padding (styles/page-layout.css) and fills
    the card edge to edge.
  */
  body:has(> app-header) > main#outlet {
    padding: 0;
  }

  body:has(> app-header) > main#outlet > * {
    max-width: none;
    margin-inline: 0;
  }

  body:has(> app-header) > main.app-shell {
    height: calc(100dvh - var(--shell-topbar-height) - var(--s-12));
    padding: 0;
  }

  body:has(> app-header) > main.app-shell > * {
    max-width: none;
  }
}

/*
  The router's outlet is a passthrough, but it is also the box the mounted page
  takes its height from. That used to be `min-height: 100%` on the page — a
  percentage against an auto-height parent, which computes to `auto`, so the page
  box was only ever as tall as its content. Anything the page pinned to its own
  edges shrank with it: the module nav, pinned top and bottom inside the page,
  rendered 212px tall with 228px of rows and grew a scrollbar on a full-height
  card, because a short page (an error state, an empty list) left nothing to
  stretch it. A flex column has no such ambiguity — the page fills the outlet,
  and still grows past it when its content is taller.
*/
main#outlet {
  display: flex;
  flex-direction: column;
}

main#outlet > * {
  flex: 1 0 auto;
}

/* Prevent media from overflowing their containers */
img,
picture,
video,
canvas,
svg {
  display: block;
  max-width: 100%;
}

/* Inherit fonts for form controls */
input,
button,
textarea,
select {
  font: inherit;
}

/* Opinionated resets for elements with notable browser defaults */
figure {
  margin: 0;
}

fieldset {
  border: none;
  min-width: 0;
}

legend {
  padding: 0;
}

address {
  font-style: inherit;
}

/* Ensure the hidden attribute works across all browsers */
[hidden] {
  display: none !important;
}

/* Accessibility utility — visually hidden but available to screen readers */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* h1 inside sectioning elements gets a reduced size in Chrome/Firefox;
   keep it consistent with our type scale */
:is(article, section, aside, nav) h1 {
  font-size: var(--font-size-4xl);
}

/* 3. Typography — DS: titles are Chivo Mono medium; h3 down stays Inter */
h1,
h2,
h3,
h4,
h5,
h6 {
  margin-bottom: var(--s-16);
  font-weight: 600;
  line-height: 1.2;
  color: var(--color-text-main);
}

h1,
h2 {
  font-family: var(--font-display);
  font-weight: 500;
  letter-spacing: 0.005em;
}

h3 {
  font-weight: 700;
}

h1 {
  font-size: var(--font-size-4xl);
}

h2 {
  font-size: var(--font-size-3xl);
}

/* NightOwl page title — Chivo Mono 500 20/28 (+0.16px tracking). Lives here
   (not styles/text.css) so it applies at first paint on pages that render
   synchronously — the components.css @import chain arrives after paint. */
.title-page {
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 28px;
  font-weight: 500;
  letter-spacing: 0.16px;
  color: var(--color-text-main);
  margin: 0;
}

h3 {
  font-size: var(--font-size-2xl);
}

h4 {
  font-size: var(--font-size-xl);
}

h5 {
  font-size: var(--font-size-lg);
}

h6 {
  font-size: var(--font-size-base);
}

/* Top-level page title inside <main> — use on the <h1> of content pages */
.page-heading {
  font-size: var(--font-size-3xl);
  margin: 0 0 var(--s-24);
}

p {
  margin: 0 0 var(--s-16) 0;
}

/* DS micro-tell: captions are italic with tight tracking */
caption,
figcaption,
.ts-caption {
  font-style: italic;
  font-size: var(--font-size-xs);
  letter-spacing: -0.04em;
  color: var(--color-text-muted);
}

a {
  color: var(--color-primary);
  text-decoration: none;
}

a:hover {
  color: var(--color-primary-hover);
}

/* 4. Form Elements */
label {
  display: block;
  font-size: var(--font-size-sm);
  font-weight: 500;
  margin-bottom: var(--s-8);
  color: var(--color-text-main);
}

/* Design-system components render their own <label> and space it themselves —
   app-input/app-select via their .field gap, app-switch/app-radio/app-checkbox
   via the .row that IS the label. The bare-form margin above is not theirs: it
   doubled the label gap on every labelled field, and on the three control rows
   it added 8px of empty box below the control, so anything centred beside one
   (the Agent JSON toggle's <> glyph) sat 4px low. */
:is(app-checkbox, app-input, app-radio, app-select, app-switch) label {
  margin: 0;
}

:is(input[type="text"], input[type="email"], input[type="password"],
  input[type="search"], input[type="url"], input[type="tel"],
  input[type="number"], input[type="date"], input[type="time"],
  input[type="datetime-local"], textarea, select) {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid transparent; /* NightOwl: inputs are flat sand wells */
  border-radius: var(--r-8);
  font-size: var(--font-size-base);
  background: var(--bg-input);
  color: var(--color-text-main);
  line-height: 1.5;

  &:focus {
    outline: none;
    border-color: var(--border-hover);
    box-shadow: 0 0 0 2px var(--color-primary-ring);
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    background: var(--color-bg-base);
  }
}

:is(input, textarea)::placeholder {
  color: var(--color-text-muted);
}

textarea {
  resize: vertical;
  min-height: 80px;
}

/* Native selects: drop the OS chrome and draw the DS chevron — a stroked
   caret matching icons.js, not a filled triangle. A url() SVG can't read
   custom properties, so the stroke is baked per theme and re-declared for the
   dark side below; `--fg-secondary` is #645C52 light / #9BA0A6 dark.
   `:not([multiple])` also lifts specificity to (0,1,1) so the background-image
   survives the `:is(input…, select)` rule's `background:` shorthand above. */
select:not([multiple]) {
  cursor: pointer;
  appearance: none;
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23645C52' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-position: right 12px center;
  background-size: 12px 12px;
  background-repeat: no-repeat;

  &:hover:not(:disabled) {
    background-color: var(--bg-surface-hover);
  }
}

/* Dark caret: system-dark unless light is pinned, plus explicitly pinned dark. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) select:not([multiple]) {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239BA0A6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  }
}
:root[data-theme="dark"] select:not([multiple]) {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239BA0A6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

input[type="file"] {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--r-8);
  font-size: var(--font-size-base);
  background: var(--color-bg-surface);
  color: var(--color-text-main);
  cursor: pointer;

  &:focus {
    outline: none;
    border-color: var(--border-hover);
    box-shadow: 0 0 0 2px var(--color-primary-ring);
  }

  &::file-selector-button {
    margin-right: var(--s-12);
    height: var(--control-h-sm);
    padding: 0 var(--s-12);
    border: 1px solid var(--color-border);
    border-radius: var(--r-4);
    background: var(--color-bg-base);
    color: var(--color-text-main);
    font-size: var(--font-size-sm);
    cursor: pointer;
  }
}

/* Checkbox and Radio */
input[type="checkbox"],
input[type="radio"] {
  width: auto;
  margin-right: var(--s-8);
  cursor: pointer;
}

/* Range Slider */
input[type="range"] {
  width: 100%;
  height: 6px;
  border-radius: 3px;
  background: var(--color-border);
  outline: none;
  cursor: pointer;
}

/* Button bare reset — variants live in <app-button> or assets/styles/btn.css */
button {
  font: inherit;
  cursor: pointer;
  border: none;
  background: none;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* 5. Tables */
table {
  width: 100%;
  border-collapse: collapse;
  border-spacing: 0;
  margin: var(--s-16) 0;
  background: var(--color-bg-surface);
  border-radius: var(--r-8);
  overflow: hidden;
}

thead {
  background: var(--color-bg-base);
}

th {
  text-align: left;
  padding: 12px 16px;
  font-weight: 600;
  font-size: var(--font-size-sm);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-muted);
  border-bottom: 1px solid var(--color-border);
}

td {
  padding: 12px 16px;
  border-bottom: 1px solid var(--color-border);
}

tbody tr:last-child td {
  border-bottom: none;
}

/* The empty-state row is not a row you can act on, so it must not light up. */
tbody tr:not(:has(.empty)):hover {
  background: var(--color-bg-base);
}

/* 6. Lists */
ul,
ol {
  margin: 0 0 var(--s-16) 0;
  padding-left: var(--s-24);
}

ul ul,
ol ol,
ul ol,
ol ul {
  margin-bottom: 0;
}

li {
  margin-bottom: var(--s-8);
}

/* 7. Code */
code,
kbd,
samp {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background: var(--color-bg-base);
  padding: 2px 6px;
  border-radius: var(--r-4);
  color: var(--color-text-main);
}

pre {
  font-family: var(--font-mono);
  background: var(--color-bg-base);
  padding: var(--s-16);
  border-radius: var(--r-8);
  overflow-x: auto;
  margin: var(--s-16) 0;
  border: 1px solid var(--color-border);
}

pre code {
  background: none;
  padding: 0;
}

/* 8. Blockquote */
blockquote {
  margin: var(--s-16) 0;
  padding-left: var(--s-24);
  border-left: 4px solid var(--color-primary);
  color: var(--color-text-muted);
  font-style: italic;
}

/* 9. Horizontal Rule */
hr {
  border: none;
  border-top: 1px solid var(--color-border);
  margin: var(--s-24) 0;
}

/* 10. Details/Summary */
details {
  background: var(--color-bg-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--r-8);
  padding: var(--s-16);
  margin: var(--s-16) 0;
}

summary {
  cursor: pointer;
  font-weight: 600;
  user-select: none;
  list-style: none;
}

summary::-webkit-details-marker {
  display: none;
}

summary::before {
  content: '';
  display: inline-block;
  width: 7px;
  height: 7px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  margin-right: var(--s-8);
  vertical-align: 1px;
  flex-shrink: 0;
}

details[open] summary::before {
  transform: rotate(45deg);
  vertical-align: -1px;
}

details[open] {
  padding-bottom: var(--s-16);
}

/* 12. Dialog */
dialog {
  border: none;
  border-radius: var(--r-12);
  padding: 0;
  box-shadow: var(--shadow-xl);
  background: var(--color-bg-surface);
  color: var(--color-text-main);
  max-width: 90vw;
  max-height: 90vh;
  margin: auto;
}

dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(2px);
}

} /* end @layer base */

/* ─── app-header pre-upgrade placeholder ─────────────────────────────────────
   Shown instantly (pure CSS, no JS) while app-header.js is still loading.
   Mirrors the real header's bar geometry so the page doesn't shift on upgrade.
   Uses ::before (brand pill) + ::after (one nav-link skeleton) as the placeholder.
──────────────────────────────────────────────────────────────────────────── */
@keyframes app-header-skel-pulse {

  0%,
  100% {
    opacity: 1;
  }

  50% {
    opacity: 0.35;
  }
}

/* ─── MPA View Transitions ───────────────────────────────────────────────────
   Opt-in to cross-document view transitions, and freeze the shell so a
   navigation never cross-fades the chrome.

   These rules MUST live here, in a render-blocking stylesheet, not in
   app-header.js's adopted sheet: the incoming document is snapshotted when it
   is ready to render, and a deferred module has not run by then. Named there,
   the outgoing page had an `app-header` group and the incoming page had none,
   so the header cross-faded out to nothing and the root group cross-faded the
   whole viewport — the "every click flickers the page" bug. The name is on the
   pre-upgrade placeholder too, for the same reason.
──────────────────────────────────────────────────────────────────────────── */
@view-transition {
  navigation: auto;
}

app-header,
app-header:not(:defined) {
  view-transition-name: app-header;
}

/* Same problem one level down. The module nav is the same nav on both sides of
   a navigation *within* a module (Agents → Builds, Settings → Secrets), but it
   lives inside each page component, so with no name of its own it rode the
   `root` group and dimmed and slid with the page body on every click — the
   "whole page flickers" report. Modules whose rows are in-page sections (MCP,
   most of Settings) never navigate at all, which is why only the link rows did
   it. Named, it gets its own group: the UA cross-fade blends the two halves
   with `plus-lighter`, so identical content holds steady, and it still fades
   properly when a navigation enters or leaves a module and the nav is only on
   one side. No frozen `animation: none` here for that reason — unlike the
   header, this element is not on every page. */
app-module-nav,
app-module-nav:not(:defined) {
  view-transition-name: app-module-nav;
}

/* Frozen regardless of prefers-reduced-motion: the shell is the same shell on
   both sides of the navigation, so there is nothing to animate between. */
::view-transition-old(app-header),
::view-transition-new(app-header) {
  animation: none;
  mix-blend-mode: normal;
}

/* Shared micro-transition for tab/section panel switches — components apply
   `animation: panel-in var(--transition-base)` to their active panel and gate
   it on prefers-reduced-motion themselves. */
@keyframes panel-in {
  from { opacity: 0; translate: 0 4px; }
  to   { opacity: 1; translate: 0 0; }
}

app-header:not(:defined) {
  display: flex;
  align-items: center;
  gap: var(--s-16);
  padding: 0 var(--s-12);
  height: var(--shell-topbar-height);
  background: var(--shell-bg);
  border-bottom: none;
  position: sticky;
  top: 0;
  z-index: 100;
  overflow: hidden;

  &::before {
    content: '';
    display: block;
    width: 36px;
    height: 36px;
    border-radius: var(--r-8);
    background: var(--shell-control);
    flex-shrink: 0;
    animation: app-header-skel-pulse 1.4s ease-in-out infinite;
  }

  &::after {
    content: '';
    display: block;
    margin-left: auto;
    width: 140px;
    height: 28px;
    border-radius: var(--r-8);
    background: var(--shell-control);
    flex-shrink: 0;
    animation: app-header-skel-pulse 1.4s ease-in-out infinite 0.2s;
  }

  @media (prefers-reduced-motion: reduce) {

    &::before,
    &::after {
      animation: none;
      opacity: 0.4;
    }
  }

  @media (min-width: 1024px) {
    /* Reserves the fixed topbar strip; the rail column is carved out by the
       body padding, so no extra placeholder box is needed for it. */
    position: fixed;
    left: 0;
    top: 0;
    width: 100%;
    height: var(--shell-topbar-height);
  }
}
