/* MDL Console — stylesheet.
 *
 * Split out of app.html on the new-console-ui branch. The page was one file
 * because there was no build step and no second static route; the route now
 * exists (server.py serves /app.css and /app.js) and the reason the split
 * happened anyway is size: this file is what a 366-line <style> block turned
 * into once the design language came from somewhere other than "whatever the
 * next view needed".
 *
 * That somewhere is OpenMetadata — see docs/NEW-CONSOLE-UI.md §1 for what was
 * taken and what deliberately was not. The short version: its two-layer token
 * scheme, its sidebar metrics, its table chrome and its split sign-in.
 *
 * TWO structural decisions, because both reverse what this file used to do.
 *
 * **Light is the default now.** :root used to BE the dark palette, with light
 * and origen each writing a full second copy. Light-first means dark overrides
 * one layer of semantic tokens and nothing else, which is the whole point of
 * having a semantic layer. `system` stays the absence of a stamp so the OS
 * preference keeps working.
 *
 * **Origen is gone.** It was a third full palette carrying its own contrast
 * measurements against origen.ae's brand values. Withdrawn on request; the
 * measurements are in git history if it comes back, and if it does it comes
 * back as an override of --brand and --bg rather than a third copy.
 *
 * No external requests. Same rule as before — the favicon is a data URI for
 * this reason — so the brand face is NOT loaded. OpenMetadata sets Inter; what
 * is borrowed here is its type SCALE (14px body, 12px label at weight 500,
 * 22-27px display) over the system stack. The one existing exception is
 * Leaflet, fetched from unpkg when a geometry cell is opened, and it stays the
 * exception.
 */

/* ========================================================================
 * 1. Tokens
 *
 * Semantic names only below this block: nothing further down mentions a grey
 * by number. Values are OpenMetadata's --om-color-* (an Untitled UI base),
 * mapped in docs/NEW-CONSOLE-UI.md §2 so the two can be diffed.
 * ==================================================================== */
:root {
  /* surfaces */
  --bg: #fafafa;            /* the page ground */
  --panel: #ffffff;         /* cards, table chrome, menus, the top bar */
  --inset: #f5f5f5;         /* a well inside a panel: inputs, meter tracks */
  --side: #fafafa;          /* the sidebar, one hair off the content ground */

  /* text */
  --text: #181d27;
  --dim: #535862;           /* secondary: sub-lines, table headers */
  --faint: #717680;         /* tertiary: placeholders, disabled, "—" */

  /* lines */
  --line: #e9eaeb;          /* dividers between rows and sections */
  --line2: #d5d7da;         /* the edge of something you can click */

  /* brand + status. Status is separate from brand on purpose: a warning must
     not read as an accent, and eleven tabs of pending counts are warnings. */
  --accent: #1570ef;
  --accent-hover: #175cd3;
  --accent-soft: #eff8ff;
  --ok: #079455;   --ok-soft: #ecfdf3;   --ok-line: #abefc6;
  --warn: #b54708; --warn-soft: #fffaeb; --warn-line: #fedf89;
  --bad: #b42318;  --bad-soft: #fef3f2;  --bad-line: #fecdca;

  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --ui: system-ui, -apple-system, "Segoe UI", Roboto,
        "Noto Sans Arabic", sans-serif;

  --r-sm: 6px; --r-md: 8px; --r-lg: 12px;
  --sh-xs: 0 1px 2px rgba(10,13,18,.05);
  --sh-sm: 0 1px 3px rgba(10,13,18,.1), 0 1px 2px -1px rgba(10,13,18,.1);
  --sh-lg: 0 12px 16px -4px rgba(10,13,18,.08), 0 4px 6px -2px rgba(10,13,18,.03);

  --side-w: 208px;
  --top-h: 60px;
}

/* Dark. Written twice — once for the OS preference, once for an explicit
   choice — because a custom property set cannot be named and reused. The
   :not([data-theme]) guard is what lets "Dark" work on a light OS and "Light"
   on a dark one; only the token values are in here, never a component rule. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --bg: #0a0d12; --panel: #14181f; --inset: #1a1f28; --side: #0d1117;
    --text: #f5f5f6; --dim: #94979c; --faint: #85888e;
    --line: #22262f; --line2: #333741;
    --accent: #2e90fa; --accent-hover: #53b1fd; --accent-soft: #102a56;
    --ok: #47cd89;   --ok-soft: #053321;   --ok-line: #085d3a;
    --warn: #fdb022; --warn-soft: #4e1d09; --warn-line: #93370d;
    --bad: #f97066;  --bad-soft: #55160c;  --bad-line: #912018;
    --sh-xs: 0 1px 2px rgba(0,0,0,.5);
    --sh-sm: 0 1px 3px rgba(0,0,0,.6);
    --sh-lg: 0 12px 16px -4px rgba(0,0,0,.5);
  }
}
:root[data-theme="dark"] {
  --bg: #0a0d12; --panel: #14181f; --inset: #1a1f28; --side: #0d1117;
  --text: #f5f5f6; --dim: #94979c; --faint: #85888e;
  --line: #22262f; --line2: #333741;
  --accent: #2e90fa; --accent-hover: #53b1fd; --accent-soft: #102a56;
  --ok: #47cd89;   --ok-soft: #053321;   --ok-line: #085d3a;
  --warn: #fdb022; --warn-soft: #4e1d09; --warn-line: #93370d;
  --bad: #f97066;  --bad-soft: #55160c;  --bad-line: #912018;
  --sh-xs: 0 1px 2px rgba(0,0,0,.5);
  --sh-sm: 0 1px 3px rgba(0,0,0,.6);
  --sh-lg: 0 12px 16px -4px rgba(0,0,0,.5);
}

/* ========================================================================
 * 2. Base
 * ==================================================================== */
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text);
       font: 400 14px/1.5 var(--ui); -webkit-font-smoothing: antialiased; }
h1, h2, h3 { margin: 0; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }

.sub { color: var(--dim); font-size: 12px; }
.muted { color: var(--dim); }
.crit { color: var(--bad); }
.high { color: var(--warn); }
.mono { font-family: var(--mono); font-size: 12.5px; }
.ar { direction: rtl; text-align: right;
      font-family: "Noto Sans Arabic", var(--ui); }

