/*
 * Application styles. Compiled by Propshaft (no preprocessing).
 *
 * The shape of the app is a fixed sidebar naming every functional area, and a
 * scrolling work area beside it. Everything below is organised in that order:
 * tokens, shell, then the components pages are built out of.
 */

/* ==========================================================================
   1. Tokens
   --------------------------------------------------------------------------
   Three theme states, and all three have to be written out. An explicit choice
   stamps data-theme on <html>; the default — no attribute — follows the OS. So
   the light palette is defined on bare :root, and the dark one twice: once
   under prefers-color-scheme (guarded, so a chosen light theme still wins on a
   dark machine) and once under [data-theme=dark] (so a chosen dark theme wins
   on a light one). A colour defined only inside one of those blocks has no
   value in the other two states, which is why every token starts here.
   ========================================================================== */

:root {
  /* Tells the browser which way to draw its own widgets — checkboxes, the select
     popup, scrollbars, the file button, focus rings. Without it those stay light
     on a dark page however the CSS is written, since the platform draws them. */
  color-scheme: light;

  --bg:            #f4f6fa;
  --surface:       #ffffff;
  --surface-sunk:  #eef1f6;
  /* A box nested inside a panel — a form's argument groups, the resource picker.
     Not --surface-sunk: that grey is right for a code chip or a table head sitting
     directly on the page, but on a white panel it reads as the page background
     showing through a hole in the panel rather than as a group within it. Dark has
     no such problem — its sunk value is *darker* than its surface, so it recesses —
     and keeps the colour it already had. */
  --surface-inset: #fbfcfe;
  --surface-hover: #f2f5f9;
  --sidebar-bg:    #ffffff;
  --wash:          #e8f2fd;   /* the tint behind a page header */
  /* A run's console. Light in the light theme, like every other panel: a black block in a white page is the
     loudest thing on it, and the console is the thing least worth shouting. */
  --console-bg:    #fbfcfe;
  --console-text:  #1f2933;

  --border:        #e2e7ee;
  --border-strong: #c9d2dd;

  --text:          #101720;
  --muted:         #5a6675;
  --subtle:        #8892a1;

  --primary:       #1f6feb;
  --primary-hover: #1a5fd0;
  --primary-text:  #ffffff;
  --primary-soft:  #eaf2ff;
  --brand:         #29b5e8;

  /* What part a block plays in a job, on the canvas: the accent rail down its left edge. Four hues, chosen
     to clear the two the diagram has already spent — the brand cyan on a scope's band and boundary, and
     --primary on a selection outline — so that a role can never be mistaken for a state. There is no fifth
     hue for a component loader: it declares both channels and so is drawn as the transform it is, and what
     makes it a loader is already said by the dashed dependency line into whatever asks it for something. */
  --role-source:    #2f9e68;
  --role-transform: #7a5bd1;
  --role-target:    #bf7016;
  --role-action:    #c1456a;
  /* The fifth shelf on the palette is not a role at all: a container has no ports to read one off. It is
     the scope vocabulary, so it borrows the hue that vocabulary already spends — the brand cyan on a
     container's band and boundary — rather than minting a fifth. A cyan tab yields cyan-banded boxes,
     which is the whole of what it has to say. Follows --brand per theme, so it is declared only here. */
  --role-structure: var(--brand);

  --danger:        #c02626;
  --danger-soft:   #fdf0f0;
  --danger-border: #f4c6c6;
  --success:       #0f7a4d;
  --success-soft:  #edfaf3;
  --success-border:#b6e6cd;
  --warning:       #92640b;
  --warning-soft:  #fdf7e6;
  /* A changed row's band in a comparison: amber leaning orange, so that it is not read as the danger red
     of a removed row beside it. */
  --changed:       #d97706;
  --warning-border:#efdca4;

  /* Syntax colour, for the code editor and the highlighted blocks beside it. Named by what a token *is*
     rather than by the language it came from: one palette has to serve YAML, Ruby, JavaScript and a
     growing set of formula dialects, and a hue that means "keyword" in one of them while meaning
     something else in the next is how a reader stops trusting the colour.

     Ace's theme is built out of these rather than out of literals (see app/javascript/lib/editor/
     theme.js). That is what lets *one* editor theme cover all three theme states: the variables flip
     underneath it and nothing in JavaScript has to be told that the page changed. The alternative —
     vendoring Ace's own light and dark themes and swapping the module over — would be two more files,
     a second place for the theme state to be wrong, and colours that are not this app's. */
  --syntax-comment:  #6a7482;
  --syntax-keyword:  #8250df;
  --syntax-string:   #0a7d55;
  --syntax-number:   #b3541e;
  --syntax-constant: #9a2a8f;
  --syntax-function: #1f6feb;
  --syntax-variable: #c1456a;
  --syntax-type:     #0f6b8c;
  --syntax-operator: #5a6675;
  --syntax-invalid:  #c02626;

  /* The editor's own furniture, which is not syntax: it has to read as part of the panel it sits in
     rather than as a window onto another application. */
  --editor-gutter:      #8892a1;
  --editor-active-line: rgb(31 111 235 / 6%);
  --editor-selection:   rgb(31 111 235 / 18%);
  --editor-indent:      #dbe1ea;

  --shadow-sm: 0 1px 2px rgb(16 23 32 / 6%);
  --shadow-md: 0 4px 16px rgb(16 23 32 / 10%);
  --shadow-lg: 0 12px 32px rgb(16 23 32 / 14%);

  --radius:    10px;
  --radius-sm: 6px;
  --radius-lg: 14px;

  --sidebar-w:  248px;
  --sidebar-cw: 68px;   /* collapsed */
  --content-w:  1120px;

  accent-color: var(--primary);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --bg:            #0d1117;
    --surface:       #161c24;
    --surface-sunk:  #11171e;
    --surface-inset: #11171e;
    --surface-hover: #1c242e;
    --sidebar-bg:    #0f151c;
    --wash:          #14212f;
    --console-bg:    #0a0e14;
    --console-text:  #cfd6e0;

    --border:        #242e3a;
    --border-strong: #38424f;

    --text:          #e6edf3;
    --muted:         #9aa7b6;
    --subtle:        #6f7d8c;

    --primary:       #4f8ef7;
    --primary-hover: #6aa1fa;
    --primary-text:  #06121f;
    --primary-soft:  #15243a;
    --brand:         #29b5e8;

    /* Lifted rather than inverted: the same four hues at the lightness a dark ground needs, each still
       clearly itself beside the others. */
    --role-source:    #4cc38a;
    --role-transform: #a98bee;
    --role-target:    #e0a33c;
    --role-action:    #e8798f;

    --danger:        #f28b82;
    --danger-soft:   #2a1618;
    --danger-border: #5d2a2a;
    --success:       #6ddba2;
    --success-soft:  #10241b;
    --success-border:#20482f;
    --warning:       #e8c26a;
    --warning-soft:  #241d0e;
    --changed:       #f5b031;
    --warning-border:#4a3b18;

    /* Lifted, like the role hues above: the same ten meanings at the lightness a dark ground needs. */
    --syntax-comment:  #7d8795;
    --syntax-keyword:  #c297f0;
    --syntax-string:   #7ee3b0;
    --syntax-number:   #f0a56a;
    --syntax-constant: #f2a2dd;
    --syntax-function: #6aa1fa;
    --syntax-variable: #f28fa4;
    --syntax-type:     #5fc8e0;
    --syntax-operator: #9aa7b6;
    --syntax-invalid:  #f28b82;

    --editor-gutter:      #6f7d8c;
    --editor-active-line: rgb(79 142 247 / 10%);
    --editor-selection:   rgb(79 142 247 / 26%);
    --editor-indent:      #242e3a;

    --shadow-sm: 0 1px 2px rgb(0 0 0 / 40%);
    --shadow-md: 0 4px 16px rgb(0 0 0 / 45%);
    --shadow-lg: 0 12px 32px rgb(0 0 0 / 55%);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --bg:            #0d1117;
  --surface:       #161c24;
  --surface-sunk:  #11171e;
  --surface-inset: #11171e;
  --surface-hover: #1c242e;
  --sidebar-bg:    #0f151c;
  --wash:          #14212f;
  --console-bg:    #0a0e14;
  --console-text:  #cfd6e0;

  --border:        #242e3a;
  --border-strong: #38424f;

  --text:          #e6edf3;
  --muted:         #9aa7b6;
  --subtle:        #6f7d8c;

  --primary:       #4f8ef7;
  --primary-hover: #6aa1fa;
  --primary-text:  #06121f;
  --primary-soft:  #15243a;
  --brand:         #29b5e8;

  /* Lifted rather than inverted: the same four hues at the lightness a dark ground needs, each still
     clearly itself beside the others. */
  --role-source:    #4cc38a;
  --role-transform: #a98bee;
  --role-target:    #e0a33c;
  --role-action:    #e8798f;

  --danger:        #f28b82;
  --danger-soft:   #2a1618;
  --danger-border: #5d2a2a;
  --success:       #6ddba2;
  --success-soft:  #10241b;
  --success-border:#20482f;
  --warning:       #e8c26a;
  --warning-soft:  #241d0e;
  --changed:       #f5b031;
  --warning-border:#4a3b18;

  /* Lifted, like the role hues above: the same ten meanings at the lightness a dark ground needs. */
  --syntax-comment:  #7d8795;
  --syntax-keyword:  #c297f0;
  --syntax-string:   #7ee3b0;
  --syntax-number:   #f0a56a;
  --syntax-constant: #f2a2dd;
  --syntax-function: #6aa1fa;
  --syntax-variable: #f28fa4;
  --syntax-type:     #5fc8e0;
  --syntax-operator: #9aa7b6;
  --syntax-invalid:  #f28b82;

  --editor-gutter:      #6f7d8c;
  --editor-active-line: rgb(79 142 247 / 10%);
  --editor-selection:   rgb(79 142 247 / 26%);
  --editor-indent:      #242e3a;

  --shadow-sm: 0 1px 2px rgb(0 0 0 / 40%);
  --shadow-md: 0 4px 16px rgb(0 0 0 / 45%);
  --shadow-lg: 0 12px 32px rgb(0 0 0 / 55%);
}

/* ==========================================================================
   2. Base
   ========================================================================== */

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 15px;
  background: var(--bg);
  color: var(--text);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

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

h1, h2, h3 { color: var(--text); font-weight: 650; letter-spacing: -.01em; }
h1 { font-size: 1.65rem; margin: 0; line-height: 1.25; }
h2 { font-size: 1.05rem; margin: 0 0 .75rem; }
h3 { font-size: .92rem; margin: 0 0 .5rem; }

code, .mono {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: .86em;
}
code {
  background: var(--surface-sunk);
  border-radius: 4px;
  padding: .1rem .35rem;
}

/* A block of code — a job definition, a config — where the chip styling of inline
   `code` is wrong: its inline padding lands at the start of the first line only,
   which in whitespace-significant text reads as an indent that is not in the file. */
.code-block {
  margin: 0;
  padding: .75rem .9rem;
  background: var(--surface-sunk);
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow-x: auto;
}
.code-block code { background: none; border-radius: 0; padding: 0; }

/* The editable twin of .code-block: a job definition being typed. `white-space:
   pre` with its own horizontal scroll rather than wrapping, because a wrapped
   long line in whitespace-significant text reads as an indent that is not there
   — the same reason .code-block does not wrap either. */
.code-area {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: .75rem;
  line-height: 1.5;
  tab-size: 2;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
  resize: vertical;
  background: var(--surface-sunk);
}

/* Where Ace loaded, it hides the textarea and puts this beside it, so every rule that sized the one has
   to size the other. Not a wrapper around the field: a wrapper would have to be rendered by the server
   for a case that may never arrive, and this way the markup a browser without JavaScript gets is exactly
   the markup that was always there. See app/javascript/lib/editor/editor.js. */
.code-editor {
  height: 18rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-sunk);
  overflow: hidden;   /* the editor scrolls inside itself; the rounded corners have to clip it */
}
/* The block rendering replaces the contents of a `<code>`, which is inline, with the highlighter's own
   divs. Declaring it a block keeps that legal as well as merely working. */
.code-block code { display: block; }

/* A panel setting that holds source. The box is taller than a one-line control because an expression
   wraps and a script does not fit at all, and `Expand` sits under it rather than beside: the panel is a
   dock and a button in the right margin of a full-width control is a button in the scrollbar. */
.field-source { display: flex; flex-direction: column; gap: .35rem; align-items: flex-end; }
.field-source .code-area,
.field-source .code-editor { width: 100%; }
.field-source .code-editor { height: 9rem; }
.field-expand { align-self: flex-end; }

/* The same setting, in a window wide enough to write it in — see job_canvas_controller#expandSource.
   Sized against the viewport rather than the content: what is being edited is a program, and a dialog
   that grew with the text would move its own buttons while somebody typed. */
/* Resizable, and a flex column so that resizing it means something: the body is the part that grows, and
   the editor inside fills the body. Ace measures its container once and then believes itself, so the
   editor only follows because `attach` puts a ResizeObserver on it — which is also what makes it survive
   a tab unhiding and the sidebar collapsing. See lib/editor/editor.js.
 *
 * `overflow` has to be something other than `visible` for `resize` to apply at all. It never actually
 * scrolls: the body takes the slack and `min-height: 0` lets it shrink, so the head and foot stay put. */
.source-dialog {
  width: min(1060px, 94vw);
  height: min(80vh, 50rem);
  max-width: 98vw;
  max-height: 96vh;
  min-width: 22rem;
  min-height: 15rem;
  display: flex;
  flex-direction: column;
  resize: both;
  overflow: auto;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-lg);
}
.source-dialog::backdrop { background: rgb(16 23 32 / 45%); }
.source-dialog-head {
  display: flex; align-items: center; gap: .6rem;
  padding: .85rem 1rem; border-bottom: 1px solid var(--border);
}
.source-dialog-head h2 { margin: 0; }
/* The part that grows. `min-height: 0` because a flex item refuses by default to shrink below its
   content, and the editor's content is a whole script. */
.source-dialog-body { flex: 1; min-height: 0; display: flex; padding: 1rem; }
/* Filling the body rather than a height of its own: the editor scrolls inside itself, which is what keeps
   the foot where the hand left it however the dialog is dragged. */
.source-dialog-body .code-area,
.source-dialog-body .code-editor { flex: 1; width: 100%; height: 100%; }
.source-dialog-foot {
  display: flex; justify-content: flex-end; gap: .5rem;
  padding: .75rem 1rem; border-top: 1px solid var(--border);
}
/* A note sitting beside the button that saves it, rather than under: the foot is
   one line of "what this does" and should read as one. */
.panel-foot .btn + .small { margin-left: .7rem; }

.muted   { color: var(--muted); }
.subtle  { color: var(--subtle); }
.small   { font-size: .85rem; }
.nowrap  { white-space: nowrap; }
/* For text that is only there for a screen reader — the heading of a column drawn
   as an icon, or the word an icon stands for. Clipped rather than `display: none`,
   which would take it out of the accessibility tree along with the layout. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}
.muted.danger, .small.danger, .danger { color: var(--danger); }

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

/* ==========================================================================
   3. Shell — sidebar + work area
   ========================================================================== */

.shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  min-height: 100vh;
}
html.sidebar-collapsed .shell { grid-template-columns: var(--sidebar-cw) minmax(0, 1fr); }

/* --- Sidebar ------------------------------------------------------------ */

.sidebar {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  gap: .25rem;
  padding: .85rem .6rem .6rem;
  background: var(--sidebar-bg);
  border-right: 1px solid var(--border);
  overflow: hidden;
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .1rem .35rem .6rem;
}
.sidebar-brand a {
  display: flex;
  align-items: center;
  gap: .5rem;
  color: var(--text);
  font-weight: 700;
  letter-spacing: .06em;
  font-size: .95rem;
  text-decoration: none;
  min-width: 0;
}
.sidebar-brand .mark { color: var(--brand); flex: none; }
/* The lowercase wordmark, wherever the brand is drawn. It is set large enough
   that the "d" and "b" ascenders stand as tall as the mark beside it, and each
   place that uses it pairs it with a mark sized to match — see .auth-brand and
   .mobile-bar .brand for the other two. The parents' tracking is meant for
   capitals, so it is undone here. */
