/*
 * portal.css — Bulma theme + compatibility bridge for the Outtask portal.
 *
 * Loaded after bulma.min.css. Three jobs:
 *   1. Retheme Bulma to the portal's visual language: warm cream page, white panels,
 *      flat structure separated by borders rather than shadows, DM Serif Display for
 *      identity/page titles, DM Sans for UI, JetBrains Mono for data (IDs, timestamps,
 *      figures). Ported from the design Gianni prototyped in Replit — the React SPA it
 *      was built for is deliberately not adopted; only the look crossed over.
 *   2. Re-express the portal's legacy helper classes (.card / .badge-* / .btn-* /
 *      bare <table> / .empty / .meta / .actions-group) on Bulma's own CSS variables,
 *      so every existing template inherits the palette + theme with no markup churn.
 *   3. Hold the component CSS that audit.html and help.html used to declare in inline
 *      <style> blocks, so those two pages can come under a CSP (see NOTE at the end).
 *
 * THE BRANDING CONTRACT (app/branding.py, /brand/{tenant}.css, loaded AFTER this file):
 * a tenant supplies primary + accent + pos/neg + font, emitted as the Bulma -h/-s/-l
 * trio, the WCAG-derived *-invert-l, and --ot-color-*. So every brand-carrying surface
 * here MUST resolve through var(--bulma-primary…) / var(--bulma-link…), never a literal.
 * Two consequences to respect when editing:
 *   - NO `!important` in this file. An !important declaration beats a normal one that
 *     resolves a variable, whatever the load order — one careless !important on a
 *     brand-carrying property silently switches that tenant back to house colours.
 *     Bulma 1.x is variable-driven throughout, so overriding its --bulma-*-* hooks is
 *     always enough. (This is exactly why the prototype's theme.css could not be taken
 *     as-is: it carried 253 !important declarations and its own private --t-* palette.)
 *   - Text ON a brand fill must use var(--bulma-primary-invert), never a literal #fff.
 *     branding.pick_invert_l chooses white vs near-black by contrast; hardcoding white
 *     produces unreadable text on a pale brand. (The prototype hardcoded #fff on its
 *     amber, which measures 3.19:1 — below the 4.5:1 floor. The house amber below is
 *     the darker #B45309 precisely so white-on-amber passes at 5.0:1.)
 *
 * The NEUTRAL SHELL (page/surface/border/text/muted) is intentionally house-owned, not
 * brandable: a tenant sets accent colours, not the whole chrome. Bare --ot-* names below
 * are portal-only; the report and the dashboards use the --ot-color-* namespace from
 * app/reports/styles/tokens.css and never load this file.
 *
 * Hex literals are fine here (the "hex lives only in tokens.css" guard covers
 * app/reports, not app/static); tests/test_style_guard.py pins the rules above instead.
 */

/* ───────────────────────────── 0. Self-hosted fonts ───────────────────────────── */
/* Self-hosted so the portal makes no CDN request and can run under a strict CSP. */