/* ========================================================================
 * 3. Shell: sidebar + top bar
 *
 * The tab strip is gone. Eight tabs in a horizontal overflow-x:auto row had two
 * problems that a sidebar does not: it scrolled on a laptop, and it had nowhere
 * to put a pending count — the number that decides which tab you open. The
 * signed-in badge shared that row and used to collide with the last tab; it now
 * lives in a bar of its own, so that class of bug is gone rather than fixed.
 * ==================================================================== */
.app { display: flex; min-height: 100vh; }

.side { flex: none; width: var(--side-w); background: var(--side);
        border-inline-end: 1px solid var(--line);
        display: flex; flex-direction: column;
        position: sticky; top: 0; height: 100vh; overflow: hidden;
        transition: width .2s ease; }
:root[data-side="collapsed"] { --side-w: 72px; }

.brandrow { flex: none; display: flex; align-items: center; gap: 10px;
            height: var(--top-h); padding: 0 14px 0 18px; }
.mark { flex: none; width: 28px; height: 28px; border-radius: 7px;
        display: flex; align-items: center; justify-content: center;
        background: var(--accent); }
.mark svg { width: 16px; height: 16px; }
.brandrow b { font-size: 14px; font-weight: 600; white-space: nowrap;
              letter-spacing: -.01em; }
.brandrow b span { font-weight: 400; color: var(--faint); }
/* The collapse control is the last thing in the row when the sidebar is open
   and takes the row to itself when it is not, so it is never the thing that
   gets clipped by the 72px width. */
.brandrow .fold { margin-inline-start: auto; }
:root[data-side="collapsed"] .brandrow { justify-content: center; padding: 0; }
:root[data-side="collapsed"] .brandrow b,
:root[data-side="collapsed"] .brandrow .mark { display: none; }
:root[data-side="collapsed"] .brandrow .fold { margin: 0; }

#nav { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden;
       display: flex; flex-direction: column; gap: 2px; padding: 6px 12px 14px; }
/* A group heading, not a tab. Only drawn for a group that has a visible tab in
   it — MAKANI_EDITION=basic keeps two of the eight, and a heading over nothing
   would announce a section this console does not serve. */
#nav .navgrp { padding: 14px 10px 5px; color: var(--faint);
               font-size: 10.5px; font-weight: 500; letter-spacing: .09em;
               text-transform: uppercase; white-space: nowrap; }
#nav .navgrp:first-child { padding-top: 4px; }
:root[data-side="collapsed"] #nav .navgrp { height: 13px; padding: 6px 0 0;
               overflow: hidden; color: transparent; }

#nav button { display: flex; align-items: center; gap: 11px; width: 100%;
              padding: 9px 11px; border: 0; border-radius: var(--r-md);
              background: none; color: var(--dim); cursor: pointer;
              font: 500 13.5px/1.2 var(--ui); text-align: start;
              white-space: nowrap; }
#nav button .ic { flex: none; display: flex; }
#nav button .ic svg { width: 18px; height: 18px; fill: none;
                  stroke: currentColor; stroke-width: 1.7;
                  stroke-linecap: round; stroke-linejoin: round; }
#nav button:hover { background: var(--accent-soft); color: var(--accent-hover); }
#nav button.on { background: var(--accent); color: #fff; box-shadow: var(--sh-xs); }
#nav button.on:hover { background: var(--accent-hover); color: #fff; }
/* The pending count. It is the reason the sidebar exists, so it is allowed the
   only warning colour in the navigation — and it keeps that colour on the
   selected item too, where a white-on-brand pill would say "0 is fine". */
#nav button .badge { margin-inline-start: auto; flex: none;
                     padding: 2px 6px; border-radius: 999px;
                     font: 500 10.5px/1.4 var(--mono);
                     font-variant-numeric: tabular-nums;
                     background: var(--warn-soft); color: var(--warn);
                     border: 1px solid var(--warn-line); }
#nav button.on .badge { background: rgba(255,255,255,.22); color: #fff;
                        border-color: transparent; }
:root[data-side="collapsed"] #nav button { justify-content: center; padding: 10px 0; }
:root[data-side="collapsed"] #nav button .label,
:root[data-side="collapsed"] #nav button .badge { display: none; }

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

.topbar { position: sticky; top: 0; z-index: 20; flex: none;
          height: var(--top-h); display: flex; align-items: center; gap: 12px;
          padding: 0 18px; background: var(--panel);
          border-bottom: 1px solid var(--line); }
/* Where you are. The tab strip answered this by being highlighted; a sidebar
   answers it too, but a model detail page is three levels deep and its name is
   the thing worth reading at the top of the window. */
.crumb { display: flex; align-items: center; gap: 7px; min-width: 0;
         color: var(--faint); font-size: 12.5px; }
.crumb b { color: var(--text); font-size: 15px; font-weight: 600;
           letter-spacing: -.01em; overflow: hidden;
           text-overflow: ellipsis; white-space: nowrap; }
.crumb .sep { flex: none; opacity: .6; }
.crumb a { color: var(--faint); text-decoration: none; white-space: nowrap; }
.crumb a:hover { color: var(--accent); }

main { flex: 1; padding: 18px 20px 64px; min-width: 0; }

/* ========================================================================
 * 4. The account menu
 * ==================================================================== */
#who { margin-inline-start: auto; flex: none; display: none;
       align-items: center; gap: 8px; position: relative; }
#who.on { display: flex; }
#who-btn { display: flex; align-items: center; gap: 9px; cursor: pointer;
           background: none; border: 1px solid transparent;
           border-radius: var(--r-md); padding: 5px 8px 5px 5px;
           color: var(--dim); font: inherit; }
#who-btn:hover, #who-btn[aria-expanded="true"] { border-color: var(--line2);
           color: var(--text); }
#who-btn .avatar { flex: none; width: 30px; height: 30px; border-radius: 50%;
           display: flex; align-items: center; justify-content: center;
           background: var(--accent); color: #fff;
           font: 600 11.5px/1 var(--ui); letter-spacing: .02em; }