.wordmark {
  font-size: 1.6rem;
  line-height: 1;
  letter-spacing: -.01em;
  text-transform: lowercase;
}
.sidebar-collapse {
  margin-left: auto;
  display: grid;
  place-items: center;
  width: 28px; height: 28px;
  border: none; border-radius: var(--radius-sm);
  background: none; color: var(--subtle);
  cursor: pointer;
}
.sidebar-collapse:hover { background: var(--surface-hover); color: var(--text); }

/* The row of bare icons under the brand — the two or three things done most
   often, reachable from every page without reading a list. */
.sidebar-quick {
  display: flex;
  gap: .25rem;
  padding: 0 .1rem .6rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: .4rem;
}
.sidebar-quick a {
  flex: 1;
  display: grid;
  place-items: center;
  height: 34px;
  border-radius: var(--radius-sm);
  color: var(--muted);
}
.sidebar-quick a:hover { background: var(--surface-hover); color: var(--text); text-decoration: none; }
.sidebar-quick a.current { background: var(--primary-soft); color: var(--primary); }

.sidebar-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
}

.nav-section { margin-bottom: 1rem; }
.nav-section > h4 {
  margin: 0 0 .3rem;
  padding: 0 .55rem;
  font-size: .7rem;
  font-weight: 650;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--subtle);
}
.nav-item {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .42rem .55rem;
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: .9rem;
  text-decoration: none;
  white-space: nowrap;
}
.nav-item:hover { background: var(--surface-hover); text-decoration: none; }
.nav-item svg { flex: none; color: var(--muted); }
.nav-item.current { background: var(--primary-soft); color: var(--primary); font-weight: 600; }
.nav-item.current svg { color: var(--primary); }
.nav-item .nav-count { margin-left: auto; font-size: .75rem; color: var(--subtle); }

/* Which workspace the Work section is about. A <details> rather than a scripted
   menu: the whole interaction is open, pick a link. */
.nav-context { padding: 0 .2rem .45rem; }
.nav-context > summary {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .4rem .55rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
  list-style: none;
  font-size: .88rem;
  font-weight: 600;
}
.nav-context > summary::-webkit-details-marker { display: none; }
.nav-context > summary::after {
  content: "";
  margin-left: auto;
  width: 6px; height: 6px;
  border-right: 1.5px solid var(--subtle);
  border-bottom: 1.5px solid var(--subtle);
  transform: rotate(45deg) translate(-2px, -2px);
}
.nav-context[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.nav-context > summary .ctx-label { overflow: hidden; text-overflow: ellipsis; }
.nav-context ul { list-style: none; margin: .25rem 0 0; padding: 0; }
.nav-context li > a { display: block; padding: .35rem .55rem; border-radius: var(--radius-sm);
                      font-size: .88rem; color: var(--text); }
.nav-context li > a:hover { background: var(--surface-hover); text-decoration: none; }
.nav-context li > a.current { color: var(--primary); font-weight: 600; }

/* --- Sidebar footer ----------------------------------------------------- */

.sidebar-foot {
  position: relative;
  border-top: 1px solid var(--border);
  padding-top: .5rem;
  display: flex;
  align-items: center;
  gap: .5rem;
  min-width: 0;
}
.avatar {
  flex: none;
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--primary-soft);
  color: var(--primary);
  font-size: .78rem;
  font-weight: 700;
}
.sidebar-foot .who { min-width: 0; line-height: 1.25; }
.sidebar-foot .who strong {
  display: block;
  font-size: .8rem; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sidebar-foot .who span {
  display: block;
  font-size: .72rem; color: var(--subtle);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The org switcher. Same <details> idiom as the workspace one, but it opens
   upward and over the nav: the footer sits at the bottom of the viewport, so an
   inline list would unfold off the end of it. Anchored to .sidebar-foot rather
   than to the summary so the menu can use the sidebar's full width — .sidebar
   clips its overflow, and .who is only part of that width. */
.org-switch > summary {
  display: flex; align-items: center; gap: .25rem;
  min-width: 0;
  cursor: pointer;
  list-style: none;
  font-size: .72rem; color: var(--subtle);
}
.org-switch > summary::-webkit-details-marker { display: none; }
.org-switch > summary > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.org-switch > summary::after {
  content: "";
  flex: none;
  width: 5px; height: 5px;
  border-right: 1.5px solid currentcolor;
  border-bottom: 1.5px solid currentcolor;
  transform: rotate(-135deg) translate(-1px, -1px);
}
.org-switch[open] > summary::after { transform: rotate(45deg) translate(-1px, -1px); }
.org-switch > summary:hover { color: var(--text); }
.org-switch > ul {
  position: absolute;
  bottom: calc(100% - .25rem);
  left: 0; right: 0;
  z-index: 20;
  margin: 0; padding: .25rem;
  list-style: none;
  max-height: 15rem; overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
}
.org-switch li > a {
  display: block;
  padding: .35rem .5rem;
  border-radius: var(--radius-sm);
  font-size: .8rem; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.org-switch li > a:hover { background: var(--surface-hover); text-decoration: none; }
.org-switch li > a.current { color: var(--primary); font-weight: 600; }

.icon-btn {
  display: grid; place-items: center;
  width: 30px; height: 30px;
  border: none; border-radius: var(--radius-sm);
  background: none; color: var(--muted);
  cursor: pointer; padding: 0;
}
.icon-btn:hover { background: var(--surface-hover); color: var(--text); }
/* A destructive icon action stays muted at rest — a row of red trash cans reads as
   a warning about the data rather than as one thing you may do to it — and only
   turns red under the pointer. */
.icon-btn.danger:hover { background: var(--danger-soft); color: var(--danger); }
.sidebar-foot form { margin: 0; }
.sidebar-foot .tools { margin-left: auto; display: flex; gap: .1rem; }

/* --- Collapsed state ---------------------------------------------------- */

html.sidebar-collapsed .shell .sidebar { padding-left: .5rem; padding-right: .5rem; }
html.sidebar-collapsed .shell .sidebar-brand a span,
html.sidebar-collapsed .shell .nav-section > h4,
html.sidebar-collapsed .shell .nav-item span,
html.sidebar-collapsed .shell .nav-item .nav-count,
html.sidebar-collapsed .shell .nav-context,
html.sidebar-collapsed .shell .sidebar-foot .who,
html.sidebar-collapsed .shell .sidebar-foot .tools { display: none; }
html.sidebar-collapsed .shell .nav-item { justify-content: center; padding: .45rem 0; }
html.sidebar-collapsed .shell .sidebar-quick { flex-direction: column; }
html.sidebar-collapsed .shell .sidebar-brand { justify-content: center; padding-right: 0; }
html.sidebar-collapsed .shell .sidebar-collapse { margin-left: 0; }
/* No room for both the wordmark and the control at 68px, and the control is
   the one that gets you back out. */
html.sidebar-collapsed .shell .sidebar-brand a { display: none; }
html.sidebar-collapsed .shell .sidebar-foot { justify-content: center; }

/* --- Work area ---------------------------------------------------------- */

.workarea { min-width: 0; display: flex; flex-direction: column; }

/* Both of these exist only at drawer width — see the media query at the end.
   Stated here rather than only there, because a grid child with no rules of its
   own takes a column of the shell: an unstyled scrim is a blank button the size
   of the page, sitting exactly where the content should be. */
.mobile-bar, .scrim { display: none; }

.content {
  flex: 1;
  width: 100%;
  max-width: var(--content-w);
  margin: 0 auto;
  padding: 1.75rem 2rem 4rem;
}

/* The Work area sits against the sidebar instead of being centred in the window:
   centred, a page of tiles drifts away from the nav that led to it, by a gap that
   grows with the monitor. Administration and System keep the centred column,
   where the content is forms and prose. See ApplicationHelper#content_width_classes,
   which decides which of these a page gets. */
.content.flush { margin-left: 0; margin-right: auto; }
/* And where the content is a grid that keeps filling — the org home, a workspace
   overview, the command picker — no cap at all, so the tiles use the window
   rather than leaving a band of empty page beside them. Panels that have a
   measure of their own (.form-panel, .lede) still hold it. */
.content.wide { max-width: none; }

/* The tint behind the top of a page, as Snowflake's home has. Sits behind the
   header only, and fades out — a full-page gradient fights every panel on it. */
.workarea::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 260px;
  background: linear-gradient(180deg, var(--wash), transparent);
  pointer-events: none;
  z-index: 0;
}
.workarea { position: relative; }
.content { position: relative; z-index: 1; }

/* ==========================================================================
   4. Page furniture
   ========================================================================== */

.page-header { margin-bottom: 1.5rem; }
.page-header .crumbs {
  display: flex; align-items: center; gap: .35rem;
  font-size: .8rem; color: var(--subtle);
  margin-bottom: .35rem;
}
.page-header .crumbs a { color: var(--muted); }
.page-header .crumbs .sep { color: var(--border-strong); }
.page-title {
  display: flex; align-items: center; gap: .75rem;
  flex-wrap: wrap;
}
.page-title h1 { margin-right: auto; }
.page-header .lede {
  margin: .5rem 0 0;
  max-width: 68ch;
  color: var(--muted);
  font-size: .9rem;
}
.page-actions { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.page-actions form { margin: 0; }

/* A block of explanation between the header and the first panel. */
.note { margin: 0 0 1.25rem; max-width: 74ch; color: var(--muted); font-size: .875rem; }
/* On a page whose content runs the full width of the container — a table, a
   grid of tiles, a stack of panels — the measure cap reads as a ragged edge
   against what is below it rather than as a comfortable line length: `.wide`
   lets the prose run to the same width as what it describes. Pages whose
   content is a form column keep the cap: there the measure is the narrower of
   the two, so removing it would over-run the form instead. */
.page-header .lede.wide, .note.wide { max-width: none; }
/* One that follows content instead of introducing it — a footnote under a grid
   of tiles — needs the gap on the other side. */
.tiles + .note, .table-wrap + .note { margin-top: 1.25rem; }
.note strong { color: var(--text); }

section { margin-top: 1.75rem; }
section > h2 { display: flex; align-items: center; gap: .5rem; }
.section-head {
  display: flex; align-items: center; gap: .75rem;
  margin-bottom: .75rem; flex-wrap: wrap;
}
.section-head h2, .section-head h3 { margin: 0; margin-right: auto; }
.section-head-sub { margin-top: 1.25rem; }

/* A label over a run of values that is not a table row — a strip of badges under
   a table, say. Weighted like a column heading rather than like body text, so
   what it names reads as another field of the same thing and not as a footnote. */
.sub-label {
  margin: 0 0 .4rem;
  font-size: .7rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--subtle); font-weight: 600;
}

/* Keeps a button_to next to a link on one line: button_to emits a block-level
   <form>, which would otherwise wrap onto its own row. */
.actions-inline { display: flex; align-items: center; gap: .25rem; flex-wrap: wrap; }
.actions-inline form { margin: 0; }

/* The same job as .actions-inline for a table row whose actions are all icons:
   tight enough that three fit the column without wrapping, and pulled to the
   right so the action column lines up down the table. */
/* A name that is a link but keeps the text colour, as in the file tree. */
a.plain-link { color: var(--text); }
.row-actions { display: inline-flex; align-items: center; gap: .1rem; justify-content: flex-end; }
.row-actions .icon-btn { padding: .25rem; }
.row-actions form { margin: 0; }

/* ==========================================================================
   5. Panels, tiles and cards
   ========================================================================== */

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}
.panel > .panel-head {
  display: flex; align-items: center; gap: .75rem;
  padding: .8rem 1.1rem;
  border-bottom: 1px solid var(--border);
}
.panel > .panel-head h2, .panel > .panel-head h3 { margin: 0; margin-right: auto; }
/* A head whose left-hand side is more than a heading — a title with a line of explanation under it. The
   heading's own `margin-right: auto` cannot push anything from inside a wrapper, so the wrapper takes it. */
.panel-head .head-text { margin-right: auto; }
.panel-head .head-text h2 { margin: 0; }
.panel-head .head-text p { margin: .15rem 0 0; }
/* The canvas's own actions, in its head. A wider gap than a table row's icons get: these are worded
   buttons and the status beside them is prose, not a row of glyphs to be scanned.
 *
 * The auto margin that splits the head in two is *theirs*, not the heading's: a head whose heading takes it
 * pushes everything after the heading to the far end, and the read-only badge belongs beside the version it
 * is about rather than against the Save button. */
.canvas-actions { gap: .4rem; margin-left: auto; }
.panel > .panel-head:has(.canvas-actions) > h2 { margin-right: 0; }
/* Locked text is not disabled text: it still selects and copies, and says so by not offering a caret. */
.code-area[readonly] { cursor: default; }
.panel-body { padding: 1.1rem; }

/* A panel that takes the window: the definition editor, whose content has no natural end and where every
   row the page keeps for itself is a row of YAML nobody can see.
 *
 * `--fill-height` is measured — see fill_height_controller.js — because what is above the panel is not a
 * constant: how many rows the meta grid takes, and whether the title or the crumbs wrap, both depend on the
 * width. The fallback is that constant anyway, for a page whose JavaScript has not run: close on a typical
 * window, and `min-height` keeps it usable on a short one. `dvh` rather than `vh` because on a phone the
 * two differ by the whole address bar. */
.panel-fill {
  display: flex;
  flex-direction: column;
  height: var(--fill-height, calc(100dvh - 25rem));
  min-height: 20rem;
  /* Negative, and measured with the height it belongs to: it cancels the part of the page's bottom padding
     that a filled panel has no use for. Zero until the measurement runs. */
  margin-bottom: var(--fill-pull, 0px);
}
.panel-fill > .panel-head, .panel-fill > .panel-foot { flex: none; }
/* Exactly one body in a fill panel takes the room left over, and it says which it is: the canvas panel has
   a second body above it holding the note about why a job cannot be structurally edited, and splitting the
   window between the two would give half of it to a paragraph. The others keep their own height, but may
   shrink and scroll rather than push the panel past the window — a job with a long list of reasons is a
   long note. `min-height: 0` throughout because a flex item's default `min-height: auto` refuses to shrink
   below its content, which for a definition is the whole file. */
.panel-fill > .panel-body { flex: 0 1 auto; min-height: 0; overflow-y: auto; }
.panel-fill > .panel-body-fill { flex: 1 1 auto; min-height: 0; overflow: visible; display: flex; }
/* Fills the body, and is the thing that scrolls. `resize` is dropped with the height no longer its own:
   a drag handle that pulled the textarea out through the bottom of its panel is worse than none.
 *
 * **Child combinators, and they are load-bearing.** These say "the one piece of code this panel is built
 * around" — the definition on its tab, which is a direct child of the fill body. Written as descendants
 * they also caught every `.code-editor` nested anywhere inside a fill panel, and the canvas is one: the
 * property panel's editors tie with `.field-source .code-editor` on specificity, lose on being earlier in
 * this file, and take `height: 100%` of a parent that has no height of its own — which computes to the
 * two pixels of their own border. The editor is then invisible rather than broken-looking, and the panel
 * reads as a setting with no control at all. */
.panel-fill > .panel-body-fill > .code-area { flex: 1; height: 100%; resize: none; }
.panel-fill > .panel-body-fill > .code-editor { flex: 1; height: 100%; }
.panel-fill > .panel-body-fill > .code-block { flex: 1; margin: 0; overflow: auto; }
/* Likewise the canvas row: in a fill panel its height is what is left after the head, the note and the
   foot, rather than a share of the viewport that knows nothing about them. Its own `resize` goes with it —
   there is nothing left to grow into. */
.panel-fill .canvas-with-palette { flex: 1; height: auto; min-height: 240px; resize: none; }
.panel-body > :first-child { margin-top: 0; }
.panel-body > :last-child { margin-bottom: 0; }
/* For a panel whose content is its own box — the canvas — where the body padding would frame it twice. */
.panel-body-flush { padding: 0; }
.panel-foot {
  padding: .8rem 1.1rem;
  border-top: 1px solid var(--border);
  background: var(--surface-sunk);
  border-radius: 0 0 var(--radius) var(--radius);
}

/* A grid of tiles. `auto-fill` rather than `auto-fit` so three tiles on a wide
   screen stay tile-sized instead of stretching to a third of the page each. */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: .85rem;
}
.tiles.tiles-wide { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }

