gloss: re-vendor at 1b7b145 and sync DESIGN.md
Gloss went borderless: surface contrast now marks bounded surfaces and
hairlines only divide rows within one. The admin UI survives the walk
unchanged — no contract it uses moved, and admin-header's underline
reads as a divider, not a boundary.
Assisted-by: GLM-5.3 via pi
diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index d84a6ca..7f682d8 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -40,9 +40,11 @@ What that inheritance actually means:
never shows two accents in a view. `-ink` and `-soft` re-derive themselves.
- `--gl-color-success` and `--gl-color-danger` are **fixed** — they mean
confirmed and destructive, never decoration, never emphasis.
-- Flat surfaces and hairline borders do the elevation work. At most one
- `--gl-shadow-float` on screen — in this app that's the dialog or a toast.
- Border or shadow, never both.
+- Surface contrast does the elevation work: a bounded surface reads by
+ being a step lighter than what surrounds it, never by an outline.
+ Hairlines are dividers inside a surface (a CardRow, a table rule), never
+ a boundary around one. At most one `--gl-shadow-float` on screen — in
+ this app that's the dialog or a toast.
- Motion is `--gl-dur-fast` on hover and focus, named properties only, never
`all`. Press is not animated. Nothing enters, nothing bounces.
- Icons are Lucide at 16–20px, stroke in `currentColor`, never filled. Nothing
@@ -140,5 +142,7 @@ Ask, in order:
4. Is it reachable from both sides of any relationship it participates in?
5. Does Gloss already define the component? Use it as documented rather than
restyling something adjacent.
-6. Is there a literal hex, duration, or new class in the diff? There shouldn't
+6. Does it pass Gloss' refusals checklist, the consolidated list of
+ generated-interface tells the system refuses to produce?
+7. Is there a literal hex, duration, or new class in the diff? There shouldn't
be. Is there a Save button? There shouldn't be.
diff --git a/public/vendor/gloss/base.css b/public/vendor/gloss/base.css
index dd67260..c5918d5 100644
--- a/public/vendor/gloss/base.css
+++ b/public/vendor/gloss/base.css
@@ -1,4 +1,4 @@
-/* Vendored from kejadlen/gloss @ d1d0aa0abdc4a70d461ff1878b412424be583091, 2026-08-31.
+/* Vendored from kejadlen/gloss @ 1b7b1455f6177993182b2bff7a262537480c53e2, 2026-09-03.
Gloss ships this as plain CSS meant to be copied into a consuming
project (see install.md) — there is no build step to run here.
Refresh with `rake gloss:vendor GLOSS=/path/to/gloss/checkout`. */
diff --git a/public/vendor/gloss/components.css b/public/vendor/gloss/components.css
index 05fd37f..df4ad5b 100644
--- a/public/vendor/gloss/components.css
+++ b/public/vendor/gloss/components.css
@@ -1,12 +1,21 @@
-/* Vendored from kejadlen/gloss @ d1d0aa0abdc4a70d461ff1878b412424be583091, 2026-08-31.
+/* Vendored from kejadlen/gloss @ 1b7b1455f6177993182b2bff7a262537480c53e2, 2026-09-03.
Gloss ships this as plain CSS meant to be copied into a consuming
project (see install.md) — there is no build step to run here.
Refresh with `rake gloss:vendor GLOSS=/path/to/gloss/checkout`. */
/* Button
- A flat, quiet button that recedes rather than performs — no gradients, no
- shine. Variants: default, primary, dark, ghost, danger. Sizes: sm, md
- (default), lg — padding and font step only, weight and radius stay put.
+ No border, no ornament — hierarchy is ink density, not an outline. A
+ STAMP (primary, dark, danger) is a solid block of colour; a QUIET action
+ (default, ghost) carries no fill at rest and picks up a flat tint of the
+ stock on hover, the same gesture as a CardRow. Variants: default, primary,
+ dark, ghost, danger. Sizes: sm, md (default), lg — padding and font step
+ only, weight and radius stay put.
+
+ The default used to pad itself with --gl-space-xs/-s, which are the rungs
+ a layout reaches for between two controls, not inside one: on a wide
+ viewport they grew the default button taller than the large one. Every
+ size now sits one rung further down the same scale — see the block at
+ the bottom.
The contract is the element itself: a bare <button> is styled directly, no
class required. An <a> that acts like a button (navigates rather than
@@ -18,36 +27,50 @@
button:not(.icon-button),
a.btn {
- --_bg: var(--gl-color-surface-card);
+ --_bg: transparent;
--_fg: var(--gl-color-text-primary);
- --_border: var(--gl-color-border-default);
- --_pad: var(--gl-space-xs) var(--gl-space-s);
+ --_pad-y: var(--gl-space-2xs);
+ --_pad-x: var(--gl-space-xs);
--_font: var(--gl-step--1);
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--gl-space-2xs);
- padding: var(--_pad);
- border: 1px solid var(--_border);
+ padding: var(--_pad-y) var(--_pad-x);
+ border: none;
border-radius: var(--gl-radius-md);
background: var(--_bg);
color: var(--_fg);
- font-family: var(--gl-font-sans);
+ font-family: var(--gl-font-mono);
font-size: var(--_font);
- font-weight: 550;
- letter-spacing: -0.01em;
- line-height: 1.2;
+ font-weight: 500;
+ letter-spacing: 0.01em;
+ line-height: 1.15;
text-align: center;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
- transition: filter var(--gl-dur-fast) var(--gl-ease),
- border-color var(--gl-dur-fast) var(--gl-ease);
+ transition: background-color var(--gl-dur-fast) var(--gl-ease),
+ color var(--gl-dur-fast) var(--gl-ease),
+ filter var(--gl-dur-fast) var(--gl-ease);
}
-:is(button:not(.icon-button), a.btn):hover:not(:disabled):not([aria-disabled="true"]) { filter: brightness(0.96); }
-:is(button:not(.icon-button), a.btn):active:not(:disabled):not([aria-disabled="true"]) { filter: brightness(0.92); }
+/* Quiet variants (default, ghost) carry no fill at rest; hovering picks up
+ a flat tint of the stock instead of darkening a border that isn't there. */
+:is(button:not(.icon-button), a.btn):not([data-variant="primary"]):not([data-variant="dark"]):not([data-variant="danger"]):hover:not(:disabled):not([aria-disabled="true"]) {
+ background: var(--gl-color-surface-fill);
+ color: var(--gl-color-text-primary);
+}
+
+/* Stamp variants (primary, dark, danger) are already ink — hovering just
+ darkens the fill in place. */
+:is(button:not(.icon-button), a.btn)[data-variant="primary"]:hover:not(:disabled):not([aria-disabled="true"]),
+:is(button:not(.icon-button), a.btn)[data-variant="dark"]:hover:not(:disabled):not([aria-disabled="true"]),
+:is(button:not(.icon-button), a.btn)[data-variant="danger"]:hover:not(:disabled):not([aria-disabled="true"]) { filter: brightness(0.96); }
+:is(button:not(.icon-button), a.btn)[data-variant="primary"]:active:not(:disabled):not([aria-disabled="true"]),
+:is(button:not(.icon-button), a.btn)[data-variant="dark"]:active:not(:disabled):not([aria-disabled="true"]),
+:is(button:not(.icon-button), a.btn)[data-variant="danger"]:active:not(:disabled):not([aria-disabled="true"]) { filter: brightness(0.92); }
button:not(.icon-button):disabled,
:is(button:not(.icon-button), a.btn)[aria-disabled="true"] {
@@ -58,7 +81,6 @@ button:not(.icon-button):disabled,
:is(button:not(.icon-button), a.btn)[data-variant="primary"] {
--_bg: var(--gl-color-accent);
--_fg: var(--gl-color-accent-fg);
- --_border: var(--gl-color-accent);
}
/* Reserved for the single most-committing action in a view — a final Save,
@@ -66,13 +88,12 @@ button:not(.icon-button):disabled,
:is(button:not(.icon-button), a.btn)[data-variant="dark"] {
--_bg: var(--gl-color-text-primary);
--_fg: var(--gl-color-surface-page);
- --_border: var(--gl-color-text-primary);
}
+/* Same quiet block as default, one step further back: text-secondary at
+ rest instead of text-primary. */
:is(button:not(.icon-button), a.btn)[data-variant="ghost"] {
- --_bg: transparent;
- --_fg: var(--gl-color-text-primary);
- --_border: transparent;
+ --_fg: var(--gl-color-text-secondary);
}
/* The source hardcodes this label white rather than deriving it — danger
@@ -82,16 +103,23 @@ button:not(.icon-button):disabled,
:is(button:not(.icon-button), a.btn)[data-variant="danger"] {
--_bg: var(--gl-color-danger);
--_fg: var(--gl-color-accent-fg);
- --_border: var(--gl-color-danger);
}
-:is(button:not(.icon-button), a.btn)[data-size="sm"] { --_pad: 0.375rem 0.75rem; --_font: 0.8125rem; }
-:is(button:not(.icon-button), a.btn)[data-size="lg"] { --_pad: 0.75rem 1.375rem; --_font: var(--gl-step-0); }
+/* Each size is one step of the space scale wide and, at the ends, one step
+ of the type scale. Medium and large share a rung vertically and part on
+ type size, which is what keeps them in order at every viewport width: the
+ scale is fluid, so two sizes that differed only by which fluid rung they
+ padded with could — and did — cross over on a wide screen. */
+:is(button:not(.icon-button), a.btn)[data-size="sm"] { --_pad-y: var(--gl-space-3xs); --_pad-x: var(--gl-space-2xs); --_font: 0.8125rem; }
+:is(button:not(.icon-button), a.btn)[data-size="lg"] { --_pad-x: var(--gl-space-s); --_font: var(--gl-step-0); }
:is(button:not(.icon-button), a.btn)[data-block] { width: 100%; }
/* Button group — [role="group"] is already the correct ARIA role for a
- toolbar of related buttons, so it doubles as the styling hook for free. */
+ toolbar of related buttons, so it doubles as the styling hook for free.
+ A hairline divider stands in for the border the buttons themselves no
+ longer carry — without one, two adjacent quiet buttons would read as a
+ single wide button. */
[role="group"] {
display: inline-flex;
isolation: isolate;
@@ -99,18 +127,25 @@ button:not(.icon-button):disabled,
[role="group"] > button { border-radius: 0; }
[role="group"] > button:first-child { border-start-start-radius: var(--gl-radius-md); border-end-start-radius: var(--gl-radius-md); }
[role="group"] > button:last-child { border-start-end-radius: var(--gl-radius-md); border-end-end-radius: var(--gl-radius-md); }
-[role="group"] > button + button { margin-inline-start: -1px; }
+[role="group"] > button + button { border-inline-start: 1px solid var(--gl-color-border-hairline); }
[role="group"] > button:hover,
[role="group"] > button:focus-visible { z-index: 1; }
/* IconButton — a square icon-only button. Always needs an aria-label; it
becomes both the accessible name and the native tooltip. There is no
attribute that distinguishes "a button that is only an icon" from a plain
- Button, so this is the one sanctioned class in this file. */
+ Button, so this is the one sanctioned class in this file.
+
+ The box is not a number of its own: it is written the way Button's height
+ arrives — two rungs of padding around one 1.15 line of the label type it
+ would carry if it had a label. So the two are the same height at every
+ viewport width, and stay that way when the scale is retuned, instead of a
+ fixed square drifting away from a fluid button. */
.icon-button {
- --_size: 2.125rem;
+ --_pad: var(--gl-space-2xs);
+ --_line: var(--gl-step--1);
+ --_size: calc(2 * var(--_pad) + 1.15 * var(--_line));
--_bg: transparent;
- --_border: transparent;
--_fg: var(--gl-color-text-secondary);
display: inline-flex;
@@ -120,7 +155,7 @@ button:not(.icon-button):disabled,
height: var(--_size);
padding: 0;
flex: none;
- border: 1px solid var(--_border);
+ border: none;
border-radius: var(--gl-radius-md);
background: var(--_bg);
color: var(--_fg);
@@ -128,13 +163,16 @@ button:not(.icon-button):disabled,
transition: background-color var(--gl-dur-fast) var(--gl-ease),
color var(--gl-dur-fast) var(--gl-ease);
}
-.icon-button:hover:not(:disabled) { background: var(--gl-color-surface-fill-2); }
+.icon-button:hover:not(:disabled) { background: var(--gl-color-surface-fill); }
.icon-button:disabled { opacity: 0.5; cursor: default; }
-.icon-button[data-size="sm"] { --_size: 1.75rem; }
-.icon-button[data-size="lg"] { --_size: 2.5rem; }
+/* Same two overrides Button's own sm and lg make, so the pairs stay level. */
+.icon-button[data-size="sm"] { --_pad: var(--gl-space-3xs); --_line: 0.8125rem; }
+.icon-button[data-size="lg"] { --_line: var(--gl-step-0); }
-.icon-button[data-variant="outline"] { --_bg: var(--gl-color-surface-card); --_border: var(--gl-color-border-default); --_fg: var(--gl-color-text-primary); }
+/* No border on either variant — "outline" keeps its name for API
+ compatibility, but reads as a raised fill instead of a bordered box. */
+.icon-button[data-variant="outline"] { --_bg: var(--gl-color-surface-fill); --_fg: var(--gl-color-text-primary); }
.icon-button[data-variant="accent"] { --_bg: var(--gl-color-accent-soft); --_fg: var(--gl-color-accent-ink); }
/* Field
The labeled field wrapper — a <label> that wraps its own caption text
@@ -343,37 +381,40 @@ fieldset > legend {
color: var(--gl-color-text-secondary);
}
/* Badge
- Mono, uppercase, tracked — the catalog-label convention for status and
- meta values (SAVED, OVERDUE, RECOMMENDED). A label, never a button.
+ A catalog mark, not a tinted pill: a small ink square in the tone colour
+ plus mono uppercase text, on no background. No border, no radius — status
+ reads as a marginal annotation on the row it belongs to, not a control.
A <span> is too generic to style globally — it is used all over running
prose for unrelated reasons — so Badge gets the one sanctioned class this
shape needs. Tone is an attribute, not a modifier class. */
.badge {
- --_bg: var(--gl-color-surface-fill);
--_fg: var(--gl-color-text-secondary);
- --_border: var(--gl-color-border-hairline);
display: inline-flex;
align-items: center;
- gap: 0.25rem;
- padding: 0.1875rem 0.5rem;
- border: 1px solid var(--_border);
- border-radius: var(--gl-radius-sm);
- background: var(--_bg);
+ gap: 0.375rem;
color: var(--_fg);
font-family: var(--gl-font-mono);
font-size: var(--gl-step--2);
+ font-weight: 500;
text-transform: uppercase;
- letter-spacing: 0.06em;
+ letter-spacing: 0.09em;
line-height: 1;
white-space: nowrap;
}
+.badge::before {
+ content: "";
+ width: 0.375rem;
+ height: 0.375rem;
+ flex: none;
+ background: var(--_fg);
+}
-.badge[data-tone="accent"] { --_bg: var(--gl-color-accent-soft); --_fg: var(--gl-color-accent-ink); --_border: transparent; }
-.badge[data-tone="success"] { --_bg: var(--gl-color-success-bg); --_fg: var(--gl-color-success); --_border: transparent; }
-.badge[data-tone="danger"] { --_bg: var(--gl-color-danger-bg); --_fg: var(--gl-color-danger); --_border: transparent; }
+.badge[data-tone="accent"] { --_fg: var(--gl-color-accent-ink); }
+.badge[data-tone="success"] { --_fg: var(--gl-color-success); }
+.badge[data-tone="danger"] { --_fg: var(--gl-color-danger); }
.badge-set {
display: flex;
@@ -393,9 +434,9 @@ fieldset > legend {
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.625rem;
+ border: none;
border-radius: var(--gl-radius-sm);
background: var(--gl-color-surface-fill);
- border: 1px solid var(--gl-color-border-default);
color: var(--gl-color-text-secondary);
font-family: var(--gl-font-mono);
font-size: var(--gl-step--2);
@@ -424,9 +465,11 @@ fieldset > legend {
gap: 0.375rem;
}
/* Card
- Flat surface, hairline border, radius-lg. `[data-elevated]` is opt-in and
- reserved for the one focused/primary card in a view — a dialog, the
- primary capture card — flat is the default everywhere else.
+ Flat surface, no border, radius-lg. `--gl-color-surface-card` is one
+ neutral step lighter than the page around it — that tonal step, not an
+ outline, is what marks the edge. `[data-elevated]` is opt-in and reserved
+ for the one focused/primary card in a view — a dialog, the primary
+ capture card — flat is the default everywhere else.
A <div>/<article> is too generic to select bare, so Card keeps one
sanctioned class. When Card's job is holding a list of rows — the common
@@ -443,7 +486,7 @@ fieldset > legend {
padding: 0;
list-style: none;
background: var(--gl-color-surface-card);
- border: 1px solid var(--gl-color-border-hairline);
+ border: none;
border-radius: var(--gl-radius-lg);
box-shadow: none;
overflow: hidden;
@@ -492,10 +535,11 @@ fieldset > legend {
.card > li > a:only-child:hover { background: var(--gl-color-surface-fill); }
/* Documentation-only extras: a body/footer pair and a responsive grid, used
- by the prose pages (rationale.md, index.md) to show a Card as a plain
- content tile rather than a data row. Neither is part of Card's props —
- they exist because a content tile's inner layout has no element of its
- own to hang a selector on. */
+ by the prose pages (rationale.md, index.md, refusals.md) to show a Card
+ as a plain content tile rather than a data row. Neither is part of
+ Card's props — they exist because a content tile's inner layout has no
+ element of its own to hang a selector on. .card-body ul is the same idea
+ applied to a checklist tile (refusals.md) instead of a single paragraph. */
.card-body {
display: flex;
flex-direction: column;
@@ -503,7 +547,9 @@ fieldset > legend {
padding: var(--gl-space-m);
}
.card-body h3 { margin: 0; font: 600 var(--gl-step-0)/1.3 var(--gl-font-sans); }
-.card-body p { margin: 0; color: var(--gl-color-text-secondary); font-size: var(--gl-step--1); }
+.card-body p,
+.card-body ul { margin: 0; color: var(--gl-color-text-secondary); font-size: var(--gl-step--1); }
+.card-body ul { padding-left: 1.125em; }
.card-footer {
display: flex;
flex-wrap: wrap;
@@ -515,8 +561,8 @@ fieldset > legend {
background: var(--gl-color-surface-fill);
}
-.card[data-link] { transition: border-color var(--gl-dur-fast) var(--gl-ease), box-shadow var(--gl-dur-fast) var(--gl-ease); }
-.card[data-link]:hover { border-color: var(--gl-color-border-default); box-shadow: var(--gl-shadow-sm); }
+.card[data-link] { transition: box-shadow var(--gl-dur-fast) var(--gl-ease); }
+.card[data-link]:hover { box-shadow: var(--gl-shadow-sm); }
.card[data-link]:focus-within { outline: 2px solid var(--gl-color-accent); outline-offset: 2px; }
.card-grid {
diff --git a/public/vendor/gloss/tokens.css b/public/vendor/gloss/tokens.css
index a7711bc..646cd46 100644
--- a/public/vendor/gloss/tokens.css
+++ b/public/vendor/gloss/tokens.css
@@ -1,4 +1,4 @@
-/* Vendored from kejadlen/gloss @ d1d0aa0abdc4a70d461ff1878b412424be583091, 2026-08-31.
+/* Vendored from kejadlen/gloss @ 1b7b1455f6177993182b2bff7a262537480c53e2, 2026-09-03.
Gloss ships this as plain CSS meant to be copied into a consuming
project (see install.md) — there is no build step to run here.
Refresh with `rake gloss:vendor GLOSS=/path/to/gloss/checkout`. */