#who-btn .nm { font-size: 13px; font-weight: 500; color: var(--text);
               max-width: 15ch; overflow: hidden; text-overflow: ellipsis;
               white-space: nowrap; }
#who-btn .caret { font-size: 9px; opacity: .7; }
#who-menu { position: absolute; inset-inline-end: 0; top: calc(100% + 6px);
            min-width: 200px; z-index: 60; padding: 5px;
            background: var(--panel); border: 1px solid var(--line);
            border-radius: var(--r-md); box-shadow: var(--sh-lg); }
#who-menu[hidden] { display: none; }
#who-menu .grp { color: var(--faint); font-size: 10px; text-transform: uppercase;
                 letter-spacing: .07em; padding: 5px 9px 3px; }
#who-menu .sep { height: 1px; background: var(--line); margin: 5px 0; }
#who-menu button { display: flex; width: 100%; align-items: center; gap: 8px;
                   text-align: start; border: 0; border-radius: var(--r-sm);
                   padding: 6px 9px; background: none; color: var(--text);
                   font: 400 12.5px var(--ui); cursor: pointer; }
#who-menu button:hover, #who-menu button:focus-visible {
                   background: var(--inset); outline: none; }
/* The tick is always in the DOM and only changes opacity, so choosing a theme
   does not shift the labels sideways by the width of a character. */
#who-menu .tick { width: 11px; flex: none; color: var(--accent); opacity: 0; }
#who-menu button[aria-checked="true"] .tick { opacity: 1; }
#who-menu button[aria-checked="true"] { color: var(--text); font-weight: 600; }

/* ========================================================================
 * 5. No page header
 *
 * There isn't one, and that is a decision rather than an omission. The crumb in
 * the top bar IS the page title — it is in the same place on every page, it is
 * the way back up from a model, and an <h1> under it saying "Models" again
 * would be the second time the word appears in 60 vertical pixels. A view that
 * needs to explain itself uses .note, once, at the top.
 * ==================================================================== */

/* ========================================================================
 * 6. Cards and the overview tiles
 * ==================================================================== */
.card { background: var(--panel); border: 1px solid var(--line);
        border-radius: var(--r-lg); box-shadow: var(--sh-xs); }
.chead { display: flex; align-items: center; gap: 10px; padding: 13px 16px;
         border-bottom: 1px solid var(--line); }
.chead h3 { font-size: 14.5px; font-weight: 600; }
.chead .sub { color: var(--faint); }
.chead .actions { margin-inline-start: auto; }
main h3 { font-size: 15px; font-weight: 600; margin: 20px 0 10px; }
main h3:first-child { margin-top: 0; }
.chead h3 { margin: 0; }