.tile {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  padding: 1rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  color: var(--text);
  text-decoration: none;
  transition: border-color .12s, box-shadow .12s, transform .12s;
}
a.tile:hover {
  border-color: var(--primary);
  box-shadow: var(--shadow-md);
  text-decoration: none;
  transform: translateY(-1px);
}
.tile .tile-icon { color: var(--primary); margin-bottom: .35rem; }
.tile .tile-title { font-weight: 620; font-size: .95rem; }
/* A command tile is titled by its engine key, which is set in code type — but as
   a title, not as an inline snippet, so it loses the chip background. */
.tile .tile-title code { background: none; padding: 0; font-size: .95rem; color: inherit; }
.tile .tile-sub { color: var(--muted); font-size: .82rem; }
.tile .tile-meta { margin-top: .4rem; color: var(--subtle); font-size: .78rem; }
/* A tile that holds a form rather than being a link — the two ways of adding a
   file, side by side, each with its own fields. */
.tile form { margin-top: .7rem; width: 100%; }
.tile form .field:last-of-type { margin-bottom: 1rem; }
.tile.is-disabled { opacity: .55; }
.tile.is-disabled:hover { border-color: var(--border); box-shadow: var(--shadow-sm); transform: none; }

/* The empty state of a list — a dashed panel saying what would be here. */
.empty {
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  padding: 2rem 1.5rem;
  text-align: center;
  color: var(--muted);
}
.empty strong { display: block; color: var(--text); margin-bottom: .25rem; }

/* Key/value block — a run's metadata, a record's summary. */
.meta-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: .9rem 1.75rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.1rem;
}
.meta-grid dt {
  font-size: .7rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--subtle); font-weight: 600;
}
.meta-grid dd { margin: .15rem 0 0; font-size: .9rem; }
/* A cell holding prose rather than a fact — a description among a row of counts and dates. Only above the
   width where the grid has two columns to give it: forcing a span onto a one-column grid would create an
   implicit second column narrower than the 180px the track asks for, and overflow the panel. */
@media (min-width: 700px) {
  .meta-grid .span-2 { grid-column: span 2; }
}
/* A cell that takes the whole width whatever the width is — the engine's complaints under the verdict that
   names them. `1 / -1` rather than a span, so it is one row on a one-column grid as well as on four. */
.meta-grid .span-all { grid-column: 1 / -1; }
/* Ruled rather than boxed: the block it sits in is already a panel, and a panel inside a panel reads as a
   second thing on the page. The rule carries the colour the verdict above is written in. */
.meta-grid .validation-failures dd {
  border-left: 2px solid var(--danger);
  padding-left: .7rem;
}
.meta-grid .validation-failures ul { margin: 0; padding-left: 1.1rem; }
.meta-grid .validation-failures li { margin-bottom: .2rem; }
.meta-grid .validation-failures p { margin: .4rem 0 0; }

/* A stack of panels down a page. Panels carry no margin of their own, so that
   one dropped into a form column or a tile grid does not push its neighbours
   around; a page that means to run several down the page says so here. */
.panel-stack { display: flex; flex-direction: column; gap: .85rem; }
/* `display: flex` above would otherwise beat the browser's own [hidden] rule. */
.panel-stack[hidden] { display: none; }

/* A tab strip over panels, for a page with more on it than one column can hold
   without becoming a scroll. The strip is a row of buttons on a rule, with the
   open one carrying the rule's colour — so the reading is "this is the sheet in
   front", not "this is a pressed button". Everything below the strip is in the
   DOM whether shown or not; see tabs_controller.js. */
.tab-bar {
  display: flex; align-items: flex-end; gap: .25rem;
  margin-bottom: 1.1rem;
  border-bottom: 1px solid var(--border);
  overflow-x: auto;              /* narrow screens scroll the strip, not the page */
  scrollbar-width: none;
}
.tab-bar::-webkit-scrollbar { display: none; }
.tab-bar > .tab {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .55rem .85rem;
  border: 0; border-bottom: 2px solid transparent;
  margin-bottom: -1px;           /* sits on the rule rather than above it */
  background: none;
  font: inherit; font-size: .875rem; font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
  cursor: pointer;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.tab-bar > .tab:hover { color: var(--text); background: var(--surface-hover); }
.tab-bar > .tab.current { color: var(--primary); border-bottom-color: var(--primary); }
.tab-bar > .tab:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
/* How many rows are behind the tab, so the count does not cost a click to read. */
.tab-bar > .tab .tab-count {
  font-size: .72rem; font-weight: 600;
  padding: .05rem .35rem;
  border-radius: 999px;
  background: var(--surface-sunk); color: var(--subtle);
}
.tab-bar > .tab.current .tab-count { background: var(--primary-soft); color: var(--primary); }

/* ==========================================================================
   6. Buttons
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .45rem .85rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  font-size: .875rem;
  font-family: inherit;
  font-weight: 500;
  line-height: 1.3;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}
.btn:hover { background: var(--surface-hover); border-color: var(--muted); text-decoration: none; }
.btn:disabled, .btn[disabled] { opacity: .5; cursor: not-allowed; }

.btn-primary {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--primary-text);
  font-weight: 600;
}
.btn-primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); }

.btn-link {
  background: none; border-color: transparent;
  color: var(--primary); padding: .35rem .5rem;
}
.btn-link:hover { background: var(--primary-soft); border-color: transparent; }
.btn-link.danger { color: var(--danger); }
.btn-link.danger:hover { background: var(--danger-soft); }

.btn-block { display: flex; width: 100%; justify-content: center; }
.btn-sm { padding: .3rem .6rem; font-size: .8rem; }

/* ==========================================================================
   7. Forms
   --------------------------------------------------------------------------
   Themed by element, not by class: half this app's fields are built with *_tag
   helpers that carry no class, so a class-only rule dresses some screens and
   leaves others in browser default. Buttons are excluded (.btn covers those),
   and so are checkboxes and radios, which want the platform's own drawing under
   accent-color rather than a border and padding.
   ========================================================================== */

input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=button]):not([type=file]),
select, textarea {
  padding: .48rem .65rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font-size: .9rem;
  font-family: inherit;
}
input:focus, select:focus, textarea:focus { border-color: var(--primary); }
textarea { line-height: 1.5; }
input::placeholder, textarea::placeholder { color: var(--subtle); }

input[type=file] { color: var(--text); font-size: .875rem; }
/* The button half of a file field is a shadow element, so it is reached by name
   rather than by styling the input. */
input[type=file]::file-selector-button {
  padding: .4rem .75rem; margin-right: .6rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
  font: inherit; font-size: .85rem; cursor: pointer;
}
input[type=file]::file-selector-button:hover { background: var(--surface-hover); }

.input, .select, .picker-field { width: 100%; }
/* A disabled input keeps whatever was typed in it — an optional-value argument
   unticked is still holding the value it will use when ticked again. It has to
   *look* inert, and the explicit background/color above defeat the browser's own
   dimming, so it is restated here. */
.input:disabled, .select:disabled, input:disabled, select:disabled, textarea:disabled {
  opacity: .5; cursor: not-allowed;
}
/* A checkbox carries no .input class, so the rule above does not reach it — and
   the label beside it has to dim too, or a live-looking word sits next to a dead
   control. */
label.checkbox:has(input:disabled) { opacity: .5; cursor: not-allowed; }

label { display: block; margin-bottom: .3rem; font-size: .875rem; color: var(--muted); }

/* One field: its label, its control, and (often) a line of description under it.
   Label and description are told apart two ways — the label is the field's
   heading, full-strength and semibold, while the description stays muted and
   smaller; and the spacing is nested, so the gap between one field and the next
   is larger than any gap within a field.

   Direct children only: a field can wrap a composite control with labels and
   notes of its own (the resource-path picker), and those are that control's. */
.field { margin-bottom: 1.4rem; }
.field:last-child { margin-bottom: 0; }
.field > label, .field .field-label {
  display: block;
  margin-bottom: .35rem;
  font-size: .875rem; font-weight: 600; color: var(--text);
}
/* A marker on the label — "required" — sits on the label's own line rather than
   between the label and the control. On its own line it reads as a heading for
   the box below it, and the focus ring (2px outline, 2px offset) drawn around
   that box overlaps it. */
.field > label .badge, .field .field-label .badge { margin-left: .4rem; vertical-align: 1px; }

/* No measure cap: a description reads as belonging to the control above it,
   which means running to the same width. */
.field > .muted.small, .field .field-hint {
  margin: .4rem 0 0; font-size: .8rem; line-height: 1.5; color: var(--muted);
}
/* The name for a thing the run will create, revealed under the source select that
   asked for it. Indented and ruled rather than merely spaced, because it is a
   follow-up question: without the rule it reads as the next argument along, and
   the two are one decision. */
.field-created {
  margin-top: .7rem; padding-left: .8rem;
  border-left: 2px solid var(--border);
}
.field-created > label { font-weight: 500; }

/* The same nesting one level out: the gap between two argument groups is larger
   than the gap between two fields within one, so the grouping is legible as
   grouping. It needs stating rather than inheriting — `.field:last-child` zeroes
   the last field's margin. */
fieldset {
  margin: 0 0 1.75rem;
  padding: 1rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-inset);
  /* A fieldset is min-width:min-content by default — the one UA rule that no
     reset here undoes — so a group holding something wide (the resource
     picker's grid) stretches past the panel and reads as a box that does not
     line up with the groups above and below it. Every other group is narrower
     than its content's minimum, so only that one ever showed it. */
  min-inline-size: 0;
}
fieldset:last-of-type { margin-bottom: 1rem; }
legend {
  padding: 0 .4rem;
  font-size: .8rem; font-weight: 650; text-transform: uppercase; letter-spacing: .05em;
  color: var(--subtle);
}
.run-form fieldset { margin-bottom: 1.75rem; }

label.checkbox { display: flex; align-items: center; gap: .5rem; color: var(--text);
                 font-size: .9rem; margin-bottom: .3rem; }
label.checkbox .input { width: auto; }
/* A checkbox with its label on one line — labels are block-level by default. */
.select-all { display: flex; align-items: center; gap: .45rem; margin: 0 0 .75rem; }
.select-all label { display: inline; margin: 0; color: var(--text); }

/* A checkbox field: the box and its label belong on one line. Any description
   wraps full-width under both rather than in between.

   Matched structurally as well as by class, because the class kept being left
   off: a `.field` is a stacked label-over-control by default, so a bare checkbox
   in one renders below its own label with a stretch of empty row beside it, and
   whoever writes the next form has to know to opt out. `:has` says what the rule
   actually means — a field whose control is a checkbox is laid out this way —
   and the class stays for the composites that need it without a direct child
   checkbox to spot. */
.field-check,
.field:has(> input[type=checkbox]) {
  display: flex; flex-wrap: wrap; align-items: center; column-gap: .5rem;
}
.field-check > input[type=checkbox], .field > input[type=checkbox] { margin: 0; }
.field-check > label, .field:has(> input[type=checkbox]) > label { margin-bottom: 0; }
.field-check > .muted.small, .field-check > .field-hint,
.field:has(> input[type=checkbox]) > .muted.small,
.field:has(> input[type=checkbox]) > .field-hint { flex-basis: 100%; }

/* A row of two or three controls that belong together — a key and its value. */
.field-row { display: flex; gap: .5rem; align-items: center; margin-bottom: .5rem; flex-wrap: wrap; }
.field-row .input { width: auto; flex: 1 1 12rem; }
.field-row label { margin: 0; min-width: 8rem; }

.form-actions {
  display: flex; gap: .6rem; align-items: center; flex-wrap: wrap;
  margin-top: 1.25rem; padding-top: 1.1rem;
  border-top: 1px solid var(--border);
}
.form-actions:first-child { margin-top: 0; padding-top: 0; border-top: none; }

/* Where a form reports something it had to go and ask for — whether a store's
   declaration reaches anything. Below the actions, next to the button that
   fetched it, and taking no room until there is something to say. */
.form-result:not(:empty) { margin-top: 1rem; }
.form-result .alert { margin-bottom: 0; }