/* DM Sans — UI text (one variable-weight file covers 400–600) */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('/static/fonts/dm-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('/static/fonts/dm-sans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
                 U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* DM Serif Display — identity + page titles only (wordmark, h1.title, login card) */
@font-face {
  font-family: 'DM Serif Display';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/dm-serif-display-normal-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'DM Serif Display';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/dm-serif-display-normal-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
                 U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* JetBrains Mono — data: IDs, timestamps, figures, code */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
                 U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
                 U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ─────────────────────────────── 1. Tokens ─────────────────────────────── */
:root {
  /* The house default brand, used when a tenant has set no branding of their own.
   * Amber hsl(26 90% 37%) = #B45309. The lightness is deliberate: at 37% the WCAG-derived
   * --bulma-primary-invert-l resolves to 100%, so white text on an amber fill measures
   * 5.0:1, and the same amber as TEXT on the cream page measures 4.8:1 — one colour that
   * works as both a fill and a label. The brighter #D97706 fails white text at 3.19:1.
   * A tenant's own trio replaces all four values from /brand/{tenant}.css. */
  --bulma-primary-h: 26deg;
  --bulma-primary-s: 90%;
  --bulma-primary-l: 37%;
  --bulma-primary-invert-l: 100%;
  /* `link` is the role app/branding.py maps a tenant's ACCENT onto; default it to the
   * same amber so an untenanted portal reads as one colour. */
  --bulma-link-h: 26deg;
  --bulma-link-s: 90%;
  --bulma-link-l: 37%;
  --bulma-link-invert-l: 100%;

  /* type + shape */
  --bulma-family-primary: 'DM Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI',
                          Roboto, Arial, sans-serif;
  /* Bulma's own root mono family, so its <code>/<pre> follow without a rule of their own */
  --bulma-family-code: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --ot-family-display: 'DM Serif Display', Georgia, 'Times New Roman', serif;
  --bulma-radius-small: 4px;
  --bulma-radius: 6px;
  --bulma-radius-large: 8px;

  /* Neutral shell — warm cream page, white panels, warm-gray rules. House-owned. */
  --ot-page:          #FAF9F7;
  --ot-surface:       #FFFFFF;
  --ot-surface-2:     #F5F3EF;   /* table header, nav hover, input, tinted row */
  --ot-surface-3:     #EDE9E3;   /* stronger hover, tag background */
  --ot-row-alt:       #FCFBF9;   /* zebra stripe — deliberately fainter than surface-2 */
  --ot-border:        #E8E4DD;
  --ot-border-strong: #D9D4CC;
  --ot-text:          #1C1917;   /* warm near-black */
  --ot-text-2:        #57534E;
  --ot-muted:         #A8A29E;

  --bulma-body-background-color: var(--ot-page);
  --bulma-border: var(--ot-border);

  /* Bulma resolves body/heading ink from these; point them at the warm neutrals so every
   * stock component follows without needing its own rule. */
  --bulma-text-strong: var(--ot-text);
  --bulma-text: var(--ot-text-2);
  --bulma-text-weak: var(--ot-muted);

  /* ── Sidebar, as its own token set ──────────────────────────────────────────
   * The chrome is themed through these rather than reading the --ot-surface and --bulma
   * roles directly, so the internal shell below can recolour the whole sidebar without
   * touching a single component rule — and without an !important, which would cancel a
   * tenant's brand on everything it touched.
   *
   * The active-item defaults resolve the BRAND: that is the one place a tenant's colour
   * lands in the chrome, and it must stay that way on a tenant page. `-on-scheme`, not
   * `-primary`, because this is brand text on a light surface and a tenant may pick a pale
   * colour — branding.py clamps --bulma-primary-on-scheme-l to <=40% so it stays legible.
   */
  --ot-sidebar-bg:        var(--ot-surface-3);
  --ot-sidebar-border:    var(--ot-border);
  --ot-sidebar-text:      var(--ot-text);
  --ot-sidebar-text-2:    var(--ot-text-2);
  --ot-sidebar-muted:     var(--ot-muted);
  /* An ink wash rather than a surface step, so hover works whatever the sidebar's shade */
  --ot-sidebar-hover:     hsla(30, 10%, 12%, 0.05);
  --ot-sidebar-active-fg: var(--bulma-primary-on-scheme);
  --ot-sidebar-active-bd: var(--bulma-primary);
  --ot-sidebar-active-bg: hsla(var(--bulma-primary-h), var(--bulma-primary-s),
                               var(--bulma-primary-l), 0.07);
  --ot-sidebar-eyebrow:   transparent;   /* the internal shell reveals its label with this */
}

/* ── The internal shell ───────────────────────────────────────────────────────
 * Outtask's own cross-tenant pages (/, /audit, /admin/*) get a cool, dark chrome instead of
 * the warm client one — ported from the prototype, which kept a separate AdminLayout for
 * exactly this. It is not decoration: these pages act on every tenant at once, and chrome
 * identical to a client's own portal is how someone edits the wrong thing.
 *
 * A tenant page never carries the attribute (deps._ctx sets `shell` only when there is no
 * tenant in the URL), so nothing here can reach one. And because it redefines only the
 * tokens above plus the brand roles, no component rule knows this shell exists.
 */
.ot-shell[data-shell="admin"] {
  --ot-sidebar-bg:        #171E29;
  --ot-sidebar-border:    hsla(0, 0%, 100%, 0.09);
  --ot-sidebar-text:      #E7EAEE;
  --ot-sidebar-text-2:    #A7B0C0;
  --ot-sidebar-muted:     #8A93A6;
  --ot-sidebar-hover:     hsla(0, 0%, 100%, 0.06);
  --ot-sidebar-active-fg: #E7EAEE;
  --ot-sidebar-active-bd: #6C90F0;
  --ot-sidebar-active-bg: hsla(222, 74%, 56%, 0.20);
  --ot-sidebar-eyebrow:   #8FAAF5;

  /* The accent too, or an amber CTA would sit on a cool page and read as a stray control
   * from the client portal. Blue at l=48% — hsl(222 74% 48%) = #2056D5 — chosen the same way
   * the house amber's l=37% was: white on that fill measures 6.26:1, and the same blue AS
   * TEXT on this shell's #F3F4F6 page measures 5.69:1, so one colour serves as both.
   * (The prototype's #3E6AE1 is hsl(222 74% 56%): 4.65:1 as a fill, but only 4.22:1 as
   * text, and text is the use that fails.) `branding.pick_invert_l('#2056D5')` returns
   * 100%, so the invert pinned here is the value that module would derive, not a guess.
   * Safe to state literally: an ops page has no tenant, so no brand is being overridden. */
  --bulma-primary-h: 222deg;
  --bulma-primary-s: 74%;
  --bulma-primary-l: 48%;
  --bulma-primary-invert-l: 100%;
  --bulma-link-h: 222deg;
  --bulma-link-s: 74%;
  --bulma-link-l: 48%;
  --bulma-link-invert-l: 100%;

  /* A cooler page than the client portal's cream, so the two do not read as one app. */
  --ot-page:              #F3F4F6;
  --ot-surface-2:         #EEF0F3;
  --ot-surface-3:         #E4E7EC;
  --ot-row-alt:           #F8F9FA;
  --ot-border:            #DFE3E8;
  --ot-border-strong:     #C9CFD8;
}

body {
  background-color: var(--ot-page);
  color: var(--ot-text-2);
  -webkit-font-smoothing: antialiased;
}

.section { background: transparent; padding-top: 1.5rem; }

/*
 * Force light scheme. Bulma flips to dark via `@media (prefers-color-scheme:dark){:root{…}}`,
 * whose `:root` ties `[data-theme=light]` on specificity and wins on source order — so the
 * <html data-theme="light"> flag alone can't hold light on a dark-mode OS. Re-asserting Bulma's
 * light scheme values under the higher-specificity `:root[data-theme=light]` beats the media
 * block. The portal stays light to match the (light-only) report. (Values = Bulma 1.0.4 light
 * defaults for every var its dark block overrides; regenerate if Bulma is upgraded.)
 */
:root[data-theme="light"] {
  --bulma-white-on-scheme-l: 35%;
  --bulma-white-on-scheme: hsla(var(--bulma-white-h),var(--bulma-white-s),var(--bulma-white-on-scheme-l),1);
  --bulma-black-on-scheme-l: 4%;
  --bulma-black-on-scheme: hsla(var(--bulma-black-h),var(--bulma-black-s),var(--bulma-black-on-scheme-l),1);
  --bulma-light-on-scheme-l: 36%;
  --bulma-light-on-scheme: hsla(var(--bulma-light-h),var(--bulma-light-s),var(--bulma-light-on-scheme-l),1);
  --bulma-dark-on-scheme-l: 21%;
  --bulma-dark-on-scheme: hsla(var(--bulma-dark-h),var(--bulma-dark-s),var(--bulma-dark-on-scheme-l),1);
  --bulma-text-on-scheme-l: 29%;
  --bulma-text-on-scheme: hsla(var(--bulma-text-h),var(--bulma-text-s),var(--bulma-text-on-scheme-l),1);
  --bulma-primary-on-scheme-l: 21%;
  --bulma-primary-on-scheme: hsla(var(--bulma-primary-h),var(--bulma-primary-s),var(--bulma-primary-on-scheme-l),1);
  --bulma-link-on-scheme-l: 58%;
  --bulma-link-on-scheme: hsla(var(--bulma-link-h),var(--bulma-link-s),var(--bulma-link-on-scheme-l),1);
  --bulma-info-on-scheme-l: 25%;
  --bulma-info-on-scheme: hsla(var(--bulma-info-h),var(--bulma-info-s),var(--bulma-info-on-scheme-l),1);
  --bulma-success-on-scheme-l: 23%;
  --bulma-success-on-scheme: hsla(var(--bulma-success-h),var(--bulma-success-s),var(--bulma-success-on-scheme-l),1);
  --bulma-warning-on-scheme-l: 23%;
  --bulma-warning-on-scheme: hsla(var(--bulma-warning-h),var(--bulma-warning-s),var(--bulma-warning-on-scheme-l),1);
  --bulma-danger-on-scheme-l: 40%;
  --bulma-danger-on-scheme: hsla(var(--bulma-danger-h),var(--bulma-danger-s),var(--bulma-danger-on-scheme-l),1);
  --bulma-scheme-brightness: light;
  --bulma-scheme-main-l: 100%;
  --bulma-scheme-main-bis-l: 98%;
  --bulma-scheme-main-ter-l: 96%;
  --bulma-soft-l: 90%;
  --bulma-bold-l: 20%;
  --bulma-soft-invert-l: 20%;
  --bulma-bold-invert-l: 90%;
  --bulma-background-l: 96%;
  --bulma-border-weak-l: 93%;
  --bulma-border-l: 86%;
  --bulma-text-weak-l: 48%;
  --bulma-text-l: 29%;
  --bulma-text-strong-l: 21%;
  --bulma-text-title-l: 14%;
  --bulma-hover-background-l-delta: -5%;
  --bulma-active-background-l-delta: -10%;
  --bulma-hover-border-l-delta: -10%;
  --bulma-active-border-l-delta: -20%;
  --bulma-hover-color-l-delta: -5%;
  --bulma-active-color-l-delta: -10%;
  --bulma-shadow-h: 221deg;
  --bulma-shadow-s: 14%;
  --bulma-shadow-l: 4%;
}

/* ────────────────────────── 2. Shell + sidebar ───────────────────────── */
/*
 * A 240px sidebar, not a brand-coloured top bar: the brand arrives as the logo, the
 * wordmark and the active item rather than as a slab, which is what keeps an arbitrary
 * tenant colour safe (a dark brand fill is what forced the invert-l work). Ported from
 * the prototype's React layout; base.html renders it from nav.nav_items().
 */
.ot-shell { display: flex; min-height: 100vh; }

.ot-sidebar {
  width: 240px; flex-shrink: 0;
  background: var(--ot-sidebar-bg);
  border-right: 1px solid var(--ot-sidebar-border);
  display: flex; flex-direction: column;
  position: sticky; top: 0; align-self: flex-start; height: 100vh;
}

.ot-sidebar-brand {
  display: flex; align-items: center; justify-content: space-between;
  padding: 1.5rem 1.25rem 0.25rem;
}
.ot-wordmark {
  display: flex; align-items: center;
  color: var(--ot-sidebar-text); text-decoration: none;
  /* 1.5rem = the prototype's 24px; at 1.35rem it read a size small beside it */
  font-family: var(--ot-family-display); font-size: 1.5rem;
  letter-spacing: -0.02em; line-height: 1.2; min-width: 0;
}
.ot-wordmark:hover { color: var(--ot-sidebar-text); }

/* The internal shell labels itself under the wordmark. Transparent on a tenant page, so the
 * element is inert there rather than conditional in the template. */
.ot-shell-label {
  font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.12em;
  font-weight: 600; color: var(--ot-sidebar-eyebrow); margin-top: 1px;
}
.ot-wordmark span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ot-nav { padding: 1.25rem 0.75rem; flex: 1; overflow-y: auto; }
.ot-nav-section {
  font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ot-sidebar-muted); padding: 0.75rem 0.75rem 0.25rem; margin-top: 0.5rem;
}
.ot-nav-item {
  display: flex; align-items: center; gap: 0.625rem;
  padding: 0.5rem 0.75rem; margin-bottom: 2px;
  border-radius: var(--bulma-radius);
  font-size: 0.875rem; color: var(--ot-sidebar-text-2); text-decoration: none;
  border-left: 2px solid transparent;
  transition: background-color 0.12s ease, color 0.12s ease;
}
.ot-nav-item:hover { background: var(--ot-sidebar-hover); color: var(--ot-sidebar-text); }
/* Active item is the one place the brand lands in the chrome — see the token defaults. */
.ot-nav-item.is-active {
  color: var(--ot-sidebar-active-fg);
  font-weight: 600;
  border-left-color: var(--ot-sidebar-active-bd);
  background: var(--ot-sidebar-active-bg);
}
.ot-nav-icon { flex-shrink: 0; }

.ot-sidebar-user {
  display: flex; align-items: center; gap: 0.625rem;
  border-top: 1px solid var(--ot-sidebar-border); padding: 0.875rem 1.25rem;
}
.ot-avatar {
  width: 28px; height: 28px; flex-shrink: 0; border-radius: 50%;
  border: 1px solid var(--ot-sidebar-border);
  display: grid; place-items: center;
  font-size: 0.625rem; letter-spacing: 0.02em; color: var(--ot-sidebar-muted);
}
.ot-user-meta { min-width: 0; flex: 1; display: flex; flex-direction: column; }
.ot-user-email {
  font-size: 0.75rem; color: var(--ot-sidebar-text-2); line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ot-user-action { font-size: 0.6875rem; color: var(--ot-sidebar-muted); }
.ot-user-action:hover { color: var(--ot-sidebar-text); }
.ot-user-key { color: var(--ot-sidebar-muted); display: flex; padding: 0.25rem; }
.ot-user-key:hover { color: var(--ot-sidebar-text); }

.ot-main { flex: 1; min-width: 0; background: var(--ot-page); }
.ot-main > .container { padding: 2rem 2.5rem 4rem; max-width: 1400px; }

/* The mobile header, burger and scrim exist only below the sidebar breakpoint. */
.ot-topbar, .ot-burger, .ot-scrim { display: none; }

@media screen and (max-width: 1023px) {
  /*
   * Off-canvas sidebar driven by the hidden checkbox in base.html — no JavaScript, so the
   * portal templates stay script-free. `~` reaches the topbar, sidebar and scrim because
   * all three are later siblings of the input.
   */
  .ot-shell { display: block; }

  .ot-topbar {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem; padding: 0.75rem 1.25rem;
    background: var(--ot-surface); border-bottom: 1px solid var(--ot-border);
    position: sticky; top: 0; z-index: 10;
  }
  .ot-topbar .ot-wordmark { font-size: 1.15rem; }

  /* Three bars, drawn rather than imported */
  .ot-burger {
    display: flex; flex-direction: column; justify-content: center; gap: 4px;
    width: 2.25rem; height: 2.25rem; padding: 0 0.4rem;
    cursor: pointer; border-radius: var(--bulma-radius);
    flex-shrink: 0;
  }
  .ot-burger span { display: block; height: 2px; background: var(--ot-text-2); border-radius: 1px; }
  .ot-burger:hover { background: var(--ot-surface-2); }

  .ot-sidebar {
    position: fixed; z-index: 30; top: 0; left: 0; height: 100vh;
    width: 260px; max-width: 84vw;
    background: var(--ot-sidebar-bg);
    transform: translateX(-100%);
    transition: transform 0.18s ease;
  }
  .ot-menu-toggle:checked ~ .ot-sidebar {
    transform: translateX(0);
    box-shadow: 0 0 24px hsla(30, 12%, 20%, 0.18);
  }
  /* The scrim is a <label> for the same checkbox, so tapping outside closes the menu. */
  .ot-menu-toggle:checked ~ .ot-scrim {
    display: block; position: fixed; inset: 0; z-index: 20;
    background: hsla(30, 10%, 12%, 0.35);
  }

  .ot-main > .container { padding: 1.25rem 1.25rem 3rem; }
}

/* Someone navigating by keyboard must be able to reach the burger. */
.ot-menu-toggle:focus-visible + .ot-topbar .ot-burger,
.ot-burger:focus-visible { outline: 2px solid var(--bulma-primary); outline-offset: 2px; }

/* ─────────────────────── 2b. KPI row (tenant home) ───────────────────── */
/*
 * The headline figures, as hairline-separated columns rather than boxes — the numbers
 * carry the emphasis, not their containers. Figures are tabular-nums so a column of
 * amounts stays aligned. Populated only for a PUBLISHED report (tenant.home_metrics).
 */
.ot-kpi-row {
  display: flex; flex-wrap: wrap;
  border-top: 1px solid var(--ot-border);
  border-bottom: 1px solid var(--ot-border);
  margin-bottom: 1.75rem;
}
.ot-kpi {
  flex: 1 1 12rem; display: flex; flex-direction: column; gap: 0.3rem;
  padding: 1.1rem 1.5rem 1.15rem;
  border-left: 1px solid var(--ot-border);
}
.ot-kpi:first-child { border-left: none; padding-left: 0; }
.ot-kpi-label {
  font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ot-muted);
}
.ot-kpi-value {
  font-family: var(--ot-family-display); font-size: 1.75rem; line-height: 1.1;
  color: var(--ot-text); letter-spacing: -0.01em; font-variant-numeric: tabular-nums;
}
.ot-kpi-note { font-size: 0.75rem; color: var(--ot-muted); }
/* pos/neg follow the report's tokens, so a figure carries the same meaning in both */
.ot-kpi-note.is-pos { color: var(--bulma-success-on-scheme); }
.ot-kpi-note.is-neg { color: var(--bulma-danger-on-scheme); }

@media screen and (max-width: 767px) {
  .ot-kpi { flex-basis: 100%; border-left: none; padding-left: 0; }
  .ot-kpi + .ot-kpi { border-top: 1px solid var(--ot-border); }
}


/* ── Page head ───────────────────────────────────────────────────────────────
 * Title on the left, an identifier on the right, baseline-aligned and separated from the
 * body by the same hairline the KPI row uses. Ported from the prototype's page headers so
 * a page reading "Organisatie / Acme Holding / acme" looks the same either side.
 */
.ot-page-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  padding-bottom: 1.1rem; margin-bottom: 1.5rem;
  border-bottom: 1px solid var(--ot-border);
}
.ot-page-head .title { margin-bottom: 0.2rem; }
.ot-page-head .meta { margin: 0; }

