Deleted CSS left its modifiers behind — restore six base rules, and check for the rest #98

Merged
bvandeusen merged 4 commits from dev into main 2026-08-05 09:41:19 -04:00
Owner

Four things looked wrong after the last deploy. Two were the same bug, and it is not design drift — it is deleted CSS.

The failure

Removing a rule from a scoped stylesheet leaves its modifiers behind. The selector still exists, so nothing reads as unused, and the element renders with no base styling at all.

class what survived what it looked like
.btn-workspace :hover the Workspace link as raw browser blue, underlined
.milestone-header .clickable, :hover children are written for a flex ROW — .ms-name { flex: 1 }, the progress track, .ms-pct — so without the parent they stacked, and a one-line milestone became five
.milestone-group nothing no card around each milestone
.ds-header its h1 descendant no spacing of its own
.system-card --archived, :hover .system-actions same flex-row collapse; every child still declares flex: 1 / flex-shrink: 0
.systems-list nothing the systems list rendering with browser bullets and indent
.graph-embed its :deep() descendant the panel is a flex column whose header is flex-shrink: 0; without this the height: 100% below resolves against auto and does nothing — a comment specifying a rule that could not work

vue-tsc --noEmit is the frontend's entire check, and a dead style typechecks perfectly.

The check

scripts/check_dangling_styles.py, in the lint lane, stdlib only, knows no class name or convention (rule #115). It reports an element whose every static class has no base rule anywhere while at least one carries modifier rules.

It was wrong about four on its first run — .pane.empty and td.num are base rules for the element carrying those classes. It now records a compound's whole class set. A check with false reports is one that gets skimmed.

Seven reports remain, all wrappers that are bare on purpose, each now carrying a comment saying so — which is what makes a new entry a signal rather than noise.

Known blind spot, recorded in the script: a class with no rule anywhere is invisible, being indistinguishable from a semantic-only hook. .systems-list was found by reading the file beside a class that was half-styled.

Header overlap

The nav pill bar was position: absolute; left: 50%, so it never participated in layout — out of room it overlapped the brand and utility cluster instead of pushing them. A sixth link reached that at ~1270px, an ordinary window.

Now 1fr auto 1fr. A 1fr track has an auto minimum, so neither side can be squeezed under its content and the two stay equal to each other — which is what keeps the bar centred in the viewport rather than in the leftover space. Overflow becomes the header growing, not two things sharing pixels.

Token preview

The checkerboard was on the whole specimen stage, so every swatch sat in a frame of checks and the pattern became the loudest thing on the page. It is now a background layer under the colour: an opaque value hides it, a 15% tint shows exactly as much as it should. Text specimens lose the box entirely, and name/value/purpose are one line each with the full text on hover — they wrapped freely before, so a card was two to five lines tall depending on how long its color-mix() happened to be.

The design-token check caught --tp-fill on the commit that introduced it: set inline only, so nothing in any stylesheet declared it. Declaring it on .tp-swatch is the real fix — a token resolving to nothing now renders as bare checks rather than an invalid gradient.

Also

  • .btn-cta joins the shared button family. The gradient-and-glow brand moment was living in one view's scoped block, which is what made it deletable. The project header actions are now one family at one size instead of four stylings.
  • The shared button shape gained inline-flex + gap, so a button carrying an icon centres it without each caller rebuilding the row.
  • A project's goal is a textarea. It rendered as "Maintain Scribe as the reliabl" in a sidebar with room to spare.

Not fixed, and visible

The delete-confirmation modals still use a separate bespoke .modal-btn family. That is #2279's territory (DRY pass 4 lists ~12 usages) and will read as inconsistent beside the corrected header row.

Verification

CI green on a6d6550 — typecheck, lint + design-token check + dangling-style check, unit, integration, plugin hooks, image build. No migration.

Four things looked wrong after the last deploy. Two were the same bug, and it is not design drift — it is deleted CSS. ## The failure Removing a rule from a scoped stylesheet leaves its modifiers behind. The selector still exists, so nothing reads as unused, and the element renders with **no base styling at all**. | class | what survived | what it looked like | |---|---|---| | `.btn-workspace` | `:hover` | the Workspace link as **raw browser blue, underlined** | | `.milestone-header` | `.clickable`, `:hover` | children are written for a flex ROW — `.ms-name { flex: 1 }`, the progress track, `.ms-pct` — so without the parent they **stacked**, and a one-line milestone became five | | `.milestone-group` | nothing | no card around each milestone | | `.ds-header` | its `h1` descendant | no spacing of its own | | `.system-card` | `--archived`, `:hover .system-actions` | same flex-row collapse; every child still declares `flex: 1` / `flex-shrink: 0` | | `.systems-list` | nothing | the systems list rendering with **browser bullets and indent** | | `.graph-embed` | its `:deep()` descendant | the panel is a flex column whose header is `flex-shrink: 0`; without this the `height: 100%` below resolves against `auto` and does nothing — a comment specifying a rule that could not work | `vue-tsc --noEmit` is the frontend's entire check, and **a dead style typechecks perfectly**. ## The check `scripts/check_dangling_styles.py`, in the lint lane, stdlib only, knows no class name or convention (rule #115). It reports an element whose every static class has no base rule anywhere while at least one carries modifier rules. It was wrong about four on its first run — `.pane.empty` and `td.num` **are** base rules for the element carrying those classes. It now records a compound's whole class *set*. A check with false reports is one that gets skimmed. Seven reports remain, all wrappers that are bare on purpose, each now carrying a comment saying so — which is what makes a *new* entry a signal rather than noise. Known blind spot, recorded in the script: a class with **no** rule anywhere is invisible, being indistinguishable from a semantic-only hook. `.systems-list` was found by reading the file beside a class that was half-styled. ## Header overlap The nav pill bar was `position: absolute; left: 50%`, so it never participated in layout — out of room it **overlapped** the brand and utility cluster instead of pushing them. A sixth link reached that at ~1270px, an ordinary window. Now `1fr auto 1fr`. A `1fr` track has an auto minimum, so neither side can be squeezed under its content and the two stay equal to each other — which is what keeps the bar centred in the *viewport* rather than in the leftover space. Overflow becomes the header growing, not two things sharing pixels. ## Token preview The checkerboard was on the whole specimen stage, so every swatch sat in a frame of checks and the pattern became the loudest thing on the page. It is now a background layer **under** the colour: an opaque value hides it, a 15% tint shows exactly as much as it should. Text specimens lose the box entirely, and name/value/purpose are one line each with the full text on hover — they wrapped freely before, so a card was two to five lines tall depending on how long its `color-mix()` happened to be. The design-token check caught `--tp-fill` on the commit that introduced it: set inline only, so nothing in any stylesheet declared it. Declaring it on `.tp-swatch` is the real fix — a token resolving to nothing now renders as bare checks rather than an invalid gradient. ## Also - `.btn-cta` joins the shared button family. The gradient-and-glow brand moment was living in one view's scoped block, which is what made it deletable. The project header actions are now one family at one size instead of four stylings. - The shared button shape gained `inline-flex` + `gap`, so a button carrying an icon centres it without each caller rebuilding the row. - A project's goal is a textarea. It rendered as "Maintain Scribe as the reliabl" in a sidebar with room to spare. ## Not fixed, and visible The delete-confirmation modals still use a separate bespoke `.modal-btn` family. That is #2279's territory (DRY pass 4 lists ~12 usages) and will read as inconsistent beside the corrected header row. ## Verification CI green on `a6d6550` — typecheck, lint + design-token check + dangling-style check, unit, integration, plugin hooks, image build. No migration.
bvandeusen added 4 commits 2026-08-05 09:41:11 -04:00
fix(ui): restore four base rules a CSS sweep deleted, and check for the rest
CI & Build / Python lint (push) Failing after 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 23s
CI & Build / integration (push) Successful in 27s
CI & Build / Python tests (push) Canceled after 38s
CI & Build / Build & push image (push) Canceled after 0s
4a9744172f
Operator reported four things looking wrong. Two were the same bug, and it is
not a design drift — it is deleted CSS.

Removing a rule from a scoped stylesheet leaves its modifiers behind. The
selector still exists, so nothing reads as unused, and the element renders with
no base styling at all:

  .btn-workspace      base gone, :hover survived — the Workspace link rendered
                      as raw browser blue, underlined
  .milestone-header   base gone, .clickable and :hover survived. Every child is
                      written for a flex ROW (.ms-name { flex: 1 }, the progress
                      track, .ms-pct), so without the parent they stacked and a
                      one-line milestone became five. That is the "projects
                      section uses space poorly" — a deletion, not a redesign.
  .milestone-group    no rule at all; the card around each milestone
  .ds-header          only its h1 descendant survived

vue-tsc cannot see any of it. A dead style typechecks perfectly.

scripts/check_dangling_styles.py finds the shape: an element whose every static
class has no base rule anywhere, while at least one carries modifier rules. It
reports 11 more. Reported and not gated, because a genuinely bare wrapper is
legitimate — the signal is the count growing. Runs in the lint lane, stdlib
only, and knows no class name or convention (rule #115).

Also from the same report:

- The header pill bar was `position: absolute; left: 50%`, so it did not
  participate in layout: out of room, it OVERLAPPED the brand and the utility
  cluster instead of pushing them. A sixth link reached that at ~1270px, an
  ordinary window. Now `1fr auto 1fr` — a 1fr track has an auto minimum, so
  neither side can be squeezed under its content and the two stay equal, which
  is what keeps the bar centred in the viewport rather than in the leftover
  space. Overflow becomes the header growing, not two things sharing pixels.

- The token preview put its checkerboard on the whole specimen stage, so every
  swatch sat in a frame of checks and the pattern read as the loudest thing on
  the page. The checks now sit UNDER the colour as a second background layer:
  an opaque value hides them, a 15% tint shows exactly as much as it should.
  Text-bearing specimens lose the box entirely, and name/value/purpose are one
  line each with the full text on hover — they wrapped freely before, so a card
  was two lines tall or five depending on how long its color-mix() happened to
  be, and the grid had no rhythm.

- .btn-cta joins the shared button family: the gradient-and-glow brand moment
  the system carries tokens for, which had been living in one view's scoped
  block. That is what made it deletable. The header actions are now one size
  and one family instead of four sizes and two.

- The shared button shape gained inline-flex + gap, so a button carrying an
  icon centres it without each caller rebuilding the row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
fix(design): declare --tp-fill so the token check can see it
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Canceled after 32s
6ac821178f
The swatch set it inline only, and a custom property that exists nowhere in a
stylesheet is exactly what check_design_tokens reports as unresolvable — it was
right, and it caught this on the commit that introduced it.

Declaring it on .tp-swatch is the real fix rather than a silencer: a token that
resolves to nothing now renders as bare checks instead of an invalid gradient,
which is what the inline value would produce when empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
fix(project): the goal field is a textarea, not a one-line input
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m1s
CI & Build / Build & push image (push) Successful in 39s
46271ccaa7
A project goal is a paragraph in practice. This one rendered as "Maintain
Scribe as the reliabl" with no way to read the rest but arrowing through it,
in a sidebar with room to spare. Description gets two more rows for the same
reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
fix(ui): walk the eleven dangling-style reports — two were real
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 45s
a6d6550483
#2444. Each needed reading rather than a batch fix, and the split was 2 real
losses, 4 false reports, 5 wrappers that are bare on purpose.

REAL:

  .system-card   was a flex row, and every child still says so —
                 .system-swatch and .system-actions are flex-shrink: 0,
                 .system-body and .system-form--inline are flex: 1.
                 align-items: flex-start is why the swatch carries
                 margin-top: 0.3rem: nudged onto the first line of text.
  .systems-list  no rule AT ALL, so the systems list rendered with browser
                 bullets and indent. Invisible to the check — see below.
  .graph-embed   the panel is a flex column whose header is flex-shrink: 0,
                 so this is the item that takes the remaining height. Without
                 it the `height: 100%` on the line below resolves against auto
                 and does nothing, which left the comment above it specifying
                 a rule that could not work.

FALSE REPORTS, and the checker was wrong rather than the code:

`.pane.empty` and `td.num` are base rules for the element that carries those
classes — the check read any compound with more than a lone class as a
modifier. It now records a compound's whole class SET and clears an element
carrying all of them, which is exact: recording the classes individually would
have cleared `.pane` everywhere on the strength of a rule that only applies
alongside `.empty`. Four reports gone, and a check with false reports is one
that gets skimmed.

BARE ON PURPOSE — .rb, .topic-group, .new-topic, .sub-list, .dash-head, and
both .detail-row rows. Each namespaces descendant rules and assumes nothing
about layout, which is the tell that separates them from a deleted base. All
seven now carry a comment saying so, so the next reader doesn't re-litigate
them and a NEW entry in the report means something actually changed.

Also recorded in the script: it cannot see a class with no rule anywhere, since
that is indistinguishable from a semantic-only hook. `.systems-list` was found
by reading the file beside a class that WAS half-styled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
bvandeusen merged commit 11c243c0fa into main 2026-08-05 09:41:19 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledScribe#98