/* GENERATED FILE -- DO NOT EDIT.
 *
 * Rendered from design/vsp-theme.css by scripts/sync_theme.py. Edit the
 * source and re-run the script; `scripts/sync_theme.py --check` fails CI if
 * this file and the source have diverged.
 */

/* Copyright (c) 2026 VSP AI and Robotics. All rights reserved.
 *
 * VSP AI & Robotics design system, applied to Frappe Desk and to the
 * website templates. `design/vsp-theme.css` is the only hand-edited copy; it
 * is not served directly. `scripts/sync_theme.py` renders it into each app's
 * `public/css/<app>.css`, substituting vsp_nest for the app name so the
 * @font-face URLs resolve inside that app's own asset namespace. CI runs
 * `scripts/sync_theme.py --check` and fails if a generated file has drifted
 * from this source.
 *
 * Two copies exist rather than one shared file because the apps are sold and
 * installed separately -- vsp_people runs on sites with no CRM present, and
 * CI proves it on every commit -- so neither app may import an asset from the
 * other, and the monorepo root is not on any site's asset path.
 *
 * Colour direction is "Signal": Vector Blue carries every action, gold is
 * reserved for state and for the mark. The two hard rules from the design
 * system, both of them contrast failures, are enforced throughout:
 *
 *   1. Gold (#E8B928) is never text or a hairline on a light ground -- 1.7:1.
 *      Its voice on light is `ore` (#8A6410, 5.4:1).
 *   2. Vector Blue (#1440B0) is never text on obsidian -- 2.3:1. On dark
 *      grounds it is a fill and border colour only; blue text uses `signal`
 *      (#3B7BF5, 5.1:1).
 *
 * Frappe's own status hues (--red-*, --green-*, --orange-*, indicator
 * classes) are deliberately NOT overridden here. Operators read Draft /
 * Submitted / Completed / Pending / Overdue by colour faster than they read
 * the label, and re-tinting them to brand hues is the single most expensive
 * thing a re-skin can break. The design system's semantic values are exposed
 * as --vsp-status-* for app-authored UI that needs them, and nothing else.
 *
 * Density is likewise untouched: no control heights, row heights or page
 * padding are set. Re-skinning is a colour, type and radius job. Changing
 * Desk density forks this app from every future Frappe upgrade.
 */

/* ------------------------------------------------------------------ *
 * Typefaces
 *
 * Both families are SIL OFL 1.1 and MUST be self-hosted. A font CDN link
 * breaks under a strict CSP and then fails silently to a fallback that
 * nobody notices until a screenshot reaches a client.
 *
 * The WOFF2 files live in `apps/vsp_nest/vsp_nest/public/fonts/`, committed
 * alongside their OFL licence text, which the licence requires to travel with
 * them. `design/FONTS.md` records where each file came from and how to verify
 * the browser actually loaded it rather than falling back.
 *
 * DEVIATION from design.md, on the product owner's call after seeing it on
 * screen: the display face is NOT Archivo Expanded. The design document picks
 * it to inherit the wordmark's wide, flat-terminalled caps, and at wordmark
 * scale it does that. At Desk scale it does not -- an expanded grotesque set
 * at 18-30px next to 13px Plex Sans reads as a different product's chrome
 * bolted on, and it eats horizontal room in the navbar and page head, the two
 * bars that have none to give. Display is now IBM Plex Sans at 600/700 with
 * tightened tracking: same superfamily as body and data, so headings and
 * tables sit together without a seam.
 *
 * --vsp-font-display is kept as its own token rather than being deleted. It
 * is the single line to change if a display face is reintroduced, and it
 * keeps the display/body distinction visible in the component rules below.
 * ------------------------------------------------------------------ */

@font-face {
	font-family: "IBM Plex Sans";
	src: url("/assets/vsp_nest/fonts/ibm-plex-sans-400.woff2") format("woff2");
	font-weight: 400;
	font-display: swap;
}

@font-face {
	font-family: "IBM Plex Sans";
	src: url("/assets/vsp_nest/fonts/ibm-plex-sans-500.woff2") format("woff2");
	font-weight: 500;
	font-display: swap;
}

