diff --git a/tests/Architecture/TypographyTokenContractTest.php b/tests/Architecture/TypographyTokenContractTest.php
new file mode 100644
index 0000000..8fe425c
--- /dev/null
+++ b/tests/Architecture/TypographyTokenContractTest.php
@@ -0,0 +1,214 @@
+
+ */
+ private const EXCLUDED_PATHS = [
+ 'web/css/vendor/',
+ 'web/css/vendor-overrides/',
+ 'web/css/base/variables.base.css',
+ ];
+
+ /**
+ * Patterns that identify intentional exceptions (icon fonts, em-relative badges, etc.).
+ *
+ * @var list
+ */
+ private const EXCEPTION_MARKERS = [
+ 'icon-font metric',
+ 'em-relative sizing',
+ 'intentional px',
+ ];
+
+ // ──────────────────────────────────────────────────────────────
+ // font-size
+ // ──────────────────────────────────────────────────────────────
+
+ public function testFontSizeUsesTokens(): void
+ {
+ $violations = $this->scanForRawValues(
+ '/font-size\s*:\s*+(?!var\(--text-|var\(--app-|inherit|unset|initial|revert|small\b|0[;\s])/',
+ self::EXCEPTION_MARKERS,
+ );
+
+ $this->assertSame(
+ [],
+ $violations,
+ "Raw font-size values found instead of --text-* tokens:\n" . implode("\n", $violations),
+ );
+ }
+
+ // ──────────────────────────────────────────────────────────────
+ // line-height
+ // ──────────────────────────────────────────────────────────────
+
+ public function testLineHeightUsesTokens(): void
+ {
+ $violations = $this->scanForRawValues(
+ '/(?assertSame(
+ [],
+ $violations,
+ "Raw line-height values found instead of --leading-* tokens:\n" . implode("\n", $violations),
+ );
+ }
+
+ // ──────────────────────────────────────────────────────────────
+ // font-weight
+ // ──────────────────────────────────────────────────────────────
+
+ public function testFontWeightUsesTokens(): void
+ {
+ $violations = $this->scanForRawValues(
+ '/font-weight\s*:\s*+(?!var\(--font-|var\(--app-font-weight\)|var\(--app-form-label-font-weight|inherit|unset|initial|revert|normal|bold\b|bolder|lighter)/',
+ self::EXCEPTION_MARKERS,
+ );
+
+ $this->assertSame(
+ [],
+ $violations,
+ "Raw font-weight values found instead of --font-* tokens:\n" . implode("\n", $violations),
+ );
+ }
+
+ // ──────────────────────────────────────────────────────────────
+ // Token definitions exist
+ // ──────────────────────────────────────────────────────────────
+
+ public function testTypographyTokensAreDefined(): void
+ {
+ $content = $this->readProjectFile('web/css/base/variables.base.css');
+
+ $requiredTokens = [
+ '--text-2xs', '--text-xs', '--text-sm', '--text-base', '--text-lg',
+ '--text-xl', '--text-2xl', '--text-3xl', '--text-4xl', '--text-5xl',
+ '--leading-none', '--leading-tight', '--leading-snug', '--leading-normal',
+ '--leading-relaxed', '--leading-loose',
+ '--font-regular', '--font-medium', '--font-semibold', '--font-bold',
+ '--tracking-tight', '--tracking-normal', '--tracking-wide',
+ ];
+
+ foreach ($requiredTokens as $token) {
+ $this->assertStringContainsString(
+ $token . ':',
+ $content,
+ "Typography token {$token} is missing from variables.base.css",
+ );
+ }
+ }
+
+ public function testHeadingTokensUseClamp(): void
+ {
+ $content = $this->readProjectFile('web/css/base/variables.base.css');
+
+ $clampTokens = ['--text-2xl', '--text-3xl', '--text-4xl', '--text-5xl'];
+
+ foreach ($clampTokens as $token) {
+ $this->assertMatchesRegularExpression(
+ '/' . preg_quote($token, '/') . ':\s*clamp\(/',
+ $content,
+ "Heading token {$token} must use clamp() for responsive scaling",
+ );
+ }
+ }
+
+ // ──────────────────────────────────────────────────────────────
+ // Helpers
+ // ──────────────────────────────────────────────────────────────
+
+ /**
+ * Scan all CSS files in web/css/ for lines matching the violation pattern,
+ * excluding allowed paths and lines that contain an exception marker.
+ *
+ * @param string $violationPattern Regex that matches a raw value
+ * @param list $exceptionMarkers Strings whose presence on the same line excuses the match
+ * @return list Violation descriptions ("file:line: content")
+ */
+ private function scanForRawValues(string $violationPattern, array $exceptionMarkers): array
+ {
+ $root = $this->projectRootPath();
+ $cssDir = $root . '/web/css';
+ $this->assertDirectoryExists($cssDir);
+
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($cssDir),
+ );
+
+ $violations = [];
+
+ /** @var \SplFileInfo $file */
+ foreach ($iterator as $file) {
+ if (!$file->isFile() || $file->getExtension() !== 'css') {
+ continue;
+ }
+
+ $relativePath = str_replace($root . '/', '', $file->getPathname());
+
+ // Skip excluded paths
+ foreach (self::EXCLUDED_PATHS as $excluded) {
+ if (str_starts_with($relativePath, $excluded)) {
+ continue 2;
+ }
+ }
+
+ $content = file_get_contents($file->getPathname());
+ $this->assertNotFalse($content, 'Could not read file: ' . $relativePath);
+
+ $lines = explode("\n", $content);
+ foreach ($lines as $lineNumber => $line) {
+ if (!preg_match($violationPattern, $line)) {
+ continue;
+ }
+
+ // Check if the line contains a documented exception marker
+ $isException = false;
+ foreach ($exceptionMarkers as $marker) {
+ if (str_contains($line, $marker)) {
+ $isException = true;
+ break;
+ }
+ }
+
+ if ($isException) {
+ continue;
+ }
+
+ // Skip CSS custom property definitions (--app-font-size, --app-line-height, etc.)
+ if (preg_match('/^\s*--app-/', $line)) {
+ continue;
+ }
+
+ $violations[] = sprintf('%s:%d: %s', $relativePath, $lineNumber + 1, trim($line));
+ }
+ }
+
+ sort($violations);
+ return $violations;
+ }
+}
diff --git a/web/css/base/variables.base.css b/web/css/base/variables.base.css
index bcf6b60..eaee950 100644
--- a/web/css/base/variables.base.css
+++ b/web/css/base/variables.base.css
@@ -74,6 +74,37 @@
--app-empty-bg: var(--app-card-background-color);
--app-empty-text: var(--app-color);
--app-empty-hint: var(--app-muted-color);
+
+ /* ── Typography scale (major-third 1.25 ratio) ──────────────── */
+ --text-2xs: 0.625rem;
+ --text-xs: 0.75rem;
+ --text-sm: 0.875rem;
+ --text-base: 1rem;
+ --text-lg: 1.125rem;
+ --text-xl: 1.25rem;
+ --text-2xl: clamp(1.25rem, 1.1rem + 0.5vw, 1.5rem);
+ --text-3xl: clamp(1.5rem, 1.3rem + 0.75vw, 1.875rem);
+ --text-4xl: clamp(1.75rem, 1.5rem + 1vw, 2.25rem);
+ --text-5xl: clamp(2.25rem, 1.9rem + 1.5vw, 3rem);
+
+ /* ── Line-height tokens ─────────────────────────────────────── */
+ --leading-none: 1;
+ --leading-tight: 1.2;
+ --leading-snug: 1.375;
+ --leading-normal: 1.5;
+ --leading-relaxed: 1.625;
+ --leading-loose: 1.75;
+
+ /* ── Font-weight tokens ─────────────────────────────────────── */
+ --font-regular: 400;
+ --font-medium: 500;
+ --font-semibold: 600;
+ --font-bold: 700;
+
+ /* ── Letter-spacing tokens ──────────────────────────────────── */
+ --tracking-tight: -0.01em;
+ --tracking-normal: 0;
+ --tracking-wide: 0.025em;
}
/* Icons */
diff --git a/web/css/components/app-active-filter-chips.css b/web/css/components/app-active-filter-chips.css
index 23e938f..be060b2 100644
--- a/web/css/components/app-active-filter-chips.css
+++ b/web/css/components/app-active-filter-chips.css
@@ -38,9 +38,9 @@
align-items: center;
gap: 0.45rem;
padding: 0.3rem 0.65rem;
- font-size: 0.78rem;
- line-height: 1.15;
- font-weight: 500;
+ font-size: var(--text-xs);
+ line-height: var(--leading-tight);
+ font-weight: var(--font-medium);
color: var(--app-form-element-color);
--app-background-color: var(--app-form-element-background-color);
--app-border-color: var(--app-form-element-border-color);
@@ -76,7 +76,7 @@
width: 1rem;
height: 1rem;
border-radius: 999px;
- font-size: 0.58rem;
+ font-size: var(--text-2xs);
background: var(--app-muted-border-color);
}
diff --git a/web/css/components/app-badges.css b/web/css/components/app-badges.css
index 68cd4a5..df52e4f 100644
--- a/web/css/components/app-badges.css
+++ b/web/css/components/app-badges.css
@@ -7,9 +7,9 @@
gap: 0.35em;
padding: 0.15em 0.6em;
border-radius: 999px;
- font-size: 0.85em;
- font-weight: 400;
- line-height: 1.2;
+ font-size: 0.85em; /* em-relative sizing — intentional, scales with parent */
+ font-weight: var(--font-regular);
+ line-height: var(--leading-tight);
border: 1px solid transparent;
white-space: nowrap;
}
@@ -26,12 +26,12 @@
align-items: center;
justify-content: center;
color: inherit;
- line-height: 1;
- font-size: 10px;
+ line-height: var(--leading-none);
+ font-size: 10px; /* icon-font metric — intentional px */
}
.badge .badge-copy-icon .bi {
- font-size: 0.95em;
+ font-size: 0.95em; /* em-relative sizing — intentional, scales with parent */
}
.badge .badge-copy-icon .bi-check2 {
diff --git a/web/css/components/app-brand.css b/web/css/components/app-brand.css
index b7a7870..7a8508d 100644
--- a/web/css/components/app-brand.css
+++ b/web/css/components/app-brand.css
@@ -14,17 +14,17 @@ img.brand-logo {
}
.brand-name {
- line-height: 1;
- font-weight: bold;
+ line-height: var(--leading-none);
+ font-weight: var(--font-bold);
color: var(--app-contrast);
}
.brand-info {
- line-height: 1;
+ line-height: var(--leading-none);
}
.brand-slogan {
- font-size: 0.85em;
+ font-size: 0.85em; /* em-relative sizing — intentional, scales with parent */
color: var(--app-muted-color);
}
diff --git a/web/css/components/app-breadcrumb.css b/web/css/components/app-breadcrumb.css
index 3961740..ec78cc5 100644
--- a/web/css/components/app-breadcrumb.css
+++ b/web/css/components/app-breadcrumb.css
@@ -5,7 +5,7 @@
display: flex;
align-items: center;
gap: 3px;
- font-size: 11px;
+ font-size: var(--text-xs);
}
.app-breadcrumb a {
diff --git a/web/css/components/app-dashboard-titlebar.css b/web/css/components/app-dashboard-titlebar.css
index 66a4539..3aa160d 100644
--- a/web/css/components/app-dashboard-titlebar.css
+++ b/web/css/components/app-dashboard-titlebar.css
@@ -22,7 +22,7 @@
.app-dashboard-titlebar [role="button"] {
--app-button-padding-horizontal: 10px;
--app-button-padding-vertical: 6px;
- font-size: 13px;
+ font-size: var(--text-sm);
margin: 0;
}
@@ -37,7 +37,7 @@
background: var(--app-muted-border-color);
width: 35px;
height: 29px;
- font-size: 15px;
+ font-size: 15px; /* icon-font metric — intentional px */
border-radius: var(--app-border-radius);
display: inline-flex;
align-items: center;
@@ -49,7 +49,7 @@
display: flex;
align-items: center;
gap: 7px;
- line-height: 1;
+ line-height: var(--leading-none);
}
.app-dashboard-titlebar h1 a:hover i {
diff --git a/web/css/components/app-details-card.css b/web/css/components/app-details-card.css
index c38265d..1f4a369 100644
--- a/web/css/components/app-details-card.css
+++ b/web/css/components/app-details-card.css
@@ -12,8 +12,8 @@
details.app-details-card > summary {
margin: 0;
padding: calc(var(--app-spacing) * 0.75) var(--app-spacing);
- font-size: 0.85rem;
- line-height: 1.25;
+ font-size: var(--text-sm);
+ line-height: var(--leading-tight);
text-transform: none;
background: var(--app-card-sectioning-background-color);
display: flex;
diff --git a/web/css/components/app-details-titlebar.css b/web/css/components/app-details-titlebar.css
index f97b73f..c78f154 100644
--- a/web/css/components/app-details-titlebar.css
+++ b/web/css/components/app-details-titlebar.css
@@ -22,13 +22,13 @@
.app-details-titlebar [role="button"] {
--app-button-padding-horizontal: 10px;
--app-button-padding-vertical: 6px;
- font-size: 13px;
+ font-size: var(--text-sm);
margin: 0;
}
.app-details-titlebar button i,
.app-details-titlebar [role="button"] i {
- font-size: 10px;
+ font-size: 10px; /* icon-font metric — intentional px */
}
.app-details-titlebar-actions {
@@ -47,7 +47,7 @@
background: var(--app-background-color);
width: 35px;
height: 29px;
- font-size: 15px;
+ font-size: 15px; /* icon-font metric — intentional px */
border-radius: var(--app-border-radius);
display: inline-flex;
align-items: center;
@@ -62,7 +62,7 @@
display: flex;
align-items: center;
gap: 7px;
- line-height: 1;
+ line-height: var(--leading-none);
}
.app-details-titlebar details.dropdown {
margin-bottom: 0;
diff --git a/web/css/components/app-details-validation-summary.css b/web/css/components/app-details-validation-summary.css
index 64d0a55..5fe6cc7 100644
--- a/web/css/components/app-details-validation-summary.css
+++ b/web/css/components/app-details-validation-summary.css
@@ -27,7 +27,7 @@
.app-details-validation-summary-title {
margin: 0;
- font-weight: 600;
+ font-weight: var(--font-semibold);
color: var(--app-validation-summary-title-color);
}
diff --git a/web/css/components/app-details.css b/web/css/components/app-details.css
index 0a2d61e..d20628b 100644
--- a/web/css/components/app-details.css
+++ b/web/css/components/app-details.css
@@ -44,13 +44,13 @@
display: inline-flex;
align-items: center;
justify-content: center;
- font-weight: 600;
+ font-weight: var(--font-semibold);
color: var(--app-muted-color);
}
.user-avatar-placeholder,
.entity-avatar-placeholder {
- font-size: 1.5rem;
+ font-size: var(--text-2xl);
}
.user-avatar-image {
diff --git a/web/css/components/app-empty-state.css b/web/css/components/app-empty-state.css
index 9d95219..cd14dc8 100644
--- a/web/css/components/app-empty-state.css
+++ b/web/css/components/app-empty-state.css
@@ -41,15 +41,15 @@
.app-empty-state-message {
margin: 0;
color: var(--app-empty-hint);
- font-size: 12px;
- line-height: 1.4;
+ font-size: var(--text-xs);
+ line-height: var(--leading-snug);
}
.app-empty-state-hint {
margin: 0;
color: var(--app-empty-hint);
- font-size: 0.85rem;
- line-height: 1.3;
+ font-size: var(--text-sm);
+ line-height: var(--leading-snug);
opacity: 0.75;
}
diff --git a/web/css/components/app-filter-drawer.css b/web/css/components/app-filter-drawer.css
index 145fa86..2aec688 100644
--- a/web/css/components/app-filter-drawer.css
+++ b/web/css/components/app-filter-drawer.css
@@ -55,8 +55,8 @@
.app-filter-drawer-header h2 {
margin: 0;
- font-size: 0.8125rem;
- line-height: 1.2;
+ font-size: var(--text-sm);
+ line-height: var(--leading-tight);
}
.app-filter-drawer-header .icon-button {
@@ -111,7 +111,7 @@
.app-filter-drawer-body .app-list-toolbar .app-field > span,
.app-filter-drawer-body .app-list-toolbar > label > span {
margin: 0 !important;
- line-height: 1.2;
+ line-height: var(--leading-tight);
}
.app-filter-drawer-body .app-list-toolbar input:not([type="hidden"]),
@@ -153,7 +153,7 @@
.app-list-toolbar
.multi-select
.multi-select-header-option {
- line-height: 1.2;
+ line-height: var(--leading-tight);
}
.app-filter-drawer-body #address-book-custom-filters {
diff --git a/web/css/components/app-flash.css b/web/css/components/app-flash.css
index d2341b8..8db0981 100644
--- a/web/css/components/app-flash.css
+++ b/web/css/components/app-flash.css
@@ -60,8 +60,8 @@
position: relative;
overflow: hidden;
color: var(--app-notice-border-color);
- font-size: 0.875rem;
- line-height: 1.4;
+ font-size: var(--text-sm);
+ line-height: var(--leading-snug);
pointer-events: auto;
margin: 0;
animation: toast-slide-in 250ms ease-out;
@@ -159,7 +159,7 @@
opacity: 0.6;
cursor: pointer;
padding: 2px 4px;
- line-height: 1;
+ line-height: var(--leading-none);
flex-shrink: 0;
}
diff --git a/web/css/components/app-form-info-tile.css b/web/css/components/app-form-info-tile.css
index 6a3dce2..5631d10 100644
--- a/web/css/components/app-form-info-tile.css
+++ b/web/css/components/app-form-info-tile.css
@@ -28,13 +28,13 @@
.app-form-info-tile-label {
display: block;
color: var(--app-muted-color);
- line-height: 1.3;
+ line-height: var(--leading-snug);
}
.app-form-info-tile-value {
display: block;
color: var(--app-contrast);
- line-height: 1.35;
+ line-height: var(--leading-snug);
}
.app-form-info-tile--success {
diff --git a/web/css/components/app-forms.css b/web/css/components/app-forms.css
index 4e343c0..78be6d2 100644
--- a/web/css/components/app-forms.css
+++ b/web/css/components/app-forms.css
@@ -1,7 +1,7 @@
@layer components {
/* Form element enhancements — validation hints, color inputs, checkbox/radio labels, disabled states. */
.form-hint {
- font-size: small;
+ font-size: var(--text-sm);
}
ul.form-hint-list {
@@ -25,19 +25,19 @@
content: "\F26B";
font-family: "Bootstrap-icons";
margin-left: 4px;
- font-size: 10px;
+ font-size: 10px; /* icon-font metric — intentional px */
}
ul.form-hint-list li.is-invalid:after {
content: "\F33A";
font-family: "Bootstrap-icons";
margin-left: 4px;
- font-size: 10px;
+ font-size: 10px; /* icon-font metric — intentional px */
}
label:has([type="checkbox"], [type="radio"]) {
display: flex;
- line-height: 1;
+ line-height: var(--leading-none);
align-items: center;
}
diff --git a/web/css/components/app-list-tabs.css b/web/css/components/app-list-tabs.css
index f11f357..5e7e1ba 100644
--- a/web/css/components/app-list-tabs.css
+++ b/web/css/components/app-list-tabs.css
@@ -14,7 +14,7 @@
padding: 5px 10px 5px 10px;
color: var(--list-tabs-muted);
cursor: pointer;
- font-weight: 500;
+ font-weight: var(--font-medium);
border-bottom: 2px solid transparent;
text-decoration: none;
}
diff --git a/web/css/components/app-list-titlebar.css b/web/css/components/app-list-titlebar.css
index 658c29c..a21a5a0 100644
--- a/web/css/components/app-list-titlebar.css
+++ b/web/css/components/app-list-titlebar.css
@@ -22,7 +22,7 @@
.app-list-titlebar [role="button"] {
--app-button-padding-horizontal: 10px;
--app-button-padding-vertical: 6px;
- font-size: 13px;
+ font-size: var(--text-sm);
margin: 0;
}
@@ -37,7 +37,7 @@
background: var(--app-muted-border-color);
width: 35px;
height: 35px;
- font-size: 16px;
+ font-size: 16px; /* icon-font metric — intentional px */
border-radius: var(--app-border-radius);
display: inline-flex;
align-items: center;
diff --git a/web/css/components/app-list-toolbar.css b/web/css/components/app-list-toolbar.css
index 74ff3e2..92ab648 100644
--- a/web/css/components/app-list-toolbar.css
+++ b/web/css/components/app-list-toolbar.css
@@ -13,7 +13,7 @@
width: auto !important;
--app-form-element-spacing-horizontal: 10px;
--app-form-element-spacing-vertical: 6px;
- font-size: 13px !important;
+ font-size: var(--text-sm) !important;
height: auto !important;
}
.app-list-toolbar > * > * {
@@ -21,7 +21,7 @@
width: auto !important;
--app-form-element-spacing-horizontal: 10px;
--app-form-element-spacing-vertical: 6px;
- font-size: 13px !important;
+ font-size: var(--text-sm) !important;
height: auto !important;
}
.app-list-toolbar > * > * > * {
@@ -29,7 +29,7 @@
width: auto !important;
--app-form-element-spacing-horizontal: 10px;
--app-form-element-spacing-vertical: 6px;
- font-size: 13px !important;
+ font-size: var(--text-sm) !important;
height: auto !important;
}
@@ -55,7 +55,7 @@
.app-list-toolbar > label > span {
display: block;
- font-size: 11px !important;
+ font-size: var(--text-xs) !important;
}
.app-list-toolbar .app-grid-page-size select {
@@ -76,7 +76,7 @@
.multi-select
.multi-select-header
.multi-select-header-placeholder {
- font-size: 13px;
+ font-size: var(--text-sm);
}
.app-list-toolbar .multi-select .multi-select-header,
@@ -95,6 +95,6 @@
border: 0 !important;
padding: 0 !important;
padding-left: 3px !important;
- font-size: 11px !important;
+ font-size: var(--text-xs) !important;
}
}
diff --git a/web/css/components/app-search.css b/web/css/components/app-search.css
index c60e26e..9f0ed4a 100644
--- a/web/css/components/app-search.css
+++ b/web/css/components/app-search.css
@@ -15,8 +15,8 @@
border-radius: 6px;
background: var(--app-card-background-color);
color: var(--app-muted-color);
- font-size: 0.75rem;
- line-height: 1;
+ font-size: var(--text-xs);
+ line-height: var(--leading-none);
padding: 0.2rem 0.4rem;
pointer-events: none;
}
@@ -28,13 +28,13 @@
border-radius: var(--app-border-radius);
text-align: center;
color: var(--app-muted-color);
- font-size: 0.9rem;
+ font-size: var(--text-sm);
cursor: text;
}
.app-search-empty-state .bi-search {
display: inline-block;
- font-size: 1.25rem;
+ font-size: var(--text-xl);
margin-bottom: 0.5rem;
color: var(--app-color);
}
@@ -57,7 +57,7 @@ ul.app-search-results > li > a {
}
ul.app-search-results .badge {
- font-size: 10px;
+ font-size: 10px; /* icon-font metric — intentional px */
padding: 1px;
min-width: 18px;
text-align: center;
@@ -73,7 +73,7 @@ ul.app-search-results .badge {
ul.app-search-preview {
position: relative;
- font-size: 11px;
+ font-size: var(--text-xs);
padding: 0 0 0 0;
margin: 0;
opacity: 0.75;
@@ -120,7 +120,7 @@ input#side-search {
--app-form-element-spacing-horizontal: 10px;
--app-form-element-spacing-vertical: 6px;
height: auto;
- font-size: 13px;
+ font-size: var(--text-sm);
padding-right: 3rem;
margin-bottom:0;
}
@@ -160,18 +160,18 @@ ul.app-search-preview:has(li) {
.search-result-title {
- font-weight: 600;
+ font-weight: var(--font-semibold);
text-decoration: none;
}
.search-result-desc {
margin: 0;
color: var(--app-muted-color);
- font-size: small;
+ font-size: var(--text-sm);
}
.search-result-type {
color: var(--app-muted-color);
- font-weight: 600;
+ font-weight: var(--font-semibold);
}
}
diff --git a/web/css/components/app-tabs.css b/web/css/components/app-tabs.css
index 8eded2e..95bf390 100644
--- a/web/css/components/app-tabs.css
+++ b/web/css/components/app-tabs.css
@@ -40,8 +40,8 @@
border-radius: var(--app-border-radius);
background-color: transparent;
color: var(--app-secondary);
- font-size: 13px;
- line-height: 1;
+ font-size: var(--text-sm);
+ line-height: var(--leading-none);
cursor: pointer;
z-index: 1;
margin-block: auto;
diff --git a/web/css/components/app-tile.css b/web/css/components/app-tile.css
index 185a0d5..64c66f2 100644
--- a/web/css/components/app-tile.css
+++ b/web/css/components/app-tile.css
@@ -67,14 +67,14 @@
border-radius: 10px;
background: var(--tile-icon-bg);
color: var(--tile-icon-color);
- font-size: 1.1rem;
+ font-size: var(--text-lg);
}
.app-tile-count {
position: absolute;
top: calc(var(--app-spacing) * 1);
right: calc(var(--app-spacing) * 1);
- font-weight: 700;
+ font-weight: var(--font-bold);
color: var(--tile-count-color);
}
@@ -93,7 +93,7 @@
right: calc(var(--app-spacing) * 1);
bottom: calc(var(--app-spacing) * 1);
color: var(--tile-link-color);
- font-size: 0.9rem;
+ font-size: var(--text-sm);
}
.app-tile.is-small {
@@ -106,7 +106,7 @@
width: 25px;
height: 25px;
border-radius: 6px;
- font-size: 12px;
+ font-size: 12px; /* icon-font metric — intentional px */
margin-right: 10px;
}
@@ -121,14 +121,14 @@
}
.app-tile.is-small .app-tile-label {
margin-top: 0;
- font-weight: 400;
+ font-weight: var(--font-regular);
}
.app-stats-table-header {
border-bottom: 1px solid var(--app-border);
padding: 4px 10px 4px 10px;
text-transform: uppercase;
- font-size: 13px;
+ font-size: var(--text-sm);
letter-spacing: 0.4px;
background: var(--app-blockquote-background-color);
}
diff --git a/web/css/components/app-tooltips.css b/web/css/components/app-tooltips.css
index 192bee9..1ce5ecc 100644
--- a/web/css/components/app-tooltips.css
+++ b/web/css/components/app-tooltips.css
@@ -22,7 +22,7 @@
border: 1px solid var(--tooltip-border);
border-radius: var(--tooltip-radius);
box-shadow: var(--tooltip-shadow);
- font-size: 0.75rem;
+ font-size: var(--text-xs);
padding: 0.35rem 0.6rem;
white-space: nowrap;
}
diff --git a/web/css/layout/app-shell.css b/web/css/layout/app-shell.css
index ad19053..9a7ba77 100644
--- a/web/css/layout/app-shell.css
+++ b/web/css/layout/app-shell.css
@@ -32,42 +32,42 @@
h4,
h5,
h6 {
- --app-font-weight: 700;
+ --app-font-weight: var(--font-bold);
}
h1 {
- --app-font-size: 1.5rem;
- --app-line-height: 1.125;
+ --app-font-size: var(--text-2xl);
+ --app-line-height: var(--leading-tight);
--app-typography-spacing-top: 3rem;
}
h2 {
- --app-font-size: 1.4rem;
- --app-line-height: 1.15;
+ --app-font-size: var(--text-xl);
+ --app-line-height: var(--leading-tight);
--app-typography-spacing-top: 2.625rem;
}
h3 {
- --app-font-size: 1.3rem;
- --app-line-height: 1.175;
+ --app-font-size: var(--text-lg);
+ --app-line-height: var(--leading-tight);
--app-typography-spacing-top: 2.25rem;
}
h4 {
- --app-font-size: 1.2rem;
- --app-line-height: 1.2;
+ --app-font-size: var(--text-base);
+ --app-line-height: var(--leading-tight);
--app-typography-spacing-top: 1.874rem;
}
h5 {
- --app-font-size: 1.1rem;
- --app-line-height: 1.225;
+ --app-font-size: var(--text-base);
+ --app-line-height: var(--leading-tight);
--app-typography-spacing-top: 1.6875rem;
}
h6 {
- --app-font-size: 1rem;
- --app-line-height: 1.25;
+ --app-font-size: var(--text-sm);
+ --app-line-height: var(--leading-tight);
--app-typography-spacing-top: 1.5rem;
}
@@ -75,7 +75,7 @@
tfoot th,
thead td,
thead th {
- --app-font-weight: 600;
+ --app-font-weight: var(--font-semibold);
--app-border-width: 2px;
}
@@ -177,7 +177,7 @@
th {
text-transform: uppercase;
- font-size: 11px;
+ font-size: var(--text-xs);
letter-spacing: 0.4px;
border-top: 1px solid var(--app-border);
}
@@ -562,8 +562,8 @@
sub,
sup {
position: relative;
- font-size: 0.75em;
- line-height: 0;
+ font-size: 0.75em; /* em-relative sizing — intentional, scales with parent */
+ line-height: 0; /* intentional px — sub/sup vertical alignment */
vertical-align: baseline;
}
@@ -654,7 +654,7 @@
hgroup > :not(:first-child):last-child {
--app-color: var(--app-muted-color);
--app-font-weight: unset;
- font-size: 1rem;
+ font-size: var(--text-base);
}
:where(ol, ul) li {
@@ -848,7 +848,7 @@
box-shadow: var(--app-box-shadow);
color: var(--app-color);
font-weight: var(--app-font-weight);
- font-size: 1rem;
+ font-size: var(--text-base);
line-height: var(--app-line-height);
text-align: center;
text-decoration: none;
@@ -872,7 +872,7 @@
).small {
--app-button-padding-vertical: var(--app-button-padding-vertical-small);
--app-button-padding-horizontal: var(--app-button-padding-horizontal-small);
- font-size: 13px;
+ font-size: var(--text-sm);
}
[role="button"]:is(:hover, :active, :focus),
@@ -1170,7 +1170,7 @@
kbd,
pre,
samp {
- font-size: 0.875em;
+ font-size: 0.875em; /* em-relative sizing — intentional, scales with parent */
font-family: var(--app-font-family);
}
@@ -1240,7 +1240,7 @@
--app-box-shadow: none;
--app-button-padding-vertical: var(--app-button-padding-vertical-small);
--app-button-padding-horizontal: var(--app-button-padding-horizontal-small);
- font-size: 0.75rem;
+ font-size: var(--text-xs);
}
.code-toolbar > .toolbar .copy-to-clipboard-button:is(:hover, :focus) {
@@ -1291,7 +1291,7 @@
select,
textarea {
margin: 0;
- font-size: 1rem;
+ font-size: var(--text-base);
line-height: var(--app-line-height);
font-family: inherit;
letter-spacing: inherit;
@@ -1780,7 +1780,7 @@
border: var(--app-border-width) solid var(--app-border-color);
border-radius: 1.25em;
background-color: var(--app-background-color);
- line-height: 1.25em;
+ line-height: 1.25em; /* em-relative sizing — intentional, switch control layout */
}
[type="checkbox"][role="switch"]:not([aria-invalid]) {
@@ -2159,7 +2159,7 @@
}
details summary {
- line-height: 1rem;
+ line-height: var(--leading-none);
list-style-type: none;
cursor: pointer;
transition: color var(--app-transition);
@@ -2937,7 +2937,7 @@
--app-nav-link-spacing-horizontal: 0.25rem;
align-items: center;
justify-content: start;
- font-size: 0.875em;
+ font-size: 0.875em; /* em-relative sizing — intentional, scales with parent */
}
nav[aria-label="breadcrumb"] ol,
diff --git a/web/css/layout/app-sidebar.css b/web/css/layout/app-sidebar.css
index 4c8e6c4..c41334f 100644
--- a/web/css/layout/app-sidebar.css
+++ b/web/css/layout/app-sidebar.css
@@ -3,9 +3,9 @@
background: var(--app-sidebar-background-color);
--app-sidebar-gap: 0.5rem;
--app-sidebar-item-gap: 10px;
- --app-sidebar-title-size: 10px;
- --app-sidebar-summary-size: 12px;
- --app-sidebar-tenant-size: 14px;
+ --app-sidebar-title-size: 10px; /* icon-font metric — intentional px */
+ --app-sidebar-summary-size: var(--text-xs);
+ --app-sidebar-tenant-size: var(--text-sm);
--app-sidebar-titlebar-height: 30px;
--app-sidebar-border-width: 2px;
--app-sidebar-summary-max-width: 142px;
@@ -37,7 +37,7 @@
}
.app-sidebar-group.app-sidebar-admin-group details summary i {
- font-size: 10px;
+ font-size: 10px; /* icon-font metric — intentional px */
position: relative;
top: -4px;
margin-right: 3px;
@@ -100,8 +100,8 @@
}
.app-sidebar-tenant-name {
- font-weight: 600;
- font-size: 17px;
+ font-weight: var(--font-semibold);
+ font-size: var(--text-base);
color: var(--app-contrast);
text-align: left;
overflow: hidden;
@@ -126,7 +126,7 @@
background: transparent;
color: var(--app-muted-color);
padding: 0.25rem;
- font-size: 0.9rem;
+ font-size: var(--text-sm);
cursor: pointer;
margin-bottom: 0;
}
diff --git a/web/css/layout/app-topbar.css b/web/css/layout/app-topbar.css
index d0f05f9..acaacc4 100644
--- a/web/css/layout/app-topbar.css
+++ b/web/css/layout/app-topbar.css
@@ -129,8 +129,8 @@ main:has(#app-details-aside-section) .app-topbar-menu-item-detail-sidebar {
.app-topbar-tenant-name {
display: inline-block;
max-width: min(20rem, 38vw);
- font-size: 0.86rem;
- line-height: 1.2;
+ font-size: var(--text-sm);
+ line-height: var(--leading-tight);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -224,15 +224,15 @@ main:has(#app-details-aside-section) .app-topbar-menu-item-detail-sidebar {
.app-topbar-hotkey {
color: var(--app-muted-color);
- font-size: 0.82em;
+ font-size: var(--text-xs);
white-space: nowrap;
}
.app-topbar-menu-list li.app-topbar-menu-heading {
padding: calc(var(--app-spacing) * 0.5) 0.5rem calc(var(--app-spacing) * 0.35);
color: var(--app-muted-color);
- font-weight: 500;
- letter-spacing: 0.03em;
+ font-weight: var(--font-medium);
+ letter-spacing: var(--tracking-wide);
text-transform: uppercase;
pointer-events: none;
}
diff --git a/web/css/pages/app-docs.css b/web/css/pages/app-docs.css
index b70a185..20e2ad2 100644
--- a/web/css/pages/app-docs.css
+++ b/web/css/pages/app-docs.css
@@ -83,7 +83,7 @@
}
.app-docs-nav-group summary small {
- font-size: 0.7rem;
+ font-size: var(--text-xs);
letter-spacing: 0.05em;
text-transform: uppercase;
display: inline;
@@ -107,7 +107,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- font-size: 0.875rem;
+ font-size: var(--text-sm);
border-radius: 0;
text-decoration: none;
}
@@ -120,7 +120,7 @@
.app-docs-nav a.active {
color: var(--app-primary);
border-left-color: var(--app-primary);
- font-weight: 500;
+ font-weight: var(--font-medium);
}
/* ─── TOC ─────────────────────────────────────────────────────── */
@@ -132,7 +132,7 @@
.app-docs-toc > small {
display: block;
padding: 0 0 0.5rem;
- font-size: 0.7rem;
+ font-size: var(--text-xs);
letter-spacing: 0.05em;
text-transform: uppercase;
}
@@ -151,7 +151,7 @@
display: block;
text-decoration: none;
color: inherit;
- line-height: 1.35;
+ line-height: var(--leading-snug);
padding-block: 0.12rem;
}
@@ -171,7 +171,7 @@
}
.app-docs-toc-level-3 a {
- font-size: 0.75rem;
+ font-size: var(--text-xs);
padding-left: 0.45rem;
}
@@ -208,41 +208,41 @@
.app-docs-content h5,
.app-docs-content h6 {
scroll-margin-top: 1.5rem;
- line-height: 1.3;
+ line-height: var(--leading-snug);
}
.app-docs-content h1 {
color: var(--app-h1-color);
margin-top: 0;
- font-size: 2.75rem;
+ font-size: var(--text-4xl);
}
.app-docs-content h2 {
color: var(--app-h2-color);
- font-size: 1.375rem;
+ font-size: var(--text-xl);
margin-top: 2.5rem;
padding-top: 1rem;
border-top: 1px solid var(--app-border);
}
.app-docs-content h3 {
color: var(--app-h3-color);
- font-size: 1.125rem;
+ font-size: var(--text-lg);
margin-top: 1.75rem;
}
.app-docs-content h4 {
color: var(--app-h4-color);
- font-size: 1rem;
+ font-size: var(--text-base);
margin-top: 1.25rem;
}
.app-docs-content h5,
.app-docs-content h6 {
color: var(--app-h5-color);
- font-size: 0.9rem;
+ font-size: var(--text-sm);
margin-top: 1rem;
}
.app-docs-content p {
margin-block: 0.875rem;
- line-height: 1.7;
+ line-height: var(--leading-loose);
}
.app-docs-content a {
@@ -268,7 +268,7 @@
.app-docs-content li {
margin-block: 0.3rem;
- line-height: 1.6;
+ line-height: var(--leading-relaxed);
}
.app-docs-content li > ul,
@@ -314,7 +314,7 @@
.app-docs-content pre code {
background: none;
padding: 0;
- font-size: 0.85rem;
+ font-size: var(--text-sm);
color: var(--app-color);
}
@@ -341,7 +341,7 @@
.app-docs-content table {
width: 100%;
border-collapse: collapse;
- font-size: 0.875rem;
+ font-size: var(--text-sm);
margin-block: 1.25rem;
overflow-x: auto;
display: block;
@@ -356,7 +356,7 @@
.app-docs-content th {
background: var(--app-code-background-color);
- font-weight: 600;
+ font-weight: var(--font-semibold);
color: var(--app-color);
}
diff --git a/web/css/pages/app-error.css b/web/css/pages/app-error.css
index 59e627d..d26f749 100644
--- a/web/css/pages/app-error.css
+++ b/web/css/pages/app-error.css
@@ -9,15 +9,15 @@
}
.error-page__code {
- font-size: 5rem;
- font-weight: 700;
- line-height: 1;
+ font-size: var(--text-5xl);
+ font-weight: var(--font-bold);
+ line-height: var(--leading-none);
color: var(--color-text-muted);
}
.error-page__message {
margin-top: 1rem;
- font-size: 1.125rem;
+ font-size: var(--text-lg);
color: var(--color-text);
}
@@ -25,7 +25,7 @@
display: inline-block;
margin-top: 2rem;
padding: 0.5rem 1.25rem;
- font-size: 0.875rem;
+ font-size: var(--text-sm);
text-decoration: none;
border: 1px solid var(--color-border);
border-radius: 0.375rem;
diff --git a/web/css/pages/app-login.css b/web/css/pages/app-login.css
index 3da9906..fe992e5 100644
--- a/web/css/pages/app-login.css
+++ b/web/css/pages/app-login.css
@@ -596,7 +596,7 @@
border: 0;
color: var(--app-muted-color);
cursor: pointer;
- font-size: 1rem;
+ font-size: var(--text-base);
border-radius: var(--app-border-radius);
transition: color 0.15s ease;
}
@@ -646,7 +646,7 @@
.login-tenant-select-label {
display: block;
margin-bottom: 0.5rem;
- font-weight: 600;
+ font-weight: var(--font-semibold);
}
.login-tenant-choice-grid {
@@ -717,8 +717,8 @@
align-items: center;
justify-content: flex-start;
padding-left: 0.5rem;
- font-size: 0.875rem;
- font-weight: 600;
+ font-size: var(--text-sm);
+ font-weight: var(--font-semibold);
color: var(--app-contrast);
}
@@ -743,7 +743,7 @@
z-index: 1;
display: inline-block;
padding: 0 0.75rem;
- font-size: 0.875rem;
+ font-size: var(--text-sm);
color: var(--app-muted-color);
background: var(--app-card-background-color);
}
@@ -795,8 +795,8 @@
display: inline-flex;
align-items: center;
justify-content: center;
- font-size: 0.875rem;
- font-weight: 700;
+ font-size: var(--text-sm);
+ font-weight: var(--font-bold);
color: var(--app-primary);
}
@@ -806,7 +806,7 @@
height: auto;
justify-content: flex-start;
text-align: left;
- font-weight: 700;
+ font-weight: var(--font-bold);
color: var(--app-text);
}
@@ -817,7 +817,7 @@
.login-tenant-context-name {
margin: 0;
- font-size: 11px;
+ font-size: var(--text-xs);
white-space: nowrap;
text-overflow: ellipsis;
max-width: 130px;
@@ -843,7 +843,7 @@
padding: 0;
gap: 10px;
- font-size: 0.72rem;
+ font-size: var(--text-xs);
}
.login-help-links-item {
@@ -854,20 +854,20 @@
}
.login-security-note {
- font-size: 0.72rem;
+ font-size: var(--text-xs);
margin-bottom: 1px;
color: color-mix(in srgb, var(--app-contrast) 78%, var(--app-muted-color));
}
.login-security-note i {
color: var(--app-primary);
- font-size: 0.85rem;
+ font-size: var(--text-sm);
}
.login-security-note-detail {
width: 100%;
display: block;
- font-size: 9px;
+ font-size: var(--text-2xs);
color: color-mix(in srgb, var(--app-muted-color) 75%, transparent);
}