.inline-form { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
.inline-form .input, .inline-form .select { width: auto; }
.inline-form label { margin: 0; }

/* A form on its own page — sits in a panel of readable width. */
.form-panel { max-width: 680px; }
.form-panel.form-wide { max-width: 900px; }

details.field > summary {
  cursor: pointer; color: var(--muted); font-size: .85rem;
  padding: .3rem 0; margin-bottom: .5rem;
}
details.field { background: none; border: none; padding: 0; }

/* ==========================================================================
   8. Messages
   ========================================================================== */

.flash, .alert {
  display: flex; gap: .6rem; align-items: flex-start;
  padding: .7rem 1rem;
  border: 1px solid;
  border-radius: var(--radius-sm);
  margin-bottom: 1rem;
  font-size: .88rem;
}
/* The bare `.alert` is a warning, and comes *first*: it is the base every
   modifier overrides, and a single class each means whichever rule is written
   last wins. Written the other way round — as it was until 2026-08-26 — every
   `class="alert alert-error"` in this app rendered warning-yellow, which is a
   whole category of message this app never actually managed to show. */
.alert {
  background: var(--warning-soft); border-color: var(--warning-border); color: var(--warning);
}
.flash-notice, .alert-notice, .alert-success {
  background: var(--success-soft); border-color: var(--success-border); color: var(--success);
}
.flash-alert, .alert-error, .alert-danger {
  background: var(--danger-soft); border-color: var(--danger-border); color: var(--danger);
}
.alert ul, .errors ul { margin: .2rem 0; padding-left: 1.2rem; }
/* The dismiss, pushed to the far end of the message and drawn in the message's own colour rather than the
   muted grey an icon button is elsewhere: on a coloured band a grey glyph reads as disabled. */
.flash-dismiss {
  margin: -.2rem -.4rem -.2rem auto;
  width: 24px; height: 24px;
  color: inherit; opacity: .7;
}
.flash-dismiss:hover { color: inherit; opacity: 1; background: rgb(0 0 0 / .07); }

/* The job canvas: palette, paper and property panel side by side.
 *
 * **The height belongs to the row, not to any one of the three.** A flex line is as tall as its tallest
 * item, so while the paper alone carried the height a palette of seventy blocks — or a property panel for a
 * block with twenty settings — simply made the whole row that tall and ran off the bottom of the page. The
 * scrollbars each column already asks for never appeared, because nothing was ever bounded. With the height
 * here, the three columns are stretched to it and each scrolls inside its own share.
 *
 * The paper sizes itself to its container, so that container needs a definite height — `height: 100%`
 * against this one — or a percentage height resolves to zero and the diagram draws correctly into a box
 * nobody can see. Resizable, because how much room a diagram needs is a property of the diagram, and now
 * the drag takes all three columns with it rather than sliding the paper past its neighbours. */
.canvas-with-palette {
  display: flex;
  align-items: stretch;
  height: 60vh;
  min-height: 320px;
  resize: vertical;
  overflow: hidden;
}
.canvas {
  position: relative;
  overflow: hidden;
  background: var(--surface-sunk);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.canvas-paper { width: 100%; height: 100%; }

.canvas-with-palette .canvas { flex: 1; min-width: 0; height: 100%; }
.palette {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  flex: 1;
  /* A flex item will not shrink below its content unless told to — which for a column of every block the
     engine has is the whole catalog. Both of these say "take the row's height and scroll the rest". */
  min-height: 0;
  padding: .6rem;
  overflow: hidden;
}
.palette-filter { width: 100%; }

/* The shelf tabs. Five glyphs where five words would not fit: the row stays put down to the pane's 180px
   minimum, and the open tab expands to carry its label so the palette always says which shelf is showing.
   The underline is the role's own rail colour — the colour the block will wear once it is dropped. */
.palette-tabs {
  display: flex;
  flex: none;
  border-bottom: 1px solid var(--border);
  /* Pulled out to the pane's edges: the row reads as the head of the palette rather than as a control
     floating inside it, which is also how the property panel's own tabs sit. */
  margin: -.6rem -.6rem 0;
  padding: 0 .2rem;
}
.palette-tab {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .3rem;
  min-width: 0;
  padding: .35rem .1rem .3rem;
  border: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: .72rem;
  cursor: pointer;
  /* Over the container's own border, so the underline and the rule are one line rather than two. */
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.palette-tab:hover { color: var(--text); }
.palette-tab svg { flex: none; }
.palette-tab[aria-selected="true"] {
  color: var(--text);
  font-weight: 600;
  border-bottom-color: var(--role);
  /* The open tab is the only one wide enough for a word, so it takes what it needs and the glyphs share
     what is left. */
  flex: none;
  padding-inline: .4rem;
}
/* Every other tab is its glyph alone. Display rather than a removed element, because the label is the
   button's accessible name in waiting and the `aria-label` beside it is what carries it meanwhile. */
.palette-tab[aria-selected="false"] .palette-tab-label { display: none; }

/* A shelf's own name. Off while browsing — the open tab says it — and on while filtering, when matches
   from every shelf are on screen together and nothing else would tell them apart. */
.palette-shelf-name {
  display: none;
  margin: .5rem 0 .2rem;
  padding-left: .4rem;
  border-left: 3px solid var(--role);
  font-size: .7rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  font-weight: 600;
}
.palette.is-filtering .palette-shelf-name { display: block; }
/* While filtering the module headings would be a third level of caption over a handful of matches. */
.palette.is-filtering .palette-group { display: none; }
.palette-shelf[hidden] { display: none !important; }
/* The filter stays put at the top and the list under it takes what is left: a catalog you have to scroll
   past to reach the box that filters it would be the wrong way round. */
.palette-list { flex: 1; min-height: 0; overflow-y: auto; }
.palette-empty { padding: .4rem; }
.palette-group {
  margin: .6rem 0 .2rem;
  font-size: .7rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--subtle);
}
.palette-item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .4rem;
  padding: .3rem .45rem;
  /* Clear of the rail, which is drawn inside the row rather than beside it. */
  padding-left: .7rem;
  border: 1px solid transparent;
  /* Tighter than the app's radius: the rail is inset, so it takes the row's corners with it, and at 6px on
     a row this short it curves away into a bracket resting against the name rather than an edge belonging
     to it. */
  border-radius: 3px;
  cursor: grab;
  /* The rail the block will wear once it is dropped, down the edge the row is nearest. Inset rather than a
     left border, for two reasons: the border is already spent on the hover outline, whose `border-color`
     would repaint the rail along with the other three sides — and an inset shadow follows the row's own
     corner radius, so the rail reads as the row's left side rather than as a bar resting against it. Which
     is the same distinction shapes.js#railAttrs draws on the paper, and the reason it follows the body's
     corners there too.

     --role comes from the shelf the row is on, so this is one rule for all five. A row on the Structure
     shelf wears the cyan its container will band itself with rather than a rail it will not have: the
     colour is the promise, and the shape of it is the box's own business. */
  box-shadow: inset 3px 0 0 var(--role);
}
.palette-item:hover { background: var(--surface-hover); border-color: var(--border); }
/* The grip between the paper and the property panel. Wider than the line it draws — a 1px divider is a
   1px target — so the hit area is the element and the line is a border inside it. */
.pane-grip {
  flex: none;
  width: .5rem;
  align-self: stretch;
  cursor: col-resize;
  background: var(--surface);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
/* The divider line is drawn once, on the grip's canvas-facing edge. Its other edge abuts the pane it
   resizes, which is the same colour, so the grip reads as part of the pane rather than as a third column. */
/* A pane (and its grip) can be left out entirely - the palette on a view-only canvas - and `display` on
   both would otherwise beat the browser's own [hidden] rule, as it does for .panel-stack. */
.pane[hidden], .pane-grip[hidden] { display: none; }
.pane-grip[data-side="left"] { border-right: 1px solid var(--border); }
.pane-grip[data-side="right"] { border-left: 1px solid var(--border); }
.pane-grip:hover, .pane-grip:focus-visible, .pane-grip.is-dragging { background: var(--border); }
.pane-grip:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

/* A docked pane — the palette down one side of the paper, the property panel down the other. They hold
 * different things and are the same object: a column whose width is a preference and which folds away.
 *
 * Each is split in two, the pane and its content. That is forced on the property panel — the controller
 * refills it with `replaceChildren` and would carry the head away with the old block's fields — and the
 * palette follows it so that one set of rules covers both.
 *
 * The width is a preference, not a constant. How much room the settings need is a property of the block
 * selected, and a `Reference` field with a long application name in a 17rem column is read four words at a
 * time; how much the palette needs is a property of the catalog, and a module with long block names in a
 * 15rem column is read as an ellipsis. So each starts at the width it always had, is dragged from its grip,
 * and is remembered per browser. Collapsing is the same preference at its limit — a job being *arranged*
 * rather than configured wants the paper, not the panels — and leaves a rail behind so there is something
 * to press. */
.pane {
  display: flex;
  flex-direction: column;
  flex: none;
  /* A pane with a long list in it must scroll inside the row, not stretch it. */
  min-height: 0;
  background: var(--surface);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.palette-pane { width: 15rem; }
.properties-pane { width: 17rem; }
.pane-head {
  display: flex;
  align-items: center;
  gap: .35rem;
  flex: none;
  padding: .35rem .5rem;
  border-bottom: 1px solid var(--border);
}
/* Takes the middle, so each pane's fold button is pushed out to its own outer edge — the edge it folds
   towards, and the way its chevron points. */
.pane-title {
  flex: 1;
  font-size: .7rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--subtle);
}
.pane-toggle {
  flex: none;
  padding: .1rem .3rem;
  line-height: 1;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--subtle);
  cursor: pointer;
}
.pane-toggle:hover { color: var(--text); background: var(--surface-hover); }
.pane-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: -1px; }
.properties {
  flex: 1;
  min-height: 0;
  padding: .6rem .7rem;
  overflow-y: auto;
}
/* Collapsed: a rail holding the button that brings it back, and nothing else. `width` rather than `display:
   none` on the pane so the paper's resize is the same event in both directions and the diagram re-fits.
   The rail keeps the border its grip was drawing for it, or it reads as a sliver of empty page rather than
   as the pane folded up against the edge. */
.pane.is-collapsed { width: 2rem; }
.pane.is-collapsed > :not(.pane-head),
.pane.is-collapsed .pane-title { display: none; }
.pane.is-collapsed .pane-head {
  justify-content: center;
  padding: .35rem 0;
  border-bottom: 0;
}
.pane.is-collapsed .pane-toggle { transform: rotate(180deg); }
.palette-pane.is-collapsed { border-right: 1px solid var(--border); }
.properties-pane.is-collapsed { border-left: 1px solid var(--border); }
.properties .field { margin-bottom: .7rem; }
/* The step's own name, above what its class takes: separated by a rule, because it is a fact about this
   block where everything under it is a fact about its type. */
.properties .field-identity { padding-bottom: .6rem; border-bottom: 1px solid var(--border); }
.identity-row { display: flex; align-items: center; gap: .35rem; }
.identity-row .input { min-width: 0; flex: 1; }
/* The name in its resting state: the row's heading, beside the button that turns it into the box above.
   Its own margin is dropped — `.fields-type` carries one for a heading standing on its own line. */
.identity-row .fields-type { flex: 1; min-width: 0; margin: 0; overflow-wrap: anywhere; }
/* The other end of the panel from the name, and separated the same way: what is between them describes the
   block's class, and these two are about this block. */
/* A required field nobody has filled in. Marked, not blocked — see job_canvas_controller.js#markIfMissing.
   Colour on the label as well as the border, since a border alone is the one cue a colour-blind reader may
   not separate from focus. */
.properties .field-missing > label { color: var(--danger); }
.properties .field-missing .input, .properties .field-missing .select { border-color: var(--danger); }
.properties .field-missing .badge-required { background: var(--danger-soft); color: var(--danger); }
.properties .input, .properties .select { width: 100%; }
.fields-type { margin: 0 0 .6rem; font-weight: 600; }
.fields-type .muted { display: block; font-weight: 400; margin-top: .15rem; }
.field-help { margin: .15rem 0 0; }

/* --- the description dock --------------------------------------------------
   What the selected setting is for, at the foot of the panel, one description at
   a time. The column above it is labels and controls at a steady rhythm; it used
   to be two-thirds prose, since every field carried its own three lines and
   scrolled the fourth control out of sight.

   A fixed floor under its height, so that moving from one setting to the next
   never shifts the controls above it — and a ceiling, so that the longest
   description in the catalog scrolls inside the dock rather than squeezing the
   settings into a strip. Inset *and* bordered: it is a floor under the column
   rather than one more row in it, so it says where the settings stop. */
.properties-dock {
  position: relative;
  flex: none;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  min-height: 7.5rem;
  max-height: 40%;
  padding: .55rem .7rem .65rem;
  /* Heavier and darker than the rules inside the panel, which separate one setting from the next. This one
     separates the settings from the thing answering for them, so it has to read as the panel's own floor
     rather than as one more gap in the column. */
  border-top: 2px solid var(--border-strong);
  background: var(--surface-inset);
}
/* The grip is a column of its own beside the pane, painted the pane's colour so it reads as part of it —
   so the dock's floor, drawn inside the pane, stopped a grip's width short of the panel's left edge and
   left a notch above it. The dock reaches across the grip to finish the line. `pointer-events: none` so
   the strip is paint only and the grip is still grabbable for its whole height; a pixel of it is left
   uncovered at the left so the grip's own divider line, drawn on its canvas-facing edge, still shows. */
.properties-dock::before {
  content: "";
  position: absolute;
  top: -2px;
  bottom: 0;
  left: calc(-.5rem + 1px);
  width: calc(.5rem - 1px);
  box-sizing: border-box;
  border-top: 2px solid var(--border-strong);
  background: var(--surface-inset);
  pointer-events: none;
}
/* The dock keeps the height and the floor; what is in it is what scrolls — the longest description in the
   catalog would otherwise clip the strip the dock reaches across the grip with, since a scrolling box
   clips what is positioned outside it. */
.dock-body { min-height: 0; overflow-y: auto; }
.dock-label { margin: 0 0 .25rem; font-size: .8rem; font-weight: 600; color: var(--text); }
.dock-help { margin: 0; line-height: 1.5; }
/* The other spellings a job may have written the setting under. Monospaced,
   because unlike the label these are the file's words rather than a reader's. */
.dock-aliases {
  margin: .3rem 0 0;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: .72rem;
}
.dock-aliases[hidden] { display: none; }
/* Under a reference field: where its value comes from, a literal or one of the job's arguments. Smaller
   than the field it belongs to, because it is a choice about the field rather than a second field. */
.field-binding { margin-top: .25rem; }
.properties .field-binding .select { font-size: .75rem; padding: .15rem .3rem; color: var(--subtle); }
/* A bound field shows the placeholder it will be saved as and is not typed into. Said in the styling as
   well as in the attribute, so it does not look like a field somebody has simply not filled in. */
.properties .input[readonly] { background: var(--surface-hover); color: var(--subtle); }
/* --- settings that hold other settings -------------------------------------
   A map of lists of objects is edited one level at a time: going deeper replaces
   the panel rather than indenting it, with the breadcrumb as the address and the
   way back. Indenting was the alternative and it loses on width — this panel is
   17rem, and by the third level an XPath no longer fits the box you edit it in.
   See vault/DIB Web/Job Canvas.md. */
.settings-crumbs {
  display: flex; flex-wrap: wrap; align-items: center; gap: .15rem;
  margin: 0 0 .6rem; padding-bottom: .5rem;
  font-size: .78rem;
  border-bottom: 1px solid var(--border);
}
.settings-crumbs button {
  font: inherit; color: var(--primary); background: none; border: 0; cursor: pointer;
  padding: .05rem .2rem; border-radius: var(--radius-sm);
}
.settings-crumbs button:hover { background: var(--primary-soft); }
.settings-crumbs .crumb-sep { color: var(--subtle); }
.settings-crumbs .crumb-here { font-weight: 600; padding: .05rem .2rem; }

/* How many of a thing this level holds, above the things themselves. Full width
   is exactly what the indented alternative had no room for. */
.properties .level-note { margin: 0 0 .5rem; }

/* One entry of a map or item of a list. A row rather than a field, because what
   is on it is a key and a way in, not a labelled control. */