/* An item inside a card that is a thing rather than a table row (a report, an entity
 * group). Hairline-separated, never boxed — same reasoning as the KPI row. */
.ot-org-item { padding: 0.9rem 0; border-top: 1px solid var(--ot-border); }
.ot-org-item:first-child { border-top: none; padding-top: 0; }
.ot-org-item:last-child { padding-bottom: 0; }
.ot-org-title {
  font-family: var(--ot-family-display); font-size: 1.125rem; line-height: 1.25;
  color: var(--ot-text); letter-spacing: -0.01em; margin: 0;
}

/* ───────────────────────── 3. Compatibility bridge ────────────────────── */

/* Card → flat white panel, separated by a border rather than a shadow */
.card {
  background: var(--ot-surface);
  border: 1px solid var(--ot-border);
  border-radius: var(--bulma-radius-large);
  margin-bottom: 1.25rem;
  box-shadow: none;
}
/*
 * Round the table's bottom corners to the card's — but per AXIS. The `overflow: hidden`
 * shorthand this used to carry also reset overflow-x, and since `.card > .table-scroll`
 * outranks `.table-scroll { overflow-x: auto }`, every card-wrapped table was CLIPPED
 * rather than scrollable: on a phone about two thirds of each row was unreachable.
 * border-radius still clips, because neither axis is `visible`.
 */