@font-face {
	font-family: "IBM Plex Sans";
	src: url("/assets/vsp_nest/fonts/ibm-plex-sans-600.woff2") format("woff2");
	font-weight: 600;
	font-display: swap;
}

@font-face {
	font-family: "IBM Plex Sans";
	src: url("/assets/vsp_nest/fonts/ibm-plex-sans-700.woff2") format("woff2");
	font-weight: 700;
	font-display: swap;
}

@font-face {
	font-family: "IBM Plex Mono";
	src: url("/assets/vsp_nest/fonts/ibm-plex-mono-400.woff2") format("woff2");
	font-weight: 400;
	font-display: swap;
}

@font-face {
	font-family: "IBM Plex Mono";
	src: url("/assets/vsp_nest/fonts/ibm-plex-mono-500.woff2") format("woff2");
	font-weight: 500;
	font-display: swap;
}

/* ------------------------------------------------------------------ *
 * Tokens
 *
 * Selector note: `html:root` rather than `:root`. Frappe declares its own
 * variables in a bare `:root` block, and several bundles (login.bundle.css
 * is the one that has bitten this app before) re-declare them in their own
 * `:root` after this stylesheet loads, winning the tie on source order.
 * Adding the type selector raises specificity from (0,1,0) to (0,1,1), so
 * these values hold regardless of which bundle loads last -- without
 * reaching for !important, which would also override the dark block below.
 *
 * The dark block uses `html[data-theme="dark"]`, the attribute Frappe itself
 * sets on <html>. It carries the same (0,1,1) specificity as the light block
 * and appears after it, so it wins whenever the attribute is present. Do NOT
 * convert this to `prefers-color-scheme`: that fights the user's explicit
 * theme choice in the Desk.
 * ------------------------------------------------------------------ */

html:root {
	/* ---- brand primitives: identical in both themes ---- */
	--vsp-obsidian: #05070d;
	--vsp-hex-deep: #0a1e5c;
	--vsp-vector: #1440b0;
	--vsp-signal: #3b7bf5;
	--vsp-gold: #e8b928;
	--vsp-filament: #f7dc7a;
	--vsp-ore: #8a6410;

	/* ---- semantic set, for app-authored UI only ----
	   These do not override Frappe's indicator palette; see the file header.
	   Every use must pair the colour with a word, so the meaning survives
	   greyscale print and colour-blind readers. */
	--vsp-status-draft: #77809a;
	--vsp-status-info: #2e6fe0;
	--vsp-status-success: #1e8e5f;
	--vsp-status-warning: #c4762a;
	--vsp-status-danger: #d24a4a;

	/* ---- type stacks ----
	   Display and body resolve to the same family on purpose; see the note
	   above the @font-face block. Headings separate themselves by weight and
	   tracking, not by face. */
	--vsp-font-display: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
	--vsp-font-body: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
	--vsp-font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;

	/* ---- the active-item rail ----
	   The one gold element on every Desk screen. On light grounds it resolves
	   to `ore`, not `gold`: a 2px bar is a non-text graphic and WCAG 1.4.11
	   asks 3:1 of it, which #E8B928 on #FFFFFF (1.7:1) misses outright. Same
	   role, same hue family, legible on a warehouse monitor. */
	--vsp-rail: var(--vsp-ore);

	/* ---- Frappe surface + text mapping, light ---- */
	--bg-color: #f2f4fa;
	--fg-color: #ffffff;
	--card-bg: #ffffff;
	--control-bg: #f2f4fa;
	--navbar-bg: #ffffff;
	--text-color: #0b1020;
	--heading-color: #05070d;
	--text-light: #3b4560;
	--text-muted: #69738f;
	--border-color: #dde3f0;
	--dark-border-color: #c6cfe3;
	--highlight-color: #f2f4fa;
	--vsp-text-faint: #8b95ae;

	/* ---- action ----
	   --primary is the v15 name; --primary-color and --btn-primary are older
	   aliases still read by a few bundles (notably the website and login
	   ones). Setting all three costs nothing and avoids a grey Submit button
	   on whichever surface reads the alias. */
	--primary: #1440b0;
	--primary-color: #1440b0;
	--btn-primary: #1440b0;
	--text-on-primary: #ffffff;

	/* Tag fills, applied-filter chips and the selected sidebar row all take
	   this tint. In Signal it is BLUE, matching the blue text that sits on
	   it -- a gold-tinted fill under blue text is the specific failure this
	   system exists to correct. */
	--subtle-accent: rgba(20, 64, 176, 0.09);
	--sidebar-select-color: rgba(20, 64, 176, 0.09);
	--vsp-link: #123aa0;
	--vsp-focus-ring: #1440b0;

	/* ---- shape ----
	   Small and near-uniform: 2px chips, 3px controls, 4px cards, 6px ceiling.
	   999px is reserved for status indicator pills, and nothing else -- a
	   pill reads as state, a squared rectangle reads as action, and using
	   pill radius on a button destroys that distinction. */
	--border-radius-sm: 2px;
	--border-radius: 3px;
	--border-radius-md: 3px;
	--border-radius-lg: 4px;
	--border-radius-xl: 6px;
	--border-radius-full: 999px;

	/* ---- elevation ----
	   Near-flat and border-led. One barely-there shadow in light; hierarchy
	   comes from a 1px border and a change of surface. Drop shadows on every
	   card produce visual mud at Desk row counts. */
	--shadow-base: 0 1px 2px rgba(11, 16, 32, 0.06), 0 1px 0 rgba(11, 16, 32, 0.04);
	--shadow-sm: 0 1px 2px rgba(11, 16, 32, 0.06);
	--vsp-scrim: rgba(5, 7, 13, 0.55);

	--font-stack: var(--vsp-font-body);
}