.properties .row {
  display: flex; align-items: center; gap: .35rem;
  width: 100%; text-align: left;
  padding: .3rem .35rem;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.properties .row + .row { margin-top: .3rem; }
.properties .row-drill { cursor: pointer; font: inherit; }
.properties .row-drill:hover, .properties .row-into:hover { border-color: var(--border-strong); }
.properties .row-drill:hover { background: var(--surface-hover); }
.properties .row-index { color: var(--subtle); min-width: 1rem; font-variant-numeric: tabular-nums; }
/* The summary takes the slack, so the chevron stays at the right edge however
   long what is inside is — and it is what is clipped when that runs out, since
   the row is a description and the full value is one click away. */
.properties .row-sub {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.properties .row-chev { color: var(--subtle); }
.properties .row-into {
  display: flex; align-items: center; gap: .35rem;
  flex: 1; min-width: 0;
  font: inherit; text-align: left; cursor: pointer;
  padding: .2rem .3rem;
  background: var(--surface-inset); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
/* A key is half the width of its row at most: what it is filed under is shorter
   than what is filed, nearly always, and the value is the part worth the space. */
.properties .row .input-key { flex: 0 1 8rem; min-width: 0; }
.properties .row .input { width: auto; flex: 1; min-width: 0; }

/* A list of plain values — a block's `fields:`, a level of sort keys. One row per
   value, and the input *is* the row: the frame the other rows carry would be a box
   drawn around a box, and these are not a way into anything. */
.properties .row-value { padding: 0; background: none; border: 0; }
.properties .row-value + .row-value { margin-top: .25rem; }
/* One of those lists sitting beside a key, which is a column rather than a control:
   the key lines up with the first value, not with the middle of the stack. */
.properties .row-stacked { align-items: flex-start; }
.properties .row-stacked .input-key { margin-top: .1rem; }
.properties .row .field-rows { flex: 1; min-width: 0; }

/* Adding one. Dashed, because it is an offer rather than a thing that is there. */
.properties .btn-add {
  width: 100%; text-align: left; margin-top: .3rem;
  border-style: dashed; color: var(--muted);
}
.properties .btn-add:hover { color: var(--primary); border-color: var(--primary); }
.properties .btn-icon {
  flex: none; padding: .1rem .25rem; line-height: 1;
  background: none; border: 0; border-radius: var(--radius-sm);
  color: var(--subtle); cursor: pointer;
}
.properties .btn-icon:hover { color: var(--danger); background: var(--surface-hover); }

/* A setting whose values the engine can list: a text box that still takes anything, with the offer beside
   it. The refresh button is quiet because re-listing is the rare act — the list is fetched when the field
   is first used, and only a change made in the far system needs it asked again. */
.field-combo { display: flex; gap: .3rem; align-items: center; }
.field-combo .input { flex: 1 1 auto; }
.field-values { margin: .15rem 0 0; min-height: 1em; }
.badge-required { margin-left: .3rem; }
/* The tick box of a setting the definition says nothing about: the tick is the engine's default until
   somebody touches it, and the badge beside the label is what says so. Dimmed rather than styled further,
   so that a panel of untouched settings does not read as a panel of warnings. */
.properties .field-defaulted input[type="checkbox"] { opacity: .65; }
/* The selected block is outlined on the paper, so the panel is clearly about *that* one. The class lands
   on the body rect rather than on the element's group, because that is what `attr("body/class")` sets. */
.canvas-paper .joint-element rect.selected { stroke: var(--primary); stroke-width: 2; }
/* And the selected link, on the line itself. A stroke set as a presentation attribute by JointJS, so the
   rule here wins without !important — see lib/canvas/shapes.js, which sets the line's attrs one path at a
   time so that this class survives a link being edited. */
.canvas-paper .joint-link path.selected { stroke: var(--primary); stroke-width: 3; }

/* A link's own properties, which are the engine's link grammar rather than any block's settings: the ends
   it joins, then one control per thing the grammar lets a link say. The list under a filter's mode is the
   mode's argument, so it sits directly under it rather than in a field of its own. */
.properties .fields-link [data-link-part] > .field:last-child { margin-bottom: 0; }
.properties .fields-link textarea { margin-top: .3rem; }
.properties .fields-link [hidden] { display: none; }
.palette-item[hidden] { display: none; }
/* The name gives way first: a long class name truncates, where a kind clipped to "component l" says
   nothing at all. */
.palette-item-name { font-size: .85rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.palette-item-kind { font-size: .65rem; color: var(--subtle); white-space: nowrap; flex: none; }
/* While a block is being dragged in, the paper is the drop target and says so. */
.canvas.dropping { outline: 2px dashed var(--primary); outline-offset: -4px; }

/* Where the paper is, when what is on it is not the whole job.
 *
 * Floating over the diagram rather than sitting in a row above it: the paper is sized to fill this box
 * exactly, and a row in normal flow would push the bottom of every diagram out of sight. */
.canvas-scope {
  position: absolute;
  top: .5rem;
  left: .5rem;
  right: .5rem;
  z-index: 2;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .4rem;
  padding: .3rem .55rem;
  font-size: .8rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-sm);
}
.canvas-scope[hidden] { display: none; }
.canvas-scope .btn-link { padding: 0; font-size: .8rem; }
/* The level actually on the paper. The crumbs before it are buttons and look like them; this one is where
   you are, and a button that goes nowhere would be the one thing on the bar that does nothing. */
.canvas-scope-here { font-weight: 600; }
/* And the paper itself, which is the part being read. The bar can be read straight past — a border around
   everything cannot, which is the point of it: nothing that is saved from in here reaches the job's own
   steps. */
.canvas.in-scope { box-shadow: inset 0 0 0 2px var(--primary); }
.canvas-placeholder {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  pointer-events: none;
}
/* Dragging the background pans, so the cursor says so — and text selection across the diagram while
   panning would otherwise highlight every label the pointer crossed. */
.canvas-paper .joint-paper { cursor: grab; user-select: none; }
.canvas-paper .joint-element { cursor: move; }
.canvas-paper .joint-paper:active { cursor: grabbing; }
.alert p { margin: .4rem 0 0; }
.alert .muted, .alert .small { color: inherit; opacity: .85; }
.errors {
  background: var(--danger-soft); border: 1px solid var(--danger-border); color: var(--danger);
  border-radius: var(--radius-sm); padding: .6rem 1rem; margin-bottom: 1rem;
}

/* ==========================================================================
   9. Tables
   ========================================================================== */

.table-wrap {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  overflow-x: auto;
}
.table { width: 100%; border-collapse: collapse; font-size: .875rem; }
.table th, .table td {
  text-align: left;
  padding: .6rem .9rem;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.table thead th {
  position: sticky; top: 0;
  background: var(--surface-sunk);
  font-size: .72rem; font-weight: 650;
  text-transform: uppercase; letter-spacing: .05em;
  color: var(--subtle);
  white-space: nowrap;
}
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: var(--surface-hover); }
.table td .muted, .table td.muted { font-size: .84rem; }
/* A table with no header row still wants its first row to sit off the top edge. */
.table > tbody:first-child > tr:first-child > td { padding-top: .75rem; }

/* A column that is one icon: no heading text, and only as wide as the glyph. */
.table .col-icon { width: 1px; padding-right: 0; color: var(--subtle); }
.table tbody .col-icon svg { display: block; }

/* What a run consumed can be several sources long. Kept to one line with the whole
   of it in the cell's title, so that fifty rows stay the same height and the column
   the eye is scanning — the run's name — does not get squeezed by the longest one. */
.table .col-sources {
  max-width: 26rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The jobs index. A description is the only cell there holding a sentence, so it is
   given a share of the table outright rather than left with whatever the fixed
   columns leave over. The width comes from the email beside it, which is a name to
   recognise rather than a line to read: truncated to one line, with the whole of it
   in the cell's title. */
.table .col-description { width: 34%; }
.table .col-actor {
  max-width: 11rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* A row that opens what it describes. The link at its end is still what a keyboard
   reaches; this is the pointer's shortcut to it — see clickable_rows_controller.js.
   Only a pointer, so the cursor is the whole of the affordance: the row already
   lights up on hover, and a second treatment would make an ordinary table look
   like a list of buttons. */
.table tbody tr.row-link { cursor: pointer; }

/* Sortable headings. The whole heading is the link rather than an arrow beside it,
   so the target is the width of the column instead of a glyph. The arrow keeps its
   space whether or not it is showing, so headings do not shift sideways as the sort
   moves along the row. */
.table-sortable thead th:has(> .th-sort) { padding: 0; }
.th-sort {
  display: flex; align-items: center; gap: .3rem;
  padding: .6rem .9rem;
  color: inherit; font: inherit; text-transform: inherit; letter-spacing: inherit;
}
.th-sort:hover { color: var(--text); text-decoration: none; background: var(--surface-hover); }
.table thead th[aria-sort=ascending] .th-sort,
.table thead th[aria-sort=descending] .th-sort { color: var(--primary); }

/* --------------------------------------------------------------------------
   Sort indicators, shared by both kinds of table

   Two kinds of table sort in this app: the server-rendered ones (the run
   history), and the Tabulator grids (the file tree). They have to read the same
   whichever you are looking at, so the indicator is defined once, here, for
   both — the `.sort-arrow` span ApplicationHelper#sort_header emits, and the
   `.tabulator-col-sorter` Tabulator draws in its own header.

   What it says:

   - a column that *can* be sorted shows stacked up/down triangles, and only
     while the pointer is on its heading. A table showing eight pairs of arrows
     at rest is mostly arrows, and the affordance is worth nothing once every
     column has it.
   - the column that *is* sorted shows one filled triangle, always, in the
     accent, pointing the way the rows run — up for ascending.

   Both key off `aria-sort` on the heading, which is where ARIA puts it and what
   Tabulator already emits, so one set of state rules serves both.
   -------------------------------------------------------------------------- */

/* Tabulator's own arrow is a single bordered box; ours is two pseudo-elements on
   the container around it, so its arrow goes and the container is what is drawn.
   Drawing on the container also keeps this out of the specificity war with the
   six stock rules that colour that arrow per state (see §13). */
.tabulator.grid .tabulator-header .tabulator-col .tabulator-col-sorter .tabulator-arrow { display: none; }

.sort-arrow { position: relative; flex: none; width: 7px; height: 14px; }
:is(.sort-arrow, .tabulator-col-sorter)::before,
:is(.sort-arrow, .tabulator-col-sorter)::after {
  content: "";
  position: absolute; right: 0;
  border-left: 3.5px solid transparent;
  border-right: 3.5px solid transparent;
}
:is(.sort-arrow, .tabulator-col-sorter)::before {
  bottom: calc(50% + 1px); border-bottom: 4px solid var(--subtle);
}
:is(.sort-arrow, .tabulator-col-sorter)::after {
  top: calc(50% + 1px); border-top: 4px solid var(--subtle);
}

/* At rest, a sortable-but-unsorted column shows nothing; its heading reveals the
   pair. Kept in the layout rather than removed, so nothing moves when it appears. */
[aria-sort=none] :is(.sort-arrow, .tabulator-col-sorter) { opacity: 0; transition: opacity .1s ease; }
[aria-sort=none]:hover :is(.sort-arrow, .tabulator-col-sorter),
[aria-sort=none]:focus-within :is(.sort-arrow, .tabulator-col-sorter) { opacity: 1; }

/* Sorted: the triangle pointing the other way is dropped, and the survivor takes
   the accent. */
[aria-sort=ascending]  :is(.sort-arrow, .tabulator-col-sorter)::after  { display: none; }
[aria-sort=descending] :is(.sort-arrow, .tabulator-col-sorter)::before { display: none; }
[aria-sort=ascending]  :is(.sort-arrow, .tabulator-col-sorter)::before { border-bottom-color: var(--primary); }
[aria-sort=descending] :is(.sort-arrow, .tabulator-col-sorter)::after  { border-top-color: var(--primary); }

/* The strip above a table: a search box on the left, and room for whatever else a
   page filters by. Sits outside .table-wrap, since it is about the query and not
   about the rows. */
.table-toolbar {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin: 0 0 .85rem;
}
/* A labelled control in a toolbar — the maps grid's dimension picker is the first.
   A <label> is display:block by default, so its text and its control sit on two
   lines and the control then rides high against whatever is beside it; this is
   what puts the pair on one baseline with the search box and the buttons. */
/* `margin: 0` is the half that actually aligns it: a <label> carries `margin-bottom: .3rem` from the
   base form rule, and a flex row centres each item's *margin* box — so the label's control sat a couple
   of pixels above everything beside it even once the two were the same height. */
.toolbar-field { display: inline-flex; align-items: center; gap: .4rem; margin: 0; }
.toolbar-field > span { white-space: nowrap; }
/* `display: inline-flex` above would otherwise beat the browser's own [hidden] rule: the table's Replace field
   is shown and hidden by it. */
.toolbar-field[hidden] { display: none; }
/* One height for every control on a toolbar row. Given identical padding, border and
   font, a <select> still measures ~2px taller than an <input>, because a select's
   line box is the browser's rather than its text's — and two controls of different
   heights beside each other cannot both be centred on the row. So they are told the
   height rather than left to work it out and disagree. */
.table-toolbar :is(.input, select, input[type=search]) { height: 2.125rem; }
/* `.btn` is `display: inline-flex`, which beats the browser's own `[hidden]` rule — so a button toggled by
   a controller stays on screen unless this says otherwise. The same trap as `.picker-controls
   label[hidden]` below, stated once for every button instead. */
.btn[hidden] { display: none; }
/* Pushed to the far end of a toolbar row: a control that undoes the row belongs after everything it undoes,
   not in the middle of it. `margin-left: auto` rather than a spacer element, so the row collapses correctly
   when the control is hidden. */
.table-toolbar .toolbar-end { margin-left: auto; }

.search-field { position: relative; display: flex; align-items: center; flex: 0 1 22rem; }
.search-field-compact { flex: 1 1 12rem; }
.search-field svg {
  position: absolute; left: .6rem; color: var(--subtle); pointer-events: none;
}
/* The room for that icon. Claimed with the element-shaped selector the base input
   rule uses (see §Forms — this app themes inputs by element, not by class), because
   a plain `.search-field .input` loses to it on specificity and the text would then
   start underneath the magnifier. */
.search-field
  input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=button]):not([type=file]) {
  padding-left: 2rem;
}
/* Safari draws a decoration inside a search field that collides with the icon. */
.search-field .input::-webkit-search-decoration { -webkit-appearance: none; }

/* Paging under a table. The disabled ends are spans rather than links, so they are
   not tab stops and cannot be followed; they keep their box so the row of controls
   does not resize as you reach either end. */
.pager {
  display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
  margin-top: .85rem;
}
.pager-count { margin-right: auto; }
.pager-links { display: flex; align-items: center; gap: .35rem; }
.pager-where { padding: 0 .35rem; }
.pager [aria-disabled=true] { opacity: .45; cursor: default; }

/* ==========================================================================
   10. Badges
   ========================================================================== */

.badge {
  display: inline-flex; align-items: center; gap: .3rem;
  background: var(--surface-sunk);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: .1rem .55rem;
  font-size: .74rem; font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}
a.badge:hover { text-decoration: none; border-color: currentColor; }
.badge-ok      { background: var(--success-soft); border-color: var(--success-border); color: var(--success); }
.badge-danger  { background: var(--danger-soft);  border-color: var(--danger-border);  color: var(--danger); }
.badge-info    { background: var(--primary-soft); border-color: var(--primary);        color: var(--primary); }
.badge-warning { background: var(--warning-soft); border-color: var(--warning-border); color: var(--warning); }
.badge-muted   { color: var(--muted); }
/* A setting the definition says nothing about, whose control is showing what the block will do instead.
   Quiet by design: it is the ordinary state of most settings on most blocks, so it must not compete with
   the required badge beside it, which is a thing to act on. */
.badge-default { margin-left: .3rem; padding: .05rem .4rem; background: none; color: var(--subtle);
                 font-weight: 500; }
/* `.badge` sets a display, which beats the browser's own rule for `[hidden]` — so a badge toggled by the
   `hidden` property stays on screen unless this says otherwise. */
.badge[hidden] { display: none; }

/* Several badges read as a set — one per value, wrapping rather than running off. */
.badge-row { display: flex; flex-wrap: wrap; gap: .35rem; }
/* Several ids in one cell — the steps a step links to. Wraps rather than widening the column, and needs
   the gap because adjacent <code> pills would otherwise read as one long id. */
.id-list { display: inline-flex; flex-wrap: wrap; gap: .3rem; }

/* ==========================================================================
   11. Odds and ends
   ========================================================================== */

.breadcrumbs { display: flex; flex-wrap: wrap; gap: .3rem; align-items: center;
               margin-bottom: 1rem; font-size: .85rem; }

/* The run's process tree: a tally and a strip for the run as a whole, then a row per process with a status
   icon, what it is doing, its rate now and a chart of its rate since it started. Drawn by
   lib/runs/process_tree.js from what the run's engine reported, and kept once the run is over: the last
   report is what each process handled, and the charts are how it went. No progress bars and no estimates —
   a total is rarely known, and earlier runs of a job are a poor guide to this one. See
   vault/DIB Web/Run Progress Tree.md. */
.process-tree {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunk);
  padding: .6rem .7rem;
  margin-bottom: .6rem;
  font-size: .8rem;
  display: flex;
  flex-direction: column;
  gap: .5rem;
}
.process-tree[hidden] { display: none; }

.pt-tally { color: var(--muted); }

/* One segment per process, in tree order: how many have finished, not how long is left. */
.pt-strip { display: flex; gap: 2px; height: 6px; }
.pt-strip i { flex: 1; border-radius: 2px; background: var(--border); }
.pt-strip i.is-completed { background: var(--success); }
.pt-strip i.is-failed    { background: var(--danger); }
.pt-strip i.is-cancelled { background: var(--border-strong); }
.pt-strip i.is-running {
  background: repeating-linear-gradient(-45deg, var(--primary) 0 5px, var(--primary-soft) 5px 10px);
  background-size: 14px 14px;
  animation: pt-stripe 1s linear infinite;
}
@keyframes pt-stripe { to { background-position: 14px 0; } }

.pt-rows { list-style: none; margin: 0; padding: 0; }
.pt-row {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
  padding: .4rem 0 .4rem calc(var(--depth, 0) * 1rem);
  border-top: 1px solid var(--border);
}
.pt-row:first-child, .pt-scope:first-child { border-top: none; }
/* A nested assembly's heading: its name, with its processes indented below it. */
.pt-scope {
  padding: .4rem 0 .1rem calc(var(--depth, 0) * 1rem);
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-weight: 600;
  display: flex;
}
.pt-scope + .pt-row { border-top: none; }
/* Before the engine's first report: the panel keeps its space and says so. */
.pt-placeholder { color: var(--muted); }
.pt-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: .1rem; }

/* The name grows and the numbers do not, so rates and times line up down the right however long a process
   is called. */
.pt-line { display: flex; align-items: baseline; gap: .6rem; }
.pt-name {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.pt-rate, .pt-ms {
  flex: none;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
}
.pt-ms { color: var(--muted); min-width: 4.5ch; text-align: right; }
.pt-label { color: var(--muted); font-size: .76rem; overflow-wrap: anywhere; }
.pt-label.is-error { color: var(--danger); }
.pt-row.is-pending .pt-name, .pt-row.is-cancelled .pt-name { color: var(--muted); }

/* An icon rather than a word: five statuses down a column of twenty processes is a wall of text, and what a
   watcher is looking for is which one is moving, and which one broke. */
.pt-icon { width: 16px; height: 16px; flex: none; margin-top: 1px; }
.pt-icon .pt-track { fill: none; stroke: var(--primary-soft); stroke-width: 2; }
.pt-icon .pt-arc {
  fill: none; stroke: var(--primary); stroke-width: 2; stroke-linecap: round;
  transform-origin: 9px 9px; animation: pt-spin .9s linear infinite;
}
@keyframes pt-spin { to { transform: rotate(360deg); } }
.pt-icon.is-completed circle { fill: var(--success); }
.pt-icon.is-failed circle    { fill: var(--danger); }
.pt-icon.is-completed path,
.pt-icon.is-failed path { fill: none; stroke: #fff; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.pt-icon.is-pending circle   { fill: none; stroke: var(--border-strong); stroke-width: 1.6; stroke-dasharray: 2.4 2.4; }
.pt-icon.is-cancelled circle { fill: none; stroke: var(--subtle); stroke-width: 1.6; }
.pt-icon.is-cancelled path   { stroke: var(--subtle); stroke-width: 1.6; }

/* The rate since the process started, scaled to its own peak. No axes: what it is for is the shape. */
/* Every chart is on the run's one time axis, so every chart starts in the same column: the row's indent is
   taken back off the chart, which is as wide as the panel's widest row. */
.pt-spark {
  display: block; height: 22px; margin-top: .15rem;
  width: calc(100% + var(--depth, 0) * 1rem);
  margin-left: calc(var(--depth, 0) * -1rem);
}
.pt-spark .pt-area  { fill: var(--primary); fill-opacity: .14; }
.pt-spark .pt-curve { fill: none; stroke: var(--primary); stroke-width: 1.4; }
.pt-spark.is-completed .pt-area  { fill: var(--success); }
.pt-spark.is-completed .pt-curve { stroke: var(--success); }
.pt-spark.is-failed .pt-area     { fill: var(--danger); }
.pt-spark.is-failed .pt-curve    { stroke: var(--danger); }
.pt-spark.is-cancelled .pt-area  { fill: var(--subtle); }
.pt-spark.is-cancelled .pt-curve { stroke: var(--subtle); }
/* The run's time axis under every chart, and the band a process that counts nothing gets on it. */
.pt-spark .pt-axis { fill: none; stroke: var(--border); stroke-width: 1; }
.pt-spark .pt-band { fill: var(--primary); fill-opacity: .55; }
.pt-spark.is-completed .pt-band { fill: var(--success); }
.pt-spark.is-failed .pt-band    { fill: var(--danger); }
.pt-spark.is-cancelled .pt-band { fill: var(--subtle); }

@media (prefers-reduced-motion: reduce) {
  .pt-icon .pt-arc, .pt-strip i.is-running { animation: none; }
}

.console {
  background: var(--console-bg);
  color: var(--console-text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem 1.1rem;
  margin: 0;
  overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .8rem; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word;
  max-height: 480px;
}

/* --- The run page ---------------------------------------------------------
   A summary above three tabs: progress and log, arguments and configuration, outputs. */
.run-summary { margin-bottom: 1rem; }
/* A failure's reason sits above the tabs, where every tab shows it. Capped, because an engine stack trace
   would otherwise push the tabs off the screen. */
.run-error { margin-bottom: 1rem; max-height: 12rem; overflow: auto; }
.run-error span { white-space: pre-wrap; word-break: break-word; }

.run-progress-panel { display: flex; flex-direction: column; gap: .6rem; }
.run-progress-panel[hidden] { display: none; }
.run-progress-panel > .section-head { margin: 0; min-height: 1.5rem; }
.run-progress-panel .run-stream-status { margin-right: auto; }
.run-progress { display: flex; flex-direction: column; gap: .75rem; }
.run-progress .process-tree { margin: 0; max-height: 18rem; overflow: auto; }

/* Side by side once there is room for both: the tree on the left, the console taking the rest, and the two
   together the height of the window below the tab strip (see fill_height_controller.js), each scrolling on
   its own. Narrower than this they stack, at their own heights. */
@media (min-width: 1100px) {
  .run-progress-panel {
    height: var(--fill-height, calc(100dvh - 22rem));
    min-height: 24rem;
    margin-bottom: var(--fill-pull, 0px);
  }
  .run-progress-panel > .section-head { flex: none; }
  .run-progress {
    flex: 1 1 auto; min-height: 0;
    display: grid;
    grid-template-columns: minmax(18rem, 1fr) minmax(0, 2fr);
  }
  /* A run that never reported a tree gives the console the whole width. */
  .run-progress:has(> .process-tree[hidden]) { grid-template-columns: minmax(0, 1fr); }
  .run-progress .process-tree,
  .run-progress .console { max-height: none; height: 100%; min-height: 0; }
}

/* Arguments and sources in one column, configuration in the other; one column when the window is narrow. */
.run-inputs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 30rem), 1fr));
  gap: 1.25rem 1.75rem;
  align-items: start;
}
/* The grid and the column space these sections, so the page-wide gap above a section would only push the
   tab's content down from the strip. */