.cards { display: grid; gap: 12px; margin-bottom: 12px;
         grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
.cards .card { padding: 13px 15px 14px; }
.cards .card .k { color: var(--faint); font-size: 10.5px; font-weight: 500;
                  text-transform: uppercase; letter-spacing: .09em; }
.cards .card .n { font-size: 26px; font-weight: 600; letter-spacing: -.02em;
                  line-height: 1.15; margin: 8px 0 2px;
                  font-variant-numeric: tabular-nums; }
.cards .card .sub { font-size: 12px; }
/* The meter is the point of the redesign on this page. `78 of 80 published`
   was a sub-line you had to read; six of them side by side could not be
   compared. A 6px track can. */
.cards .card .meter { margin-top: 10px; }

/* Overview only. auto-fill keeps making tracks as the viewport grows, so on a
   wide screen six cards sat in the first six of eleven tracks, starved to the
   minimum while most of the row was empty. auto-fit collapses the empty ones,
   and the max-width stops six cards stretching to 300px each on a 1900px
   monitor. The same width governs the domain table below. */
.overview { max-width: 1180px; }
.overview .cards { grid-template-columns: repeat(auto-fit, minmax(184px, 1fr)); }
.overview .card .n { white-space: nowrap; }
.overview .card .n small { font-size: 13px; font-weight: 400;
                           color: var(--faint); letter-spacing: 0; }

/* The two queue cards keep their own row and cap their width, so the pair reads
   as a group rather than as two orphans in a six-column grid. `attn` is the
   waiting state: a warning rail down the leading edge and a warning-tinted
   icon well, so the fact that something is waiting survives being printed in
   greyscale. */
.overview .queues { grid-template-columns: repeat(auto-fit, minmax(200px, 250px));
                    justify-content: start; margin-bottom: 20px; }
.overview .queues .card { display: flex; align-items: center; gap: 13px;
                    cursor: pointer; }
.overview .queues .card:hover { border-color: var(--line2);
                    box-shadow: var(--sh-sm); }
.overview .queues .card .qic { flex: none; width: 36px; height: 36px;
                    border-radius: 10px; display: flex; align-items: center;
                    justify-content: center; background: var(--inset);
                    border: 1px solid var(--line); color: var(--dim); }
.overview .queues .card .qic svg { width: 18px; height: 18px; fill: none;
                    stroke: currentColor; stroke-width: 1.8;
                    stroke-linecap: round; stroke-linejoin: round; }
.overview .queues .card .n { font-size: 22px; margin: 0; }
.overview .queues .card .k { text-transform: none; letter-spacing: 0;
                    font-size: 12.5px; color: var(--dim); margin-top: 2px; }
.overview .card.attn { box-shadow: inset 3px 0 0 var(--warn), var(--sh-xs); }
:root:dir(rtl) .overview .card.attn {
                        box-shadow: inset -3px 0 0 var(--warn), var(--sh-xs); }
.overview .card.attn .n { color: var(--warn); }
.overview .card.attn .qic { background: var(--warn-soft); color: var(--warn);
                    border-color: var(--warn-line); }

/* A three-column tally does not want the full width: `Published` ends up an
   inch from `Models` with nothing tying the row together. Counts are right
   aligned and tabular so the digits line up. `end`/`start` rather than
   right/left, as everywhere here — the console renders Arabic. */
.tally .card { max-width: 720px; }
.tally td:nth-child(n+2), .tally th:nth-child(n+2) {
  text-align: end; font-variant-numeric: tabular-nums; }
.tally td.gap { color: var(--warn); font-weight: 600; }

/* ========================================================================
 * 7. Tables
 *
 * A table now lives in a card: the card carries the border, the radius and the
 * shadow, and .wrap inside it is nothing but the horizontal scroll container
 * that panWideTables() grabs. That separation is what makes a sticky header
 * possible — the header sticks to the scroller, and the scroller is no longer
 * the thing with a border on it.
 * ==================================================================== */
.wrap { overflow: auto; max-height: 74vh; overscroll-behavior: contain; }
.card > .wrap:first-child > table > thead > tr > th:first-child {
  border-start-start-radius: var(--r-lg); }
.card > .wrap:first-child > table > thead > tr > th:last-child {
  border-start-end-radius: var(--r-lg); }

/* The pan gesture. `grab` is only set when something is actually hidden,
   because a grab cursor on a table that already fits promises a gesture that
   does nothing. */
.wrap.panme { cursor: grab; }
.wrap.panning { cursor: grabbing; user-select: none; }

table { width: 100%; border-collapse: separate; border-spacing: 0;
        font-size: 13.5px; }
th, td { text-align: start; padding: 10px 14px;
         border-bottom: 1px solid var(--line); vertical-align: top; }
/* Title case, not uppercase, and no letter-spacing: the spacing was there to
   make shouted text legible and on mixed case it only pulls the words apart.
   The labels carry their own capitals, so a header that needs a real acronym
   gets one — "SQL", "Description (EN)" — which text-transform could not have
   produced from a lowercase string.
   Sticky, which is the change: 42 columns and 500 sample rows means the header
   was off screen for most of the reading. */
th { position: sticky; top: 0; z-index: 2;
     background: var(--bg); color: var(--dim);
     font-size: 11.5px; font-weight: 500; white-space: nowrap; }
tbody tr:hover > td { background: var(--bg); }
tbody tr:last-child > td { border-bottom: 0; }
main a { color: var(--accent); text-decoration: none; }
main a:hover { text-decoration: underline; }
td a { font-weight: 500; }

/* A sortable header is a button that has to keep looking like a header, so it
   borrows the th's own type rather than the button defaults. */
.sortbtn { background: none; border: 0; padding: 0; cursor: pointer;
           font: inherit; color: inherit; }
.sortbtn:hover { color: var(--accent); }
.sortbtn.on { color: var(--text); font-weight: 600; }

/* Headers elsewhere are labels this page wrote. In a data sample they are
   identifiers the database owns — UDM_GISID is not what the column is called,
   and a query is written against the real name — so they get the identifier
   face. */
.wrap.data th { font-family: var(--mono); font-size: 11px; }
/* A sampled value can be 200 characters of address text; one line each keeps
   the row grid readable, and the full value is in the title. */
.cell { max-width: 260px; overflow: hidden; text-overflow: ellipsis;
        white-space: nowrap; }

/* An identifier is one unbreakable word, and a relationship name is two model
   names and two column names joined by underscores —
   `dim_facility_types_facility_type__dim_facilities_facility_type` is 62
   characters. `anywhere` rather than `break-word` because only `anywhere`
   lowers the cell's min-content width, which is what was pushing the accept and
   reject buttons off the end of the join queue on a 1440px screen: the table
   could not be narrower than its longest identifier. They still only break when
   there is no other way to fit. */
td.mono, td .mono, td .sub { overflow-wrap: anywhere; }
/* ...but with a floor. Left to itself the auto table layout takes an
   `anywhere`-breakable column all the way down to its narrowest character and
   hands the slack to a column that did not ask for it, so `dim_facilities` came
   out as three lines of four characters. */
td.mono { min-width: 132px; }

/* The page footer of a table. Client-side paging: the rows are already in hand
   (the endpoints cap at 500), so this is a redraw of 25 <tr> rather than a
   request. What it buys is not speed — it is that the browser's own find, the
   scroll position and the sticky header all stop fighting a 500-row DOM. */
.cfoot { display: flex; align-items: center; gap: 12px; padding: 10px 14px;
         border-top: 1px solid var(--line); font-size: 12.5px;
         color: var(--dim); flex-wrap: wrap; }
.cfoot b { color: var(--text); font-variant-numeric: tabular-nums; }
.cfoot .pages { margin-inline-start: auto; display: flex; align-items: center;
                gap: 5px; }
.pg { min-width: 29px; height: 29px; padding: 0 8px; cursor: pointer;
      border: 1px solid var(--line2); border-radius: var(--r-sm);
      background: var(--panel); color: var(--dim);
      font: 500 12.5px/1 var(--mono); }
.pg:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }
.pg.on { background: var(--accent); border-color: var(--accent); color: #fff; }
.pg:disabled { opacity: .4; cursor: default; }
.cfoot .pages > span { color: var(--faint); padding: 0 2px; }

/* ========================================================================
 * 8. Controls
 * ==================================================================== */
input, select, textarea {
  background: var(--panel); color: var(--text); font: inherit;
  border: 1px solid var(--line2); border-radius: var(--r-md);
  padding: 7px 10px; box-shadow: var(--sh-xs); }
input::placeholder { color: var(--faint); }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft); }
textarea { width: 100%; font-family: var(--mono); font-size: 12px; }

/* Three levels, where there used to be one. `.act` alone was a bordered
   button, and "save" next to "cancel" differed only in text colour. */
button.act { display: inline-flex; align-items: center; gap: 6px;
             cursor: pointer; font: 500 13px var(--ui);
             padding: 7px 11px; border-radius: var(--r-md);
             background: var(--panel); color: var(--dim);
             border: 1px solid var(--line2); box-shadow: var(--sh-xs); }