.card > .table-scroll {
  border-radius: 0 0 var(--bulma-radius-large) var(--bulma-radius-large);
  overflow-x: auto; overflow-y: hidden;
}
.card-header {
  padding: 0.875rem 1.25rem;
  border-bottom: 1px solid var(--ot-border);
  background: transparent;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
}
.card-header h2 {
  font-size: 0.875rem; font-weight: 600; color: var(--ot-text); margin: 0;
  letter-spacing: -0.005em;
}
.card-header .meta { font-size: 0.8125rem; color: var(--ot-muted); font-weight: 400; }
.card-body { padding: 1.25rem; color: var(--ot-text-2); }
.meta { color: var(--ot-muted); }

.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Bare <table> → uppercase letterspaced header, hairline rows (templates use unclassed <table>) */
.container table, .table {
  width: 100%; border-collapse: collapse; font-size: 0.875rem;
  background: var(--ot-surface); color: var(--ot-text);
}
.container th, .table th {
  padding: 0.625rem 1rem; text-align: left; white-space: nowrap;
  background: var(--ot-surface-2);
  color: var(--ot-muted);
  font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
  border-bottom: 1px solid var(--ot-border-strong);
}
.container td, .table td {
  padding: 0.75rem 1rem; border-bottom: 1px solid var(--ot-border);
  vertical-align: middle; color: var(--ot-text);
}
.container tbody tr:last-child td, .table tbody tr:last-child td { border-bottom: none; }
/* Zebra wash, well below the hover tint so the two never compete for the same row */
.container tbody tr:nth-child(even) td,
.table.is-striped tbody tr:nth-child(even) td { background: var(--ot-row-alt); }
.container tbody tr:hover td, .table tbody tr:hover td { background: var(--ot-surface-2); }
.table-container { background: transparent; }
/* Data cells read as data */
.container td code, .table td code { font-family: var(--bulma-family-code); }