.run-inputs section { margin-top: 0; }
.run-inputs-column { gap: 1.75rem; }

/* The device-authorization page. The user code is read off this screen and typed
   into Oracle's, so it is set large and letter-spaced — the failure mode being a
   transcription error, not a lack of room. */
.steps { line-height: 2; padding-left: 1.2rem; }
.user-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.6rem; letter-spacing: .18em;
  background: var(--surface-sunk); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: .15rem .55rem;
}

/* --- Resource-path picker on the run form ------------------------------- */

.arg-picker {
  margin: 0 0 .5rem; padding: .85rem;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-inset);
}
.picker-summary { margin: .4rem 0 0; }
.picker-controls { display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-end; margin-bottom: .6rem; }
.picker-controls label {
  display: flex; flex-direction: column; gap: .25rem;
  font-size: .72rem; text-transform: uppercase; letter-spacing: .05em; color: var(--subtle);
  font-weight: 600; margin: 0;
}
/* `display: flex` above would otherwise beat the browser's own [hidden] rule. */
.picker-controls label[hidden] { display: none; }
.picker-source select, .picker-filter input { min-width: 14rem; }
.picker-folder { font-weight: 600; }
.picker-leaf { color: var(--text); }
.picker-count { color: var(--muted); font-weight: 400; font-size: .8em; margin-left: .35rem; }

/* ==========================================================================
   12. Sign-in
   ========================================================================== */

.auth-page {
  min-height: 100vh;
  display: grid; place-items: center;
  padding: 2rem 1.25rem;
  background: radial-gradient(1200px 600px at 50% -10%, var(--wash), var(--bg));
}
.auth-card {
  width: 100%; max-width: 400px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 2rem;
}
.auth-card .auth-brand {
  display: flex; align-items: center; justify-content: center; gap: .5rem;
  margin-bottom: 1.25rem; color: var(--text);
  font-weight: 700; letter-spacing: .07em;
}
.auth-card .auth-brand svg { color: var(--brand); }
.auth-card .auth-brand .wordmark { font-size: 1.85rem; }
.auth-card h1 { font-size: 1.2rem; text-align: center; margin-bottom: 1.5rem; }
.auth-card label { color: var(--text); font-weight: 600; font-size: .85rem; margin-top: .9rem; }
.divider { text-align: center; color: var(--subtle); font-size: .8rem; margin: 1.35rem 0; }
.auth-theme { position: fixed; top: 1rem; right: 1rem; }

/* ==========================================================================
   13. Tabulator theme
   --------------------------------------------------------------------------
   tabulator.css is the vendored stock stylesheet — structural (virtual-DOM row
   positioning, header layout) as much as cosmetic, so it is kept whole and only
   its colours are restated here against this app's tokens. Every rule is
   deliberately one class more specific than the stock one it overrides, so it
   wins regardless of the order Propshaft emits the two files in.
   ========================================================================== */

.tabulator.grid {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text); font-size: .84rem;
}
/* A grid's menus — a heading's, a cell's right-click — are appended to <body>, outside `.tabulator.grid`,
   so they are restated here on their own. The stock ones are white whatever the theme. */
.tabulator-popup-container.tabulator-menu {
  background: var(--surface); color: var(--text); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-lg); padding: .25rem 0; font-size: .84rem;
  min-width: 11rem;
}
.tabulator-popup-container.tabulator-menu .tabulator-menu-item { padding: .35rem .85rem; }
.tabulator-popup-container.tabulator-menu .tabulator-menu-item:not(.tabulator-menu-item-disabled):hover {
  background: var(--surface-hover);
}
.tabulator-popup-container.tabulator-menu .tabulator-menu-separator { border-top-color: var(--border); margin: .25rem 0; }
.tabulator.grid .tabulator-header {
  background: var(--surface-sunk); border-bottom: 1px solid var(--border);
  color: var(--subtle); font-weight: 400;
}
/* Stock colours the table element itself, so the row rules below are not enough
   on their own — this is the one that decides what the text looks like. */
.tabulator.grid .tabulator-tableholder .tabulator-table { background: var(--surface); color: var(--text); }
.tabulator.grid .tabulator-header .tabulator-col {
  background: var(--surface-sunk); border-right: none;
  font-size: .7rem; text-transform: uppercase; letter-spacing: .05em;
}
/* Stock tints a sortable heading light grey on hover and colours the sorted one's
   title dark grey; both are a hole in this app's dark theme. The arrows themselves
   are the shared indicator in §9, which this pairs with. */
.tabulator.grid .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover {
  background-color: var(--surface-hover); color: var(--text);
}
.tabulator.grid .tabulator-header .tabulator-col[aria-sort=ascending] .tabulator-col-title,
.tabulator.grid .tabulator-header .tabulator-col[aria-sort=descending] .tabulator-col-title {
  color: var(--primary);
}
.tabulator.grid .tabulator-row { background: var(--surface); border-bottom: 1px solid var(--border); }
.tabulator.grid .tabulator-row.tabulator-row-even { background: var(--surface); }
.tabulator.grid .tabulator-row:hover { background: var(--surface-hover); }
/* Selection is this app's own three-state affair, not Tabulator's two-state one,
   so the row is tinted from the state it carries. Only a whole selection is
   tinted: a part-selected folder is saying "look inside", and colouring it as
   taken would work against that. */
.tabulator.grid .tabulator-row[data-state="all"] { background: var(--primary-soft); color: var(--primary); }
.tabulator.grid .tabulator-row[data-state="all"]:hover { background: var(--primary-soft); }
.tabulator.grid .tabulator-row .tabulator-cell { border-right: none; padding: .3rem .5rem; }
/* A cell being edited: the editor fills it, inside a border on all four sides. Stock says the same with
   `.tabulator-cell.tabulator-editing`, but the rule above outranks it — which left every editor inset by
   the cell's padding and its outline missing a right edge. */
.tabulator.grid .tabulator-row .tabulator-cell.tabulator-editing {
  padding: 0; border: 1px solid var(--primary);
}
.tabulator.grid .tabulator-placeholder { background: var(--surface); color: var(--muted); }
.tabulator.grid .tabulator-placeholder .tabulator-placeholder-contents { color: var(--muted); }

/* The expand/collapse control on a folder row: a bordered box holding a + or a −.
   Both glyphs are drawn by the *background* of a 1px element and its ::after —
   the element is the stroke — so these must set a colour rather than clear one,
   and they have to match stock's four-class selectors to win. Getting either
   wrong leaves a control that is there and clickable but invisible. */
.tabulator.grid .tabulator-row .tabulator-cell .tabulator-data-tree-control {
  background: transparent; border: 1px solid var(--border-strong); border-radius: 3px;
  height: 1rem; width: 1rem; margin-right: .4rem;
}
.tabulator.grid .tabulator-row .tabulator-cell .tabulator-data-tree-control:hover {
  background: var(--surface-hover); border-color: var(--muted);
}
/* The vertical stroke of the +. The − has none, which is what distinguishes them. */
.tabulator.grid .tabulator-row .tabulator-cell .tabulator-data-tree-control
  .tabulator-data-tree-control-expand { background: var(--text); }
/* The horizontal stroke of both. */
.tabulator.grid .tabulator-row .tabulator-cell .tabulator-data-tree-control
  .tabulator-data-tree-control-expand:after,
.tabulator.grid .tabulator-row .tabulator-cell .tabulator-data-tree-control
  .tabulator-data-tree-control-collapse:after { background: var(--text); }
/* Stock draws the elbow joining a child row to its parent in a fixed grey. */
.tabulator.grid .tabulator-row .tabulator-cell .tabulator-data-tree-branch { border-color: var(--border); }

/* The paging footer and the header filters, which arrived with the maps grid — the file tree has neither,
   so stock's own light palette had never been overridden here and drew a white bar across a dark page.

   The buttons are Tabulator's own rather than this app's `.btn`, since the grid builds them; they are given
   the same shape so the row does not read as something from another application. */
.tabulator.grid .tabulator-footer {
  background: var(--surface-sunk); border-top: 1px solid var(--border); color: var(--subtle);
}
.tabulator.grid .tabulator-footer .tabulator-page {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: var(--text); padding: .15rem .5rem;
}
.tabulator.grid .tabulator-footer .tabulator-page:not(.disabled):hover {
  background: var(--surface-hover); color: var(--text);
}
.tabulator.grid .tabulator-footer .tabulator-page.active,
.tabulator.grid .tabulator-footer .tabulator-page.active:hover {
  background: var(--primary-soft); border-color: var(--primary); color: var(--primary);
}
.tabulator.grid .tabulator-footer .tabulator-page.disabled { color: var(--subtle); opacity: .5; }
.tabulator.grid .tabulator-footer .tabulator-page-size,
.tabulator.grid .tabulator-footer .tabulator-paginator label { color: var(--subtle); }
/* What a page puts at the left-hand end of that footer, before the paging controls — the maps grid's
   count and save status. Normal weight, since the footer's own rule is bold and these are captions
   rather than controls. */
.tabulator.grid .tabulator-footer .grid-footer-note {
  display: inline-flex; align-items: center; gap: .75rem; font-weight: 400; min-width: 0;
}

/* A header filter is an input inside a header cell, which this app themes by element everywhere else —
   but stock's own rule is more specific, so it has to be restated here to stop a light box sitting under
   an uppercase dark heading. The select is the list editor's trigger, drawn as a div rather than a
   <select>, which is why it is matched by class. */
.tabulator.grid .tabulator-header .tabulator-header-filter input,
.tabulator.grid .tabulator-header .tabulator-header-filter .tabulator-list-selected,
.tabulator.grid .tabulator-header .tabulator-header-filter select {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: var(--text); font-size: .78rem; text-transform: none; letter-spacing: normal;
  padding: .15rem .35rem;
}
.tabulator.grid .tabulator-header .tabulator-header-filter input:focus,
.tabulator.grid .tabulator-header .tabulator-header-filter select:focus {
  border-color: var(--primary); outline: none;
}
/* The pop-up list a "list" header filter opens. Appended to the grid rather than to the cell, so it is
   matched from the grid down. */
.tabulator.grid .tabulator-edit-list,
.tabulator-edit-list {
  background: var(--surface); border: 1px solid var(--border); color: var(--text);
}
.tabulator-edit-list .tabulator-edit-list-item { color: var(--text); }
.tabulator-edit-list .tabulator-edit-list-item:hover,
.tabulator-edit-list .tabulator-edit-list-item.focused {
  background: var(--surface-hover); color: var(--text);
}
.tabulator-edit-list .tabulator-edit-list-item.active {
  background: var(--primary-soft); color: var(--primary);
}

/* ==========================================================================
   13b. The file tree
   --------------------------------------------------------------------------
   The workspace file grid, the pop-over the Upload button opens, and the modal
   the settings icons open. The grid itself is the shared Tabulator skin above;
   what is here is only what a file row needs beyond it.
   ========================================================================== */

/* Taller than the picker's grid: this one is the screen rather than a control
   on a form, so it is given the room and scrolls inside itself. */