button.act:hover { color: var(--text); border-color: var(--faint); }
button.act svg { width: 15px; height: 15px; }
button.act.pri { background: var(--accent); border-color: var(--accent);
                 color: #fff; }
button.act.pri:hover { background: var(--accent-hover);
                 border-color: var(--accent-hover); color: #fff; }
button.act.ghost { background: none; border-color: transparent; box-shadow: none; }
button.act.ghost:hover { background: var(--inset); }
/* The decision pair. Still `yes`/`no` in the markup — every call site and the
   pane strip use them — but they now carry a tinted ground, so accept and
   reject are two shapes rather than two shades of one. */
button.act.yes { color: var(--ok); border-color: var(--ok-line);
                 background: var(--ok-soft); }
button.act.yes:hover { border-color: var(--ok); color: var(--ok); }
button.act.no { color: var(--bad); border-color: var(--bad-line);
                background: var(--bad-soft); }
button.act.no:hover { border-color: var(--bad); color: var(--bad); }
button.act:disabled { opacity: .5; cursor: default; }

.iconbtn { display: inline-flex; align-items: center; justify-content: center;
           width: 32px; height: 32px; border-radius: var(--r-md); cursor: pointer;
           background: var(--panel); color: var(--dim);
           border: 1px solid var(--line2); box-shadow: var(--sh-xs); }
.iconbtn:hover { color: var(--accent); border-color: var(--accent); }
.iconbtn.ok { color: var(--ok); border-color: var(--ok-line); }
.iconbtn.ok:hover { border-color: var(--ok); }
/* The destructive one, matching button.act.no — an icon-only delete has no word
   left to warn with, so the colour is the whole warning. */
.iconbtn.no { color: var(--bad); border-color: var(--bad-line);
              background: var(--bad-soft); }
.iconbtn.no:hover { color: var(--bad); border-color: var(--bad); }

/* Published is a state you set, not a word you read. A switch shows which way
   it is set and which way it can go in the same glance, which is what a column
   of 219 of them needs. */
button.switch { position: relative; flex: none; width: 36px; height: 20px;
                padding: 0; border: 1px solid var(--line2); border-radius: 999px;
                background: var(--inset); cursor: pointer; vertical-align: middle;
                transition: background .15s, border-color .15s; }
button.switch::after { content: ""; position: absolute; top: 2px;
                inset-inline-start: 2px; width: 14px; height: 14px;
                border-radius: 50%; background: var(--faint);
                box-shadow: var(--sh-xs);
                transition: transform .15s, background .15s; }
button.switch[aria-checked="true"] { background: var(--accent);
                border-color: var(--accent); }
button.switch[aria-checked="true"]::after { background: #fff;
                transform: translateX(16px); }
:root:dir(rtl) button.switch[aria-checked="true"]::after {
                transform: translateX(-16px); }
button.switch:hover { border-color: var(--accent); }
/* In flight. Not :disabled with opacity — a switch that fades on click reads as
   "rejected" for the length of the round trip. */
button.switch[aria-busy="true"] { cursor: progress; }
.switchbox { display: inline-flex; align-items: center; gap: 7px; }

.toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
           margin-bottom: 12px; }
.toolbar select { max-width: 230px; }
/* Page-level controls sit at the far end of the header row, away from the table
   they act on — margin-inline-start:auto is what pins them there in both LTR
   and RTL. */
.actions { margin-inline-start: auto; display: flex; align-items: center; gap: 6px; }

/* The pane strip on a model detail page. It was a row of `.act` buttons with
   the current one tinted green, which said "accepted" rather than "here". */
.ptabs { display: flex; align-items: center; gap: 22px; height: 44px;
         padding: 0 16px; margin-bottom: 14px; overflow-x: auto;
         background: var(--panel); border: 1px solid var(--line);
         border-radius: var(--r-lg); box-shadow: var(--sh-xs); }
.ptabs button { position: relative; height: 100%; display: flex;
                align-items: center; gap: 7px; cursor: pointer;
                background: none; border: 0; color: var(--dim);
                font: 500 13.5px var(--ui); white-space: nowrap; }
.ptabs button:hover { color: var(--text); }
.ptabs button.on { color: var(--accent); font-weight: 600; }
.ptabs button.on::after { content: ""; position: absolute;
                inset-inline: 0; bottom: 4px; height: 2px;
                border-radius: 2px; background: var(--accent); }
.ptabs .ct { padding: 2px 6px; border-radius: 999px;
             border: 1px solid var(--line2); color: var(--faint);
             font: 500 10.5px/1.4 var(--mono);
             font-variant-numeric: tabular-nums; }
.ptabs button.on .ct { background: var(--accent); border-color: var(--accent);
             color: #fff; }

/* The detail header. Everything about the model that is true regardless of
   which pane is open: what it is called, what it is called in the database,
   how big it is, what identifies a row, and what it is measured in. That last
   pair used to be findable only inside the overview pane. */
.dhead { display: flex; align-items: center; gap: 13px; flex-wrap: wrap;
         padding: 14px 16px; margin-bottom: 14px;
         background: var(--panel); border: 1px solid var(--line);
         border-radius: var(--r-lg); box-shadow: var(--sh-xs); }
.dhead .dic { flex: none; width: 40px; height: 40px; border-radius: 10px;
         display: flex; align-items: center; justify-content: center;
         background: var(--accent-soft); border: 1px solid var(--accent);
         color: var(--accent); }
.dhead .dic svg { width: 20px; height: 20px; fill: none; stroke: currentColor;
         stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.dhead h2 { font: 600 17.5px/1.3 var(--mono); letter-spacing: -.01em;
         word-break: break-all; }
.dhead .meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
         margin-top: 3px; font-size: 12px; color: var(--faint); }
.dhead .meta em { font-style: normal; font-family: var(--mono); color: var(--dim); }
.dhead .actions { gap: 10px; }

/* ========================================================================
 * 9. Tags, meters, help
 * ==================================================================== */
/* Tinted, where they used to be a coloured word inside a hairline box. The
   tint is what carries them in dark mode: a bare `--bad` word on the dark
   ground sat under 4.5:1, and this is 11px text. */
.tag { display: inline-flex; align-items: center; gap: 4px;
       padding: 3px 7px; border-radius: var(--r-sm);
       font: 500 11.5px/1.35 var(--ui);
       border: 1px solid var(--line2); background: var(--inset);
       color: var(--dim); margin-inline-end: 3px; white-space: nowrap; }
.tag.pii, .tag.contact { color: var(--bad); border-color: var(--bad-line);
       background: var(--bad-soft); }
.tag.financial, .tag.quasi_identifier { color: var(--warn);
       border-color: var(--warn-line); background: var(--warn-soft); }
.tag.ok { color: var(--ok); border-color: var(--ok-line);
       background: var(--ok-soft); }
/* A glossary term rendered beside its target. A button, because clicking one
   opens it on the Glossary tab — so it needs the type and background reset
   that `.act` gets and a bare `.tag` never needed. Not font:inherit: a
   column-level term sits inside a .mono cell and would come out in the
   identifier face, and a glossary term is a word somebody says. */
button.tag { cursor: pointer; font-family: var(--ui); font-size: 11.5px;
       color: var(--accent); border-color: var(--accent);
       background: var(--accent-soft); }
button.tag:hover { color: var(--accent-hover); border-color: var(--accent-hover); }

/* The value meter. 6px rather than 4px, and the track keeps the shape it
   always had — `.bar > i` with a width — because three call sites already draw
   it that way and pair it with their own label: the column coverage cell, the
   value distribution rows and the join evidence. */
.bar { display: block; height: 6px; min-width: 44px; border-radius: 999px;
       background: var(--inset); border: 1px solid var(--line);
       overflow: hidden; }
.bar > i { display: block; height: 100%; background: var(--accent); }
.bar > i.w { background: var(--warn); }
.bar > i.b { background: var(--bad); }
/* A meter that carries its own number. The two answer different questions —
   the track compares this row against the one under it, the number is what
   gets quoted — so both are shown rather than one standing in for the other. */
.meter { display: flex; align-items: center; gap: 8px; min-width: 80px; }
.meter > .bar { flex: 1; }
.meter > b { flex: none; min-width: 34px; text-align: end;
           font: 500 11.5px/1 var(--mono); color: var(--dim);
           font-variant-numeric: tabular-nums; }

.help { display: inline-flex; align-items: center; justify-content: center;
        width: 14px; height: 14px; margin-inline-start: 5px; border-radius: 50%;
        border: 1px solid var(--line2); color: var(--faint); font-size: 9px;
        line-height: 1; cursor: help; vertical-align: middle; }
.help:hover { color: var(--accent); border-color: var(--accent); }

.pills { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
main a.pill { display: inline-block; padding: 2px 8px; border-radius: 999px;
              border: 1px solid var(--bad-line); background: var(--bad-soft);
              color: var(--bad); font: 500 11.5px var(--mono);
              text-decoration: none; }
main a.pill:hover { border-color: var(--bad); text-decoration: none; }

/* A note that explains the page rather than a row of it. One per view at most:
   the relationship queue's "the source database declares no foreign keys" is
   the premise the whole tab rests on. */
.note { display: flex; gap: 11px; align-items: flex-start; padding: 12px 14px;
        margin-bottom: 14px; font-size: 13px; line-height: 1.55;
        background: var(--accent-soft); border: 1px solid var(--accent);
        border-radius: var(--r-md); }
.note .ic { flex: none; display: flex; margin-top: 2px; }
.note svg { width: 17px; height: 17px; fill: none; stroke: var(--accent);
        stroke-width: 1.8; stroke-linecap: round; }

/* ========================================================================
 * 10. Popovers, dialogs, toast
 * ==================================================================== */
.pop { position: fixed; z-index: 30; min-width: 240px; max-width: 320px;
       background: var(--panel); border: 1px solid var(--line);
       border-radius: var(--r-lg); padding: 11px 12px;
       box-shadow: var(--sh-lg); }
.pop.wide { min-width: 340px; max-width: 450px; }
.pop .list { max-height: 220px; overflow: auto; margin: 7px 0;
             border: 1px solid var(--line); border-radius: var(--r-sm);
             padding: 5px 7px; background: var(--bg); }
.pop label { display: flex; align-items: center; gap: 7px; padding: 3px 4px;
             border-radius: 4px; cursor: pointer; font-size: 12px; }
.pop label:hover { background: var(--inset); }
.pop .toolbar { margin-bottom: 0; }

/* The same scrolling list of checkbox rows as .pop .list, outside a popover:
   the Users tab picks a principal's roles from one. */
.checklist { max-height: 160px; overflow: auto; border: 1px solid var(--line);
             border-radius: var(--r-sm); padding: 5px 7px; background: var(--bg); }
.checklist label { display: flex; align-items: center; gap: 7px; padding: 3px 4px;
                   border-radius: 4px; font-size: 12px; cursor: pointer; }
.checklist label:hover { background: var(--inset); }
.checklist input { flex: none; box-shadow: none; }

.modal { position: fixed; inset: 0; z-index: 60; background: rgba(10,13,18,.6);
         display: flex; align-items: center; justify-content: center; }
.modal .panel { display: flex; flex-direction: column; overflow: hidden;
                width: min(940px, 94vw); height: min(660px, 88vh);
                background: var(--panel); border: 1px solid var(--line);
                border-radius: var(--r-lg); box-shadow: var(--sh-lg); }
.modal .head { display: flex; align-items: center; gap: 10px;
               padding: 12px 14px; border-bottom: 1px solid var(--line); }
.modal .head h3 { font-size: 15px; font-weight: 600; }
/* The subtitle yields rather than pushing the close button onto a second row. */
.modal .head .sub { min-width: 0; overflow: hidden; text-overflow: ellipsis;
                    white-space: nowrap; }
/* A form sizes to its fields; only the map wants a fixed viewport-sized box. */
.modal .panel.form { height: auto; max-height: 88vh; width: min(820px, 94vw); }
.modal .body { padding: 14px; overflow: auto; }
.modal .body input:not([type=checkbox]),
.modal .body select, .modal .body textarea { width: 100%; }

#toast { position: fixed; inset-inline-end: 16px; inset-block-start: 74px;
         z-index: 90; display: none; max-width: 420px;
         gap: 10px; align-items: flex-start; padding: 11px 13px;
         background: var(--panel); border: 1px solid var(--ok-line);
         border-inline-start: 3px solid var(--ok);
         border-radius: var(--r-md); box-shadow: var(--sh-lg);
         font-size: 13px; }
#toast.on { display: flex; }
#toast.bad { border-color: var(--bad-line); border-inline-start-color: var(--bad); }
#toast .ic { flex: none; display: flex; margin-top: 1px; }
#toast .ic svg { width: 17px; height: 17px; fill: none; stroke: var(--ok);
             stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
#toast.bad .ic svg { stroke: var(--bad); }
#toast .msg { min-width: 0; word-break: break-word; }

/* ========================================================================
 * 11. Sign-in
 *
 * Split, following OpenMetadata's CarouselLayout: a media panel that says what
 * this console is for, and a form panel. Still an opaque cover rather than a
 * dialog — behind it is a console whose panes are all empty until a request
 * succeeds, and a half-rendered review queue under a login box invites clicking
 * at something that cannot answer.
 *
 * The left panel is CSS, not an image: OpenMetadata's own carousel art is
 * Collate's, and one more file to serve is one more static route.
 * ==================================================================== */
#gate { position: fixed; inset: 0; z-index: 200; display: none;
        background: var(--panel); }
#gate.on { display: flex; }
/* The brand sits at the top corner and the copy is centred, so it lands at the
   same height as the form opposite it. space-between put the copy on the floor
   and left a third of the panel empty — which was the shape the figure row used
   to fill before it came out. */
#gate .say { flex: 0 0 52%; position: relative; overflow: hidden;
             display: flex; flex-direction: column; justify-content: center;
             padding: 44px 46px; color: #fff;
             background: linear-gradient(155deg, #0b2b5e 0%, #123f86 48%, #1570ef 100%); }
#gate .say .brand { position: absolute; top: 44px; inset-inline-start: 46px; }
/* Below 1000px the media panel goes and the form takes the window — the same
   breakpoint OpenMetadata uses, and the width below which the two columns each
   stop being readable. */
@media (max-width: 1000px) { #gate .say { display: none; } }
#gate .say .grid { position: absolute; inset: 0; opacity: .16;
  background-image: linear-gradient(to right, rgba(255,255,255,.5) 1px, transparent 1px),
                    linear-gradient(to bottom, rgba(255,255,255,.5) 1px, transparent 1px);
  background-size: 46px 46px;
  mask-image: radial-gradient(120% 90% at 78% 8%, #000 12%, transparent 72%); }
#gate .say > * { position: relative; }
#gate .say .brand { display: flex; align-items: center; gap: 11px; z-index: 1; }
#gate .say .brand .mark { background: rgba(255,255,255,.16);
             border: 1px solid rgba(255,255,255,.3); }
#gate .say .brand b { font-size: 15px; font-weight: 600; }
#gate .say h2 { font-size: 30px; font-weight: 600; letter-spacing: -.025em;
             line-height: 1.3; max-width: 16ch; margin-bottom: 12px; }
#gate .say p { margin: 0; font-size: 14.5px; line-height: 1.65;
             color: rgba(255,255,255,.78); max-width: 42ch; }
#gate .say ul { list-style: none; margin: 28px 0 0; padding: 0;
             display: flex; flex-direction: column; gap: 13px; }