/* Badges → coloured dot + uppercase label. Semantic hues stay unbranded: re-hueing
 * "triggered" or "approved" to a brand colour would destroy the signal. */
.badge {
  display: inline-flex; align-items: center; gap: 0.35rem;
  background: transparent; padding: 0; border-radius: 0;
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.05em;
  text-transform: uppercase; white-space: nowrap; line-height: 1.4;
}
.badge::before {
  content: ''; display: inline-block; width: 6px; height: 6px;
  border-radius: 50%; flex-shrink: 0; background: currentColor;
}
.badge-ok, .badge-approved, .badge-completed { color: var(--bulma-success-on-scheme); }
.badge-pending, .badge-warn                 { color: var(--bulma-warning-on-scheme); }
.badge-triggered, .badge-rejected, .badge-error { color: var(--bulma-danger-on-scheme); }
.badge-reversed                             { color: var(--bulma-info-on-scheme); }

/* Tenant administration (app/web/tenant_admin.py) ------------------------------
 * A write-only secret field: its "configured / not set" badge, the replace input and the
 * explicit clear checkbox sit on one line. Laid out here rather than with a style attribute
 * on the input, because a style attribute is also covered by style-src and the portal is
 * close to being able to serve a strict CSP. */
.ot-secret {
  display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;
}
.ot-secret .input { flex: 1 1 16rem; min-width: 12rem; }
.ot-secret .badge { flex-shrink: 0; }