.file-grid { min-height: 12rem; }
/* The map grid grows with its rows and stops at the window, scrolling inside itself past that. How tall
   "the window" leaves it is measured in map_grid_controller.js and given to Tabulator as a number of
   pixels — the one form its `maxHeight` accepts — so there is nothing to set here but the floor that keeps
   an empty grid from collapsing onto its own header. */
.map-grid { min-height: 12rem; flex: 1; min-width: 0; }

/* --- the map editor --------------------------------------------------------
   The table, and a panel down its right-hand side for the map that is open in
   it. The same arrangement as the job canvas's property panel, and for the same
   reason: what is being edited is a small structured object with a shape per
   type, so it cannot be a cell.

   The grid keeps the height it measured for itself, so the row is given that
   height by its content rather than stretching to the window. `align-items:
   stretch` is what makes the panel as tall as the grid beside it. */
.map-editor { display: flex; align-items: stretch; gap: 0; min-height: 0; }
.map-panel {
  width: 20rem;
  flex: none;
  border-left: 1px solid var(--border);
  /* A multicolumn rule's controls are taller than the table beside them, so the panel scrolls inside the
     height the grid measured for itself — see map_grid_controller.js#fit — rather than growing the page.
     A panel that grows the page puts Save below the fold and strands the grid, which does not scroll with
     the page, above it. */
  overflow: hidden;
}
/* Save and Remove, under the controls they act on, and out of the scroll above them — a Save that scrolls
   away on a multicolumn rule is a Save nobody finds. */
.pane-foot {
  display: flex;
  gap: .5rem;
  flex: none;
  padding: .6rem .7rem;
  border-top: 1px solid var(--border);
}

/* The row the panel is showing. A class rather than Tabulator's own row selection, which re-renders the
   row — and a click on an editable cell opens both an editor and the panel, so the redraw tore down the
   editor the same click had opened. */
.map-grid .tabulator-row.is-open { box-shadow: inset 3px 0 0 var(--primary); background: var(--surface-hover); }
/* The open row with edits not yet saved: what it shows is not what the database holds, and it says so in
   the colour the panel's "Unsaved changes" is in. */
.map-grid .tabulator-row.is-open.is-dirty { box-shadow: inset 3px 0 0 var(--warning); }
.map-panel-unsaved { color: var(--warning); margin-right: auto; margin-left: .5rem; }
/* Remove is the one control in the foot that is not about the pending edits, so it stands apart from them. */
.map-panel .pane-foot [data-map-grid-target=remove] { margin-left: auto; }

/* The Source cell's editor for a rule of several parts — an In's values, a Between's two ends, a Script —
   which are the panel's own controls for that type, under the cell. On the page body rather than in the
   cell, whose height is one row and whose overflow the grid clips. */