#gate .say li { display: flex; gap: 11px; align-items: flex-start;
             font-size: 13.5px; color: rgba(255,255,255,.92); }
#gate .say li svg { flex: none; width: 18px; height: 18px; margin-top: 2px;
             fill: none; stroke: #84caff; stroke-width: 2;
             stroke-linecap: round; stroke-linejoin: round; }
#gate .say li em { display: block; font-style: normal; font-size: 12.5px;
             color: rgba(255,255,255,.6); margin-top: 2px; }
#gate form { flex: 1; min-width: 0; display: flex; align-items: center;
             justify-content: center; padding: 40px 24px; overflow-y: auto; }
#gate .box { width: 100%; max-width: 380px; }
#gate .box .mark { width: 42px; height: 42px; border-radius: 11px;
             margin-bottom: 20px; }
#gate .box .mark svg { width: 22px; height: 22px; }
#gate h2.title { font-size: 22px; font-weight: 600; letter-spacing: -.02em;
             margin-bottom: 7px; }
#gate p.hint { margin: 0 0 24px; font-size: 13.5px; line-height: 1.6;
             color: var(--dim); }
#gate p.hint code { font-family: var(--mono); font-size: 12.5px;
             background: var(--inset); border: 1px solid var(--line);
             border-radius: 4px; padding: 1px 5px; }