/* "Still to do for this tenant" — a new tenant is deliberately incomplete, so the page says
 * what is missing instead of looking finished. */
.ot-todo { margin: 0.5rem 0 0; padding-left: 1.1rem; list-style: disc; }
.ot-todo li { margin-bottom: 0.25rem; }

/* Bulma .tag (data_quality severities) — Bulma already resolves its radius from
 * --bulma-radius, so only the weight/size need saying. */
.tag { font-weight: 600; }
.tag.is-medium { font-size: 0.8125rem; }

/* Buttons → flat, bordered; brand fill only on the primary action */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0.45rem 0.9rem; border-radius: var(--bulma-radius);
  font-size: 0.8rem; font-weight: 600; line-height: 1.4; white-space: nowrap;
  border: 1px solid var(--ot-border-strong);
  background: var(--ot-surface); color: var(--ot-text-2);
  cursor: pointer; text-decoration: none;
  transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.btn:hover { background: var(--ot-surface-2); color: var(--ot-text); }
.btn-run {
  background: var(--bulma-primary); border-color: var(--bulma-primary);
  color: var(--bulma-primary-invert);
}
.btn-run:hover {
  background: var(--bulma-primary-on-scheme); border-color: var(--bulma-primary-on-scheme);
  color: var(--bulma-primary-invert);
}
.btn-approve {
  background: var(--bulma-success-light); color: var(--bulma-success-on-scheme);
  border-color: var(--bulma-success-light);
}
.btn-approve:hover { background: var(--bulma-success-light); border-color: var(--bulma-success); }
.btn-reject {
  background: var(--bulma-danger-light); color: var(--bulma-danger-on-scheme);
  border-color: var(--bulma-danger-light);
}
.btn-reject:hover { background: var(--bulma-danger-light); border-color: var(--bulma-danger); }
.btn-secondary { background: var(--ot-surface); color: var(--ot-text-2); border-color: var(--ot-border-strong); }
.btn-secondary:hover { background: var(--ot-surface-2); color: var(--ot-text); }

/*
 * Bulma's own .button variants, for the templates that use them instead of .btn.
 * .is-primary needs no rule — Bulma already resolves it from --bulma-primary, so it
 * follows the brand for free. These are the ones whose STOCK look is a saturated slab
 * that fights the flat theme: a light button should read as bordered surface, and a
 * small destructive button (admin_users' "Verwijder") as a pale tint like .btn-reject,
 * not as the loudest thing on the page.
 */
.button.is-light, .button.is-small.is-light {
  background-color: var(--ot-surface); color: var(--ot-text-2);
  border: 1px solid var(--ot-border-strong); box-shadow: none;
}
.button.is-light:hover, .button.is-small.is-light:hover {
  background-color: var(--ot-surface-2); color: var(--ot-text);
}
.button.is-small.is-danger {
  background-color: var(--bulma-danger-light); color: var(--bulma-danger-on-scheme);
  border: 1px solid var(--bulma-danger-light);
}
.button.is-small.is-danger:hover { border-color: var(--bulma-danger); }
.input.is-small, .button.is-small { font-size: 0.8125rem; }