.map-source-popover {
  position: fixed;
  z-index: 50;
  width: 20rem;
  max-width: calc(100vw - 16px);
  max-height: 60vh;
  overflow: auto;
  padding: .7rem;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.map-source-popover .field { margin-bottom: .6rem; }
.map-source-popover-actions { display: flex; justify-content: flex-end; }

/* Member lookup under a map's source or target field (lib/maps/member_lookup.js): the tree cache's members
   matching what is typed, or one level of the tree. Fixed like the source popover, so no scrolling box - the
   grid's cells sit in one - can clip it. */
.member-lookup {
  position: fixed;
  z-index: 60;
  max-width: calc(100vw - 16px);
  max-height: 50vh;
  overflow: auto;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.member-lookup-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: .2rem .6rem;
  padding: .4rem .6rem;
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; background: var(--surface);
}
.member-lookup-actions { margin-left: auto; display: flex; gap: .2rem; }
.member-lookup-crumbs { flex-basis: 100%; }
.member-lookup-crumbs .btn { padding: 0 .1rem; }
.member-lookup ul { list-style: none; margin: 0; padding: .2rem 0; }
.member-lookup li { padding: .3rem .6rem; cursor: pointer; }
.member-lookup li:hover, .member-lookup li.active { background: var(--surface-sunk); }
.member-lookup-name { display: flex; align-items: center; gap: .4rem; }
.member-lookup-name strong { white-space: nowrap; }
.member-lookup-name .muted { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.member-lookup-name .badge { flex: none; }
.member-lookup-name .badge { font-size: .7rem; padding: 0 .4rem; }
.member-lookup-drill { margin-left: auto; padding: 0 .4rem; background: none; border: 0; color: var(--text);
                       cursor: pointer; font-size: 1rem; }
.member-lookup-note { margin: 0; padding: .5rem .6rem; }

/* What a map validation found (map_grid_controller's findingsBadge / findingsList): a count in the grid,
   coloured by the loudest outcome, and each finding in the panel. Dimmed where the row changed since. */
.finding-count {
  display: inline-block; min-width: 1.4rem; padding: 0 .4rem; border-radius: 999px;
  font-size: .75rem; font-weight: 600; text-align: center; border: 1px solid var(--border);
}
.finding-warn { color: var(--warning, #b7791f); border-color: currentColor; }
.finding-error, .finding-abort { color: var(--danger, #c53030); border-color: currentColor; }
.finding-count.is-stale, .map-findings li.is-stale { opacity: .5; }
.map-findings { margin-bottom: 1rem; padding: .5rem .6rem; border: 1px solid var(--border);
                border-radius: var(--radius); background: var(--surface-sunk); }
.map-findings-head { font-weight: 600; margin-bottom: .3rem; }
.map-findings ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .4rem; }

/* A rule's own controls, built by lib/maps/rule_form.js. A repeated value and a nested rule are the two
   shapes here that a form elsewhere in the app does not already have. */
.rule-list { display: flex; flex-direction: column; gap: .3rem; }
/* The map panel's Target: a kind picker over a value box, or over an expression's or a script's controls.
   Spaced rather than stacked flush: the focus ring is drawn 4px outside a control, and the control below it
   — the code editor above all, with a background of its own — painted over the bottom of the ring.
   `:not([hidden])` because `display: flex` would otherwise override `hidden`. */
/* A control holding a value the engine cannot use — a script language it has no engine for. Marked, with a
   sentence under it, and still editable and saveable. */
.field-invalid > label { color: var(--danger); }
.field-invalid .input, .field-invalid .select { border-color: var(--danger); }
/* A grid cell holding such a value: a bar at its left edge and the text in the error colour. */
.map-cell-invalid { color: var(--danger); box-shadow: inset 3px 0 0 var(--danger); }
/* Under a code area, at its right edge, as the canvas's Expand sits under a source setting. */
.expand-button { display: block; margin: .25rem 0 0 auto; }
.target-kind { margin-bottom: .5rem; }
.target-kind-box:not([hidden]) { display: flex; flex-direction: column; gap: .5rem; }
.rule-row { display: flex; align-items: center; gap: .3rem; }
.rule-row .input, .rule-row .select { flex: 1; min-width: 0; }
.rule-columns { margin-bottom: 1.4rem; }
/* Every column of a multicolumn rule has to match, and within one any alternative will do — so a column is
   a box and its alternatives are indented inside it. The nesting is the grammar made visible; without it
   an AND and an OR are two lists that look identical. */
.rule-column {
  padding: .5rem;
  margin-bottom: .4rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-hover);
}
.rule-nested { margin: .4rem 0 .2rem .8rem; border-left: 2px solid var(--border); padding-left: .6rem; }
.rule-nested-item { margin-bottom: .4rem; }
.rule-nested-item .field { margin-bottom: .5rem; }
.rule-nested-item .field-hint { display: none; }
/* The try-it box: a value in, and what the rule would do with it beside the label. The answer sits on the
   label's row rather than under the box, so reading the pattern, the target and the result is one column
   of the eye rather than three stops. */
.rule-preview label { display: inline; margin-right: .4rem; }
.rule-preview .input { margin-top: .25rem; }
.rule-preview .muted { display: block; margin-top: .25rem; word-break: break-all; }
/* Tag and Format Class, which mean nothing to somebody who does not know the job downstream. */
.panel-details { margin-top: .8rem; border-top: 1px solid var(--border); padding-top: .6rem; }
.panel-details summary { cursor: pointer; color: var(--subtle); font-size: .8rem; }
.panel-details .field { margin-top: .7rem; }

/* Name cell: the kind's icon, then the name, then anything the row is flagged
   with. The icon is muted so the name is what the eye lands on. */
/* Bounded by the cell, and truncating *inside* itself. An inline-flex box is an
   atomic inline: where it overflows the cell the browser drops the whole box and
   leaves the cell's ellipsis alone, so a long name rendered as "…" and nothing
   else — icon included. The name has to be the part that shortens, which means
   the box may not exceed the cell and the text inside it must be allowed to
   shrink below its own width. */
.tree-name { display: inline-flex; align-items: center; gap: .4rem; min-width: 0; max-width: 100%; }
.tree-name > a, .tree-name > span {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The caret and the branch line share the cell with the name, so the cell is laid
   out as a row and the name is the item that gives way. Without this the name is
   an atomic inline beside them, and the ellipsis is the whole of what is left. */
.tabulator.grid.file-grid .tabulator-cell[tabulator-field="name"] {
  display: inline-flex; align-items: center;
}
.tabulator.grid.file-grid .tabulator-cell[tabulator-field="name"] .tree-name { flex: 0 1 auto; }
.tree-name svg { flex: none; color: var(--subtle); }
.tree-name.is-folder svg { color: var(--primary); }
/* Every name in the text colour, file or folder: a name that opens is still a link, but a column of names in
   two colours reads as two kinds of file. The pencil in the row's actions says which ones open an editor. */
.tree-name a { color: var(--text); }
.tree-name.is-folder a { font-weight: 600; }
.tree-name .badge { margin-left: .1rem; }
/* A row that is a sentence rather than a file: what is inside a mount while it is being fetched, and the
   store's own words where it could not be read. */
.tree-name.is-error { color: var(--danger); }

/* A badge inside a grid cell sits next to text at a smaller size than one in a
   heading, and must not stretch the row. */
.tabulator.grid .tabulator-cell .badge { margin-left: .35rem; font-size: .68rem; padding: 0 .3rem; }

/* --- the pop-over --------------------------------------------------------
   `details` rather than a scripted menu: it opens, closes and takes focus with
   no JavaScript, which is what keeps uploading possible when the grid is not.
   The panel is positioned rather than laid out, so opening it does not move the
   grid underneath. */
.popover { position: relative; display: inline-block; }
.popover > summary { list-style: none; cursor: pointer; }
.popover > summary::-webkit-details-marker { display: none; }
.popover > .popover-panel {
  position: absolute; right: 0; top: calc(100% + .4rem); z-index: 30;
  width: min(24rem, calc(100vw - 2rem)); max-height: min(32rem, 70vh); overflow-y: auto;
  padding: 1rem; text-align: left;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.popover .popover-head { margin-bottom: .9rem; }
.popover .popover-head p { margin: .25rem 0 0; }
.popover .field { margin-bottom: 1rem; }
.popover form { margin: 0; }
/* The second way in, under a rule: registering a file held elsewhere is a
   different act from uploading one, not a variant of it. */
.popover .popover-split {
  margin-top: 1.1rem; padding-top: 1.1rem; border-top: 1px solid var(--border);
}
.popover .popover-split p { margin: .25rem 0 .8rem; }

/* A button that opens a menu rather than doing something, so it carries the arrow
   that says so. The arrow sits hard against the right edge with the label's own
   gap absorbed, and turns over when the menu is open — the one bit of feedback
   that tells you the second click on the button is what closes it. */
.menu-btn { padding-right: .55rem; }
.menu-btn .menu-caret { margin-left: -.05rem; opacity: .85; transition: transform .12s ease; }
.popover[open] > .menu-btn .menu-caret { transform: rotate(180deg); }

/* A pop-over that is a menu rather than a form: one row per choice, each naming
   the choice and saying what it is, since the name alone is rarely enough to
   choose by. Narrower than the form panel, and the padding moves to the rows so
   the whole row is the target. */
.popover > .popover-panel.popover-menu { width: min(21rem, calc(100vw - 2rem)); padding: .35rem; }
.popover-menu-item {
  display: block; padding: .55rem .65rem; border-radius: var(--radius-sm);
  color: inherit; text-decoration: none;
}
.popover-menu-item:hover, .popover-menu-item:focus-visible { background: var(--primary-soft); }
.popover-menu-item strong { display: block; font-weight: 600; }
.popover-menu-item .small { display: block; margin-top: .1rem; }

/* The selected row is where an upload lands and what a new folder is created
   under, so it has to be visible at a glance rather than only on focus. */
.tabulator.grid.file-grid .tabulator-row.tabulator-selected,
.tabulator.grid.file-grid .tabulator-row.tabulator-selected:hover {
  /* The tint alone is easy to miss in the dark palette, so the row is also
     marked down its leading edge. */
  background: var(--primary-soft); box-shadow: inset 3px 0 0 var(--primary);
}

/* --- moving by dragging ---------------------------------------------------
   A file row drags; a folder row and a breadcrumb receive. The dragged row is
   faded rather than hidden so the tree it came from stays readable, and a target
   is outlined inside its own bounds — a border would move the row by a pixel and
   make the whole grid twitch as the pointer crosses it. */
.tabulator.grid.file-grid .tabulator-row[draggable="true"] .tree-name { cursor: grab; }
/* The actions column is frozen on the right. Stock marks the edge with a heavy rule; the grid's own border
   is enough. */
.tabulator.grid.file-grid .tabulator-frozen.tabulator-frozen-left,
.tabulator.grid.file-grid .tabulator-frozen.tabulator-frozen-right { border-left: 1px solid var(--border); border-right: none; }
/* Four icons side by side: narrower than a lone icon button, so they read as one set. */
.tabulator.grid.file-grid .row-actions { gap: 0; }
.tabulator.grid.file-grid .row-actions .icon-btn { width: 26px; }
.tabulator.grid.file-grid .tabulator-row.is-dragging { opacity: .45; }
.tabulator.grid.file-grid .tabulator-row.is-drop-target {
  background: var(--primary-soft); outline: 2px dashed var(--primary); outline-offset: -2px;
}
.crumbs .crumb-drop.is-drop-target {
  border-radius: var(--radius-sm); outline: 2px dashed var(--primary); outline-offset: 2px;
  color: var(--primary);
}

/* A plain divider between two forms sharing a panel. */
hr.rule { margin: 1rem 0; border: 0; border-top: 1px solid var(--border); }

/* --- the dialogs --------------------------------------------------------- */
.dialog {
  width: min(30rem, calc(100vw - 2rem)); padding: 1.25rem;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.dialog::backdrop { background: rgb(0 0 0 / .45); }
.dialog h3 { margin: 0 2rem .9rem 0; font-size: 1rem; }
.dialog .dialog-dismiss { position: absolute; top: .75rem; right: .75rem; margin: 0; }
.dialog .inline-form { align-items: flex-end; }
/* A dialog editing one row with more than a handful of fields - a map file dimension's two sides and its
   exceptions table. */
.dialog.dialog-wide { width: min(42rem, calc(100vw - 2rem)); }
/* A dialog asking one short thing — a row number to go to. */
.dialog.dialog-narrow { width: min(20rem, calc(100vw - 2rem)); }
/* One group of fields within a dialog, headed by what they are for ("Source", "Target"). */
.dialog .fieldset { border: 1px solid var(--border); border-radius: var(--radius); margin: 0 0 .85rem;
                    padding: .5rem .85rem .25rem; }
.dialog .fieldset legend { padding: 0 .35rem; font-weight: 600; font-size: .85rem; }
/* Two groups side by side where the dialog is wide enough - a dimension's Source and Target - one above the
   other where it is not. */
.dialog .fieldsets { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 0 .85rem; }
/* A dialog that explains rather than asks: a list of the gestures, set at the panel's own reading size. */
.dialog-list { margin: 0 0 .9rem; padding-left: 1.15rem; }
.dialog-list li { margin-bottom: .3rem; }
/* The hint belongs under the row, not beside it, whichever copy of the form
   this is. */
.dialog .form-hint, .panel-body .inline-form .form-hint { flex-basis: 100%; margin: .35rem 0 0; }

/* ==========================================================================
   14. Narrow screens
   --------------------------------------------------------------------------
   The sidebar stops being a column and becomes a drawer over the page, opened
   from a bar that only exists at this width.
   ========================================================================== */

@media (max-width: 900px) {
  .shell, html.sidebar-collapsed .shell { grid-template-columns: minmax(0, 1fr); }

  .sidebar {
    position: fixed; top: 0; left: 0; z-index: 40;
    width: var(--sidebar-w);
    transform: translateX(-100%);
    transition: transform .18s ease;
    box-shadow: var(--shadow-lg);
  }
  .shell.is-open .sidebar { transform: none; }
  /* Collapsed is a desktop state; at this width the drawer is always full. */
  html.sidebar-collapsed .shell .sidebar-brand a,
  html.sidebar-collapsed .shell .sidebar-brand a span,
  html.sidebar-collapsed .shell .nav-section > h4,
  html.sidebar-collapsed .shell .nav-item span,
  html.sidebar-collapsed .shell .nav-context,
  html.sidebar-collapsed .shell .sidebar-foot .who,
  html.sidebar-collapsed .shell .sidebar-foot .tools { display: revert; }
  html.sidebar-collapsed .shell .sidebar-brand a { display: flex; }
  html.sidebar-collapsed .shell .nav-item { justify-content: flex-start; padding: .42rem .55rem; }
  html.sidebar-collapsed .shell .sidebar-quick { flex-direction: row; }
  .sidebar-collapse { display: none; }

  .scrim {
    display: block;
    position: fixed; inset: 0; z-index: 30;
    background: rgb(0 0 0 / 45%);
    opacity: 0; pointer-events: none; transition: opacity .18s;
    border: none; padding: 0;
  }
  .shell.is-open .scrim { opacity: 1; pointer-events: auto; }

  .mobile-bar {
    display: flex; align-items: center; gap: .6rem;
    position: sticky; top: 0; z-index: 20;
    padding: .5rem .9rem;
    background: var(--sidebar-bg);
    border-bottom: 1px solid var(--border);
  }
  .mobile-bar .brand {
    display: flex; align-items: center; gap: .45rem;
    color: var(--text); font-weight: 700; letter-spacing: .06em; font-size: .9rem;
  }
  .mobile-bar .brand svg { color: var(--brand); }
  .mobile-bar .brand .wordmark { font-size: 1.45rem; }

  .content { padding: 1.25rem 1rem 3rem; }
  .page-title { align-items: flex-start; flex-direction: column; gap: .6rem; }
  .page-title h1 { margin-right: 0; }
}

@media print {
  .sidebar, .mobile-bar, .page-actions { display: none; }
  .shell { grid-template-columns: 1fr; }
}

/* A proposed edit's change, line by line (job_drafts/show). Monospace like every other view of a definition;
   added and removed lines tinted with the success/danger soft tokens so both themes follow. */
.line-diff { width: 100%; border-collapse: collapse; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: .8125rem; line-height: 1.5; }
.line-diff td { padding: 0 .5rem; vertical-align: top; }
.line-diff-number { width: 1%; min-width: 2.5rem; text-align: right; color: var(--subtle); user-select: none;
  border-right: 1px solid var(--border); }
.line-diff-text { white-space: pre-wrap; word-break: break-word; }
.line-diff-mark { display: inline-block; width: 1.25em; color: var(--muted); user-select: none; }
.line-diff-added { background: var(--success-soft); }
.line-diff-added .line-diff-mark { color: var(--success); }
.line-diff-removed { background: var(--danger-soft); }
.line-diff-removed .line-diff-mark { color: var(--danger); }
.line-diff-skip td { color: var(--subtle); font-style: italic; padding: .25rem .5rem; background: var(--surface-sunk); }

/* Two versions of a file compared (file_comparisons/show). The toolbar's version selects; the table itself is
   the file editor's grid (.text-grid), which marks removed and added rows and changed values as the editor
   marks its own edits. A changed value shows the older one struck through before it. Each run of changes is
   a Tabulator group, headed by how many unchanged rows lie before it. */
.compare-toolbar .compare-field { display: inline-flex; align-items: center; gap: .35rem; color: var(--muted); }
.tabulator.grid.text-grid .tabulator-cell del { color: var(--danger); margin-right: .15rem; }
.tabulator.grid.compare-grid .tabulator-group { background: var(--surface-sunk); color: var(--subtle);
  font-style: italic; font-weight: 400; border-bottom: 1px solid var(--border); border-top: none;
  padding: .25rem .5rem; }
.tabulator.grid.compare-grid .tabulator-group:hover { cursor: default; background: var(--surface-sunk); }
/* The band at a row's left is on the side that holds it: an added row only `to` has (the editor's own rule
   would band both number columns), a removed row only `from` has, a changed row both. */
.tabulator.grid.compare-grid .tabulator-row.is-new .text-grid-number.compare-from { box-shadow: none; }
.tabulator.grid.compare-grid .tabulator-row.is-deleted .text-grid-number.compare-from {
  box-shadow: inset 3px 0 0 var(--danger); color: var(--danger); }
.tabulator.grid.compare-grid .tabulator-row.is-changed .text-grid-number {
  box-shadow: inset 3px 0 0 var(--changed); color: var(--changed); }
/* An added or removed row's values in green or red on a tint, as the line diff shows them; a removed row
   keeps the editor's strike-through. */
.tabulator.grid.text-grid.compare-grid .tabulator-row.is-new .tabulator-cell:not(.text-grid-number) {
  background: var(--success-soft); color: var(--success); }
.tabulator.grid.text-grid.compare-grid .tabulator-row.is-deleted .tabulator-cell:not(.text-grid-number) {
  background: var(--danger-soft); color: var(--danger); }
/* A group is a heading, not something to fold away: the comparison shows only what differs already. */
.tabulator.grid.compare-grid .tabulator-group .tabulator-group-toggle { display: none; }

/* ---------------------------------------------------------------------------
   The in-app assistant (ai_conversations). A transcript, not a chat app: the
   member's messages sit right on a tint, the assistant's read as page text at
   full measure, and its tool activity is a quiet list between them. A proposal
   is the one thing drawn as a card, because it is the one thing to act on.
   --------------------------------------------------------------------------- */
.assistant { max-width: 860px; }
.assistant-transcript { display: flex; flex-direction: column; gap: .75rem; margin-bottom: 1rem; }
.assistant-msg p { margin: 0 0 .6rem; }
.assistant-msg p:last-child, .assistant-msg ul:last-child, .assistant-msg ol:last-child { margin-bottom: 0; }
.assistant-msg ul, .assistant-msg ol { margin: 0 0 .6rem; padding-left: 1.4rem; }
.assistant-msg .code-block { margin: .4rem 0 .6rem; }
.assistant-msg-user {
  align-self: flex-end; max-width: 80%;
  background: var(--primary-soft); border: 1px solid var(--border);
  border-radius: 10px; padding: .6rem .85rem;
}
.assistant-msg-ai { line-height: 1.55; }
.assistant-tools { display: flex; flex-direction: column; gap: .25rem; }
.assistant-tool {
  display: flex; align-items: baseline; gap: .4rem;
  color: var(--muted); font-size: .8125rem;
}
.assistant-tool svg { flex: none; align-self: center; }
.assistant-tool-refused svg { color: var(--danger); }
.assistant-draft {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  background: var(--surface); border: 1px solid var(--border-strong); border-left: 3px solid var(--primary);
  border-radius: 8px; padding: .7rem .9rem; margin: .25rem 0;
}
.assistant-draft .badge { margin-left: .4rem; vertical-align: 1px; }
.assistant-working { display: flex; align-items: center; gap: .5rem; color: var(--muted); margin-bottom: 1rem; }
.assistant-composer { margin: 1rem 0 .75rem; }
.assistant-composer textarea { width: 100%; }
.assistant-composer .form-actions { display: flex; align-items: center; gap: .75rem; }
.spinner {
  width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid var(--border-strong); border-top-color: var(--primary);
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 2.4s; } }

/* The file views (FileViewsController): a stored file a page at a time, as a table or as lines. See
   file_grid_controller.js. The row number is the one column that is ours rather than the file's, so it is
   drawn as a gutter — muted, and never mistaken for a column of data. */
.text-grid { min-height: 15rem; }
.tabulator.grid.text-grid .tabulator-cell.text-grid-number {
  color: var(--text-muted); background: var(--surface-sunk); font-variant-numeric: tabular-nums;
}
/* A value as it was written: leading spaces and runs of them are part of what a field holds, and a log
   line's indentation is how it is read. */
.tabulator.grid.text-grid .text-grid-value { white-space: pre; }
.tabulator.grid.text-grid .tabulator-cell[tabulator-field="line"] {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: .82rem;
}
/* A row narrower or wider than the header, flagged on its number rather than tinted across the whole row —
   a ragged file can be mostly ragged rows, and a page of warning colour says less than a column of marks. */
.tabulator.grid.text-grid .tabulator-row.is-ragged .text-grid-number { box-shadow: inset 3px 0 0 var(--warning); }
/* Where Go to or Find landed. */
.tabulator.grid.text-grid .tabulator-row.is-found { box-shadow: inset 3px 0 0 var(--primary); background: var(--surface-hover); }
/* What Find looks for, wherever the page holds it — drawn as the text editor draws its matches (theme.js):
   an outline for each, filled as a selection in the row Find landed on. An inset shadow rather than a border,
   so that marking a value does not widen it. */
.tabulator.grid.text-grid mark.find-match {
  background: transparent; color: inherit; border-radius: 2px; box-shadow: inset 0 0 0 1px var(--border-strong);
}
.tabulator.grid.text-grid .tabulator-row.is-found mark.find-match { background: var(--editor-selection); }
.input-number { width: 7.5rem; }
/* A column's title is the file's own header, so it is shown as written: the grid theme's small capitals
   would turn `Entity` and `ENTITY` — two different columns to anything reading the file — into one. */
.tabulator.grid.text-grid .tabulator-col-title { text-transform: none; letter-spacing: normal; }
/* A file view's title line: the name, the version as a badge (the version list behind it), then the size and
   how this version came to be — one line where the window allows, wrapping after the name where not. Takes
   the auto margin `.page-title h1` otherwise would, so the actions stay at the far end. */
.file-heading { display: flex; align-items: baseline; gap: .3rem .6rem; flex-wrap: wrap; margin-right: auto;
                flex: 1 1 22rem; min-width: 0; }
.page-title .file-heading h1 { margin-right: 0; }
.file-heading .badge { align-self: center; }
.file-heading .badge svg { margin-right: -.15rem; }
.file-meta { color: var(--muted); font-size: .85rem; }
.file-versions tr.is-current td { background: var(--surface-hover); }
/* The table's own tools at the toolbar's far end — filter, clear, CSV settings — sized to the toolbar's
   controls so the row stays one height. A pressed toggle is tinted while what it shows is showing. */
.toolbar-icons { display: inline-flex; align-items: center; gap: .15rem; }
/* A full button among the icons (the map editor's Add a map) keeps the toolbar's own spacing from them. */
.toolbar-icons .btn { margin-left: .35rem; }
.table-toolbar .icon-btn { width: 2.125rem; height: 2.125rem; }
.icon-btn[hidden] { display: none; }
.icon-btn[aria-pressed="true"] { background: var(--primary-soft); color: var(--primary); }
/* A toolbar button that opens a field, held down while it is open: the table's Replace. */
.btn[aria-pressed="true"] { background: var(--primary-soft); color: var(--primary); }
.icon-btn.is-filtering { color: var(--primary); }
/* The filter boxes under the headings, until the filter toggle shows them (lib/grids/filters.js) — any grid
   marked `filters-toggled`: a file's table, two versions compared, a map file. */
.grid.filters-toggled:not(.filters-shown) .tabulator-header-filter { display: none; }
/* Room for the + that appears between two rows or two columns: it straddles the grid's edge. */
.text-grid-wrap { position: relative; }
.grid-inserter {
  position: absolute; z-index: 20; display: grid; place-items: center;
  width: 18px; height: 18px; margin: -9px 0 0 -9px; padding: 0;
  border: 1px solid var(--primary); border-radius: 50%;
  background: var(--surface); color: var(--primary); cursor: pointer;
  box-shadow: 0 1px 3px rgb(0 0 0 / 20%);
}
.grid-inserter:hover { background: var(--primary); color: var(--surface); }
.grid-insert-line { position: absolute; z-index: 19; background: var(--primary); pointer-events: none; }
.grid-inserter[hidden], .grid-insert-line[hidden] { display: none; }
/* The placeholder of an empty file being edited: a way to its first row. */
.text-grid-empty { display: flex; flex-direction: column; align-items: center; gap: .6rem; }
/* Table edits (file_grid_controller.js), shown until Save: a typed cell tinted, a new row or column marked
   with the primary colour's edge, a deleted row or column struck through — still on screen, so what Save
   will do is visible and a mistaken delete is one click to restore. */
.tabulator.grid.text-grid .tabulator-cell.is-edited { background: var(--warning-soft); }
.tabulator.grid.text-grid .tabulator-row.is-new .text-grid-number { box-shadow: inset 3px 0 0 var(--success); color: var(--success); }
.tabulator.grid.text-grid .tabulator-row.is-deleted .tabulator-cell:not(.text-grid-number),
.tabulator.grid.text-grid .tabulator-cell.is-deleted { text-decoration: line-through; color: var(--text-muted); }
.tabulator.grid.text-grid .tabulator-col.is-deleted .tabulator-col-title { text-decoration: line-through; color: var(--text-muted); }
.tabulator.grid.text-grid .tabulator-col.is-new .tabulator-col-title { color: var(--success); }
.tabulator.grid.text-grid .tabulator-row.is-selected { background: var(--primary-soft, var(--surface-hover)); }
.tabulator.grid.text-grid .tabulator-row.is-selected .text-grid-number { box-shadow: inset 3px 0 0 var(--primary); }
.tabulator.grid.text-grid .text-grid-number { cursor: default; }
[data-file-grid-editable-value="true"] .tabulator.grid.text-grid .text-grid-number { cursor: pointer; }
/* `.flash` is a flex row, which beats the browser's own `[hidden]` rule — the same trap as `.btn[hidden]`. */
.flash[hidden] { display: none; }

/* Ace's search box (ext-searchbox.js), opened by Find/Replace or Ctrl/⌘-F/H in a code editor. It brings its
   own stylesheet, written for a white page; these restate its colours in this app's variables so it reads in
   all three theme states, the same way lib/editor/theme.js does for the editor itself. Selectors carry
   `.code-editor` so they outrank Ace's, which it injects after this file. */
.code-editor .ace_search {
  background: var(--surface); color: var(--text); border-color: var(--border-strong);
  box-shadow: 0 4px 14px rgb(0 0 0 / 25%); font-family: inherit;
}
.code-editor .ace_search_field {
  background: var(--surface-sunk); color: var(--text); border-color: var(--border-strong);
}
.code-editor .ace_search_field:focus { outline: 2px solid var(--primary); outline-offset: -1px; }
.code-editor .ace_searchbtn, .code-editor .ace_button {
  background-color: var(--surface); color: var(--text); border-color: var(--border-strong);
}
.code-editor .ace_searchbtn:hover, .code-editor .ace_button:hover { background-color: var(--surface-hover); }
.code-editor .ace_button.checked { border-color: var(--primary); color: var(--primary); }
.code-editor .ace_search_counter { color: var(--text-muted); }
/* The previous/next chevrons are borders, so they take the text colour; the close glyph is a dark image,
   inverted where the page is dark. Only the close button — inverting a whole button flips its background
   too, which is how the arrows first came out white on a dark page. */
.code-editor .ace_searchbtn.prev::after, .code-editor .ace_searchbtn.next::after { border-color: var(--text-muted); }
.code-editor .ace_button { color: var(--text); opacity: .85; }
:root[data-theme="dark"] .code-editor .ace_searchbtn_close { filter: invert(1); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .code-editor .ace_searchbtn_close { filter: invert(1); }
}