#gate label { display: flex; font-size: 13px; font-weight: 500;
             color: var(--dim); margin: 0 0 6px; }
#gate .row { margin-bottom: 15px; }
#gate input { width: 100%; padding: 10px 12px; font-size: 14px; }
#gate button[type="submit"] { width: 100%; margin-top: 8px; padding: 11px;
             font: 600 14px var(--ui); cursor: pointer;
             border-radius: var(--r-md); border: 1px solid var(--accent);
             background: var(--accent); color: #fff; box-shadow: var(--sh-xs); }
#gate button[type="submit"]:hover { background: var(--accent-hover);
             border-color: var(--accent-hover); }
#gate button[type="submit"]:disabled { opacity: .5; cursor: default; }
/* An alert block rather than a red line. The 403 case — authenticated but
   without the steward role — is not something retyping a password fixes, and
   the message has to have room to say so. */
#gate .err { display: flex; gap: 9px; align-items: flex-start;
             margin-bottom: 15px; padding: 10px 12px; font-size: 12.5px;
             line-height: 1.55; color: var(--bad);
             background: var(--bad-soft); border: 1px solid var(--bad-line);
             border-radius: var(--r-md); }
#gate .err[hidden] { display: none; }
#gate .err svg { flex: none; width: 16px; height: 16px; margin-top: 1px;
             fill: none; stroke: currentColor; stroke-width: 2;
             stroke-linecap: round; }
#gate .foot { display: flex; align-items: center; gap: 7px; margin-top: 18px;
             font-size: 12px; color: var(--faint); }
#gate .foot svg { flex: none; width: 14px; height: 14px; fill: none;
             stroke: currentColor; stroke-width: 1.8; }

/* ========================================================================
 * 12. Join graph
 *
 * Colour carries the decision and nothing else does, so the three states have
 * to be distinguishable without reading a label — which is also why each one
 * has its own dash pattern. A 1000x620 viewBox at width:100% would be 970px
 * tall in a wide window and push its own legend off the screen; fixed height,
 * letterboxed by preserveAspectRatio, so the whole graph is on screen at once,
 * which is the only reason to draw it rather than read the table.
 * ==================================================================== */