.actions-group { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
form { display: inline; }

.empty { text-align: center; color: var(--ot-muted); padding: 2rem; font-size: 0.875rem; }

/* ───────────────────────── 4. Bulma form controls ────────────────────── */
.input, .textarea, .select select {
  background-color: var(--ot-surface);
  color: var(--ot-text);
  border-color: var(--ot-border-strong);
  box-shadow: none;
}
.input::placeholder, .textarea::placeholder { color: var(--ot-muted); }
.input:hover, .textarea:hover, .select select:hover { border-color: var(--ot-muted); }
/* Focus ring resolves from the brand, so it re-tints per tenant */
.input:focus, .input.is-focused, .textarea:focus, .select select:focus {
  border-color: var(--bulma-primary);
  box-shadow: 0 0 0 3px hsla(var(--bulma-primary-h), var(--bulma-primary-s), var(--bulma-primary-l), 0.14);
}
.label { color: var(--ot-text-2); font-size: 0.8125rem; font-weight: 500; }
.select:not(.is-multiple):not(.is-loading)::after { border-color: var(--ot-muted); }
.help.is-danger { color: var(--bulma-danger-on-scheme); }

/* ───────────────────────── 5. Flash notifications ────────────────────── */
.notification {
  background-color: var(--ot-surface-2);
  color: var(--ot-text);
  border: 1px solid var(--ot-border);
  border-radius: var(--bulma-radius);
  font-size: 0.875rem;
}
.notification.is-success, .notification.is-success.is-light {
  background-color: var(--bulma-success-light); border-color: var(--bulma-success-light);
  color: var(--bulma-success-on-scheme);
}
.notification.is-danger, .notification.is-danger.is-light {
  background-color: var(--bulma-danger-light); border-color: var(--bulma-danger-light);
  color: var(--bulma-danger-on-scheme);
}
.notification.is-warning, .notification.is-warning.is-light {
  background-color: var(--bulma-warning-light); border-color: var(--bulma-warning-light);
  color: var(--bulma-warning-on-scheme);
}
.notification.is-info, .notification.is-info.is-light {
  background-color: var(--bulma-info-light); border-color: var(--bulma-info-light);
  color: var(--bulma-info-on-scheme);
}
.notification strong { color: inherit; }

/* ───────────────────────────── 6. Typography ──────────────────────────── */
/* Display serif for page-level titles only; UI chrome stays sans. */
.title {
  color: var(--ot-text);
  font-family: var(--ot-family-display);
  font-weight: 400;
  letter-spacing: -0.015em;
}
.subtitle { color: var(--ot-text-2); }
h1, h2, h3, h4, h5, h6 { color: var(--ot-text); }
p { color: var(--ot-text-2); }
strong { color: var(--ot-text); }
a { color: var(--bulma-primary-on-scheme); }
a:hover { color: var(--ot-text); }
.has-text-grey { color: var(--ot-muted); }
.is-size-7 { color: var(--ot-muted); }
.is-clickable { cursor: pointer; }
/*
 * Bulma's `.level.is-mobile` stays flex at EVERY width — that is the whole point of the
 * modifier — and flex items refuse to shrink below their content, so a long finding title
 * next to its severity tag pushed the tag past the viewport and made the whole page scroll
 * sideways at 375px (data_quality.html's finding headers). Let the row wrap instead, and
 * allow the left side to shrink: side-by-side while it fits, stacked when it doesn't.
 * Bulma pins flex-shrink:0 on .level-left/-right/-item, so min-width:0 alone is not
 * enough to let a long title give way — both are needed.
 */
.level.is-mobile { flex-wrap: wrap; gap: 0.5rem; }
.level.is-mobile > .level-left,
.level.is-mobile > .level-right,
.level.is-mobile .level-item { min-width: 0; flex-shrink: 1; }
.level.is-mobile .title { overflow-wrap: anywhere; }

/* Small uppercase eyebrow label (data_quality .heading) */
.heading {
  color: var(--ot-muted); font-size: 0.7rem;
  text-transform: uppercase; letter-spacing: 0.06em;
}

code {
  /* -on-scheme, not -primary: this is brand text on a LIGHT surface, and a tenant may set
   * a pale brand colour. app/branding.py clamps --bulma-primary-on-scheme-l to <=40%. */
  font-size: 0.8rem; background: var(--ot-surface-2); color: var(--bulma-primary-on-scheme);
  border: 1px solid var(--ot-border);
  padding: 2px 6px; border-radius: var(--bulma-radius-small);
  font-family: var(--bulma-family-code);
}

/* Bulma box (data_quality score panel, login card) → same flat panel as .card */
.box {
  background: var(--ot-surface);
  border: 1px solid var(--ot-border);
  border-radius: var(--bulma-radius-large);
  box-shadow: none;
  color: var(--ot-text);
}

details summary {
  color: var(--bulma-primary-on-scheme);
  cursor: pointer;
  font-size: 0.8125rem;
}
details summary:hover { color: var(--ot-text); }

/* ─────────────────────────────── 7. Login ─────────────────────────────── */
.hero.is-fullheight { background: var(--ot-page); }
.hero-body { background: transparent; }
.hero .box {
  border-radius: 10px;
  padding: 2.25rem 2rem;
  box-shadow: 0 1px 3px hsla(30, 12%, 20%, 0.05);
}
.hero .title { font-size: 1.5rem; }
/* Give the single call-to-action on the page some presence */
.hero .button.is-primary.is-fullwidth { height: 2.5rem; font-weight: 600; }

/* ──────────────────── 8. Audit log components (audit.html) ────────────── */
/* Moved out of audit.html's inline <style> so that page can come under a CSP. */
.tab-bar { display: flex; gap: 0.25rem; }
.tab-btn {
  padding: 0.3rem 0.875rem; border-radius: var(--bulma-radius);
  font-size: 0.8125rem; font-weight: 500;
  border: 1px solid var(--ot-border-strong);
  background: var(--ot-surface); color: var(--ot-text-2); cursor: pointer;
}
.tab-btn.active {
  background: var(--bulma-primary); color: var(--bulma-primary-invert);
  border-color: var(--bulma-primary);
}
.tab-btn:hover:not(.active) { background: var(--ot-surface-2); color: var(--ot-text); }

.stats-row {
  display: flex; gap: 1px;
  background: var(--ot-border); border-bottom: 1px solid var(--ot-border);
}
.stat {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 1.25rem 1rem; background: var(--ot-surface); gap: 0.25rem;
}
.stat-value {
  font-size: 1.75rem; font-weight: 700; color: var(--ot-text); line-height: 1;
  font-variant-numeric: tabular-nums;
}
.stat-label {
  font-size: 0.7rem; color: var(--ot-muted);
  text-transform: uppercase; letter-spacing: 0.05em;
}
.stat-alert .stat-value { color: var(--bulma-danger-on-scheme); }
.stat-ok    .stat-value { color: var(--bulma-success-on-scheme); }

.run-group { border-bottom: 1px solid var(--ot-border-strong); }
.run-group:last-child { border-bottom: none; }
.run-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.6rem 1.25rem; background: var(--ot-surface-2);
  border-bottom: 1px solid var(--ot-border);
  gap: 1rem; flex-wrap: wrap;
}
.run-label {
  font-size: 0.75rem; font-weight: 600; color: var(--ot-text);
  text-transform: uppercase; letter-spacing: 0.05em;
}
.run-time {
  font-size: 0.8rem; color: var(--ot-text-2);
  font-family: var(--bulma-family-code); font-variant-numeric: tabular-nums;
}
.run-tenants { font-size: 0.775rem; color: var(--ot-muted); }