html[data-theme="dark"] {
	/* Dark is not an inversion. --primary is DEEPER than Vector Blue here
	   (#1F55CC), because a saturated blue that reads correct on white glows
	   on obsidian. Depth is carried by surface tone rather than shadow -- a
	   shadow on #05070D is invisible -- so a raised element is lighter:
	   #05070D ground, #0B0F1C surface, #111730 sunken. */
	--vsp-rail: var(--vsp-gold);

	--bg-color: #05070d;
	--fg-color: #0b0f1c;
	--card-bg: #0b0f1c;
	--control-bg: #111730;
	--navbar-bg: #0b0f1c;
	--text-color: #f0f3fa;
	--heading-color: #ffffff;
	--text-light: #b6c0d8;
	--text-muted: #8b97b4;
	--border-color: #1c2440;
	--dark-border-color: #2c3760;
	--highlight-color: #111730;
	--vsp-text-faint: #6e7a98;

	--primary: #1f55cc;
	--primary-color: #1f55cc;
	--btn-primary: #1f55cc;
	--text-on-primary: #ffffff;

	--subtle-accent: rgba(59, 123, 245, 0.16);
	--sidebar-select-color: rgba(59, 123, 245, 0.16);
	--vsp-link: #79a6ff;
	--vsp-focus-ring: #3b7bf5;

	/* Elevation by tone, not by shadow. Left declared but effectively off. */
	--shadow-base: none;
	--shadow-sm: none;
}

/* ------------------------------------------------------------------ *
 * Type
 *
 * Inside the Desk, 13px is the working size and stays that way -- density is
 * load-bearing there, and scaling Desk type up to match marketing pages costs
 * operators rows per screen. Only the family, the numerals and the display
 * faces are set here.
 * ------------------------------------------------------------------ */

body,
.page-container,
.modal-dialog {
	font-family: var(--vsp-font-body);
}

/* `.page-card-head` is the login card's title. It is a DIV with a class, not a
   heading element, so the element selectors below never reach it and Frappe's
   own (0,1,0) class rule would win the cascade even if they did. It is named
   explicitly for that reason. */
h1,
h2,
h3,
h4,
.navbar-brand,
.page-card-head {
	font-family: var(--vsp-font-display);
	font-weight: 700;
	text-wrap: balance;
}

/* Desk page titles stay at 600. The design system's `pageTitle` level is
   18px/600 -- a route the operator changes forty times an hour is wayfinding,
   not a headline, and bolding it competes with the record's own title. */
.page-title .title-text,
.page-head .title-text {
	font-weight: 600;
}

