feat(design): the panel now asks whether the app agrees with its own sheet
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 58s
CI & Build / Build & push image (push) Successful in 43s

Retiring rulebook #2 left the /design drift panel with no data source, and
because its empty state was well-written the feature read as working while it
could only ever render "nothing designated" (#2419). The original question is
genuinely gone: theme.css is generated from design system 2, so checking the
system against a sheet derived from it would be a tautology.

The question that survives is the one no server can answer. A generated sheet
still has to be LOADED and APPLIED, and nothing checked that it was:

  absent      the record declares a token the app doesn't have — the sheet was
              never regenerated after the record changed, or never loaded
  differs     the app has it with another value — a stale sheet, or a later
              rule that overrode it
  unrecorded  the app declares a token in the record's own family that the
              record has never heard of

Both sides go through the same engine so the comparison is honest: declared
values are set on an offscreen probe and read back, which performs the same
var() substitution the browser already did to the live values. Comparing raw
strings would mark every derived token as drift.

The designation moved with the feature — design_rulebook_id becomes
ui_design_system_id, with a migration deleting the retired key rather than
leaving an inert row. The prose extractor it fed goes too (#2288 said its
runtime role ended when the import landed).

Three orphans of the same shape, found alongside and fixed here:

- darkOverriddenNames hardcoded [data-theme="dark"]. The sheet went dark-first
  months ago, so it matched nothing and the "mode-aware" flag silently left the
  gallery. Now matches the SHAPE of a mode selector, which also holds for an
  install whose modes aren't light and dark.
- groupFor's prefix table never heard of --fs-, so 110 tokens sat under
  "other". Groups now come from the record where there is one; the table can
  only know families that shipped with the product (rule #115).
- The type scale was a hand-written table of nine sizes marked "no token",
  true when written and false since the scale was recorded. Now rendered from
  whatever size tokens the sheet declares, so it can't go stale twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
This commit is contained in:
2026-08-03 20:50:03 -04:00
co-authored by Claude Opus 5
parent 841506b10c
commit 5b824c1626
12 changed files with 704 additions and 671 deletions
+237 -102
View File
@@ -6,7 +6,7 @@
* runtime rather than parsed from source, so what you see here is what the app
* is using right now.
*
* HONESTY RULE, and the reason parts of this page say "not implemented":
* HONESTY RULE, and the reason parts of this page can say "not implemented":
* a gallery of hand-written look-alikes drifts from the app within a month and
* then lies — which is the same failure this whole surface exists to catch. So
* every specimen below is either a REAL component imported from the app, or a
@@ -18,87 +18,179 @@
* `assets/components.css` is now the single definition (#2273), so the
* specimens below are the app's real classes — they cannot drift from the app
* without drifting the app itself.
*
* The panel at the top applies the same rule one level up: the sheet the app
* loads is generated from a design system, and nothing checked that the app
* ever loaded it (#2419).
*/
import { computed, onMounted, ref } from "vue";
import { computed, onMounted, ref, watch } from "vue";
import { fetchDesignExpectations } from "@/api/design";
import { fetchUiDesignSystem } from "@/api/design";
import { fetchResolvedTokens, type ResolvedToken } from "@/api/designSystems";
import DesignTabs from "@/components/DesignTabs.vue";
import PriorityBadge from "@/components/PriorityBadge.vue";
import StatusBadge from "@/components/StatusBadge.vue";
import TagPill from "@/components/TagPill.vue";
import { useTheme } from "@/composables/useTheme";
import {
compareToTokens,
rankFindings,
compareToApp,
rankAgreements,
summarise,
type Expectation,
type Finding,
valueForMode,
type Agreement,
type RecordedToken,
} from "@/utils/designDrift";
import { groupTokens, readTokens, type DesignToken, type TokenGroup } from "@/utils/designTokens";
import {
groupTokens,
readTokens,
resolveDeclared,
type DesignToken,
type TokenGroup,
} from "@/utils/designTokens";
const { theme } = useTheme();
const tokens = ref<DesignToken[]>([]);
const expectations = ref<Expectation[]>([]);
const designRulebookId = ref<number | null>(null);
const driftLoaded = ref(false);
const showCleanRows = ref(false);
/** The designation, and the two ways it can be absent — see api/design.ts. */
const systemId = ref<number | null>(null);
const systemTitle = ref<string | null>(null);
const checkLoaded = ref(false);
const checkFailed = ref(false);
const showAgreeingRows = ref(false);
/** The record, as fetched. Kept raw because the mode narrowing has to be redone
* whenever the theme changes — a comparison against the wrong mode's values
* would report every mode-aware token as drift. */
const records = ref<ResolvedToken[]>([]);
const recorded = ref<RecordedToken[]>([]);
const resolved = ref<Map<string, string>>(new Map());
/** Narrow the record to the live mode and re-read the app. Idempotent. */
function recheck() {
tokens.value = readTokens();
recorded.value = records.value.map((t) => ({
name: t.name,
value: valueForMode(t.value_by_mode, theme.value),
groupName: t.group_name,
}));
const declared = new Map<string, string>();
for (const token of recorded.value) {
if (token.value) declared.set(token.name, token.value);
}
resolved.value = resolveDeclared(declared);
}
/**
* Read on mount, not at module scope: the values depend on the live cascade,
* which needs the app's stylesheets applied and the theme attribute set.
*/
onMounted(async () => {
tokens.value = readTokens();
recheck();
try {
const response = await fetchDesignExpectations();
designRulebookId.value = response.rulebook_id;
expectations.value = response.expectations;
const designation = await fetchUiDesignSystem();
systemId.value = designation.design_system_id;
systemTitle.value = designation.title;
if (designation.design_system_id !== null && designation.title !== null) {
records.value = (await fetchResolvedTokens(designation.design_system_id)).tokens;
recheck();
}
} catch {
// The gallery is useful without the panel, so a failed fetch degrades to
// "no drift data" rather than taking the page down with it.
designRulebookId.value = null;
// "couldn't check" rather than taking the page down with it. It says so
// rather than showing the same empty state as "nothing designated" — that
// conflation is what let this feature sit dead (#2419).
checkFailed.value = true;
} finally {
driftLoaded.value = true;
checkLoaded.value = true;
}
});
const findings = computed<Finding[]>(() =>
rankFindings(compareToTokens(expectations.value, tokens.value)),
// Toggling the theme on this page is the most likely thing anyone does here.
watch(theme, () => recheck());
const agreements = computed<Agreement[]>(() =>
rankAgreements(compareToApp(recorded.value, resolved.value, tokens.value)),
);
const driftSummary = computed(() => summarise(findings.value));
const visibleFindings = computed(() =>
showCleanRows.value ? findings.value : findings.value.filter((f) => f.status !== "ok"),
const summary = computed(() => summarise(agreements.value));
const visibleAgreements = computed(() =>
showAgreeingRows.value
? agreements.value
: agreements.value.filter((a) => a.status !== "ok"),
);
/** Named but unvalued roles — skipped by the comparison, worth stating once. */
const unvaluedCount = computed(
() => records.value.filter((t) => !valueForMode(t.value_by_mode, theme.value)).length,
);
const grouped = computed(() => groupTokens(tokens.value));
const STATUS_LABEL: Record<Agreement["status"], string> = {
absent: "not in the app",
differs: "app renders another value",
unrecorded: "not in the record",
ok: "agrees",
};
/* Gallery ------------------------------------------------------------------ */
/**
* Groups come from the RECORD where there is one, and from the name prefix
* otherwise. The prefix table in designTokens.ts can only know the families
* that shipped with the product; the record knows the ones this install
* authored, and grouping 110 tokens under "other" because a table never heard
* of their prefix is a gallery nobody reads.
*/
const recordGroups = computed(() => {
const out = new Map<string, string>();
for (const token of records.value) {
if (token.group_name) out.set(token.name, token.group_name);
}
return out;
});
const grouped = computed(() => groupTokens(tokens.value, recordGroups.value));
/** Order for the prefix-derived groups only; record groups sort by name. */
const GROUP_ORDER: TokenGroup[] = ["color", "radius", "glow", "gradient", "focus", "layout", "other"];
const orderedGroups = computed(() =>
GROUP_ORDER.filter((g) => grouped.value.has(g)).map((g) => ({ group: g, tokens: grouped.value.get(g)! })),
);
const orderedGroups = computed(() => {
const fromRecord = new Set(recordGroups.value.values());
const rank = (g: string) => {
const i = GROUP_ORDER.indexOf(g as TokenGroup);
return i < 0 ? GROUP_ORDER.length : i;
};
const keys = [...grouped.value.keys()].sort((a, b) => {
// The record's own groups lead: they are the system, and the prefix-derived
// ones are whatever else the sheet happens to carry.
const byOrigin = Number(!fromRecord.has(a)) - Number(!fromRecord.has(b));
if (byOrigin !== 0) return byOrigin;
if (fromRecord.has(a)) return a.localeCompare(b);
return rank(a) - rank(b);
});
return keys.map((group) => ({ group, tokens: grouped.value.get(group)! }));
});
/** A token whose value reads as a colour is worth showing as a swatch. */
function isColourish(value: string): boolean {
return /^(#|rgba?\(|hsla?\(|color-mix\()/.test(value.trim());
}
/** Rule 65's four variants — none of which exists as a shared artifact (#2273). */
const RULEBOOK_BUTTONS = [
{ name: "Primary", spec: "Moss #4A5D3F bg, Parchment text, no border" },
{ name: "Secondary", spec: "Bronze #8B7355 bg, Parchment text, no border" },
{ name: "Ghost", spec: "transparent, Parchment text, 0.5px Pewter border" },
{ name: "Destructive", spec: "Oxblood #6B2118 bg, Parchment text, pair with icon" },
/** The button family as shared classes, described by the roles they reach for. */
const BUTTON_VARIANTS = [
{ name: "Primary", spec: "action-primary background, text-on-action label, no border" },
{ name: "Secondary", spec: "action-secondary background, text-on-action label, no border" },
{ name: "Ghost", spec: "transparent, primary text, one-pixel border" },
{ name: "Danger", spec: "action-destructive background, text-on-action label" },
];
const TYPE_SPECIMENS = [
{ token: "Display", spec: "40 / 500 / Fraunces" },
{ token: "H1", spec: "32 / 500 / Fraunces" },
{ token: "H2", spec: "24 / 500 / Fraunces" },
{ token: "H3", spec: "18 / 500 / Inter" },
{ token: "Body", spec: "15 / 400 / Inter" },
{ token: "Body small", spec: "13 / 400 / Inter" },
{ token: "Label", spec: "12 / 500 / Inter" },
{ token: "Code", spec: "13 / 400 / JetBrains Mono" },
{ token: "Tiny", spec: "11 / 500 / Inter, uppercase +0.08em" },
];
/**
* The type scale, read from whatever size tokens the sheet actually declares.
*
* Was a hand-written table of nine sizes marked "no token" — true when written,
* and false since the scale was recorded. Deriving it from the live tokens is
* what stops it going stale a second time: if the scale is removed, this
* section empties out and says so.
*/
const sizeTokens = computed(() => tokens.value.filter((t) => /-size(-|$)/.test(t.name)));
</script>
<template>
@@ -116,78 +208,108 @@ const TYPE_SPECIMENS = [
</p>
</header>
<!-- Drift: what the rulebook claims vs what the tokens do. -->
<!-- Does the running app match the design system it was generated from? -->
<section class="design-section">
<h2>Rulebook drift</h2>
<h2>Agreement with the record</h2>
<p v-if="!driftLoaded" class="muted">Checking against the design rulebook</p>
<p v-if="!checkLoaded" class="muted">
Checking the running app against its design system
</p>
<div v-else-if="designRulebookId === null" class="gap-notice">
<strong>No design rulebook designated.</strong>
<div v-else-if="checkFailed" class="gap-notice">
<strong>The design system couldn't be read.</strong>
<p>
This install hasn't said which rulebook describes its design system, so
there is nothing to check the tokens against. Designate one in
<router-link to="/settings">Settings</router-link> and this panel will
compare every colour and token the rulebook names against what the
stylesheet actually resolves to.
The gallery below is still live — it comes from the browser, not the
server — but nothing is being compared against the record right now.
This is a failure, not an empty result.
</p>
</div>
<div v-else-if="systemId === null" class="gap-notice">
<strong>No design system designated for this UI.</strong>
<p>
This install hasn't said which design system its own interface is
built from, so there is nothing to check the running app against.
Designate one in <router-link to="/settings">Settings</router-link>
and this panel will report every token the record declares that the
app doesn't have, renders differently, or has never heard of.
</p>
</div>
<div v-else-if="systemTitle === null" class="gap-notice">
<strong>Design system #{{ systemId }} could not be read.</strong>
<p>
It is designated in <router-link to="/settings">Settings</router-link>,
but it has since been deleted or is no longer shared with you. Nothing
is being checked — this is a misconfiguration, not a clean result.
</p>
</div>
<template v-else>
<p class="section-note">
<strong>{{ driftSummary.violated }}</strong> violated ·
<strong>{{ driftSummary.missing }}</strong> missing ·
{{ driftSummary.ok }} matching, from {{ driftSummary.total }} checkable
claims in rulebook #{{ designRulebookId }}.
<strong>{{ summary.absent }}</strong> not in the app ·
<strong>{{ summary.differs }}</strong> rendering another value ·
{{ summary.unrecorded }} not in the record ·
{{ summary.ok }} agreeing, across {{ summary.total }} tokens checked
against <strong>{{ systemTitle }}</strong> in
<code>{{ theme }}</code> mode.
<template v-if="unvaluedCount">
{{ unvaluedCount }} recorded {{ unvaluedCount === 1 ? "role has" : "roles have" }}
no value yet and {{ unvaluedCount === 1 ? "was" : "were" }} not checked.
</template>
</p>
<div class="gap-notice">
<strong>This compares the rulebook against the TOKENS only.</strong>
<strong>This compares the record against the TOKENS only.</strong>
<p>
A value hardcoded in a component — where a token should have been
referenced — is invisible here, because the drift isn't in the tokens
at all. Reading it would mean bundling every component's source into
the app. That check belongs in CI and is tracked separately, so treat
a clean panel as "the tokens agree", not "the app agrees".
referenced — is invisible here, because the drift isn't in the
tokens at all. Reading it would mean bundling every component's
source into the app. That check belongs in CI and is tracked
separately, so treat a clean panel as "the tokens agree", not "the
app agrees".
</p>
</div>
<p v-if="!findings.length" class="muted">
The rulebook names nothing this panel can check. Rules that state values
— colours, token names — produce claims; rules that state judgement
don't, by design.
<p v-if="!agreements.length" class="muted">
The record declares no valued tokens, so there is nothing to compare
yet. Give its roles values and this panel starts reporting.
</p>
<ul v-else class="spec-list">
<li v-for="finding in visibleFindings" :key="`${finding.expectation.kind}:${finding.expectation.value}`">
<li v-for="row in visibleAgreements" :key="row.name">
<span class="spec-name">
<span
v-if="finding.expectation.kind !== 'token'"
v-if="isColourish(row.live || row.recorded)"
class="swatch"
:style="{ background: finding.expectation.value }"
:style="{ background: row.live || row.recorded }"
aria-hidden="true"
/>
<code>{{ finding.expectation.value }}</code>
<code>{{ row.name }}</code>
</span>
<span class="spec-detail">
rule #{{ finding.expectation.rule_id }} {{ finding.expectation.rule_title }}
<span v-if="finding.matches.length" class="matches">
· {{ finding.matches.join(", ") }}
</span>
</span>
<span class="spec-status" :class="finding.status">
{{ finding.status === "violated" ? "forbidden, but present"
: finding.status === "missing" ? "not in the stylesheet" : "ok" }}
<template v-if="row.status === 'differs'">
record <code>{{ row.recorded }}</code> · app
<code>{{ row.live }}</code>
</template>
<template v-else-if="row.status === 'unrecorded'">
app <code>{{ row.live }}</code>
</template>
<template v-else>
record <code>{{ row.recorded }}</code>
</template>
<span v-if="row.groupName" class="matches"> · {{ row.groupName }}</span>
</span>
<span class="spec-status" :class="row.status">{{ STATUS_LABEL[row.status] }}</span>
</li>
</ul>
<button
v-if="findings.length && driftSummary.ok"
v-if="agreements.length && summary.ok"
class="reveal-toggle"
@click="showCleanRows = !showCleanRows"
@click="showAgreeingRows = !showAgreeingRows"
>
{{ showCleanRows ? "Hide" : "Show" }} the {{ driftSummary.ok }} matching claims
{{ showAgreeingRows ? "Hide" : "Show" }} the {{ summary.ok }} agreeing tokens
</button>
</template>
</section>
@@ -249,7 +371,7 @@ const TYPE_SPECIMENS = [
<button class="btn-primary btn-inline">Inline</button>
</div>
<ul class="spec-list">
<li v-for="b in RULEBOOK_BUTTONS" :key="b.name">
<li v-for="b in BUTTON_VARIANTS" :key="b.name">
<span class="spec-name">{{ b.name }}</span>
<span class="spec-detail">{{ b.spec }}</span>
<span class="spec-status ok">shared</span>
@@ -257,23 +379,21 @@ const TYPE_SPECIMENS = [
</ul>
</section>
<!-- Typography: the families load, the scale does not exist as tokens. -->
<!-- Type: rendered at the sizes the sheet declares, not described. -->
<section class="design-section">
<h2>Type scale</h2>
<div class="gap-notice">
<strong>Families load; the scale has no tokens.</strong>
<div v-if="!sizeTokens.length" class="gap-notice">
<strong>The scale has no tokens.</strong>
<p>
Fraunces, Inter and JetBrains Mono are imported (rule 59), but rule 60's
scale is not expressed as custom properties, so sizes and weights are
set ad hoc per component. Listed here as specification, not as a live
specimen there is nothing to read.
Nothing in the stylesheet declares a size token, so sizes are being set
ad hoc per component. There is nothing live to show here.
</p>
</div>
<ul class="spec-list">
<li v-for="t in TYPE_SPECIMENS" :key="t.token">
<span class="spec-name">{{ t.token }}</span>
<span class="spec-detail">{{ t.spec }}</span>
<span class="spec-status missing">no token</span>
<ul v-else class="spec-list">
<li v-for="t in sizeTokens" :key="t.name">
<span class="spec-name" :style="{ fontSize: t.value }">Ag</span>
<span class="spec-detail"><code>{{ t.name }}</code></span>
<span class="spec-status ok">{{ t.value || "—" }}</span>
</li>
</ul>
</section>
@@ -292,7 +412,7 @@ const TYPE_SPECIMENS = [
<span v-else class="swatch swatch-none" aria-hidden="true" />
<code class="token-name">{{ token.name }}</code>
<code class="token-value">{{ token.value || "—" }}</code>
<span v-if="token.overriddenInDark" class="token-flag" title="Re-declared in the dark block">
<span v-if="token.modeAware" class="token-flag" title="Re-declared under a mode selector">
mode-aware
</span>
</li>
@@ -430,6 +550,7 @@ const TYPE_SPECIMENS = [
color: var(--color-text-secondary);
font-size: 0.85rem;
flex: 1;
word-break: break-word;
}
.spec-status {
@@ -438,6 +559,25 @@ const TYPE_SPECIMENS = [
letter-spacing: 0.08em;
padding: 0.1rem 0.45rem;
border-radius: var(--radius-sm);
white-space: nowrap;
}
/* `absent` is the one status that can mean the whole sheet failed to load, so
it carries the same weight as a high-priority finding; `differs` is a wrong
value on screen right now; `unrecorded` is bookkeeping and reads quietest. */
.spec-status.absent {
background: var(--color-priority-high-bg);
color: var(--color-priority-high);
}
.spec-status.differs {
background: var(--color-priority-medium-bg);
color: var(--color-priority-medium);
}
.spec-status.unrecorded {
background: var(--color-surface);
color: var(--color-text-muted);
}
.spec-status.missing {
@@ -445,11 +585,6 @@ const TYPE_SPECIMENS = [
color: var(--color-priority-medium);
}
.spec-status.violated {
background: var(--color-priority-high-bg);
color: var(--color-priority-high);
}
.spec-status.ok {
background: var(--color-status-done-bg);
color: var(--color-status-done);
+28 -22
View File
@@ -4,7 +4,7 @@ import { useSettingsStore } from "@/stores/settings";
import { useAuthStore } from "@/stores/auth";
import { useToastStore } from "@/stores/toast";
import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, getProfile, updateProfile, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, type UserProfile } from "@/api/client";
import { listRulebooks } from "@/api/rulebooks";
import { fetchDesignSystems } from "@/api/designSystems";
import type { User } from "@/types/auth";
import PaginationBar from "@/components/PaginationBar.vue";
import TagInput from "@/components/TagInput.vue";
@@ -32,11 +32,12 @@ const kbWritePathThreshold = ref("0.68");
// gate: that one BLOCKS a create and must be unforgiving of noise, this one only
// suggests a merge the operator reviews (services/dedup.py).
const kbDuplicateThreshold = ref("0.82");
// Which rulebook describes this install's design system, for the /design drift
// panel. Empty = none designated, which is the normal state for a fresh install
// rather than a misconfiguration — the panel explains itself when unset.
const designRulebookId = ref("");
const designRulebooks = ref<{ id: number; title: string }[]>([]);
// Which design system this install's own UI is built from, for the /design
// agreement panel. Empty = none designated, which is the normal state for a
// fresh install rather than a misconfiguration — the panel explains itself when
// unset. Replaced design_rulebook_id when the rulebook was retired (#2419).
const uiDesignSystemId = ref("");
const designSystems = ref<{ id: number; title: string }[]>([]);
const savingKbInject = ref(false);
const kbInjectSaved = ref(false);
@@ -107,8 +108,8 @@ async function saveKbInject() {
kb_writepath_threshold: String(wpT),
kb_duplicate_threshold: String(dupT),
// Empty string DELETES the setting (see routes/settings.py), which is
// exactly right for "no design rulebook" — absent rather than zero.
design_rulebook_id: designRulebookId.value,
// exactly right for "no design system" — absent rather than zero.
ui_design_system_id: uiDesignSystemId.value,
});
kbInjectSaved.value = true;
setTimeout(() => (kbInjectSaved.value = false), 2000);
@@ -499,13 +500,15 @@ onMounted(async () => {
if (allSettings.kb_duplicate_threshold !== undefined) {
kbDuplicateThreshold.value = allSettings.kb_duplicate_threshold;
}
designRulebookId.value = allSettings.design_rulebook_id ?? "";
uiDesignSystemId.value = allSettings.ui_design_system_id ?? "";
// Best-effort: the picker degrades to "none available" rather than blocking
// the whole settings page if rulebooks can't be listed.
// the whole settings page if design systems can't be listed.
try {
designRulebooks.value = (await listRulebooks()).map((r) => ({ id: r.id, title: r.title }));
designSystems.value = (await fetchDesignSystems()).design_systems.map(
(s) => ({ id: s.id, title: s.title }),
);
} catch {
designRulebooks.value = [];
designSystems.value = [];
}
if (allSettings.notify_task_reminders !== undefined) {
notifyTaskReminders.value = allSettings.notify_task_reminders !== "false";
@@ -1278,19 +1281,22 @@ function formatUserDate(iso: string): string {
</p>
</div>
<div class="field">
<label for="design-rulebook">Design-system rulebook</label>
<select id="design-rulebook" v-model="designRulebookId" class="input" style="max-width: 22rem">
<option value="">None — don't check for design drift</option>
<option v-for="rb in designRulebooks" :key="rb.id" :value="String(rb.id)">
{{ rb.title }}
<label for="ui-design-system">This app's design system</label>
<select id="ui-design-system" v-model="uiDesignSystemId" class="input" style="max-width: 22rem">
<option value="">None don't check the interface against a record</option>
<option v-for="ds in designSystems" :key="ds.id" :value="String(ds.id)">
{{ ds.title }}
</option>
</select>
<p class="field-hint">
Which rulebook describes how this app should look. Once set, the
<router-link to="/design">Design</router-link> page compares every colour
and token your rules name against what the stylesheet actually resolves
to, and reports where they disagree. Leave it as None if your rules
don't describe a design system — nothing else depends on this.
Which design system this interface is supposed to be built from. Once
set, the <router-link to="/design">Design</router-link> page compares
every token the system declares against what the browser has actually
resolved, and reports the ones the app is missing, renders differently,
or has never heard of. That catches a stylesheet that was regenerated
but never shipped — which the record alone cannot tell you, since the
sheet is generated from it. Leave it as None if this install's
interface isn't described by one of your design systems.
</p>
</div>
<div class="field">