.graph { width: 100%; height: 620px; max-height: 72vh; display: block;
         background: var(--panel); border: 1px solid var(--line);
         border-radius: var(--r-lg); touch-action: none; }
.graph .edge { fill: none; stroke-width: 1.6; }
.graph .edge.accepted { stroke: var(--accent); }
.graph .edge.pending  { stroke: var(--warn); stroke-dasharray: 5 4; }
.graph .edge.rejected { stroke: var(--faint); stroke-dasharray: 2 5; opacity: .7; }
.graph .edge.hit { stroke: transparent; stroke-width: 14; cursor: pointer; }
.graph .edge.hit:hover + .edge { stroke-width: 3.2; }
.graph .fan { fill: var(--bad); font-size: 10px; font-weight: 600; }
.graph .node { cursor: pointer; }
.graph .node circle { fill: var(--panel); stroke: var(--accent); stroke-width: 1.6; }
.graph .node.unpublished circle { stroke: var(--faint); stroke-dasharray: 3 3; }
.graph .node:hover circle { fill: var(--accent-soft); }
.graph .node text { fill: var(--text); font-family: var(--mono); font-size: 10px;
                    pointer-events: none; }
.graph .node.unpublished text { fill: var(--faint); }
.isolated { margin-top: 12px; }
.legend { display: flex; gap: 14px; flex-wrap: wrap; align-items: center;
          margin-top: 10px; }
.legend span { display: inline-flex; align-items: center; gap: 6px;
               color: var(--dim); font-size: 12px; }
.legend i { display: inline-block; width: 22px; height: 0;
            border-top: 2px solid currentColor; }

/* ========================================================================
 * 13. Geometry, distributions, the SQL box
 * ==================================================================== */
.mapbody { flex: 1; min-height: 0; background: var(--bg); }
/* Leaflet's own popups and controls inherit the page font otherwise. */
.mapbody .leaflet-container { font: 12px/1.4 var(--ui); height: 100%; }
/* The value pane keeps a fixed share of the dialog: the map is what is being
   looked at, the text is what gets copied out of it. */
.fmt { border-top: 1px solid var(--line); padding: 9px 12px 12px;
       display: flex; flex-direction: column; gap: 6px;
       height: 34%; min-height: 0; }
.fmt .toolbar { margin: 0; }
.fmtbody { margin: 0; flex: 1; min-height: 0; overflow: auto;
           white-space: pre-wrap; word-break: break-all; font-size: 11px;
           color: var(--text); background: var(--bg);
           border: 1px solid var(--line); border-radius: var(--r-sm);
           padding: 8px 10px; }
.geowrap { margin: 8px 0; border: 1px solid var(--line);
           border-radius: var(--r-sm); background: var(--bg); }
svg.geo { display: block; }
svg.geo path { fill: color-mix(in srgb, var(--accent) 22%, transparent);
               stroke: var(--accent); stroke-width: .8; stroke-linejoin: round; }
svg.geo path.l { fill: none; stroke-width: 1.1; }
svg.geo path.s { fill: none; stroke: var(--faint); stroke-width: 1; }
svg.geo circle { fill: var(--accent); fill-opacity: .75; }
.geobtn { background: none; border: 0; padding: 0; cursor: pointer; font: inherit;
          color: var(--accent); vertical-align: middle; white-space: nowrap; }
.geobtn:hover { color: var(--text); }

/* The enum label editor sits inside a cell of the column table, so it must not
   inherit that table's row borders or padding. */
.funnel { background: none; border: 0; padding: 0 0 0 5px; cursor: pointer;
          color: var(--line2); vertical-align: middle; }
.funnel:hover { color: var(--accent); }
.funnel.on { color: var(--accent); }
.dist { width: 100%; font-size: 12px; }
.dist td { border: 0; padding: 3px 6px 3px 0; vertical-align: middle; }
.dist tr.pick { cursor: pointer; }
.dist tr.pick:hover { background: var(--inset); }
.enums td { border: 0; padding: 2px 4px 2px 0; vertical-align: middle; }
.enums input { width: 150px; padding: 3px 7px; }

/* Run/stop floats in the corner of the SQL box. The inset clears the native
   resize grip, and the textarea keeps a gutter so a long line does not run
   underneath it. */
.sqlbox { position: relative; }
.sqlbox textarea { padding-inline-end: 56px; resize: vertical; }
.fab { position: absolute; inset-inline-end: 14px; inset-block-end: 16px;
       width: 34px; height: 34px; border-radius: 50%; display: flex;
       align-items: center; justify-content: center; cursor: pointer;
       background: var(--panel); color: var(--ok);
       border: 1px solid var(--line2); box-shadow: var(--sh-sm); }
.fab:hover { border-color: currentColor; }
.fab.stop { color: var(--bad); }

/* ========================================================================
 * 14. Layout odds and ends
 * ==================================================================== */
.grid2 { display: grid; gap: 10px; grid-template-columns: 1fr 1fr; }
.grid4 { display: grid; gap: 10px; grid-template-columns: repeat(4, 1fr); }
@media (max-width: 900px) { .grid4 { grid-template-columns: 1fr 1fr; } }
details.row summary { cursor: pointer; }
.ixprog { font-variant-numeric: tabular-nums; }

/* Narrow window: the sidebar collapses to icons and stays. It is 72px, it is
   how you leave the page you are on, and a drawer that has to be opened before
   you can navigate is worse on the one screen size where navigation is hard. */
@media (max-width: 860px) {
  :root { --side-w: 72px; }
  .brandrow { justify-content: center; padding: 0; }
  .brandrow b, .brandrow .mark { display: none; }
  #nav .navgrp { height: 13px; padding: 6px 0 0; overflow: hidden;
                 color: transparent; }
  #nav button { justify-content: center; padding: 10px 0; }
  #nav button .label, #nav button .badge { display: none; }
  main { padding: 14px 12px 56px; }
  #who-btn .nm { display: none; }
}