/* Tracking tightens as size grows. With display and body on one family, this
   and the weight above are the whole of the heading/body distinction, so they
   carry more work than they would under a separate display face. */
h1 {
	letter-spacing: -0.02em;
}

h2,
.page-card-head {
	letter-spacing: -0.015em;
}

h3,
h4 {
	letter-spacing: -0.01em;
}

/* The mono face is for data: identifiers, figures, code. Not for prose that
   happens to sit in a table.

   An earlier version of this rule keyed on `[data-filter]`, which looked like
   a numeric-column hook and is not one -- Frappe puts `data-filter` on EVERY
   filterable list cell regardless of fieldtype, so an Employee list rendered
   first and last names in IBM Plex Mono, and put the status indicator's label
   in it too. List rows carry no fieldtype in the DOM, so there is nothing to
   select on there. */
.number-widget-box .number,
.number-card-value,
code,
pre,
.control-value[data-fieldtype="Currency"],
.control-value[data-fieldtype="Int"],
.control-value[data-fieldtype="Float"],
.dt-cell--col-currency,
.dt-cell--col-int,
.dt-cell--col-float {
	font-family: var(--vsp-font-mono);
	font-variant-numeric: tabular-nums;
	font-feature-settings: "tnum";
}

/* What list rows get instead: tabular figures without a face change. IBM Plex
   Sans carries `tnum`, so a column of amounts still lines up vertically while
   names stay in the reading face. */
.list-row-col .filterable,
.list-row-col [data-filter] {
	font-variant-numeric: tabular-nums;
	font-feature-settings: "tnum";
}

/* The number tile's value is the design system's `dataLg` level. Frappe's
   workspace tile emits it as a bare `.number` inside `.number-widget-box` --
   `.number-card-value` is the dashboard-chart variant, and targeting only
   that left every workspace tile at Frappe's stock 20px/600 sans.

   The long selector is deliberate and is not padding. desk.bundle sizes this
   element through `.widget.number-widget-box .widget-body .widget-content
   .number`, five classes deep; anything shorter sets the family (nothing else
   claims it) and silently loses font-size and font-weight, which is exactly
   what the first version of this rule did. Matching the specificity and
   winning on load order beats reaching for !important. */
.widget.number-widget-box .widget-body .widget-content .number,
.number-card-value {
	font-size: 26px;
	font-weight: 500;
	line-height: 1.15;
	letter-spacing: -0.01em;
}

/* Uppercase is for labels only, never for a sentence.

   Every list header column, not just the subject: the design system's
   `tableHeader` is one level, and applying it to the first column alone left
   "EMPLOYEE NAME" in tracked-out mono caps beside "First Name" and "Status"
   in sentence-case sans, which reads as a rendering bug. 11px buys back the
   width that 0.12em tracking and uppercase cost. */
.list-row-head .list-row-col,
.dt-cell--header .dt-cell__content,
.sidebar-label,
.section-head,
.form-section .section-head {
	font-family: var(--vsp-font-mono);
	font-size: 11px;
	font-weight: 500;
	letter-spacing: 0.12em;
	text-transform: uppercase;
}

/* ------------------------------------------------------------------ *
 * Buttons
 *
 * Three tiers, and only three: primary in Vector Blue for the single
 * highest-value action in a view, secondary for routine, subtle for
 * tertiary and toolbar controls. Colour only -- heights and padding stay
 * stock.
 * ------------------------------------------------------------------ */

.btn-primary {
	background-color: var(--primary);
	border-color: var(--primary);
	color: var(--text-on-primary);
}

.btn-primary:hover,
.btn-primary:focus {
	/* One step deeper, not a hue change: the button must stay recognisably
	   the same control while pressed. */
	background-color: color-mix(in srgb, var(--primary) 86%, var(--vsp-obsidian));
	border-color: color-mix(in srgb, var(--primary) 86%, var(--vsp-obsidian));
	color: var(--text-on-primary);
}