.alert-cards { display: flex; flex-direction: column; gap: 0; }
.alert-card {
  padding: 1rem 1.25rem 1rem 1.375rem;
  border-bottom: 1px solid var(--ot-border);
  border-left: 3px solid transparent;
  display: flex; flex-direction: column; gap: 0.45rem;
}
.alert-card:last-child { border-bottom: none; }
/* A whole row is a large area, so the tint is a hint and the left border carries the
 * signal. Bulma's *-light shades (l≈90%) are tuned for small chips and read as alarming
 * across a full-width block, so derive a near-white wash from the same hue instead. */
.alert-card--triggered {
  border-left-color: var(--bulma-danger);
  background: hsl(var(--bulma-danger-h) var(--bulma-danger-s) 97.5%);
}
.alert-card--passed {
  border-left-color: var(--bulma-success);
  background: hsl(var(--bulma-success-h) var(--bulma-success-s) 98.5%);
  padding-top: 0.6rem; padding-bottom: 0.6rem;
}
.alert-card-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.alert-meta { font-size: 0.775rem; color: var(--ot-muted); white-space: nowrap; }
.alert-detail { font-size: 0.8125rem; color: var(--ot-text-2); line-height: 1.55; }
.alert-footer {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.75rem; flex-wrap: wrap; margin-top: 0.1rem;
}
.alert-footer-left { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.alert-value {
  font-size: 0.775rem; color: var(--ot-text-2); font-family: var(--bulma-family-code);
}
.alert-model {
  font-size: 0.775rem; color: var(--ot-muted); font-family: var(--bulma-family-code);
}

/* ────────────────────── 9. Help page prose (help.html) ────────────────── */
/* Moved out of help.html's inline <style> so that page can come under a CSP. */
.prose p { margin-bottom: 1rem; font-size: 0.875rem; color: var(--ot-text-2); line-height: 1.65; }
.prose p:last-child { margin-bottom: 0; }
.prose ul { margin: 0 0 0.5rem 1.1rem; }
.prose ul li { font-size: 0.875rem; color: var(--ot-text-2); line-height: 1.65; margin-bottom: 0.35rem; }
.prose table { margin-top: 0.5rem; }
.prose td { font-size: 0.8125rem; color: var(--ot-text-2); vertical-align: top; }
.prose .badge { font-size: 0.7rem; }

.note {
  background: var(--ot-surface-2);
  border-left: 3px solid var(--bulma-primary);
  padding: 0.75rem 1rem;
  border-radius: 0 var(--bulma-radius-small) var(--bulma-radius-small) 0;
  color: var(--ot-text-2);
}

ol.steps { counter-reset: step; list-style: none; margin: 0.5rem 0 0; padding: 0; }
ol.steps > li {
  counter-increment: step; position: relative;
  padding: 0 0 0.9rem 2.4rem; font-size: 0.875rem;
  color: var(--ot-text-2); line-height: 1.6;
}
ol.steps > li::before {
  content: counter(step); position: absolute; left: 0; top: 0;
  width: 1.6rem; height: 1.6rem;
  background: var(--bulma-primary); color: var(--bulma-primary-invert);
  border-radius: 50%; display: grid; place-items: center;
  font-size: 0.8rem; font-weight: 700;
}
ol.steps > li:not(:last-child)::after {
  content: ""; position: absolute; left: 0.775rem; top: 1.7rem; bottom: 0;
  width: 2px; background: var(--ot-border-strong);
}

/* ────────────────────── 10. Per-tenant branding hooks ─────────────────── */
/* The tenant stylesheet (/brand/{tenant}.css) loads after this file and overrides the
 * Bulma trio + --ot-color-* tokens; these two rules only give its logo somewhere to land. */
.ot-logo { border-radius: 9px; object-fit: contain; }

/*
 * NOTE (CSP): with audit.html's and help.html's <style> blocks folded in above, the
 * portal's remaining inline blockers are SCRIPTS, not styles — base.html's burger toggle
 * and audit.html's tab switcher (plus its onclick attributes). De-inlining those two is
 * what's left before the portal can serve the report's strict policy. See CLAUDE.md
 * "Next steps" item 3.
 */
