{#
framework/_cp_tokens.html.twig
══════════════════════════════════════════════════════════════════════════════
SINGLE SOURCE of cp-shell design tokens + shared component primitives.
Change the whole system's look HERE — every cp-shell module inherits it.
Per-module ACCENT: a module layout sets `--cp-accent` (+ hover/dim) on its
`.cp-shell` wrapper via inline style; every primitive below reads var(--cp-accent),
so one variable re-accents an entire module without touching this file.
Namespacing: component classes live under `.cp-scope` so they never leak into
legacy pages. `--cp-primary*` are kept as aliases of `--cp-accent*` for back-compat.
══════════════════════════════════════════════════════════════════════════════
#}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@600;700;800&family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
/* ── ACCENT (default; a module overrides --cp-accent on its .cp-shell) ── */
--cp-accent:#1f5673; --cp-accent-hover:#16465f; --cp-on-accent:#fff; --cp-accent-dim:rgba(31,86,115,.10);
/* back-compat aliases */
--cp-primary:var(--cp-accent); --cp-primary-hover:var(--cp-accent-hover); --cp-on-primary:var(--cp-on-accent);
/* ── neutrals / surfaces ── */
--cp-surface:#f7f9fb; --cp-surface-low:#eef3f6; --cp-surface-container:#e7eef2; --cp-surface-white:#fff;
--cp-text:#15242c; --cp-text-muted:#3f5560; --cp-text-faint:#76899a;
--cp-outline:#e2e9ee; --cp-outline-variant:#e2e9ee;
/* ── semantic ── */
--cp-success:#2e7d32; --cp-error:#ba1a1a; --cp-warn:#ef6c00; --cp-info:#2c6e9b;
--cp-success-dim:rgba(46,125,50,.10); --cp-error-dim:rgba(186,26,26,.10); --cp-warn-dim:rgba(239,108,0,.12);
/* ── shape / depth ── */
--cp-radius:8px; --cp-radius-lg:13px; --cp-radius-pill:99px;
--cp-shadow-card:0 1px 6px rgba(21,36,44,.06),0 0 0 1px rgba(31,86,115,.05);
--cp-shadow-pop:0 8px 28px rgba(21,36,44,.16);
/* ── type ── */
--cp-font-head:'Montserrat','DM Sans',system-ui,sans-serif;
--cp-font-body:'DM Sans',system-ui,sans-serif;
--cp-font-mono:'JetBrains Mono','Courier New',monospace;
}
/* ── scope base ── */
.cp-scope{ font-family:var(--cp-font-body); color:var(--cp-text); }
.cp-scope h1,.cp-scope h2,.cp-scope h3,.cp-scope h4{ font-family:var(--cp-font-head); color:var(--cp-text); letter-spacing:-.01em; }
/* Accent colour is for LINKS. The :not() list has to exclude anchors that are really
buttons, or they get the module accent painted on top of their own background.
It already skipped the cp-native button classes but not the legacy ones, so
<a class="btn erp-btn erp-btn-primary">Save Proposal</a> on the proposal page
rendered #2b5fb8 text on its #854f00 background — a contrast ratio of 1.1:1
against the 4.5:1 AA floor, i.e. an all but invisible primary action. */
.cp-scope a:not(.cp-btn):not(.cp-nav-link):not(.btn):not(.erp-btn){ color:var(--cp-accent); }
/* buttons keep their own text colour regardless of the link rule above */
.cp-scope a.cp-btn-primary,.cp-scope a.cp-btn-success,.cp-scope a.cp-btn-danger{ color:var(--cp-on-accent); }
.cp-scope a.cp-btn-ghost{ color:var(--cp-text-muted); }
/* ── Phase 4: converge button SHAPE, never button COLOUR ──────────────────────
Measured across 14 rendered pages, `.btn.btn-primary` came back in 5 colours
and 2 radii. The colours are DELIBERATE — amber #854f00, indigo #4f46e5 etc.
are module accents from the token system, and flattening them would delete a
feature. The radii are not: the legacy `style.css` carries
`.btn:not(.btn-link):not(.btn-circle){ border-radius:2px }` at (0,3,0), so a
page that does not load a newer button skin falls through to 2px while its
neighbours render 8px. Same class, same product, different shape.
This wins on specificity (0,4,0) and only inside a cp scope, so legacy pages
are untouched. It sets radius alone — colour, padding and font stay with
whatever owns them. */
.cp-scope .btn:not(.btn-link):not(.btn-circle),
.cp-content .btn:not(.btn-link):not(.btn-circle){ border-radius:var(--cp-radius); }
/* ── card ── */
.cp-card{ background:var(--cp-surface-white); border:1px solid var(--cp-outline); border-radius:var(--cp-radius-lg); box-shadow:var(--cp-shadow-card); padding:18px 20px; margin-bottom:16px; }
.cp-card-t{ font-family:var(--cp-font-head); font-size:var(--cp-fs-base, 13px); font-weight:700; margin:0 0 14px; display:flex; align-items:center; gap:8px; }
.cp-card-t .mi{ font-size:var(--cp-fs-lg, 16px); color:var(--cp-accent); }
.cp-card-sub{ font-size:12px; color:var(--cp-text-faint); font-weight:400; margin-left:auto; }
/* ── metric strip ── */
.cp-metrics{ display:grid; grid-template-columns:repeat(auto-fill,minmax(172px,1fr)); gap:13px; margin-bottom:18px; }
.cp-metric{ background:var(--cp-surface-white); border:1px solid var(--cp-outline); border-radius:var(--cp-radius-lg); box-shadow:var(--cp-shadow-card); padding:15px 17px; min-width:0; }
/* A metric card may render as <a> when its figure is traceable to a list/report.
An anchor is inline and inherits link colour, which would collapse the card and
tint the number, so undo both. Shared here so every dashboard gets it for free. */
a.cp-metric{ display:block; color:inherit; text-decoration:none; transition:border-color .15s, box-shadow .15s, transform .15s; }
a.cp-metric:hover, a.cp-metric:focus{ color:inherit; text-decoration:none; border-color:var(--cp-accent, var(--cp-primary)); box-shadow:var(--cp-shadow-card-hover, var(--cp-shadow-card)); transform:translateY(-2px); }
/* Where a metric has no list route to open, it jumps to the card on this page that
breaks it down. Flash that card so the landing is not silent. scroll-margin keeps
it clear of the sticky topbar. */
.cp-card[id]{ scroll-margin-top:78px; }
.cp-card:target{ border-color:var(--cp-accent, var(--cp-primary)); box-shadow:0 0 0 3px var(--cp-accent-dim, rgba(31,86,115,.14)); }
.cp-metric .mi{ font-size:18px; color:var(--cp-accent); }
.cp-metric-val{ font-family:var(--cp-font-head); font-size:22px; font-weight:800; letter-spacing:-.02em; margin-top:6px; line-height:1.15; overflow-wrap:anywhere; font-variant-numeric:tabular-nums; }
.cp-metric-lbl{ font-family:var(--cp-font-mono); font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.05em; color:var(--cp-text-faint); margin-top:2px; }
/* ── table ── */
.cp-table{ width:100%; border-collapse:collapse; font-size:var(--cp-fs-sm, 12.5px); }
.cp-table th{ font-family:var(--cp-font-mono); font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.05em; color:var(--cp-text-faint); text-align:left; padding:9px 11px; border-bottom:1px solid var(--cp-outline); background:var(--cp-surface-low); white-space:nowrap; }
.cp-table td{ padding:10px 11px; border-bottom:1px solid var(--cp-surface-container); color:var(--cp-text-muted); vertical-align:middle; }
.cp-table tbody tr:hover td{ background:var(--cp-accent-dim); }
.cp-table td.num,.cp-table th.num{ text-align:right; font-variant-numeric:tabular-nums; }
.cp-table td.strong{ color:var(--cp-text); font-weight:600; }
/* ── definition grid (document detail) ── */
.cp-dl{ display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:2px 22px; }
.cp-dl .cp-dl-i{ margin-bottom:10px; }
.cp-dl dt{ font-family:var(--cp-font-mono); font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.05em; color:var(--cp-text-faint); margin-bottom:2px; }
.cp-dl dd{ font-size:var(--cp-fs-base, 13px); color:var(--cp-text); margin:0; font-weight:500; }
/* ── buttons ── */
.cp-btn{ display:inline-flex; align-items:center; gap:6px; padding:8px 15px; border-radius:var(--cp-radius); font-family:var(--cp-font-body); font-size:var(--cp-fs-sm, 12.5px); font-weight:600; border:1px solid transparent; cursor:pointer; text-decoration:none; transition:background .12s,border-color .12s; line-height:1.2; }
.cp-btn .mi{ font-size:var(--cp-fs-md, 15px); }
.cp-btn-primary{ background:var(--cp-accent); color:var(--cp-on-accent); }
.cp-btn-primary:hover{ background:var(--cp-accent-hover); color:var(--cp-on-accent); text-decoration:none; }
.cp-btn-ghost{ background:var(--cp-surface-white); color:var(--cp-text-muted); border-color:var(--cp-outline); }
.cp-btn-ghost:hover{ background:var(--cp-surface-low); color:var(--cp-text); text-decoration:none; }
.cp-btn-success{ background:var(--cp-success); color:var(--cp-on-accent, #fff); }
.cp-btn-danger{ background:var(--cp-error); color:var(--cp-on-accent, #fff); }
.cp-btn-sm{ padding:5px 10px; font-size:12px; }
/* ── pill / status ── */
.cp-pill{ display:inline-flex; align-items:center; gap:5px; font-family:var(--cp-font-mono); font-size:11px; font-weight:600; letter-spacing:.04em; text-transform:uppercase; padding:3px 10px; border-radius:var(--cp-radius-pill); }
.cp-pill-accent{ background:var(--cp-accent-dim); color:var(--cp-accent); }
.cp-pill-success{ background:var(--cp-success-dim); color:var(--cp-success); }
.cp-pill-warn{ background:var(--cp-warn-dim); color:var(--cp-warn); }
.cp-pill-danger{ background:var(--cp-error-dim); color:var(--cp-error); }
.cp-pill-muted{ background:var(--cp-surface-container); color:var(--cp-text-muted); }
/* ── filter bar / fields ── */
.cp-filters{ display:flex; flex-wrap:wrap; gap:10px; align-items:flex-end; margin-bottom:14px; }
.cp-field{ display:flex; flex-direction:column; gap:4px; }
.cp-field label{ font-family:var(--cp-font-mono); font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.05em; color:var(--cp-text-faint); }
.cp-field input,.cp-field select{ padding:7px 10px; border:1px solid var(--cp-outline); border-radius:var(--cp-radius); font-size:var(--cp-fs-sm, 12.5px); background:var(--cp-surface-white); color:var(--cp-text); font-family:var(--cp-font-body); min-width:150px; }
/* ── empty / note ── */
.cp-empty{ padding:34px 20px; text-align:center; color:var(--cp-text-faint); font-size:var(--cp-fs-base, 13px); }
.cp-empty .mi{ font-size:34px; opacity:.5; display:block; margin-bottom:8px; }
.cp-empty-act{ margin-top:10px; }
/* Loading + error. A list had no shared way to say "working" or "this failed", so pages
either showed nothing or invented their own — one more vocabulary each time. Same
footprint as .cp-empty so a list can swap between the three states without reflowing. */
.cp-loading{ padding:34px 20px; text-align:center; color:var(--cp-text-faint); font-size:var(--cp-fs-base, 13px); }
.cp-loading::before{ content:''; display:inline-block; width:18px; height:18px; margin-bottom:8px;
border:2px solid var(--cp-outline); border-top-color:var(--cp-accent); border-radius:50%;
animation:cp-spin .7s linear infinite; }
@keyframes cp-spin{ to{ transform:rotate(360deg); } }
@media (prefers-reduced-motion: reduce){ .cp-loading::before{ animation:none; } }
.cp-error{ padding:14px 16px; border-radius:var(--cp-radius); font-size:var(--cp-fs-base, 13px); line-height:1.5;
background:var(--cp-error-dim); color:var(--cp-error); border:1px solid var(--cp-error); }
.cp-error .mi{ vertical-align:-4px; margin-right:6px; }
/* Form controls. 138 form-control uses on the proposal page alone had no cp equivalent, so
every form reached for Bootstrap 3 defaults. 32px min-height is the density floor from the
contract — the audit found controls rendering at 30px and smaller. */
.cp-field{ margin-bottom:12px; }
.cp-label{ display:block; font-size:12px; font-weight:600; color:var(--cp-text-muted); margin-bottom:4px; }
.cp-label .req{ color:var(--cp-error); margin-left:2px; }
.cp-input,.cp-select,.cp-textarea{ width:100%; min-height:32px; padding:6px 10px; font-family:inherit;
font-size:var(--cp-fs-base, 13px); color:var(--cp-text); background:var(--cp-surface-white);
border:1px solid var(--cp-outline); border-radius:var(--cp-radius); }
.cp-input:focus,.cp-select:focus,.cp-textarea:focus{ outline:none; border-color:var(--cp-accent);
box-shadow:0 0 0 3px var(--cp-accent-dim); }
.cp-input[disabled],.cp-select[disabled]{ background:var(--cp-surface-low); color:var(--cp-text-faint); }
.cp-textarea{ min-height:74px; resize:vertical; }
.cp-hint{ font-size:var(--cp-fs-xs, 11.5px); color:var(--cp-text-faint); margin-top:3px; }
.cp-field.is-invalid .cp-input,.cp-field.is-invalid .cp-select{ border-color:var(--cp-error); }
.cp-field.is-invalid .cp-hint{ color:var(--cp-error); }
.cp-note{ font-size:12px; color:var(--cp-text-faint); line-height:1.5; }
</style>