/* On /login specifically: website.bundle.css defines
   `.btn.btn-primary { background-color: var(--btn-primary); }` at the same
   specificity as the token block above, and login.bundle.css loads AFTER this
   stylesheet and re-declares --btn-primary in its own :root, winning the tie
   and reverting the login button to Frappe's default grey. Forcing the shared
   --btn-primary with !important would leak into every other context that
   reads it, so this targets the login button directly with a two-class
   selector and a primitive that no bundle redeclares. */
.btn-login.btn-primary {
	background-color: var(--vsp-vector);
	border-color: var(--vsp-vector);
	color: #ffffff;
}

.btn-login.btn-primary:hover,
.btn-login.btn-primary:focus {
	background-color: #123aa0;
	border-color: #123aa0;
	color: #ffffff;
}

/* ------------------------------------------------------------------ *
 * Inputs and focus
 *
 * Desk is heavily keyboard-driven -- Ctrl+G, arrow navigation, inline edit --
 * so every interactive element keeps a visible focus state in both themes.
 * Never remove the outline; if it clashes, restyle it.
 * ------------------------------------------------------------------ */

/* The radius is set explicitly rather than left to `--border-radius`. Frappe
   declares `.form-control` twice in desk.bundle -- once at `--border-radius`
   and again, later, at `--border-radius-sm` -- so retuning the tokens alone
   lands controls on 2px while the design system puts them on `md` (3px)
   and reserves 2px for chips and swatches. */
.form-control,
.input-with-feedback,
.awesomplete > input {
	background-color: var(--control-bg);
	border-color: var(--dark-border-color);
	border-radius: var(--border-radius-md);
	color: var(--text-color);
}

.form-control:focus,
.input-with-feedback:focus,
.awesomplete > input:focus,
.btn:focus-visible,
a:focus-visible,
[tabindex]:focus-visible {
	border-color: var(--vsp-focus-ring);
	outline: none;
	box-shadow: 0 0 0 2px var(--subtle-accent);
}

/* ------------------------------------------------------------------ *
 * Sidebar, navbar and the gold rail
 * ------------------------------------------------------------------ */

.navbar {
	background-color: var(--navbar-bg);
	border-bottom: 1px solid var(--border-color);
}

.navbar-brand .app-logo {
	height: 24px;
	width: auto;
}

/* The selected workspace. Blue tint from --sidebar-select-color, plus the one
   gold element that appears on every Desk screen: a 2px vertical bar flush to
   the container edge. One gold thing per view -- a screen where several
   things are gold reads as a casino, not a robotics company.

   The bar is an inset box-shadow, not a border-left. A real border is part of
   the box, so only the selected row gained 2px of left border while its
   unselected siblings had none -- every label in the sidebar jumped 2px
   sideways as you moved between workspaces. An inset shadow paints in the
   same place and takes no layout. */
.standard-sidebar-item.selected,
.sidebar-item-container .desk-sidebar-item.selected {
	background-color: var(--sidebar-select-color);
	box-shadow: inset 2px 0 0 var(--vsp-rail);
	color: var(--heading-color);
}

/* ------------------------------------------------------------------ *
 * Tags, filters and cards
 * ------------------------------------------------------------------ */

.filter-button,
.tag-pill,
.data-pill {
	background-color: var(--subtle-accent);
	color: var(--vsp-link);
	border-radius: var(--border-radius-md);
}

/* `.widget` is not a card class. Frappe uses it for every workspace block
   INCLUDING `.widget.spacer`, an empty layout element with no content, so a
   bare `.widget` rule drew a bordered, shadowed 18px strip wherever a
   workspace put vertical space between sections -- two of them on the CRM
   workspace, reading as broken empty panels. Chrome goes on blocks that hold
   something. */
.widget:not(.spacer),
.form-section,
.frappe-card {
	background-color: var(--card-bg);
	border: 1px solid var(--border-color);
	border-radius: var(--border-radius-lg);
	box-shadow: var(--shadow-base);
}

/* Belt and braces: a spacer is invisible by definition, and Frappe may add
   other empty layout blocks later. */
.widget.spacer {
	background: none;
	border: none;
	box-shadow: none;
}

.page-card .page-card-head {
	border-bottom: 2px solid var(--primary);
}

/* Overlays are the one place a real shadow is allowed, because they genuinely
   leave the plane. */
