diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css
index fb65cf8..5a10ca7 100644
--- a/apps/web/src/styles.css
+++ b/apps/web/src/styles.css
@@ -50,6 +50,36 @@
--shadow-sm: 0 1px 2px rgba(20, 35, 55, 0.04);
--shadow: 0 8px 25px rgba(20, 35, 55, 0.07);
--shadow-lg: 0 18px 45px rgba(20, 35, 55, 0.12);
+ /* Button system: one compact scale shared by every action control. */
+ --button-height: 2.375rem;
+ --button-height-compact: 2.125rem;
+ --button-height-large: 2.625rem;
+ --icon-button-size: 2.5rem;
+ --button-radius: 10px;
+ --button-radius-compact: 8px;
+ --button-focus-ring: #0f6e66;
+ --button-primary-bg: #11776e;
+ --button-primary-hover: #0f6e66;
+ --button-primary-active: #0d5f58;
+ --button-primary-text: #fffdf7;
+ --button-secondary-bg: #fffdf7;
+ --button-secondary-text: #584a38;
+ --button-secondary-border: #bdb29d;
+ --button-secondary-hover-bg: #ecfaf6;
+ --button-secondary-hover-text: #0f6e66;
+ --button-secondary-hover-border: #72cbbd;
+ --button-danger-bg: #b64d48;
+ --button-danger-hover: #a83f3a;
+ --button-danger-active: #8f3531;
+ --button-danger-text: #fffdf7;
+ --button-disabled-bg: #ece6da;
+ --button-disabled-text: #6f6350;
+ --button-disabled-border: #d9d1bf;
+ --field-height: 2.375rem;
+ --field-radius: 10px;
+ --field-border: var(--line-strong);
+ --field-bg: #ffffff;
+ --field-focus-border: var(--button-focus-ring);
font-synthesis: none;
text-rendering: optimizeLegibility;
}
@@ -86,17 +116,25 @@ a {
outline-offset: 2px;
}
:focus-visible {
- outline: 3px solid rgba(49, 91, 234, 0.3);
+ outline: 3px solid var(--button-focus-ring);
outline-offset: 2px;
}
button,
input,
select,
textarea {
- min-height: 2.6rem;
- border: 1px solid var(--line-strong);
- border-radius: 9px;
- background: #fff;
+ border: 1px solid var(--field-border);
+ border-radius: var(--field-radius);
+ background: var(--field-bg);
+}
+button {
+ min-height: var(--button-height);
+ border-radius: var(--button-radius);
+}
+input,
+select,
+textarea {
+ min-height: var(--field-height);
}
input,
select,
@@ -106,45 +144,59 @@ textarea {
input:focus,
select:focus,
textarea:focus {
- border-color: #7895f3;
- box-shadow: 0 0 0 3px rgba(49, 91, 234, 0.1);
+ border-color: var(--field-focus-border);
+ box-shadow: none;
outline: none;
}
button {
- padding: 0.5rem 0.85rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.4rem;
+ padding: 0.45rem 0.85rem;
+ border-color: var(--button-secondary-border);
+ color: var(--button-secondary-text);
+ background: var(--button-secondary-bg);
cursor: pointer;
- font-weight: 700;
- box-shadow: var(--shadow-sm);
+ font-weight: 750;
+ box-shadow: none;
transition:
border-color 150ms ease,
color 150ms ease,
background 150ms ease,
+ box-shadow 150ms ease,
transform 150ms ease;
}
button:hover:not(:disabled) {
- color: var(--primary);
- border-color: #9caff0;
- background: #f7f9ff;
+ color: var(--button-secondary-hover-text);
+ border-color: var(--button-secondary-hover-border);
+ background: var(--button-secondary-hover-bg);
}
button:active:not(:disabled) {
transform: translateY(1px);
}
button:disabled {
cursor: not-allowed;
- opacity: 0.5;
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ box-shadow: none;
+ opacity: 1;
}
button[type='submit'],
-.primary-action {
- color: #fff;
- border-color: var(--primary);
- background: var(--primary);
- box-shadow: 0 5px 12px rgba(49, 91, 234, 0.18);
+button.primary-action,
+a.primary-action {
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-bg);
+ background: var(--button-primary-bg);
+ box-shadow: 0 3px 0 var(--button-primary-active);
}
button[type='submit']:hover:not(:disabled),
-.primary-action:hover {
- color: #fff;
- border-color: var(--primary-hover);
- background: var(--primary-hover);
+button.primary-action:hover:not(:disabled),
+a.primary-action:hover {
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-hover);
+ background: var(--button-primary-hover);
}
h1,
h2,
@@ -192,7 +244,7 @@ code {
left: 1rem;
z-index: 100;
padding: 0.7rem 1rem;
- color: #fff;
+ color: var(--button-primary-text);
background: var(--primary);
border-radius: 8px;
}
@@ -398,12 +450,13 @@ main > section > header p,
flex-wrap: wrap;
}
.primary-action {
- min-height: 2.6rem;
+ min-height: var(--button-height);
display: inline-flex;
align-items: center;
- padding: 0.5rem 0.9rem;
- border-radius: 9px;
- font-weight: 800;
+ gap: 0.4rem;
+ padding: 0.45rem 0.9rem;
+ border-radius: var(--button-radius);
+ font-weight: 750;
text-decoration: none;
}
.selection-summary {
@@ -1115,7 +1168,7 @@ button.fleet-cross-health-item {
}
.fleet-card-admin-actions button {
flex: 1 1 7rem;
- min-height: 2.25rem;
+ min-height: var(--button-height-compact);
font-size: 0.75rem;
}
.fleet-card-section + .pagination {
@@ -1126,7 +1179,7 @@ button.fleet-cross-health-item {
margin-top: 0.9rem;
}
.pagination button {
- min-height: 2.3rem;
+ min-height: var(--button-height-compact);
}
/* States and forms */
@@ -2081,7 +2134,7 @@ dd {
align-items: flex-start;
margin: 0.55rem 0 0;
padding: 0.5rem 0.6rem;
- border: 1px solid rgba(217, 87, 87, 0.2);
+ border: 1px solid rgba(168, 64, 59, 0.2);
border-radius: 10px;
color: #98393a;
background: rgba(253, 233, 231, 0.72);
@@ -2621,10 +2674,15 @@ main ul[aria-label='已配置实例'] > li {
--workbench-border: #d9d1bf;
--workbench-border-strong: #bdb29d;
--workbench-text: #584a38;
- --workbench-muted: #7f725f;
- --workbench-mint: #178f84;
+ --workbench-muted: #6f6350;
+ --workbench-mint: #0f6e66;
--workbench-mint-soft: #dff3e8;
--workbench-yellow: #f7cd67;
+ --workbench-warning-text: #825f0c;
+ --workbench-warning-soft: #fdf1d2;
+ --workbench-mint-border: #b3d7c6;
+ --workbench-warning-border: #e5cd96;
+ --workbench-danger-border: #e0b7b2;
--workbench-danger: #b64d48;
--workbench-danger-soft: #f9e5e2;
font-family:
@@ -2761,28 +2819,39 @@ main {
align-items: center;
}
-.primary-action {
- min-height: 2.5rem;
- padding: 0 0.95rem;
- border: 1px solid #11776e;
- border-radius: 10px;
- color: #fff;
- background: var(--workbench-mint);
- box-shadow: none;
+button.primary-action,
+a.primary-action {
+ min-height: var(--button-height);
+ padding: 0.45rem 0.95rem;
+ border: 1px solid var(--button-primary-bg);
+ border-radius: var(--button-radius);
+ color: var(--button-primary-text);
+ background: var(--button-primary-bg);
+ box-shadow: 0 3px 0 var(--button-primary-active);
font: inherit;
font-weight: 750;
letter-spacing: 0;
cursor: pointer;
transition:
+ border-color 150ms ease,
+ color 150ms ease,
background-color 150ms ease,
+ box-shadow 150ms ease,
transform 100ms ease;
}
-.primary-action:hover {
- background: #11776e;
+button.primary-action:hover:not(:disabled),
+a.primary-action:hover {
+ border-color: var(--button-primary-hover);
+ background: var(--button-primary-hover);
+ transform: translateY(-1px);
}
-.primary-action:active {
- transform: scale(0.98);
+button.primary-action:active:not(:disabled),
+a.primary-action:active {
+ border-color: var(--button-primary-active);
+ background: var(--button-primary-active);
+ box-shadow: 0 1px 0 var(--button-primary-active);
+ transform: translateY(1px);
}
.fleet-workspace,
@@ -3042,12 +3111,13 @@ main {
display: grid;
justify-items: end;
}
-.row-action-trigger {
- width: 2.75rem;
- min-height: 2.75rem;
+button.row-action-trigger {
+ width: var(--icon-button-size);
+ min-height: var(--icon-button-size);
display: grid;
place-items: center;
padding: 0;
+ border-radius: var(--button-radius);
}
.row-action-trigger svg {
width: 1rem;
@@ -3117,7 +3187,7 @@ main {
transform: translateX(0.92rem);
}
.compact-switch input:focus-visible + span {
- outline: 3px solid rgba(23, 143, 132, 0.24);
+ outline: 3px solid var(--button-focus-ring);
outline-offset: 2px;
}
@@ -3176,17 +3246,28 @@ main {
margin-bottom: 0.75rem;
font-size: 0.82rem;
}
-.icon-button {
- width: 2.75rem;
- height: 2.75rem;
- border: 0;
- border-radius: 50%;
+button.icon-button,
+a.icon-button {
+ width: var(--icon-button-size);
+ height: var(--icon-button-size);
+ min-height: var(--icon-button-size);
+ padding: 0;
+ border: 1px solid transparent;
+ border-radius: var(--button-radius);
color: var(--workbench-muted);
background: var(--workbench-surface-muted);
- font-size: 1.35rem;
+ box-shadow: none;
+ font-size: 1.2rem;
cursor: pointer;
}
+button.icon-button:hover:not(:disabled),
+a.icon-button:hover {
+ color: var(--button-secondary-hover-text);
+ border-color: var(--button-secondary-hover-border);
+ background: var(--button-secondary-hover-bg);
+}
+
.icon-button svg {
width: 1.1rem;
height: 1.1rem;
@@ -3210,12 +3291,12 @@ main {
.schedule-drawer select,
.schedule-drawer textarea {
width: 100%;
- min-height: 2.75rem;
+ min-height: var(--button-height-large);
padding: 0.58rem 0.72rem;
border: 1px solid var(--workbench-border-strong);
- border-radius: 10px;
+ border-radius: var(--field-radius);
color: var(--workbench-text);
- background: #fff;
+ background: var(--field-bg);
font: inherit;
letter-spacing: 0;
}
@@ -3226,8 +3307,8 @@ main {
.schedule-drawer input:focus,
.schedule-drawer select:focus,
.schedule-drawer textarea:focus {
- border-color: var(--workbench-mint);
- outline: 3px solid rgba(23, 143, 132, 0.16);
+ border-color: var(--field-focus-border);
+ outline: 3px solid var(--button-focus-ring);
}
.field-block {
@@ -3250,7 +3331,7 @@ main {
gap: 0.45rem;
}
.inline-field button {
- min-height: 2.75rem;
+ min-height: var(--button-height-compact);
border: 1px solid var(--workbench-border-strong);
border-radius: 10px;
color: var(--workbench-text);
@@ -3273,10 +3354,10 @@ main {
}
.segmented-control button {
flex: 1 1 0;
- min-height: 2.2rem;
- padding: 0 0.5rem;
+ min-height: var(--button-height-compact);
+ padding: 0.3rem 0.5rem;
border: 1px solid transparent;
- border-radius: 9px;
+ border-radius: var(--button-radius-compact);
color: var(--workbench-muted);
background: transparent;
font: inherit;
@@ -3290,7 +3371,7 @@ main {
background: #fff;
}
.segmented-control button:focus-visible {
- outline: 3px solid rgba(23, 143, 132, 0.16);
+ outline: 3px solid var(--button-focus-ring);
}
.weekday-picker {
display: flex;
@@ -3490,8 +3571,8 @@ main {
overflow-wrap: anywhere;
}
.organization-list .icon-button {
- width: 2.1rem;
- height: 2.1rem;
+ width: var(--icon-button-size);
+ height: var(--icon-button-size);
flex: none;
}
.organization-list .icon-button svg {
@@ -3499,10 +3580,13 @@ main {
height: 0.9rem;
}
.organization-list .icon-button.danger {
- color: #b04141;
+ color: var(--button-danger-bg);
}
.organization-list .icon-button:disabled {
- opacity: 0.45;
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ opacity: 1;
cursor: not-allowed;
}
.organization-empty {
@@ -3523,10 +3607,13 @@ main {
border-bottom: 1px solid var(--workbench-border);
}
.identity-toolbar button:first-child {
- min-height: 2.1rem;
- padding: 0 0.7rem;
+ min-height: var(--button-height-compact);
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ padding: 0.3rem 0.7rem;
border: 1px solid var(--workbench-border);
- border-radius: 999px;
+ border-radius: var(--button-radius-compact);
color: var(--workbench-muted);
background: #fff;
font: inherit;
@@ -3653,10 +3740,14 @@ main {
justify-content: flex-end;
}
.identity-actions button {
- min-height: 2.1rem;
- padding: 0 0.8rem;
+ min-height: var(--button-height-compact);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.35rem;
+ padding: 0.3rem 0.8rem;
border: 1px solid var(--workbench-border-strong);
- border-radius: 999px;
+ border-radius: var(--button-radius-compact);
color: var(--workbench-text);
background: #fff;
font: inherit;
@@ -3665,13 +3756,25 @@ main {
cursor: pointer;
}
.identity-actions button:disabled {
- opacity: 0.45;
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ opacity: 1;
cursor: not-allowed;
}
-.identity-actions .primary-action {
- color: #fff;
- border-color: var(--workbench-mint);
- background: var(--workbench-mint);
+.identity-actions button.primary-action {
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-bg);
+ background: var(--button-primary-bg);
+}
+.identity-actions button.primary-action:hover:not(:disabled) {
+ border-color: var(--button-primary-hover);
+ background: var(--button-primary-hover);
+}
+.identity-actions button.primary-action:disabled {
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
}
.identity-hint {
margin: 0;
@@ -3685,7 +3788,7 @@ main {
gap: 0.25rem;
margin: 0;
padding: 0.45rem 0.55rem;
- border: 1px solid rgba(217, 87, 87, 0.16);
+ border: 1px solid rgba(168, 64, 59, 0.16);
border-radius: 12px;
background: rgba(253, 233, 231, 0.72);
font-size: 0.74rem;
@@ -4046,23 +4149,24 @@ main {
color: #725d42;
background: #e9f4df;
--ink: #725d42;
- --muted: #8f806b;
- --faint: #aa9d89;
+ --muted: #756a58;
+ --faint: #7d6f5c;
--line: #ded4bf;
--line-strong: #c9bda5;
--surface: #fffdf5;
--surface-soft: #f7f3df;
--canvas: #e9f4df;
- --primary: #19b8aa;
- --primary-hover: #109d92;
+ --primary: #0f6e66;
+ --primary-hover: #0d6259;
--primary-soft: #e2f7f2;
- --cyan: #19b8aa;
- --success: #65a82d;
+ --cyan: #0f6e66;
+ --success: #4a7d1f;
--success-soft: #edf7df;
--warning: #b58b0a;
--warning-soft: #fff5cf;
- --danger: #d95757;
+ --danger: #a8403b;
--danger-soft: #fde9e7;
+ --field-bg: #fffdf7;
--radius: 18px;
--radius-sm: 14px;
--shadow-sm: 0 2px 0 rgba(114, 93, 66, 0.08);
@@ -4084,17 +4188,17 @@ body {
}
a {
- color: #138f85;
+ color: #0f6e66;
}
:focus-visible {
- outline-color: #19b8aa;
+ outline-color: var(--button-focus-ring);
}
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
- outline: 3px solid rgba(245, 195, 28, 0.38);
+ outline: 3px solid var(--button-focus-ring);
border-color: #e5b70d;
box-shadow: none;
}
@@ -4104,34 +4208,63 @@ input:not([class*='animal-']),
select,
textarea {
border-color: var(--line-strong);
- border-radius: 16px;
+ border-radius: var(--button-radius);
background: #fffdf7;
}
button:not([class*='animal-']):hover:not(:disabled) {
- color: #117f76;
- border-color: #72cbbd;
- background: #ecfaf6;
+ color: var(--button-secondary-hover-text);
+ border-color: var(--button-secondary-hover-border);
+ background: var(--button-secondary-hover-bg);
}
+/*
+ * The primary action used to be declared with a bare `.primary-action` selector here, which
+ * lost the cascade to `button:not([class*='animal-'])` above. That left white label text on the
+ * cream secondary surface, so "立即投递" and "刷新" looked like empty pills. Every primary-action
+ * rule now carries the element qualifier so it always outranks the generic button surface.
+ */
button[type='submit']:not([class*='animal-']),
-.primary-action {
- color: #fff;
- border-color: #19b8aa;
- border-radius: 999px;
- background: #19b8aa;
- box-shadow: 0 5px 0 #0d887f;
+button.primary-action,
+a.primary-action {
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-bg);
+ border-radius: var(--button-radius);
+ background: var(--button-primary-bg);
+ box-shadow: 0 3px 0 var(--button-primary-active);
}
button[type='submit']:not([class*='animal-']):hover:not(:disabled),
-.primary-action:hover {
- color: #fff;
- border-color: #31c9bb;
- background: #31c9bb;
- box-shadow: 0 6px 0 #0d887f;
+button.primary-action:hover:not(:disabled),
+a.primary-action:hover {
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-hover);
+ background: var(--button-primary-hover);
+ box-shadow: 0 4px 0 var(--button-primary-active);
transform: translateY(-1px);
}
+button[type='submit']:not([class*='animal-']):active:not(:disabled),
+button.primary-action:active:not(:disabled),
+a.primary-action:active {
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-active);
+ background: var(--button-primary-active);
+ box-shadow: 0 1px 0 var(--button-primary-active);
+ transform: translateY(1px);
+}
+
+button[type='submit']:not([class*='animal-']):disabled,
+button.primary-action:disabled,
+a.primary-action[aria-disabled='true'] {
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ box-shadow: none;
+ transform: none;
+ opacity: 1;
+}
+
h1,
h2,
h3 {
@@ -4229,7 +4362,7 @@ h1 {
height: 0.48rem;
margin-right: 0.38rem;
border-radius: 50%;
- background: #19b8aa;
+ background: #0f6e66;
}
.connection-status::before {
@@ -4252,7 +4385,7 @@ h1 {
.connection-status span[data-state='reconnecting']::before,
.connection-status span[data-state='closed']::before {
- background: #d95757;
+ background: #a8403b;
}
.app-layout {
@@ -4366,7 +4499,7 @@ main > section > header h1,
.fleet-search-glyph {
position: static;
- color: #8f806b;
+ color: #6f6350;
transform: none;
}
@@ -4396,7 +4529,7 @@ main > section > header h1,
.fleet-active-chip,
.fleet-clear-filters {
- border-radius: 999px;
+ border-radius: var(--button-radius-compact);
}
.fleet-active-chip {
@@ -4455,11 +4588,11 @@ main > section > header h1,
.fleet-card::before {
width: 0.45rem;
border-radius: 20px 0 0 20px;
- background: #19b8aa;
+ background: #0f6e66;
}
.fleet-card[data-status='offline']::before {
- background: #d95757;
+ background: #a8403b;
}
.fleet-card[data-status='auth']::before {
@@ -4477,8 +4610,8 @@ main > section > header h1,
}
.fleet-card.is-selected {
- border-color: #19b8aa;
- box-shadow: 0 0 0 3px rgba(25, 184, 170, 0.14);
+ border-color: #0f6e66;
+ box-shadow: 0 0 0 3px rgba(15, 110, 102, 0.14);
}
.fleet-card-glow {
@@ -4513,7 +4646,7 @@ main > section > header h1,
--apple-surface-strong: rgba(255, 255, 255, 0.9);
--apple-hairline: rgba(101, 83, 59, 0.14);
--apple-shadow: 0 1px 2px rgba(74, 61, 43, 0.05), 0 10px 30px rgba(74, 61, 43, 0.08);
- --apple-shadow-selected: 0 0 0 3px rgba(25, 184, 170, 0.16), 0 14px 34px rgba(37, 115, 103, 0.12);
+ --apple-shadow-selected: 0 0 0 3px rgba(15, 110, 102, 0.16), 0 14px 34px rgba(37, 115, 103, 0.12);
font-family:
-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Noto Sans SC', 'PingFang SC', system-ui,
sans-serif;
@@ -4633,7 +4766,7 @@ main > section > header h1,
}
.fleet-card.is-selected {
- border-color: rgba(25, 184, 170, 0.7);
+ border-color: rgba(15, 110, 102, 0.7);
background: rgba(247, 255, 252, 0.9);
box-shadow: var(--apple-shadow-selected);
}
@@ -4691,9 +4824,9 @@ main > section > header h1,
}
.fleet-card-select input:checked {
- border-color: #19a99d;
- background: #19b8aa;
- box-shadow: 0 2px 7px rgba(25, 169, 157, 0.24);
+ border-color: #0f6e66;
+ background: #0f6e66;
+ box-shadow: 0 2px 7px rgba(15, 110, 102, 0.24);
}
.fleet-card-select input:checked::before {
@@ -4705,7 +4838,7 @@ main > section > header h1,
}
.fleet-card-select input:focus-visible {
- outline: 3px solid rgba(25, 184, 170, 0.22);
+ outline: 3px solid var(--button-focus-ring);
outline-offset: 2px;
}
@@ -4763,12 +4896,12 @@ main > section > header h1,
.fleet-card-description {
padding: 0;
border: 0;
- color: #81745f;
+ color: #6f6350;
line-height: 1.5;
}
.fleet-card-anomalies {
- border: 1px solid rgba(217, 87, 87, 0.16);
+ border: 1px solid rgba(168, 64, 59, 0.18);
border-radius: 12px;
background: rgba(253, 233, 231, 0.72);
}
@@ -4796,7 +4929,7 @@ main > section > header h1,
}
.fleet-card-metrics dt {
- color: #8d816e;
+ color: #6f6350;
font-size: 0.66rem;
font-weight: 600;
}
@@ -4815,7 +4948,7 @@ main > section > header h1,
}
.fleet-card-sms h3 {
- color: #766a58;
+ color: #6f6350;
font-weight: 650;
}
@@ -5305,7 +5438,7 @@ svg {
.fleet-search-glyph {
position: static;
- color: #8f806b;
+ color: #6f6350;
transform: none;
}
@@ -5823,30 +5956,32 @@ svg {
flex: 0 0 auto;
}
-.fleet-card-menu-trigger,
-.fleet-card-menu-trigger:hover:not(:disabled),
-.fleet-card-menu-trigger:active:not(:disabled) {
+button.fleet-card-menu-trigger,
+button.fleet-card-menu-trigger:hover:not(:disabled),
+button.fleet-card-menu-trigger:active:not(:disabled) {
display: grid;
- width: 2.75rem;
- height: 2.75rem;
- min-height: 2.75rem;
+ width: var(--icon-button-size);
+ height: var(--icon-button-size);
+ min-height: var(--icon-button-size);
place-items: center;
padding: 0;
- border: 0;
- border-radius: 50%;
+ border: 1px solid transparent;
+ border-radius: var(--button-radius);
color: var(--workbench-muted);
+ border-color: transparent;
background: transparent;
box-shadow: none;
transform: none;
}
-.fleet-card-menu-trigger:hover:not(:disabled) {
- color: var(--workbench-text);
- background: var(--workbench-surface-muted);
+button.fleet-card-menu-trigger:hover:not(:disabled) {
+ color: var(--button-secondary-hover-text);
+ border-color: var(--button-secondary-hover-border);
+ background: var(--button-secondary-hover-bg);
}
-.fleet-card-menu-trigger:focus-visible {
- outline: 2px solid var(--workbench-mint);
+button.fleet-card-menu-trigger:focus-visible {
+ outline: 3px solid var(--button-focus-ring);
outline-offset: 2px;
}
@@ -5866,7 +6001,7 @@ svg {
padding: 0.35rem;
border: 1px solid var(--workbench-border);
border-radius: 8px;
- background: #fffef9;
+ background: var(--workbench-surface);
box-shadow: 0 10px 24px rgba(68, 55, 38, 0.14);
}
@@ -5876,13 +6011,13 @@ svg {
.fleet-card-menu-panel button:active:not(:disabled) {
display: flex;
width: 100%;
- min-height: 2.75rem;
+ min-height: var(--button-height-compact);
align-items: center;
justify-content: flex-start;
gap: 0.55rem;
- padding: 0.55rem 0.7rem;
+ padding: 0.4rem 0.65rem;
border: 0;
- border-radius: 6px;
+ border-radius: var(--button-radius-compact);
color: var(--workbench-text);
background: transparent;
box-shadow: none;
@@ -5900,7 +6035,7 @@ svg {
.fleet-card-menu-panel button:focus-visible,
.fleet-card-menu-panel .fleet-card-menu-item:focus-visible {
- outline: 2px solid var(--workbench-mint);
+ outline: 3px solid var(--button-focus-ring);
outline-offset: -2px;
}
@@ -5949,13 +6084,13 @@ svg {
}
.fleet-card-access[data-kind='local'] {
- border-color: color-mix(in srgb, var(--workbench-mint) 45%, var(--workbench-border));
+ border-color: var(--workbench-mint-border);
color: var(--workbench-mint);
}
.fleet-card-access[data-kind='wan'] {
- border-color: color-mix(in srgb, var(--workbench-yellow) 50%, var(--workbench-border));
- color: var(--workbench-yellow);
+ border-color: var(--workbench-warning-border);
+ color: var(--workbench-warning-text);
}
.fleet-card-heartbeat {
@@ -5981,7 +6116,7 @@ svg {
.fleet-card-heartbeat[data-stale='true'] > svg,
.fleet-card-heartbeat[data-stale='true'] time {
- color: var(--workbench-yellow);
+ color: var(--workbench-warning-text);
}
.fleet-card-origin-link,
@@ -6375,17 +6510,24 @@ svg {
}
.automation-workspace button.primary-action {
- color: #fff;
- border-color: #11776e;
- background: var(--workbench-mint);
- box-shadow: 0 3px 0 #0d756d;
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-bg);
+ background: var(--button-primary-bg);
+ box-shadow: 0 3px 0 var(--button-primary-active);
}
.automation-workspace button.primary-action:hover:not(:disabled) {
- color: #fff;
- border-color: #0f6e66;
- background: #11776e;
- box-shadow: 0 3px 0 #0d756d;
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-hover);
+ background: var(--button-primary-hover);
+ box-shadow: 0 4px 0 var(--button-primary-active);
+}
+
+.automation-workspace button.primary-action:disabled {
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ box-shadow: none;
}
.automation-workspace .workbench-tabs button,
@@ -6503,12 +6645,12 @@ svg {
.fleet-phone-privacy-toggle[aria-pressed='true'] {
color: var(--workbench-mint);
- border-color: rgba(17, 119, 110, 0.36);
- background: #f1faf6;
+ border-color: var(--workbench-mint-border);
+ background: var(--workbench-mint-soft);
}
.fleet-phone-privacy-toggle:focus-visible {
- outline: 2px solid var(--workbench-mint);
+ outline: 3px solid var(--button-focus-ring);
outline-offset: 2px;
}
@@ -6642,8 +6784,8 @@ svg {
width: 100%;
min-height: 2.3rem;
padding: 0.45rem 0.55rem;
- border: 1px solid var(--workbench-border);
- border-radius: 9px;
+ border: 1px solid var(--workbench-border-strong);
+ border-radius: var(--button-radius-compact);
color: var(--workbench-text);
background: var(--workbench-surface-muted);
font: inherit;
@@ -6664,13 +6806,13 @@ svg {
font-weight: 800;
}
.fleet-messages-device-item[data-active] {
- border-color: var(--workbench-mint);
+ border-color: var(--field-focus-border);
background: var(--workbench-mint-soft);
color: var(--workbench-mint);
}
.fleet-messages-device-item[data-unavailable] {
color: var(--workbench-danger);
- border-color: rgba(183, 77, 72, 0.25);
+ border-color: var(--workbench-danger-border);
background: var(--workbench-danger-soft);
cursor: default;
opacity: 0.75;
@@ -6813,8 +6955,8 @@ svg {
min-width: 0;
box-sizing: border-box;
padding: 0.55rem 0.6rem;
- border: 1px solid var(--workbench-border);
- border-radius: 9px;
+ border: 1px solid var(--workbench-border-strong);
+ border-radius: var(--button-radius-compact);
color: var(--workbench-text);
background: var(--workbench-surface-muted);
font: inherit;
@@ -6823,8 +6965,8 @@ svg {
.fleet-messages-form input:focus,
.fleet-messages-form select:focus,
.fleet-messages-form textarea:focus {
- border-color: var(--workbench-mint);
- outline: 2px solid color-mix(in srgb, var(--workbench-mint) 22%, transparent);
+ border-color: var(--field-focus-border);
+ outline: 3px solid var(--button-focus-ring);
outline-offset: 1px;
}
.fleet-messages-form textarea {
@@ -6846,10 +6988,10 @@ svg {
align-items: center;
justify-content: center;
gap: 0.4rem;
- min-height: 2.1rem;
- padding: 0.4rem 0.7rem;
- border: 1px solid rgba(17, 119, 110, 0.4);
- border-radius: 9px;
+ min-height: var(--button-height-compact);
+ padding: 0.3rem 0.7rem;
+ border: 1px solid var(--button-primary-bg);
+ border-radius: var(--button-radius-compact);
color: var(--workbench-surface);
background: var(--workbench-mint);
font: inherit;
@@ -6861,7 +7003,10 @@ svg {
.fleet-messages-more:disabled,
.fleet-messages-search button:disabled {
cursor: default;
- opacity: 0.55;
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ opacity: 1;
}
.fleet-messages-form footer button svg,
.fleet-messages-more svg,
@@ -6872,22 +7017,22 @@ svg {
.fleet-messages-result {
margin: 0.65rem 0 0;
padding: 0.55rem 0.65rem;
- border: 1px solid rgba(17, 119, 110, 0.3);
- border-radius: 9px;
+ border: 1px solid var(--workbench-mint-border);
+ border-radius: var(--button-radius-compact);
color: var(--workbench-mint);
background: var(--workbench-mint-soft);
font-size: 0.72rem;
font-weight: 750;
}
.fleet-messages-result.is-error {
- border-color: rgba(183, 77, 72, 0.3);
+ border-color: var(--workbench-danger-border);
color: var(--workbench-danger);
background: var(--workbench-danger-soft);
}
.fleet-messages-result.is-partial {
- border-color: rgba(170, 120, 35, 0.3);
- color: #8a6722;
- background: #fff8e8;
+ border-color: var(--workbench-warning-border);
+ color: var(--workbench-warning-text);
+ background: var(--workbench-warning-soft);
}
.fleet-messages-composer {
padding-top: 0.7rem;
@@ -6912,23 +7057,32 @@ svg {
gap: 0.5rem;
}
.fleet-outbox-flush {
- min-height: 2.1rem;
- padding: 0.35rem 0.7rem;
- border: 1px solid var(--workbench-border);
- border-radius: 9px;
- color: var(--workbench-text);
- background: var(--workbench-surface-muted);
+ min-height: var(--button-height-compact);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.35rem;
+ padding: 0.3rem 0.7rem;
+ border: 1px solid var(--button-primary-bg);
+ border-radius: var(--button-radius-compact);
+ color: var(--button-primary-text);
+ background: var(--button-primary-bg);
font: inherit;
font-size: 0.72rem;
font-weight: 800;
cursor: pointer;
}
.fleet-outbox-flush:hover:not(:disabled) {
- border-color: var(--workbench-mint);
+ border-color: var(--button-primary-hover);
+ background: var(--button-primary-hover);
+ color: var(--button-primary-text);
}
.fleet-outbox-flush:disabled {
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ opacity: 1;
cursor: default;
- opacity: 0.55;
}
.fleet-outbox-table {
width: 100%;
@@ -6991,22 +7145,22 @@ svg {
.fleet-messages-search input {
width: min(100%, 15rem);
min-width: 0;
- min-height: 2.1rem;
+ min-height: var(--button-height-compact);
padding: 0.4rem 0.55rem;
- border: 1px solid var(--workbench-border);
- border-radius: 9px;
+ border: 1px solid var(--workbench-border-strong);
+ border-radius: var(--button-radius-compact);
color: var(--workbench-text);
background: var(--workbench-surface-muted);
font: inherit;
font-size: 0.76rem;
}
.fleet-messages-search input:focus {
- border-color: var(--workbench-mint);
- outline: 2px solid color-mix(in srgb, var(--workbench-mint) 22%, transparent);
+ border-color: var(--field-focus-border);
+ outline: 3px solid var(--button-focus-ring);
outline-offset: 1px;
}
.fleet-messages-search button {
- min-width: 2.2rem;
+ min-width: var(--icon-button-size);
padding: 0;
}
.fleet-messages-message-tools {
@@ -7039,11 +7193,11 @@ svg {
align-items: center;
justify-content: center;
gap: 0.4rem;
- min-height: 2.1rem;
+ min-height: var(--button-height-compact);
min-width: 8.2rem;
padding: 0.4rem 0.65rem;
- border: 1px solid rgba(183, 77, 72, 0.4);
- border-radius: 9px;
+ border: 1px solid var(--button-danger-bg);
+ border-radius: var(--button-radius-compact);
color: var(--workbench-danger);
background: var(--workbench-danger-soft);
font: inherit;
@@ -7057,7 +7211,10 @@ svg {
}
.fleet-messages-delete:disabled {
cursor: default;
- opacity: 0.55;
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ opacity: 1;
}
.fleet-messages-list {
display: grid;
@@ -7143,14 +7300,14 @@ svg {
.fleet-messages-message footer span[data-state='incoming'],
.fleet-messages-message footer span[data-state='received'] {
color: var(--workbench-mint);
- border-color: rgba(17, 119, 110, 0.3);
+ border-color: var(--workbench-mint-border);
background: var(--workbench-mint-soft);
}
.fleet-messages-message footer span[data-state='outgoing'],
.fleet-messages-message footer span[data-state='sent'] {
- color: #8a6722;
- border-color: rgba(170, 120, 35, 0.3);
- background: #fff8e8;
+ color: var(--workbench-warning-text);
+ border-color: var(--workbench-warning-border);
+ background: var(--workbench-warning-soft);
}
.fleet-messages-empty,
.fleet-messages-loading-more {
@@ -7250,26 +7407,29 @@ svg {
}
.fleet-notifications-retry-all {
flex: 0 0 auto;
- min-height: 2rem;
+ min-height: var(--button-height-compact);
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.3rem 0.65rem;
- border: 1px solid rgba(17, 119, 110, 0.3);
- border-radius: 8px;
+ border: 1px solid var(--button-primary-bg);
+ border-radius: var(--button-radius-compact);
color: var(--workbench-mint);
background: var(--workbench-mint-soft);
font-size: 0.68rem;
font-weight: 800;
}
.fleet-notifications-retry-all:hover:not(:disabled) {
- border-color: rgba(17, 119, 110, 0.5);
- background: var(--workbench-mint);
- color: #fff;
+ border-color: var(--button-primary-hover);
+ background: var(--button-primary-hover);
+ color: var(--button-primary-text);
}
.fleet-notifications-retry-all:disabled {
cursor: default;
- opacity: 0.55;
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ opacity: 1;
}
.fleet-notifications-retry-all svg {
width: 0.82rem;
@@ -7291,15 +7451,15 @@ svg {
background: var(--workbench-surface-muted);
}
.fleet-notifications-counts div[data-state='ok'] {
- border-color: rgba(17, 119, 110, 0.3);
+ border-color: var(--workbench-mint-border);
background: var(--workbench-mint-soft);
}
.fleet-notifications-counts div[data-state='warn'] {
- border-color: rgba(170, 120, 35, 0.32);
- background: #fff8e8;
+ border-color: var(--workbench-warning-border);
+ background: var(--workbench-warning-soft);
}
.fleet-notifications-counts div[data-state='attention'] {
- border-color: rgba(183, 77, 72, 0.3);
+ border-color: var(--workbench-danger-border);
background: var(--workbench-danger-soft);
}
.fleet-notifications-counts dt {
@@ -7357,7 +7517,7 @@ svg {
gap: 0.4rem;
padding: 0.45rem 0.85rem;
border: 1px solid var(--workbench-border-strong);
- border-radius: 9px;
+ border-radius: var(--button-radius-compact);
color: var(--workbench-muted);
background: var(--workbench-surface-muted);
font: inherit;
@@ -7419,9 +7579,10 @@ svg {
}
.fleet-log-filters select,
.fleet-log-filters input {
- min-height: 2rem;
+ min-height: var(--button-height-compact);
padding: 0.25rem 0.5rem;
- border-radius: 8px;
+ border: 1px solid var(--workbench-border-strong);
+ border-radius: var(--button-radius-compact);
font-size: 0.68rem;
}
.fleet-log-filters select {
@@ -7536,11 +7697,15 @@ svg {
width: 7.5rem;
margin-left: auto;
padding: 0.25rem 0.5rem;
- border-radius: 8px;
+ border: 1px solid var(--workbench-border-strong);
+ border-radius: var(--button-radius-compact);
font-size: 0.68rem;
}
.fleet-log-retention-value:disabled {
- opacity: 0.55;
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ opacity: 1;
}
.fleet-log-retention-actions {
display: flex;
@@ -7615,14 +7780,14 @@ svg {
gap: 0.3rem;
}
.fleet-notification-entry-button {
- width: 1.9rem;
- height: 1.9rem;
+ width: var(--icon-button-size);
+ height: var(--icon-button-size);
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid var(--workbench-border);
- border-radius: 8px;
+ border-radius: var(--button-radius-compact);
color: var(--workbench-text);
background: var(--workbench-surface);
}
@@ -7635,8 +7800,11 @@ svg {
color: var(--workbench-danger);
}
.fleet-notification-entry-button:disabled {
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ opacity: 1;
cursor: default;
- opacity: 0.45;
}
.fleet-notification-entry-button svg {
width: 0.82rem;
@@ -7644,31 +7812,31 @@ svg {
}
.fleet-notification-entry[data-tone='ok'] > span {
color: var(--workbench-mint);
- border-color: rgba(17, 119, 110, 0.3);
+ border-color: var(--workbench-mint-border);
background: var(--workbench-mint-soft);
}
.fleet-notification-entry[data-tone='warn'] > span {
- color: #8a6722;
- border-color: rgba(170, 120, 35, 0.32);
- background: #fff8e8;
+ color: var(--workbench-warning-text);
+ border-color: var(--workbench-warning-border);
+ background: var(--workbench-warning-soft);
}
.fleet-notification-entry[data-tone='attention'] > span {
color: var(--workbench-danger);
- border-color: rgba(183, 77, 72, 0.3);
+ border-color: var(--workbench-danger-border);
background: var(--workbench-danger-soft);
}
.fleet-notifications-result {
margin: 0;
padding: 0.55rem 0.6rem;
- border: 1px solid rgba(17, 119, 110, 0.3);
- border-radius: 9px;
+ border: 1px solid var(--workbench-mint-border);
+ border-radius: var(--button-radius-compact);
color: var(--workbench-mint);
background: var(--workbench-mint-soft);
font-size: 0.7rem;
line-height: 1.4;
}
.fleet-notifications-result.is-error {
- border-color: rgba(183, 77, 72, 0.3);
+ border-color: var(--workbench-danger-border);
color: var(--workbench-danger);
background: var(--workbench-danger-soft);
}
@@ -7689,8 +7857,8 @@ svg {
}
.fleet-notification-devices li[data-state='unavailable'],
.fleet-notification-devices li[data-state='failed'] {
- border-color: rgba(183, 77, 72, 0.28);
- background: #fffafa;
+ border-color: var(--workbench-danger-border);
+ background: var(--workbench-danger-soft);
}
.fleet-notification-devices li > span {
min-width: 0;
@@ -7726,27 +7894,27 @@ svg {
}
.fleet-notification-devices li[data-state='ready'] > small {
color: var(--workbench-mint);
- border-color: rgba(17, 119, 110, 0.3);
+ border-color: var(--workbench-mint-border);
background: var(--workbench-mint-soft);
}
.fleet-notification-devices li[data-state='unavailable'] > small,
.fleet-notification-devices li[data-state='failed'] > small {
color: var(--workbench-danger);
- border-color: rgba(183, 77, 72, 0.3);
+ border-color: var(--workbench-danger-border);
background: var(--workbench-danger-soft);
}
.fleet-notification-devices li[data-tone='attention'] {
- border-color: rgba(183, 77, 72, 0.28);
- background: #fffafa;
+ border-color: var(--workbench-danger-border);
+ background: var(--workbench-danger-soft);
}
.fleet-notification-devices li[data-tone='ok'] > small {
color: var(--workbench-mint);
- border-color: rgba(17, 119, 110, 0.3);
+ border-color: var(--workbench-mint-border);
background: var(--workbench-mint-soft);
}
.fleet-notification-devices li[data-tone='attention'] > small {
color: var(--workbench-danger);
- border-color: rgba(183, 77, 72, 0.3);
+ border-color: var(--workbench-danger-border);
background: var(--workbench-danger-soft);
}
.fleet-notification-channel-picker {
@@ -7760,7 +7928,7 @@ svg {
gap: 0.4rem;
padding: 0.32rem 0.5rem;
border: 1px solid var(--workbench-border);
- border-radius: 9px;
+ border-radius: var(--button-radius-compact);
color: var(--workbench-text);
background: var(--workbench-surface);
font-size: 0.7rem;
@@ -7798,7 +7966,7 @@ svg {
font-size: 0.76rem;
}
.state-panel-button {
- min-height: 2.2rem;
+ min-height: var(--button-height-compact);
margin-left: 0.6rem;
padding: 0.3rem 0.7rem;
}
@@ -7865,3 +8033,366 @@ svg {
.field-block > label {
margin-top: 0;
}
+
+/* ==========================================================================
+ Button system: shared scale for every action control.
+
+ Appended last on purpose. The stylesheet accumulated five overlapping eras of
+ button styling, and the older element-level rules out-rank single class
+ selectors, so a later layer is the only way to make one coherent scale win.
+ ========================================================================== */
+
+/* Animal UI is themed through custom properties, which keeps us off its hashed classes. */
+:root {
+ --animal-primary-color: #11776e;
+ --animal-primary-color-hover: #0f6e66;
+ --animal-primary-color-active: #0d5f58;
+ --animal-primary-color-bg: #dff3e8;
+ --animal-error-color: #b64d48;
+ --animal-error-color-hover: #c4635e;
+ --animal-error-color-active: #8f3531;
+ --animal-text-color: #584a38;
+ --animal-text-color-secondary: #6f6350;
+ --animal-text-color-muted: #6f6350;
+ --animal-text-color-disabled: #6f6350;
+ --animal-border-color: #bdb29d;
+ --animal-border-color-hover: #0f6e66;
+ --animal-border-color-light: #d9d1bf;
+ --animal-bg-color: #fffdf7;
+ --animal-bg-color-secondary: #f8f4e8;
+ --animal-bg-color-disabled: #ece6da;
+ --animal-height-sm: 34px;
+ --animal-height-base: 38px;
+ --animal-height-lg: 42px;
+ --animal-border-radius-sm: 8px;
+ --animal-border-radius-base: 10px;
+ --animal-border-radius-lg: 12px;
+ --animal-border-width: 1px;
+ --animal-shadow-sm: none;
+ --animal-shadow-base: 0 3px 0 rgba(13, 95, 88, 0.28);
+ --animal-shadow-lg: 0 14px 30px rgba(74, 61, 43, 0.14);
+}
+
+[class*='animal-btn-'] {
+ height: auto;
+ min-height: var(--button-height);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.4rem;
+ padding: 0.45rem 0.9rem;
+ border-width: 1px;
+ border-radius: var(--button-radius);
+ box-shadow: none;
+ font-weight: 750;
+ white-space: normal;
+}
+
+[class*='animal-btn-']:focus-visible {
+ outline: 3px solid var(--button-focus-ring);
+ outline-offset: 2px;
+}
+
+[class*='animal-btn-small'] {
+ min-height: var(--button-height-compact);
+ padding: 0.3rem 0.7rem;
+ border-radius: var(--button-radius-compact);
+ font-size: 0.76rem;
+}
+
+[class*='animal-btn-middle'] {
+ min-height: var(--button-height);
+ padding: 0.45rem 0.9rem;
+ font-size: 0.82rem;
+}
+
+[class*='animal-btn-large'] {
+ min-height: var(--button-height-large);
+ padding: 0.55rem 1.1rem;
+}
+
+[class*='animal-btn-icon'] {
+ width: var(--icon-button-size);
+ height: var(--icon-button-size);
+ min-height: var(--icon-button-size);
+ padding: 0;
+}
+
+[class*='animal-btn-block'] {
+ display: flex;
+ width: 100%;
+}
+
+/* The library ships "primary" as the cream 3D card button; the console reserves it for the mint action. */
+[class*='animal-btn-primary'] {
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-bg);
+ background: var(--button-primary-bg);
+ box-shadow: 0 3px 0 var(--button-primary-active);
+}
+
+[class*='animal-btn-primary']:hover:not(:disabled) {
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-hover);
+ background: var(--button-primary-hover);
+ box-shadow: 0 4px 0 var(--button-primary-active);
+ transform: translateY(-1px);
+}
+
+[class*='animal-btn-primary']:active:not(:disabled) {
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-active);
+ background: var(--button-primary-active);
+ box-shadow: 0 1px 0 var(--button-primary-active);
+ transform: translateY(1px);
+}
+
+[class*='animal-btn-default'],
+[class*='animal-btn-dashed'] {
+ color: var(--button-secondary-text);
+ border-color: var(--button-secondary-border);
+ background: var(--button-secondary-bg);
+}
+
+[class*='animal-btn-default']:hover:not(:disabled),
+[class*='animal-btn-dashed']:hover:not(:disabled) {
+ color: var(--button-secondary-hover-text);
+ border-color: var(--button-secondary-hover-border);
+ background: var(--button-secondary-hover-bg);
+}
+
+[class*='animal-btn-text'],
+[class*='animal-btn-link'],
+[class*='animal-btn-ghost'] {
+ color: var(--workbench-muted);
+ border-color: transparent;
+ background: transparent;
+ box-shadow: none;
+}
+
+[class*='animal-btn-text']:hover:not(:disabled),
+[class*='animal-btn-link']:hover:not(:disabled),
+[class*='animal-btn-ghost']:hover:not(:disabled) {
+ color: var(--button-secondary-hover-text);
+ background: var(--button-secondary-hover-bg);
+}
+
+/* The library paints danger text/link buttons white on transparent, which is invisible here. */
+[class*='animal-btn-danger'][class*='animal-btn-primary'] {
+ color: var(--button-danger-text);
+ border-color: var(--button-danger-bg);
+ background: var(--button-danger-bg);
+ box-shadow: 0 3px 0 var(--button-danger-active);
+}
+
+[class*='animal-btn-danger'][class*='animal-btn-primary']:hover:not(:disabled) {
+ border-color: var(--button-danger-hover);
+ background: var(--button-danger-hover);
+}
+
+[class*='animal-btn-danger'][class*='animal-btn-default'],
+[class*='animal-btn-danger'][class*='animal-btn-dashed'] {
+ color: var(--button-danger-text);
+ border-color: rgba(182, 77, 72, 0.45);
+ background: var(--workbench-danger-soft);
+}
+
+[class*='animal-btn-danger'][class*='animal-btn-text'],
+[class*='animal-btn-danger'][class*='animal-btn-link'],
+[class*='animal-btn-danger'][class*='animal-btn-ghost'] {
+ color: var(--button-danger-bg);
+ background: transparent;
+}
+
+[class*='animal-btn-danger'][class*='animal-btn-text']:hover:not(:disabled),
+[class*='animal-btn-danger'][class*='animal-btn-link']:hover:not(:disabled),
+[class*='animal-btn-danger'][class*='animal-btn-ghost']:hover:not(:disabled) {
+ color: var(--button-danger-hover);
+ background: var(--workbench-danger-soft);
+}
+
+[class*='animal-btn-'][class*='animal-btn-loading'] {
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-active);
+ background: var(--button-primary-bg);
+}
+
+[class*='animal-btn-']:disabled {
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ box-shadow: none;
+ opacity: 1;
+ transform: none;
+}
+
+/* Compact actions: secondary controls that sit inside panels, tables and drawers. */
+button.state-panel-button,
+button.fleet-log-retention-actions .primary-action,
+.fleet-log-retention-actions .primary-action,
+button.fleet-notifications-retry-all,
+button.fleet-messages-delete,
+button.fleet-messages-search button,
+.fleet-messages-search button,
+button.fleet-notification-entry-button,
+.identity-actions button,
+.device-context-actions a,
+.row-actions button,
+.pagination button,
+.batch-entry-actions button,
+.fleet-card-admin-actions button,
+.segmented-control button {
+ min-height: var(--button-height-compact);
+ gap: 0.35rem;
+ padding: 0.3rem 0.7rem;
+ border-radius: var(--button-radius-compact);
+ font-size: 0.76rem;
+ font-weight: 750;
+}
+
+/* Secondary action inside a panel: keep it on the mint accent without the heavy fill. */
+button.fleet-notifications-retry-all,
+.fleet-notifications-retry-all {
+ color: var(--button-primary-hover);
+ border: 1px solid var(--button-primary-bg);
+ background: var(--workbench-mint-soft);
+ box-shadow: none;
+}
+
+button.fleet-notifications-retry-all:hover:not(:disabled),
+.fleet-notifications-retry-all:hover:not(:disabled) {
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-hover);
+ background: var(--button-primary-hover);
+}
+
+button.fleet-notifications-retry-all:disabled,
+.fleet-notifications-retry-all:disabled {
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ opacity: 1;
+}
+
+button.fleet-messages-search button,
+.fleet-messages-search button {
+ color: var(--button-primary-text);
+ border: 1px solid var(--button-primary-bg);
+ background: var(--button-primary-bg);
+ box-shadow: none;
+}
+
+button.fleet-messages-search button:hover:not(:disabled),
+.fleet-messages-search button:hover:not(:disabled) {
+ border-color: var(--button-primary-hover);
+ background: var(--button-primary-hover);
+}
+
+button.fleet-messages-search button:disabled,
+.fleet-messages-search button:disabled {
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ opacity: 1;
+}
+
+button.fleet-messages-delete,
+button.danger-button,
+.danger-button,
+button.row-actions button.is-danger {
+ color: var(--button-danger-text);
+ border: 1px solid var(--button-danger-bg);
+ background: var(--button-danger-bg);
+ box-shadow: 0 2px 0 var(--button-danger-active);
+}
+
+button.fleet-messages-delete:hover:not(:disabled),
+.danger-button:hover:not(:disabled) {
+ color: var(--button-danger-text);
+ border-color: var(--button-danger-hover);
+ background: var(--button-danger-hover);
+}
+
+button.fleet-messages-delete:disabled,
+.danger-button:disabled {
+ color: var(--button-disabled-text);
+ border-color: var(--button-disabled-border);
+ background: var(--button-disabled-bg);
+ box-shadow: none;
+ opacity: 1;
+}
+
+.identity-actions button,
+.device-context-actions a {
+ color: var(--button-secondary-text);
+ border: 1px solid var(--button-secondary-border);
+ background: var(--button-secondary-bg);
+ box-shadow: none;
+}
+
+.identity-actions button:hover:not(:disabled),
+.device-context-actions a:hover {
+ color: var(--button-secondary-hover-text);
+ border-color: var(--button-secondary-hover-border);
+ background: var(--button-secondary-hover-bg);
+}
+
+.device-context-primary {
+ color: var(--button-primary-text);
+ border-color: var(--button-primary-bg);
+ background: var(--button-primary-bg);
+}
+
+.device-context-primary:hover {
+ border-color: var(--button-primary-hover);
+ background: var(--button-primary-hover);
+}
+
+/* Filter chips stay pill shaped on purpose: they are toggles, not commands. */
+.fleet-messages-filter-chip {
+ min-height: var(--button-height-compact);
+ padding: 0.3rem 0.7rem;
+ color: var(--workbench-muted);
+ border: 1px solid var(--workbench-border-strong);
+ background: var(--workbench-surface);
+}
+
+.fleet-messages-filter-chip[aria-pressed='true'] {
+ color: var(--button-primary-hover);
+ border-color: var(--button-primary-bg);
+ background: var(--workbench-mint-soft);
+}
+
+/* Tabs and segmented controls keep their own geometry, but share the height scale. */
+.workbench-tabs button {
+ min-height: var(--button-height-large);
+}
+
+.segmented-control button {
+ border-radius: var(--button-radius-compact);
+}
+
+.segmented-control {
+ border-radius: 12px;
+}
+
+button:focus-visible {
+ outline: 3px solid var(--button-focus-ring);
+ outline-offset: 2px;
+}
+
+@media (max-width: 34rem) {
+ .workbench-heading > button.primary-action,
+ .workbench-heading > a.primary-action {
+ width: 100%;
+ }
+
+ button.state-panel-button,
+ button.fleet-notifications-retry-all,
+ button.fleet-messages-delete,
+ .identity-actions button,
+ .device-context-actions a {
+ min-height: var(--button-height);
+ flex: 1 1 auto;
+ }
+}
diff --git a/apps/web/src/ui/button-contrast.audit.test.tsx b/apps/web/src/ui/button-contrast.audit.test.tsx
new file mode 100644
index 0000000..3a50899
--- /dev/null
+++ b/apps/web/src/ui/button-contrast.audit.test.tsx
@@ -0,0 +1,100 @@
+// @vitest-environment jsdom
+import { readFile } from 'node:fs/promises';
+
+import { cleanup, render } from '@testing-library/react';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { AppShell } from '../app-shell.js';
+import { flatten } from './test-css.js';
+
+function channel(value: number): number {
+ const ratio = value / 255;
+ return ratio <= 0.04045 ? ratio / 12.92 : ((ratio + 0.055) / 1.055) ** 2.4;
+}
+
+function luminance(rgb: readonly [number, number, number]): number {
+ return 0.2126 * channel(rgb[0]) + 0.7152 * channel(rgb[1]) + 0.0722 * channel(rgb[2]);
+}
+
+function parseColor(input: string): readonly [number, number, number] | null {
+ const m = input.match(/rgba?\(([^)]+)\)/);
+ if (!m) return null;
+ const parts = m[1].split(',').map((p) => Number.parseFloat(p.trim()));
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
+}
+
+function contrast(
+ fore: readonly [number, number, number],
+ back: readonly [number, number, number],
+) {
+ const l1 = luminance(fore);
+ const l2 = luminance(back);
+ return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
+}
+
+function effectiveBackground(element: Element): readonly [number, number, number] {
+ let node: Element | null = element;
+ while (node) {
+ const value = parseColor(window.getComputedStyle(node).backgroundColor);
+ const isTransparent =
+ !value ||
+ (value[0] === 0 &&
+ value[1] === 0 &&
+ value[2] === 0 &&
+ getComputedStyle(node).backgroundColor === 'rgba(0, 0, 0, 0)');
+ if (!isTransparent) return value ?? [255, 255, 255];
+ node = node.parentElement;
+ }
+ return [233, 244, 223];
+}
+
+const ROUTES = [
+ '/fleet',
+ '/fleet/messages',
+ '/fleet/notifications',
+ '/fleet/logs',
+ '/automation',
+ '/jobs',
+ '/audit',
+ '/settings/system',
+] as const;
+
+afterEach(() => {
+ cleanup();
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+});
+
+describe('painted control contrast', () => {
+ it('keeps every rendered control at WCAG AA', { timeout: 30_000 }, async () => {
+ const raw = await readFile(
+ new URL('file:///Users/chick/.local/opt/multi-simadmin/apps/web/src/styles.css'),
+ 'utf8',
+ );
+ const style = document.createElement('style');
+ style.textContent = flatten(raw);
+ document.head.append(style);
+
+ const failures: string[] = [];
+ for (const pathname of ROUTES) {
+ const { unmount } = render();
+ await new Promise((done) => window.setTimeout(done, 40));
+ for (const element of document.querySelectorAll('button, a, [role="button"]')) {
+ const cs = window.getComputedStyle(element);
+ const fore = parseColor(cs.color);
+ if (!fore) continue;
+ const back = effectiveBackground(element);
+ const ratio = contrast(fore, back);
+ if (ratio >= 4.5) continue;
+ const label = (element.textContent ?? '').trim().slice(0, 30) || element.className;
+ failures.push(
+ `${pathname} ${ratio.toFixed(2)}:1 fg=${cs.color} bg=rgb(${back.join(',')}) <${element.tagName.toLowerCase()} class="${element.className}"> "${label}"`,
+ );
+ }
+ unmount();
+ }
+
+ for (const line of failures) console.log(line);
+ expect(failures).toEqual([]);
+ });
+});
diff --git a/apps/web/src/ui/button-system.test.ts b/apps/web/src/ui/button-system.test.ts
new file mode 100644
index 0000000..7f6c6a4
--- /dev/null
+++ b/apps/web/src/ui/button-system.test.ts
@@ -0,0 +1,268 @@
+import { readFile } from 'node:fs/promises';
+
+import { describe, expect, it } from 'vitest';
+
+type Rule = { selector: string; declarations: Record };
+
+/**
+ * The stylesheet is layered by years of incremental patches, so asserting on raw substrings
+ * proves nothing about what the browser finally paints. These helpers resolve the cascade.
+ */
+function parseDeclarations(body: string): Record {
+ const declarations: Record = {};
+ for (const part of body.split(';')) {
+ const separator = part.indexOf(':');
+ if (separator === -1) continue;
+ const property = part.slice(0, separator).trim();
+ const value = part.slice(separator + 1).trim();
+ if (property && value) declarations[property] = value;
+ }
+ return declarations;
+}
+
+function collectRules(css: string, rules: Rule[] = []): Rule[] {
+ let index = 0;
+ let preludeStart = 0;
+ while (index < css.length) {
+ if (css[index] !== '{') {
+ index += 1;
+ continue;
+ }
+ const selector = css.slice(preludeStart, index).trim();
+ let cursor = index + 1;
+ let depth = 1;
+ while (cursor < css.length && depth > 0) {
+ if (css[cursor] === '{') depth += 1;
+ else if (css[cursor] === '}') depth -= 1;
+ cursor += 1;
+ }
+ const body = css.slice(index + 1, cursor - 1);
+ if (selector.startsWith('@')) {
+ collectRules(body, rules);
+ } else if (selector) {
+ rules.push({ selector, declarations: parseDeclarations(body) });
+ }
+ preludeStart = cursor;
+ index = cursor;
+ }
+ return rules;
+}
+
+async function loadStylesheet() {
+ const css = await readFile(new URL('../styles.css', import.meta.url), 'utf8');
+ const rules = collectRules(css.replace(/\/\*[\s\S]*?\*\//g, ''), []);
+ const tokens: Record = {};
+ for (const rule of rules) {
+ if (rule.selector !== ':root') {
+ continue;
+ }
+ for (const [property, value] of Object.entries(rule.declarations)) {
+ if (property.startsWith('--')) tokens[property] = value;
+ }
+ }
+ const wins = (target: string, property: string) => {
+ let winner: string | undefined;
+ for (const rule of rules) {
+ if (!rule.selector.split(',').some((entry) => entry.replace(/\s+/g, ' ').trim() === target))
+ continue;
+ const value = rule.declarations[property];
+ if (value !== undefined) winner = value;
+ }
+ return winner;
+ };
+ const resolve = (value: string | undefined): string | undefined => {
+ if (value === undefined) return undefined;
+ return value.replace(/var\((--[a-z0-9-]+)\)/gi, (_, name: string) => tokens[name] ?? '');
+ };
+ const exact = (selector: string, property: string) =>
+ rules.filter((rule) => rule.selector.replace(/\s+/g, ' ') === selector).at(-1)?.declarations[
+ property
+ ];
+ return { rules, tokens, wins, resolve, exact };
+}
+
+function channel(value: number): number {
+ const ratio = value / 255;
+ return ratio <= 0.04045 ? ratio / 12.92 : ((ratio + 0.055) / 1.055) ** 2.4;
+}
+
+function contrastRatio(foreground: string, background: string): number {
+ const parse = (input: string) => {
+ const hex = input.trim().match(/^#([0-9a-f]{6})$/i);
+ if (!hex) throw new Error(`expected a 6 digit hex colour, got "${input}"`);
+ const int = Number.parseInt(hex[1], 16);
+ return [int >> 16, (int >> 8) & 0xff, int & 0xff] as const;
+ };
+ const luminance = (input: string) => {
+ const [red, green, blue] = parse(input);
+ return 0.2126 * channel(red) + 0.7152 * channel(green) + 0.0722 * channel(blue);
+ };
+ const lighter = Math.max(luminance(foreground), luminance(background));
+ const darker = Math.min(luminance(foreground), luminance(background));
+ return (lighter + 0.05) / (darker + 0.05);
+}
+
+describe('button system', () => {
+ it('exposes one compact height and radius scale', async () => {
+ const { tokens } = await loadStylesheet();
+
+ expect(tokens['--button-height']).toBe('2.375rem');
+ expect(tokens['--button-height-compact']).toBe('2.125rem');
+ expect(tokens['--button-height-large']).toBe('2.625rem');
+ expect(tokens['--icon-button-size']).toBe('2.5rem');
+ expect(tokens['--button-radius']).toBe('10px');
+ });
+
+ it('paints the primary action with the accessible token pair, not the old teal pill', async () => {
+ const { rules, wins, resolve } = await loadStylesheet();
+
+ // A bare `.primary-action` selector has specificity (0,1,0) and loses to the generic
+ // `button:not([class*='animal-'])` surface rule at (0,1,1). That is what turned the label
+ // white on cream, so the winning declarations must live on an element-qualified selector.
+ const bare = rules.filter(
+ (rule) =>
+ rule.declarations.background !== undefined &&
+ rule.selector.split(',').some((entry) => entry.trim() === '.primary-action'),
+ );
+ expect(bare.map((rule) => rule.selector)).toEqual([]);
+
+ expect(wins('button.primary-action', 'border-radius')).toBe('var(--button-radius)');
+ const background = resolve(wins('button.primary-action', 'background'));
+ const color = resolve(wins('button.primary-action', 'color'));
+
+ expect(background).toMatch(/^#[0-9a-f]{6}$/i);
+ expect(color).toMatch(/^#[0-9a-f]{6}$/i);
+ expect(contrastRatio(color as string, background as string)).toBeGreaterThanOrEqual(4.5);
+ });
+
+ it('keeps disabled actions legible instead of fading them out', async () => {
+ const { wins, resolve, tokens } = await loadStylesheet();
+
+ expect(wins('button.primary-action:disabled', 'opacity')).toBe('1');
+ expect(wins('button:disabled', 'opacity')).toBe('1');
+ expect(
+ contrastRatio(
+ resolve(tokens['--button-disabled-text']) as string,
+ resolve(tokens['--button-disabled-bg']) as string,
+ ),
+ ).toBeGreaterThanOrEqual(3);
+ });
+
+ it('uses one accessible field scale shared by inputs, selects and textareas', async () => {
+ const { tokens, exact } = await loadStylesheet();
+
+ expect(tokens['--field-height']).toBe('2.375rem');
+ expect(tokens['--field-radius']).toBe('10px');
+ expect(tokens['--field-border']).toBe('var(--line-strong)');
+ expect(tokens['--field-focus-border']).toBe('var(--button-focus-ring)');
+ expect(exact('button, input, select, textarea', 'border')).toBe(
+ '1px solid var(--field-border)',
+ );
+ expect(exact('button, input, select, textarea', 'background')).toBe('var(--field-bg)');
+ expect(exact('input:focus, select:focus, textarea:focus', 'border-color')).toBe(
+ 'var(--field-focus-border)',
+ );
+ });
+
+ it('normalises semantic surfaces in notification, log and message centres', async () => {
+ const { tokens, rules } = await loadStylesheet();
+ const semanticNames = [
+ '--workbench-mint-border',
+ '--workbench-warning-border',
+ '--workbench-warning-text',
+ '--workbench-warning-soft',
+ '--workbench-danger-border',
+ ];
+
+ expect(tokens['--workbench-warning-text']).toBe('#825f0c');
+ expect(semanticNames.every((name) => /^#[0-9a-f]{6}$/i.test(tokens[name] ?? ''))).toBe(true);
+ expect(
+ contrastRatio(tokens['--workbench-warning-text'], tokens['--workbench-warning-soft']),
+ ).toBeGreaterThanOrEqual(4.5);
+ expect(
+ rules
+ .filter((rule) =>
+ [
+ '.fleet-notifications-retry-all',
+ '.fleet-messages-delete',
+ '.fleet-log-filters select',
+ ].some((selector) => rule.selector.split(',').some((entry) => entry.trim() === selector)),
+ )
+ .every((rule) => !Object.values(rule.declarations).join(' ').includes('rgba(')),
+ ).toBe(true);
+
+ const css = (await readFile(new URL('../styles.css', import.meta.url), 'utf8')).replace(
+ /\/\*[\s\S]*?\*\//g,
+ '',
+ );
+ expect(css).not.toMatch(
+ /#8a6722|#fff8e8|#fffafa|rgba\(17,\s*119,\s*110|rgba\(170,\s*120,\s*35|rgba\(183,\s*77,\s*72/,
+ );
+ });
+
+ it('keeps workbench text and focus indicators on the accessible brand scale', async () => {
+ const { tokens, resolve } = await loadStylesheet();
+
+ expect(tokens['--workbench-mint']).toBe('#0f6e66');
+ expect(tokens['--workbench-muted']).toBe('#6f6350');
+ expect(tokens['--button-focus-ring']).toBe('#0f6e66');
+ expect(tokens['--animal-primary-color-hover']).toBe('#0f6e66');
+ expect(tokens['--animal-text-color-muted']).toBe('#6f6350');
+ expect(
+ contrastRatio(
+ resolve(tokens['--workbench-muted']) as string,
+ resolve(tokens['--animal-canvas-fallback'] ?? '#e9f4df') as string,
+ ),
+ ).toBeGreaterThanOrEqual(4.5);
+ });
+
+ it('does not resurrect low-contrast legacy accents', async () => {
+ const css = (await readFile(new URL('../styles.css', import.meta.url), 'utf8')).replace(
+ /\/\*[\s\S]*?\*\//g,
+ '',
+ );
+
+ expect(css).not.toMatch(/#19b8aa|#178f84|#109d92|rgba\(25,\s*184,\s*170/i);
+ expect(css).not.toMatch(/#d95757|rgba\(217,\s*87,\s*87/i);
+ expect(css).not.toMatch(/#8f806b|#81745f|#8d816e|#766a58|#7f725f/);
+ });
+
+ it('normalises Animal UI buttons through attribute selectors, not hashed classes', async () => {
+ const { rules, wins } = await loadStylesheet();
+ const selectors = rules.map((rule) => rule.selector);
+
+ expect(selectors.some((selector) => selector.includes("[class*='animal-btn-']"))).toBe(true);
+ expect(selectors.some((s) => s.includes("[class*='animal-btn-small']"))).toBe(true);
+ // Hashed module classes such as animal-btn-QoKe1 are regenerated on every library upgrade
+ // and silently stop matching, so only the semantic suffixes may appear here.
+ const semantic =
+ 'small|middle|large|icon|block|primary|default|dashed|text|link|ghost|danger|loading';
+ expect(selectors.join(' ')).not.toMatch(
+ new RegExp(`animal-btn-(?!${semantic}\\b)[A-Za-z0-9]{4,}`),
+ );
+ expect(wins("[class*='animal-btn-']", 'border-radius')).toBe('var(--button-radius)');
+ });
+
+ it('gives icon buttons a square target that matches the text scale', async () => {
+ const { wins } = await loadStylesheet();
+
+ expect(wins('button.icon-button', 'width')).toBe('var(--icon-button-size)');
+ expect(wins('button.icon-button', 'border-radius')).toBe('var(--button-radius)');
+ expect(wins('button.fleet-card-menu-trigger', 'border-radius')).toBe('var(--button-radius)');
+ expect(wins('button.row-action-trigger', 'min-height')).toBe('var(--icon-button-size)');
+ });
+
+ it('drops the capsule radius from every primary action variant', async () => {
+ const { rules } = await loadStylesheet();
+
+ const offenders = rules
+ .filter((rule) =>
+ rule.selector
+ .split(',')
+ .some((entry) => /(^|[\s>+~])\.primary-action(?![-\w])/.test(entry.trim())),
+ )
+ .filter((rule) => /999px|50%/.test(Object.values(rule.declarations).join(' ')));
+
+ expect(offenders.map((rule) => rule.selector)).toEqual([]);
+ });
+});
diff --git a/apps/web/src/ui/test-css.ts b/apps/web/src/ui/test-css.ts
new file mode 100644
index 0000000..d34f2d8
--- /dev/null
+++ b/apps/web/src/ui/test-css.ts
@@ -0,0 +1,40 @@
+// jsdom can read custom properties, but computed values that inherit through multiple
+// layers are easier to audit when the stylesheet is flattened to literals first.
+export function flatten(css: string): string {
+ const tokens: Record = {};
+ const root = /:root\s*\{([^}]*)\}/g;
+ let match: RegExpExecArray | null;
+ while ((match = root.exec(css))) {
+ for (const declaration of match[1]!.split(';')) {
+ const separator = declaration.indexOf(':');
+ if (separator === -1) continue;
+ const property = declaration.slice(0, separator).trim();
+ const value = declaration.slice(separator + 1).trim();
+ if (property.startsWith('--')) tokens[property] = value;
+ }
+ }
+
+ const resolved: Record = {};
+ function resolve(name: string, seen: ReadonlySet = new Set()): string {
+ const cacheKey = `${name}\u0000${[...seen].join('\u0001')}`;
+ if (resolved[cacheKey] !== undefined) return resolved[cacheKey]!;
+ if (seen.has(name)) return '';
+ const raw = tokens[name];
+ if (raw === undefined) return '';
+ return substitute(raw, new Set(seen).add(name));
+ }
+
+ function substitute(text: string, seen: ReadonlySet = new Set()): string {
+ let output = text;
+ for (let guard = 0; guard < 12 && /var\(/.test(output); guard += 1) {
+ output = output.replace(
+ /var\(\s*(--[a-z0-9-]+)\s*(?:,\s*([^()]*))?\)/gi,
+ (_, name: string, fallback?: string) =>
+ resolve(name, seen) || (fallback ? substitute(fallback, seen) : ''),
+ );
+ }
+ return output;
+ }
+
+ return substitute(css.replace(/^\s*--[a-z0-9-]+\s*:[^;]*;\s*$/gm, ''));
+}