.modal-backdrop {
	background-color: var(--vsp-scrim);
}

/* Wide content -- tables, code blocks, diagrams -- scrolls inside its own
   container. The page body never scrolls sideways. */
.table-responsive,
.report-wrapper,
pre {
	overflow-x: auto;
}

/* ------------------------------------------------------------------ *
 * Workspace editor chrome
 *
 * The block editor's own furniture: the add/settings buttons in the gutter,
 * the per-block pencil/drag/overflow controls, the block outline, and the
 * dashed spacer placeholder. None of it is visible in view mode -- it appears
 * only while a workspace is being edited -- but an admin edits workspaces in
 * front of whoever asked for the change, so it is part of the product.
 *
 * Nothing here hides a control. Editor.js drives block selection, insertion
 * and deletion through these exact elements; hiding one makes a block
 * unselectable, which is worse than a loud button. The aim is to bring the
 * chrome onto the system's tokens and take the shout out of it.
 *
 * Three of Frappe's values are hardcoded rather than tokenised, and that is
 * why this section is not just a restyle:
 *
 *   `.ce-toolbar__plus`      background #FFFFFF, border #E8E8EB
 *   `.widget-control svg`    stroke rgb(56, 56, 56)
 *   toolbar button shadow    rgba(13, 20, 33, 0.13) 0 3px 15px -3px
 *
 * On obsidian the first two are a white chip and a near-black glyph on a near
 * black ground; the third is a 15px-blur drop shadow in a system whose entire
 * elevation model is one 1px shadow. All three follow the tokens below now.
 * ------------------------------------------------------------------ */

.ce-toolbar__plus,
.ce-toolbar__settings-btn {
	background-color: var(--card-bg);
	border: 1px solid var(--border-color);
	border-radius: var(--border-radius-md);
	box-shadow: var(--shadow-base);
	color: var(--text-muted);
}

.ce-toolbar__plus:hover,
.ce-toolbar__settings-btn:hover {
	background-color: var(--subtle-accent);
	border-color: var(--vsp-focus-ring);
	color: var(--heading-color);
}

/* Per-block controls. `currentColor` replaces the hardcoded stroke so the
   glyphs follow the theme instead of staying near-black in both. */
.widget-control svg,
.widget-control svg *,
.new-block-button svg,
.new-block-button svg * {
	color: var(--text-muted);
	stroke: currentColor;
}

/* A block in edit mode reads as an outline, not a filled card. Frappe fills
   every one with `--bg-color`, which on this palette is a blue-grey and turns
   a workspace under edit into a stack of tinted bands. */
.widget.edit-mode {
	background-color: transparent;
	border-color: var(--border-color);
}

/* Header blocks need the long selector. desk.bundle fills them through
   `[data-page-route="Workspaces"] .codex-editor .codex-editor__redactor
   .ce-block .ce-block__content .widget.header`, six classes deep, so the rule
   above wins on every other block type and loses on headers alone -- which
   left the section titles as the only tinted bands on the page, the opposite
   of quiet. */
[data-page-route="Workspaces"] .codex-editor .codex-editor__redactor .ce-block .ce-block__content .widget.header {
	background-color: transparent;
}

/* The hovered block takes the same blue tint as a selected sidebar row, so
   "this is the thing you are about to act on" means one colour across the
   Desk rather than two. */
.widget.edit-mode:hover {
	background-color: var(--subtle-accent);
	box-shadow: none;
}

/* Focus needs its own cue. Frappe marks the focused block with
   `box-shadow: var(--shadow-base) !important`, and this system sets that
   shadow to `none` in dark -- depth there is carried by surface tone, and a
   shadow on obsidian is invisible -- so on a dark Desk the focused block
   would otherwise be indistinguishable from its neighbours. */
.ce-block--focused .widget {
	border-color: var(--vsp-focus-ring);
}

/* The spacer placeholder. Still dashed, because it has to stay tellable from
   a real block, but on the theme's border colour rather than a flat grey, and
   at the control radius. */
.widget.edit-mode.spacer {
	border: 1px dashed var(--border-color);
	border-radius: var(--border-radius-md);
	color: var(--vsp-text-faint);
}
