@minhspark/spacing-skill 2.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "spacing-skill",
3
+ "description": "Spacing & rhythm skill library for AI coding agents — anti-cramped spacing, vertical rhythm, and optical alignment for AI-generated UI.",
4
+ "plugins": [
5
+ {
6
+ "name": "spacing-skill",
7
+ "source": {
8
+ "source": "local",
9
+ "path": "./"
10
+ },
11
+ "policy": {
12
+ "installation": "AVAILABLE",
13
+ "authentication": "ON_INSTALL"
14
+ },
15
+ "category": "Design"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "spacing-skill",
3
+ "description": "Spacing & rhythm skill library for Claude Code — anti-cramped spacing, vertical rhythm, and optical alignment for AI-generated UI",
4
+ "owner": {
5
+ "name": "buidangminh23",
6
+ "email": ""
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "spacing-skill",
11
+ "description": "Anti-cramped spacing skill for AI-generated UI — three dials, one spacing scale, vertical rhythm, optical alignment, accessibility floors, and density discipline",
12
+ "version": "2.12.1",
13
+ "source": "./",
14
+ "author": {
15
+ "name": "buidangminh23",
16
+ "email": ""
17
+ }
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "spacing-skill",
3
+ "description": "Anti-cramped spacing skill for AI-generated UI — three dials, one spacing scale, vertical rhythm, optical alignment, accessibility floors, and density discipline",
4
+ "version": "2.12.1",
5
+ "author": {
6
+ "name": "buidangminh23",
7
+ "email": ""
8
+ },
9
+ "homepage": "https://github.com/buidangminh23/spacing-skill",
10
+ "repository": "https://github.com/buidangminh23/spacing-skill",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "skills",
14
+ "spacing",
15
+ "rhythm",
16
+ "alignment",
17
+ "frontend",
18
+ "design",
19
+ "whitespace",
20
+ "ui"
21
+ ]
22
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "spacing-skill",
3
+ "version": "2.12.1",
4
+ "description": "Anti-cramped spacing skill for AI-generated UI — one spacing scale, vertical rhythm, optical alignment, and density discipline.",
5
+ "skills": "./skills/"
6
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,232 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [2.12.1] - 2026-09-17
8
+
9
+ ### Added
10
+ - Publish the complete skill and native plugin manifests as `@minhspark/spacing-skill` on npm, with an explicit package file allowlist and GitHub Actions trusted publishing.
11
+ - Link the npm package from the README and document npm installation without implying automatic agent registration.
12
+
13
+ ## [2.12.0] - 2026-09-09
14
+
15
+ ### Added
16
+ - §10.B: settings/list row with a single action — make the whole 64px row the tap target (icon tile + label + chevron) instead of embedding a button; ~1.7× the tap area and it dissolves the §4.C icon-column alignment problem. Costs: one action per row, long copy moves to a group note.
17
+ - Tag-triggered GitHub Releases with changelog notes, a portable plugin ZIP, SHA-256 checksums, and validation on Linux, macOS, and Windows with Node 22 and 24.
18
+
19
+ ### Fixed
20
+ - Synchronize Claude marketplace, Codex plugin, and Gemini extension versions with the skill release. Future version bumps update all manifests together, and CI rejects version drift.
21
+
22
+ ## [2.11.0] - 2026-09-09
23
+
24
+ ### Added
25
+ - §4.C: a hug-width child (`w-auto` / `fit-content`) inside a `flex-direction:column` Stack is still stretched across the cross axis by the default `align-items:stretch` — it needs `align-self:start`. Measured 640px → 184px on one settings screen.
26
+ - §4.C: in an icon + title/description row, the action must be indented by `icon width + gap` so it shares the text column's left edge; drop the indent below `sm`, where the control goes full-width for touch.
27
+
28
+ ## [2.10.0] - 2026-09-05
29
+
30
+ ### Added
31
+ - §12: the `@page` margin box doubles as the browser's print header/footer canvas — `margin: 0` is the only way to remove them, the whitespace moves to `padding` on the print root, and that is safe only for single-page documents because root padding is not per-page.
32
+ - Anti-pattern #22 for zeroing the page margin on a multi-page document.
33
+
34
+ ## [2.9.1] - 2026-09-05
35
+
36
+ ### Added
37
+ - §4.C: bottom-aligned rows that mix an editable control with static text align the boxes, not the text — the control's touch-target `min-height` makes its box taller, so give every slot the same box and vertically center the static ones.
38
+ - Anti-pattern #21 for the same failure.
39
+
40
+ ## [2.9.0] - 2026-08-30
41
+
42
+ ### Added
43
+ - **§12 — unlayered CSS outranks every layered utility** (from a real field bug):
44
+ a design system on `@layer` (Tailwind v4 puts utilities in `@layer utilities`)
45
+ loses to any rule written outside a layer regardless of specificity, because
46
+ layer order is resolved before specificity. Measured on one login screen: an
47
+ unlayered critical `<style>` holding `input{font:inherit}` silently pulled the
48
+ field from 22px to 18px while the class stayed on the element, the rule stayed
49
+ in the stylesheet, and the whole suite stayed green. Wrap critical CSS in the
50
+ system's own base layer.
51
+ - **§12 — only `<input>` visibly resizes when the stylesheet is late**: the UA
52
+ sheet renders body text at 16px but form controls at ~13.3px, so late CSS is
53
+ reported as "the box where you type shrinks then grows" and gets hunted in the
54
+ field component instead of in CSS delivery.
55
+ - **Anti-pattern #20** — unlayered critical CSS beside a layered design system.
56
+
57
+ ## [2.8.0] - 2026-08-17
58
+
59
+ ### Added
60
+ - **§4.E — centering acts on the box, not on the ink** (from a real field bug):
61
+ the four centering strategies all assume an item's box *is* its content. When
62
+ `max-content` exceeds the container the item is clamped to the container, the
63
+ text wraps inside it, and the trailing dead space becomes pure offset — so
64
+ `align-items:center` centers a box wider than anything it paints and the group
65
+ drifts toward the leading edge. The drift is that leftover, so it grows with
66
+ the container: one logo + company-name lockup measured 10.7px off at 375px
67
+ wide and 63.2px off at 674px from identical markup, meaning a phone-only check
68
+ certifies it as fine. Fix the sizing, not the position — make the wrap
69
+ deterministic so `max-content` equals the longest line, then centering bites.
70
+ Acceptance test is `item box width === longest rendered line` at the widest
71
+ supported viewport, not an eyeball at one width.
72
+ - **Anti-pattern #19** — centering a clamped text box.
73
+
74
+ ## [2.7.0] - 2026-08-13
75
+
76
+ ### Added
77
+ - **§5.E — floor-not-clamp extends to the inline axis** (from a real field bug):
78
+ an icon rail / logo strip / avatar column given a fixed `width` to line up a
79
+ text column is the same clamp as a fixed control `height`, and fails louder.
80
+ A too-short box clips or scrolls; a too-narrow slot whose children are
81
+ `shrink-0` (or hold an image at `max-w-none`) overflows and **paints over the
82
+ next flex sibling** — no clip, no scrollbar, no warning. It stays invisible
83
+ until one row gains item N+1, so it reads as one broken row rather than a
84
+ sizing error. Measured on a checkout method list: a rail sized 64px for one
85
+ badge met a row needing 96px (Visa + `gap-1` + Mastercard) and put 32px of
86
+ logo 20px deep into its own description text. Fix is `min-width`. Also records
87
+ the tie-break: when a shared alignment spine fights the §6.B proximity ladder,
88
+ the ladder wins — let the fat row run wider instead of inflating a sparse
89
+ row's icon↔label gap past its inter-item gap.
90
+ - **Anti-pattern #18** — fixed-width icon/logo rail.
91
+
92
+ ## [2.6.1] - 2026-08-06
93
+
94
+ ### Changed
95
+ - **§12** — clarified that converting inline layout properties to classes is a
96
+ judgement per site rather than a find-and-replace: the class you move to has
97
+ to carry §8.B's question (what the narrower cell holds), so at least two are
98
+ needed — one that collapses and one that stays two-up. Sending every site to
99
+ the collapsing class reproduces anti-pattern #16. Evidence from one sweep of
100
+ 82 sites: 59 collapse, 23 do not.
101
+
102
+ ## [2.6.0] - 2026-08-06
103
+
104
+ ### Added
105
+ - **§12 — an inline style outranks every media query** (from a real field bug):
106
+ a layout property written inline (`style={{ gridTemplateColumns: … }}`) has no
107
+ responsive behaviour at all, and the failure is silent — the breakpoint rule
108
+ is written and correct, it fires, and the layout does not move. Measured on
109
+ one app: four dashboards stayed two-column on a phone with the narrow side at
110
+ 107px while the collapse rule sat in the stylesheet. The fix is to let the
111
+ inline set a **custom property** and keep the property itself in CSS, since a
112
+ variable does not outrank a media query; `!important` wins at every width
113
+ rather than the one you meant.
114
+ - **Anti-pattern #17** — layout property written inline.
115
+
116
+ ## [2.5.0] - 2026-08-06
117
+
118
+ ### Added
119
+ - **§8.B — column count follows the narrowest cell, not the grid** (from a real
120
+ field bug): the compressible-vs-not test was written only about *gaps*, while
121
+ §9.D's reflow language invites a blanket `@media (max-width:760px) { .cols-*
122
+ { 1fr } }`. Reflow requires no *horizontal scroll*, not a single column. A
123
+ cell holding one number and its label wants about half a phone's width; a
124
+ cell holding a table, a chart or prose wants all of it. Measured on one
125
+ dashboard: four one-number stat cards took **822px** of a 375px screen before
126
+ any real content, **371px** after the same four went two-up. Target the row
127
+ that holds the small cells (`:has(> .stat)`, or a modifier class as fallback)
128
+ rather than the breakpoint as a whole.
129
+ - **Anti-pattern #16** — blanket one-column collapse on mobile.
130
+
131
+ ## [2.4.1] - 2026-07-04
132
+
133
+ ### Added
134
+ - **§9.D — scroll-region overflow** (from a real field bug): an intentional
135
+ `overflow-x-auto` scroller (pill nav, toolbar, chip row) still blows the page out
136
+ when its wrapper sits in an `auto`-sized grid/flex track — the track grows to the
137
+ scroller's max-content and the scroll never engages. A grid defined with only a
138
+ desktop `lg:grid-cols-[…]` template falls back to an implicit `auto` column on
139
+ mobile. Fix: cap the track with an explicit `grid-cols-1` (`minmax(0, 1fr)`) plus
140
+ `min-w-0` on the item; verify `scrollWidth` at the narrowest width.
141
+
142
+ ## [2.4.0] - 2026-07-04
143
+
144
+ ### Added
145
+ - **§9.A — net-zero hit-box expansion.** Reaching the target-size floor by adding
146
+ padding alone spreads a tight control cluster (header icon buttons, a VN/EN toggle)
147
+ and breaks its `gap` rhythm. Pair the padding with an **equal negative margin** so the
148
+ hit box grows while the margin-box — every sibling gap and edge — stays put
149
+ (`-m-2.5 p-2.5` → a 20px icon becomes a 40px target, layout unmoved; `-my-*`/`py-*`
150
+ for a vertical-only grow). Plus: re-anchor an `absolute` badge/dot to an inner
151
+ wrapper around the icon so it doesn't jump to the padded box's corner.
152
+
153
+ ## [2.3.0] - 2026-06-30
154
+
155
+ ### Added
156
+ - **§5.A — stacked-diacritic heading floor.** Multi-line headings in scripts whose
157
+ marks stack both above *and* below a letter (Vietnamese, Thai, Arabic, Devanagari)
158
+ must hold line-height ≥ 1.1; display leading under ~1.05 collides an under-dot on
159
+ one line with a circumflex/breve on the next. Gated by content language, not taste.
160
+ - **§11 anti-pattern #15** — tight display leading colliding stacked diacritics, with the fix.
161
+
162
+ ## [2.2.1] - 2026-06-29
163
+
164
+ ### Added
165
+ - **§9.D — no-wrap overflow guidance** (from a real field bug): `white-space:nowrap`
166
+ stat/KPI values overflow the card when a multi-column metric grid sits inside a
167
+ narrow (`lg:grid-cols-2`) card. Fix: size the value to fit (font cap / fluid
168
+ `clamp()` / container-query) **and** drop columns when the container is narrow;
169
+ verify at the narrowest column, not the widest.
170
+
171
+ ## [2.2.0] - 2026-06-29
172
+
173
+ ### Added
174
+ - **§15 Continuous Improvement — the field-learnings loop.** The skill now
175
+ improves from use: every real spacing/alignment edit is captured in
176
+ `LEARNINGS.md` (Space Read + real numbers + a `covered` / `gap` / `refinement`
177
+ verdict), and lessons that generalize are distilled back into §0–§14 with a
178
+ version bump. README section count 15 → 16.
179
+ - **`LEARNINGS.md`** — append-only field journal that seeds the loop, kept out of
180
+ the context-loaded `SKILL.md` so it can grow without bloating the skill.
181
+
182
+ ## [2.1.0] - 2026-06-21
183
+
184
+ ### Added
185
+ - **§14 Document & Long-Form (Word / Google Docs / PDF / Markdown)** — page margins
186
+ (academic / business / bound), measure in documents, line-spacing presets, the
187
+ indent-vs-space single-separator rule, heading styles with asymmetric spacing,
188
+ lists/quotes/tables in prose, print pagination (widow/orphan, keep-with-next),
189
+ a concept→tool mapping table (CSS / Word / Docs / Markdown), and a document
190
+ pre-flight checklist. Scope and description broadened to cover written documents.
191
+
192
+ ## [2.0.1] - 2026-06-21
193
+
194
+ ### Removed
195
+ - All references to other skills from the README and SKILL.md.
196
+
197
+ ### Fixed
198
+ - Applied a 4-lens adversarial review (accuracy / coherence / intelligence / completeness).
199
+ - Snapped off-scale recipe values to the scale (inline inset `28→24`; landing `clamp()` MAX `200/80 → 160/64`, now with the required `rem` term).
200
+ - Reconciled the optical-nudge ceiling to a single value (≤4px) across §2.D / §7 / §11.
201
+ - Added the DENSITY computation rule (start at range midpoint → sum §1.B deltas → clamp `[1,10]`; content shifts override the surface band) and override-clamp semantics.
202
+ - Mapped the modular-scale ratio to DENSITY; clarified derived control insets, baseline-RIGOR precedence, and the Material line-box note.
203
+
204
+ ### Added
205
+ - New coverage: chart/data-viz spacing, scroll-padding/scroll-margin under sticky headers, forced-colors/dark-mode, motion, print/email, intrinsic sizing, multi-line wrap gaps, and intentional truncation.
206
+
207
+ ## [2.0.0] - 2026-06-21
208
+
209
+ ### Changed
210
+ - Substantial rewrite into the most complete edition (14 sections). Built from a
211
+ multi-agent research pass across 12 spacing/alignment dimensions, synthesized
212
+ into one coherent skill. Install name unchanged (`design-spacing-rhythm`).
213
+
214
+ ### Added
215
+ - **§0 Space Inference** — signals, the one-line "Space Read", anti-default discipline.
216
+ - **§4 Layout grid & alignment** — 12-col grid, measure (45–75ch), flex/grid alignment toolkit, subgrid for aligned card internals.
217
+ - **§9 Accessibility floors** — verified WCAG numbers: 2.5.8 (24px AA) + spacing exception, 2.5.5 (44px AAA), 1.4.12 text-spacing, 1.4.10 reflow at 320px, focus-ring spacing; Apple 44pt / Material 48dp.
218
+ - **§12 Implementation per stack** — Tailwind, plain CSS, MUI/Chakra/Radix/shadcn, design tokens, debugging.
219
+ - **§13 Operating procedure** — 8-step pipeline, conversational override protocol, full pre-flight checklist.
220
+ - Expanded scale (px-keyed, Tailwind-aligned), primitive vs semantic tokens, modular-scale macro tier, container queries, fluid `clamp()` spacing, logical properties, safe-area insets, and a 14-row anti-pattern catalog.
221
+
222
+ ## [1.0.0] - 2026-06-21
223
+
224
+ ### Added
225
+ - Initial release of `spacing-skill` (`design-spacing-rhythm`).
226
+ - Three-dial configuration: `SPACING_STEP`, `DENSITY`, `ALIGNMENT_RIGOR`.
227
+ - Single Tailwind-compatible spacing scale (no magic numbers).
228
+ - Sections: space inference, spacing primitives, vertical rhythm, whitespace
229
+ hierarchy, optical alignment, responsive spacing, component recipes,
230
+ anti-pattern catalog, pre-flight checklist, and stack-specific notes.
231
+ - Claude Code plugin manifests (`.claude-plugin/plugin.json`,
232
+ `.claude-plugin/marketplace.json`).
package/LEARNINGS.md ADDED
@@ -0,0 +1,188 @@
1
+ # Field Learnings — `design-spacing-rhythm`
2
+
3
+ Append-only journal for the **§15 Continuous-Improvement loop** in
4
+ `skills/spacing-skill/SKILL.md`. Every real spacing/alignment edit lands here as
5
+ one dated entry; lessons that generalize are distilled into the skill (with a
6
+ `CHANGELOG.md` bump) and then pruned here to a one-line pointer.
7
+
8
+ > Accessibility floors (§9) outrank any preference recorded in this file. Log
9
+ > only edits you actually made — **evidence, not vibes.**
10
+
11
+ ## How to add an entry
12
+
13
+ 1. Make (and ideally render) the spacing/alignment edit.
14
+ 2. Append one entry at the **top** of *Entries* using the template below.
15
+ 3. If the verdict is `gap` or `refinement` **and** the lesson generalizes, fold
16
+ it into the right section of `SKILL.md`, bump `CHANGELOG.md`, then replace the
17
+ entry here with a one-line pointer: `→ folded into §n @ vX.Y.Z`.
18
+
19
+ ### 2026-09-09 — PCC4SH / màn Cài đặt + menu tài khoản — `w-auto` loses to the Stack, and a leading icon moves the spine
20
+
21
+ - Space Read: settings screen (PWA mobile-first, also used on desktop) · balanced · STEP 4 (inherited Tailwind v4 + PCC4SH tokens) · DENSITY 5 · RIGOR 8 · ladder 8 → 16 → 24; every tap target ≥48px and ≥12px apart (the brief's a11y floor outranks every dial)
22
+ - Did: the screen was four white cards stacked, each holding exactly one full-bleed navy 64px button, so nothing read as more or less important than anything else — textbook anti-pattern #7 (a card per group) plus #1 (uniform weight). Collapsed them into three labelled cards ("Tài khoản" / "Trên máy này" / "Thoát tài khoản") whose members are rows: 44px icon tile, title + description, then the action. Secondary actions moved to a new 48px `inline` size (`w-full sm:w-auto`) instead of the 64px full-bleed primary. Two alignment bugs surfaced only after rendering: (1) at 1280px the `inline` buttons were still 640px wide — `sm:w-auto` was set but the row is a `flex-col`, so `align-items:stretch` overrode it; `sm:self-start` fixed it to 184px. (2) the action sat at the card inset while its own description started 56px in, behind the icon tile — added `sm:pl-14` (44 + 12) so title, copy and control share one left edge (measured descLeft 554 = btnLeft 554).
23
+ - Taught: `width` is a main-axis word. In a vertical Stack the cross axis belongs to `align-items`, so every "hug your content" utility is a no-op until an `align-self` backs it — and the failure is invisible on mobile, where full-bleed happened to be the intent, then appears at the first wide breakpoint. Second: a leading icon silently defines a text column, and any control that is a sibling of the icon block re-aligns to the container inset instead. The two edges are 56px apart, which reads as sloppy rather than as a deliberate outdent.
24
+ - Verdict: gap(§4.C)
25
+ - Action: folded into §4.C @ v2.11.0 — both bullets. Visible-misalignment failure modes caught on first render, so hardened on first occurrence per the §15.D carve-out.
26
+ - Follow-up (same day, after the owner asked for a different look): the row/button split was dropped entirely for the **whole row as the tap target** — 64px rows carrying icon tile, one-line label and a chevron, with the long explanation moved to a note at the foot of the group. Measured 341×65px per target versus 279×48px before, ~1.7× the area, and the §4.C indent problem stopped existing because there is no separate control to align. Kept both §4.C bullets: the `align-self` trap is general, and the measurement stands. Added the row-as-target rule to §10.B @ v2.12.0.
27
+
28
+
29
+ ### 2026-09-05 — PCC4SH / Mẫu 02-VT print margins — the page margin box is also the browser's header/footer canvas
30
+
31
+ - Space Read: printed A4 accounting form · balanced · STEP 4 · RIGOR 8 · paper margin must stay ~10mm, but nothing may print inside it
32
+ - Did: the user saw `11:52 5/9/26 · Hệ thống quản lý công trình` across the top of every printed slip and the page URL across the bottom. Those are Chrome's own header/footer, drawn **inside the `@page` margin box** — not content, so no selector reaches them. Moved the slip to a named page `@page phieu-mot-trang { margin: 0 }` and put the 10mm back as `padding` on the print root (`main[data-print-page='phieu-kho']`). Kept the default `@page { margin: 10mm }` for the multi-page reports.
33
+ - Taught: `@page` margin is doing **two** jobs — paper whitespace *and* the browser chrome's canvas. Zeroing it is the only CSS lever that removes the chrome, and the whitespace then has to be re-created as padding on the content. The catch that decides where you can apply it: **body/root padding is not per-page.** Left/right padding repeats on every sheet, but top/bottom lands only on the first and last page — so a multi-page document with `margin: 0` prints its middle pages edge-to-edge and the printer's hardware margin clips the top and bottom rows. Safe for a one-page form, a defect for a long report. Scope the named page accordingly, and prefer the failure that adds whitespace (named page ignored ⇒ 10mm margin + 10mm padding) over the one that removes it.
34
+ - Verdict: gap(§12)
35
+ - Action: folded into §12 @ v2.10.0 — added the print-margin dual-role rule and anti-pattern #22. A user-visible defect with no selector-based fix, so hardened on first occurrence per §15.D's failure-mode carve-out.
36
+
37
+ ### 2026-09-05 — PCC4SH / Mẫu 02-VT slip signature row — bottom-aligned boxes ≠ bottom-aligned text → folded into §4.C
38
+
39
+ - Space Read: printed accounting form, 4-column signature block · balanced · STEP 4 · DENSITY 4 · RIGOR 8 · four signer names must land on one baseline; the gap above them is hand-signing space
40
+ - Did: signature row was a 4-track grid, each column `flex-col` with `margin-top:auto` on the name. Two columns held `<input class="mau02vt-input">` (`min-height:44px`, `padding:.3rem .2rem`), two held a bare `<span>` (~22px, line-height 1.3 × 1.05rem). Both flush at the bottom, yet the input text sat ~11px higher — the user rejected it on sight. Fix: `.mau02vt-signature-name { min-height:44px; padding:.3rem .2rem; text-align:center }` on **all four**, plus `span.mau02vt-signature-name { display:flex; align-items:center; justify-content:center }`. Print keeps only the spans, so `@media print` resets `min-height:0; padding:0`.
41
+ - Taught: `margin-top:auto` bottom-aligns the **box**; the reader sees the **text**. Any row mixing an editable control with static text inherits the control's touch-target `min-height` (44/48px) as an alignment constraint — the control centers its text inside that tall box, the span fills its short one. Invisible until the two sit side by side. The a11y floor (§9) silently authored a layout rule.
42
+ - Verdict: gap(§4.C)
43
+ - Action: folded into §4.C @ v2.9.1 — added the mixed-control/text bullet and anti-pattern #21. A visible misalignment the user caught on first render → hardened on first occurrence, per the §15.D carve-out for failure-mode warnings.
44
+
45
+ ### 2026-09-01 — PCC4SH / shell taskbar — a centered clock needs balanced grid tracks and a compact mobile face
46
+
47
+ - Space Read: authenticated app shell header · balanced · STEP 8 (Tailwind 4px utilities, 8px layout rhythm) · DENSITY 4 · RIGOR 8 · title/control groups stay at the edges while the clock owns the center track
48
+ - Did: changed the header row from one flex line to `grid-template-columns: minmax(0,1fr) auto minmax(0,1fr)` with `gap-2` (8px) below `sm` and `gap-3` (12px) from `sm` upward. Measured the built CSS at 320px: header inner row 288px after 16px side padding, clock 65.86px, clock center delta **−0.0078px**, edge overlap **0px**; the clock keeps only `HH:mm` visually while its DOM/accessible label retains seconds. Measured the desktop shell at 1440px with the 368px navigation rail: taskbar content center x=904, clock center x=904, delta **0px**, overlap **0px**, inner row 1040px.
49
+ - Taught: the first symmetric grid was mathematically centered but left only 92px per side at 320px while the sync icon + avatar cluster needed 104px, creating a 4px collision. A responsive clock face is the smallest change that preserves the center track, the existing controls, and the 48px touch floor; keep the full seconds display where the side groups have room.
50
+ - Verdict: covered
51
+ - Action: none
52
+
53
+ ### 2026-08-30 — PCC4SH / login screen — unlayered critical CSS beats `@layer utilities` → folded into §12
54
+
55
+ - Space Read: `auth form · balanced · STEP 4 (Tailwind inherited) · DENSITY 4 · RIGOR 8 · label↔input 8 < field↔field 20 < group↔group 32`
56
+ - Did: user reported the login field's text "starts small, then grows a moment later". Measured by disabling the built stylesheet in the live page: field `13.33px` (UA default for `<input>`; box 22px tall) vs `22px` with CSS (box 60px) — the only element on the screen whose size visibly changes, because the UA sheet renders body text at 16px but form controls at ~13.3px. Fixed delivery two ways: `experimental.inlineCss` (production; CSS ships inside the HTML, no render-blocking round trip) plus a two-line critical `<style>` in the root layout for dev/stale-cache/404-chunk paths. Then rebuilt the screen's ladder: dropped a stray `my-auto` on the `<h1>` that fought the action block's `mt-auto` for the leftover height, so two sibling gaps had been landing at a *random* 46px/47px; now a fixed 32px each. Field internals `gap-1.5` (6px, off a 4-grid) → `gap-2` (8px). Repo-wide: 84 half-step spacing utilities snapped onto the grid, and 153 ad-hoc `text-[…rem]` magic sizes folded into named tokens so one type scale remains.
57
+ - Taught: **the critical block itself was the regression.** Written unlayered, `input,button,select,textarea{font:inherit}` outranked `.text-input{font-size:1.375rem}` — Tailwind v4 puts utilities in `@layer utilities`, and layer order is resolved *before* specificity, so a bare element selector wins over a class. Field silently rendered 18px instead of 22px with the class still on the element, the rule still in the stylesheet, and all 901 tests green. Wrapping it in `@layer base { … }` restored 22px while keeping the 18px floor when the stylesheet is missing. Second, smaller lesson: because only `<input>` moves visibly, users describe late-CSS as a *field* bug, so the hunt starts in the wrong component.
58
+ - Verdict: gap(§12)
59
+ - Action: folded into §12 @ v2.9.0 — added the unlayered-CSS rule + the "only inputs visibly resize" note, and anti-pattern #20. A visible, silent, green-tests failure mode → hardened on first occurrence (§15.D's wait-for-a-repeat gates default changes, not failure-mode warnings).
60
+
61
+ ### Template
62
+
63
+ ```text
64
+ ### YYYY-MM-DD — <project / surface> — <one-line lesson>
65
+ - Space Read: <surface · airy|balanced|packed · STEP · DENSITY · RIGOR · the ratio that mattered>
66
+ - Did: <the change + the real numbers>
67
+ - Taught: <what the situation revealed>
68
+ - Verdict: covered | gap(§n) | refinement(§n)
69
+ - Action: none | folded into §n @ vX.Y.Z | pending pattern
70
+ ```
71
+
72
+ ## Entries
73
+
74
+ <!-- newest first -->
75
+
76
+ ### 2026-08-17 — PCC4SH / login screen — a lockup is centred by its box, not by its ink → folded into §4.E
77
+ - Space Read: login-form · balanced · STEP 4 (Tailwind inherited) · DENSITY 4 · RIGOR 6 · one 400px measure for fields and buttons alike; the logo group must shrink to its rendered text before centring means anything
78
+ - Did: two edits on `apps/web`. (1) `Logo.tsx` — the company name's two parts became `block` spans for the `surface` tone, so the span's `max-content` equals its longer line (205.6px) instead of the whole name on one line. The lockup went from filling the column to shrink-wrapping at 321.6px. Measured left/right gaps before → after: 375px wide `0/21.4` → `10.7/10.7`; 674px wide `0/126.4` → `63.2/63.2`; 360px `3.2/3.2`; 320px still wraps (the existing `min-[360px]` breakpoint already concedes that) with no h-scroll. (2) `dang-nhap/page.tsx` — the button column was `w-full` (448px) against a `max-w-[400px]` field column, **24px proud each side** at 674px and matching only by accident on phones where both hit the viewport cap; both now read one `FORM_COLUMN` constant, re-measured identical at `137 → 537`.
79
+ - Taught: §4.E lists four centring strategies but assumes the item's box *is* its content. When `max-content` exceeds the container the item is clamped to the container, the text wraps inside it, and the trailing dead space becomes pure offset — `align-items:center` then centres a box wider than anything it paints. The failure is invisible on a phone (leftover ≈ 10px reads as a rendering quirk) and screams on a laptop (63px), so the usual mobile-first check certifies it as fine. It is a *sizing* bug wearing an alignment costume: no nudge, `justify-*` or `text-align` fixes it, because every one of them still operates on the oversized box. Sibling columns that must share a measure have the same shape — two literals of the same width match by luck until one viewport separates them.
80
+ - Verdict: gap(§4.E)
81
+ - Action: folded into §4.E @ v2.8.0 — also added anti-pattern #19. Hardened on first occurrence: a visible, measurable rendering bug rather than a baseline-default change, same precedent as §9.D @ v2.2.1 and §5.E @ v2.7.0.
82
+
83
+ ### 2026-08-13 — Personal-Web / checkout method list — a fixed-width icon rail overflows onto its own label → folded into §5.E
84
+ - Space Read: payment-method-list (modal) · balanced · STEP 8 (Tailwind inherited) · DENSITY 5 · RIGOR 8 · the logo rail must be a floor, not a clamp
85
+ - Did: logo rail `w-16` → `min-w-16` on both method rows in `src/PaymentModal.tsx`. The rail held one VietQR badge (58px) and fit 64px; a second row added Visa (52px) + `gap-1` (4) + Mastercard (40) = 96px into the same 64px. Measured in the browser: box 64 / content 96 → **32px overflow**, logo content edge at x=127 against a text edge at x=107 = **20px of glyph-on-glyph overlap**. After: box 96 = content 96, overflow 0, logo↔text gap back to the row's own `gap-3` (12). Re-measured at button widths 268/300/340px (320px viewport upward): overflow 0, gap 12, no in-button h-scroll.
86
+ - Taught: §5.E already says *use a floor (`min-height`), never a clamp (`height`)* — but states it only for the **block** axis on controls, so it does not fire when the fixed dimension is an inline-axis **slot** (icon rail, logo strip, avatar column, badge tray). The inline case is worse than the block case: a too-short box clips or scrolls, whereas a too-narrow slot whose children are `shrink-0` with `max-w-none` **paints over the next flex sibling** — no clip, no scrollbar, no console warning. It also stays invisible until someone adds item N+1 to *one* row, so it reads as "that one row is broken" rather than a systemic sizing error, and the row that still has N items keeps looking correct.
87
+ - Verdict: refinement(§5.E)
88
+ - Action: folded into §5.E @ v2.7.0 — also added anti-pattern #18. Hardened on first occurrence (visible rendering bug / failure-mode warning, not a baseline-default change), same precedent as §9.D @ v2.2.1.
89
+
90
+ ### 2026-08-06 — EduPortal / sweeping 90 inline grids — the conversion is a judgement per site (covered; §12 clarified)
91
+ - Space Read: `whole app, mobile · balanced · STEP 8 · DENSITY 6 · RIGOR 7 · each two-column grid stacks or stays two-up by what its narrower cell holds`
92
+ - Did: converted every inline two-column grid in the app after v2.6.0 named the defect — **82 sites in 29 files**, plus **8 fixed rails** (`1fr 340px`, `1fr 300px`) found on the way. 59 took a collapsing class, 23 a stay-two-up class, 33 more were correctly left alone (`auto-fit` already reflows, three-plus columns are not splits, one column count is driven by state). 14 sites had the wide cell second and kept their proportions via `--split-main: 0.667fr` rather than being flipped.
93
+ - Taught: two things, neither a new rule. First, the fixed rails are **already forbidden** — §4.A says column widths are fluid and §9.D says no fixed-px widths on layout containers — and they were still there on eight screens, leaving the main column **7px** on a 375px phone after the pane padding. A rule that exists and is violated eight times is evidence about how the value spreads: one inline layout property gets copy-pasted, and neither the author nor a reviewer sees a breakpoint failing because nothing fails loudly. Second, and worth adding to §12: the conversion cannot be mechanical. Sending all 82 to the collapsing class would reproduce anti-pattern #16, so the class you convert to has to carry §8.B's judgement, which means you need at least two classes before you start.
94
+ - Verdict: covered
95
+ - Action: one clarifying sentence into §12 @ v2.6.1 tying the mechanism to §8.B's judgement, with the 59/23 split as the evidence. No new rule — §4.A, §9.D, §12 and §8.B each already say their half.
96
+
97
+ ### 2026-08-06 — EduPortal / admin + teacher dashboards on a phone — an inline style outranks every media query → folded into §12
98
+ - Space Read: `dashboard mobile · balanced · STEP 8 · DENSITY 6 · RIGOR 7 · content column and side column must stack; every control clears 44px`
99
+ - Did: the main split of four dashboards was `style={{ gridTemplateColumns: '1.5fr 1fr' }}` inline, so `@media (max-width: 760px)` never reached it — admin, teacher, parent and pupil all stayed two-column on a phone, the narrow side measuring **107px** on the teacher screen. Nine call sites in all. Replaced with a `.ds-split` class whose ratio comes from `var(--split-main, 1.5fr)`, so a screen sets a **variable** and CSS keeps the **property**. Measured the targets at the same time: **eleven** controls at 33px tall on the admin screen (the tab row), 42px buttons and fixed 40×40 icon buttons on the teacher screen. `.btn`, `.icon-btn`, `.tab-btn` floored at 44 on the phone breakpoint. After: admin 0 controls under 44, no grid still split.
100
+ - Taught: the responsive section of this skill assumes the breakpoint can reach the value. In a JSX codebase it frequently cannot, and the failure is **silent in both directions** — the rule is written and correct, the breakpoint fires, the layout does not move, and nothing in devtools flags it as a conflict because there is no conflict: inline simply wins. `!important` "fixes" it and is the wrong tool, since it then wins at every width rather than the one you meant. The custom-property indirection is the fix that keeps both: inline expresses intent, CSS keeps authority.
101
+ - Verdict: gap(§12)
102
+ - Action: folded into §12 @ v2.6.0 — also added anti-pattern #17. Per §15.D this is a failure-mode warning with a measured before and after, not a baseline-dial change, so the wait-for-a-repeat guardrail does not gate it.
103
+
104
+ ### 2026-08-06 — EduPortal / mobile command dock — size the children, don't pick a `justify-content` (covered; §4.C refinement pending)
105
+ - Space Read: `command-dock mobile · balanced · STEP 8 · DENSITY 6 · RIGOR 7 · six icon targets share the bar width, 44px touch floor does not yield`
106
+ - Did: a full-width bar of six icon buttons sat `justify-content: flex-start` with fixed-width children — ink ended at 315px of a 512px bar, **187px empty**. Measured the buttons at the same time: **44×38**, so width cleared the touch floor and height did not. Put `flex: 1 1 0; min-width: 44px; min-height: 44px` on the actions: 75×44 each, gap 8, dead space 187 → **1px**, floor met on all six.
107
+ - Taught: nothing the skill lacks — §9.A and §10.A both put the mobile tap target at 44 hard, and §4.C already says `space-evenly` for a segmented control, which is what a six-action dock is. Logged because the *reason* the wrong value was there is worth naming: `flex-start` is correct for the **overflow** case this bar also has (`overflow-x: auto`), and it had been chosen for that case and left to govern the case where everything fits. One alignment cannot answer both.
108
+ - Refinement worth watching: for a row that is scrollable *and* usually fits, put the sizing on the **children** (`flex: 1 1 0` + `min-width:` the a11y floor) rather than choosing a `justify-content` on the parent. Children fill the space when there is room, hold their floor when there is not, and the overflow scrolls — one declaration covers both states, where §4.C's `justify-content` table only answers the fits-case. Minimum width is then arithmetic and checkable: `n × floor + (n−1) × gap + padding` (here 6×44 + 5×8 + 20 = 324px).
109
+ - Verdict: covered
110
+ - Action: none for §9/§10 — the floor was already written and was simply violated. The §4.C sizing-vs-alignment point is a single datapoint; per §15.D, harden into §4.C only on a repeat.
111
+
112
+ ### 2026-08-06 — EduPortal / student + admin dashboards on a phone — a blanket one-column collapse spends the screen → folded into §8.B
113
+ - Space Read: `dashboard mobile · balanced · STEP 8 · DENSITY 6 · RIGOR 7 · four one-number stat cards must not each take a phone's width; ladder 8→16→24`
114
+ - Did: the only phone breakpoint was `@media (max-width:760px) { .cols-2,.cols-3,.cols-4 { 1fr } }`, so a four-up stat row became four full-width blocks — measured **822px** of stat cards (4 × 189 + gaps) before the first real content on a 375px screen. Added `.ds-grid:has(> .card.stat) { repeat(2,1fr) }` in its own rule so a browser without `:has()` falls back to the old one-column behaviour rather than a broken one: **371px** after. Also `.card` 22→16 padding, `.stat .stat-val` 2.1rem→1.55rem, `.page-title` 2rem→1.5rem (32px of Fredoka took two lines for a greeting and pushed the page's own action below the fold), `.page-head` wrap→`flex-direction:column` (`align-items:flex-end` only reads as alignment while the row fits), and `.sidebar` `100vh`→`100dvh`.
115
+ - Taught: **reflow is "no horizontal scroll", not "one column"** — §9.D's collapse language invites a blanket single-column mobile rule, and §8.B's compressible-vs-not test was written only about *gaps*. The same test governs column count, and the deciding fact is what the narrowest **cell** holds, not what the grid is: one number + label wants half a phone's width, a table or prose wants all of it. There is no repo-wide scale here (values ran 7/12/13/14/16/20/22/52), so §0.A signal 1 gave nothing to conform to — which is exactly when the magic numbers creep in.
116
+ - Verdict: gap(§8.B)
117
+ - Action: folded into §8.B @ v2.5.0 — also added anti-pattern #16. Per §15.D this is a failure-mode warning with a measured before/after, not a baseline-dial change, so the wait-for-a-repeat guardrail does not gate it.
118
+
119
+ ### 2026-08-06 — EduPortal / same edit, self-audit — I shipped three magic numbers into the fix (covered, no skill gap)
120
+ - Space Read: as above
121
+ - Did: the first pass of that same breakpoint wrote `.ds-grid{gap:14px}`, `.stat{gap:10px}`, `.page-head{margin-bottom:18px}`. All three are off-scale. Snapped per §11 triage (`10–15→8/12/16`, `17–22→16`) to `16 / 8 / 24`, which also restores a clean ladder: intra-card **8** → grid gutter **16** → head↔content **24** (2×, then 1.5× — §6.B's floor, not its target).
122
+ - Taught: nothing the skill does not already say — §2 and §11 cover this exactly. Worth logging as evidence rather than as a lesson: the numbers appeared because I set them to "feel tighter on a phone" *before* deriving the ramp, i.e. I skipped §13.A step 3. The failure mode is reaching for a value while thinking about the viewport instead of about the tier.
123
+ - Verdict: covered
124
+ - Action: none. Logged because §15 asks for evidence, and a violation of a rule that exists is evidence about the operator, not about the skill.
125
+
126
+ ### 2026-07-04 — Personal-Web / policy hub — mobile pill-nav blew out the whole page → folded into §9.D
127
+ - Space Read: docs/policy hub · balanced · STEP 8 · DENSITY 4 · RIGOR 7 · aside pill-nav must scroll-x inside its box, page contained at 320
128
+ - Did: policy grid `grid gap-8 lg:grid-cols-[240px_1fr_280px]` → added base `grid-cols-1` + `min-w-0` on the nav `<aside>`. The mobile pill-nav (10 `shrink-0` items in an `overflow-x-auto` row) had inflated the implicit `auto` grid column to ~2390px → page h-scroll at EVERY width <1024px (both VI & EN). After: 0 overflow at 320–1280 both langs; the nav scrolls internally.
129
+ - Taught: an `overflow-x-auto` scroller does NOT contain itself when its wrapper sits in an `auto`-sized grid/flex track — the track grows to the scroller's max-content and the page overflows. A grid defined with only `lg:grid-cols-[…]` falls back to an implicit `auto` column on mobile. Cap the track (`grid-cols-1` = `minmax(0,1fr)`) + `min-w-0` on the item so the scroll engages. Extends §9.D from nowrap-text to scroll-regions.
130
+ - Verdict: gap(§9.D)
131
+ - Action: folded into §9.D @ v2.4.1
132
+
133
+ ### 2026-07-04 — Personal-Web / mobile header — enlarge tap targets without moving the row → folded into §9.A
134
+ - Space Read: mobile app-bar (nav utility) · balanced · STEP 8 · DENSITY 4 · RIGOR 7 · header controls on a fixed `gap-4`; targets must reach 44 without shifting the row
135
+ - Did: cart icon-button 20×20 → 40×40, hamburger 24×18 → 44×38, VN/EN toggle 16 → 36, close-× 28 → 48 — all via net-zero `-m-2.5 p-2.5` (`-my-2.5 py-2.5` on the inline VN/EN); wrapped the cart icon+badge in an inner `relative` span so the count badge stayed on the glyph, not the padded corner. Verified 0 layout shift (before/after screenshots identical) and no 320–390px overflow.
136
+ - Taught: §9.A said "pad the hit area" but was silent on the tight-cluster case where padding visibly spreads a fixed-`gap` row. An equal negative margin cancels the padding's margin-box, so the target grows at zero layout cost; and an `absolute` overlay must be re-anchored to an inner icon wrapper or it drifts to the enlarged box's corner.
137
+ - Verdict: refinement(§9.A)
138
+ - Action: folded into §9.A @ v2.4.0
139
+
140
+ ### 2026-06-30 — Personal-Web / policy hub → scroll-spy (all sections, one scroll) — §8 scroll-margin under sticky aside (covered)
141
+ - Space Read: docs/policy hub · balanced · STEP 8 · DENSITY 4 · RIGOR 7 · sticky aside + sections stacked `gap-8`, active follows scroll
142
+ - Did: switched from tab (1 section) to all sections stacked; IntersectionObserver `rootMargin: "-25% 0px -60% 0px"` picks the topmost in-view section to highlight the sticky menu; nav anchors smooth-scroll with `scroll-mt-24` so the target clears the top edge; footer `#hash` still lands correctly.
143
+ - Taught: nothing new — §8 scroll-margin/scroll-padding under a sticky element + §4 sticky-aside. The rootMargin band (top ~25–40%) is the standard scroll-spy trigger zone; keep `scroll-mt` ≥ the visual top offset so anchored sections aren't clipped.
144
+ - Verdict: covered
145
+ - Action: none
146
+
147
+ ### 2026-06-30 — Personal-Web / policy page → sidebar policy hub — §4 grid + sticky sidebar + tab nav (covered)
148
+ - Space Read: docs/policy hub · balanced · STEP 8 · DENSITY 4 · RIGOR 7 · two-col `[280px_1fr]`, sticky menu (top-24), one section shown at a time
149
+ - Did: redesigned `/chinh-sach` to breadcrumb + title, `lg:grid-cols-[280px_1fr]` gap-12, sticky left menu (`lg:sticky lg:top-24 lg:self-start`) with numbered active-highlight items, content card `p-10`. Menu collapses to horizontal pill tabs on mobile (`flex overflow-x-auto` → `lg:flex-col`). Active section tied to URL hash so footer `#hoan-tien` opens the right tab.
150
+ - Taught: nothing new — §4 layout grid + sticky-aside pattern + §10 nav/section recipe applied to a CellphoneS-style policy hub. Mobile menu = compressible layout region (§8.B) so it drops to a scroll-row.
151
+ - Verdict: covered
152
+ - Action: none
153
+
154
+ ### 2026-06-30 — Personal-Web / shared footer + policy page — §6 ladder + §10 footer recipe + §4 measure (covered)
155
+ - Space Read: site footer (utility) · balanced · STEP 8 · DENSITY 4 · RIGOR 7 · group↔group 40px > intra 8–12px; policy = long-form, measure ~70ch
156
+ - Did: shared Footer (brand / 3 policy links / 6 payment logos / copyright) with proximity ladder — link list `gap-2.5` (10) < column groups `gap-10` (40) < copyright divider `pt-6` (24) after `border-t`; `py-12` footer band (lighter than the `py-24` CTA above it). Policy page `max-w-3xl` (~70ch), section `gap-14`, bullet list `gap-3.5`, heading↔items `mt-5`, `scroll-mt-28` for anchored links.
157
+ - Taught: nothing new — textbook §6 proximity ladder, §10 footer/section recipe, §4 measure for the long-form policy. No skill gap.
158
+ - Verdict: covered
159
+ - Action: none
160
+
161
+ ### 2026-06-30 — Personal-Web / footer CTA heading — tight display leading collides Vietnamese diacritics → folded into §5.A
162
+ - Space Read: marketing closing-CTA · airy · STEP 8 · DENSITY 2 · RIGOR 4 · two-line VN display heading must clear stacked tone marks
163
+ - Did: heading `leading-[0.95]` → `leading-[1.1]` across the footer CTA + 4 section headings (footer later nudged to `1.2`); the dot-below on line 1 (`TẠO`/`TUYỆT`) was touching the circumflex/breve on line 2 (`ĐIỀU`/`Ờ`/`Ệ`).
164
+ - Taught: Vietnamese (and Thai / Arabic / Devanagari) caps stack marks both above AND below; sub-1.05 display leading that is fine for Latin all-caps collides marks between wrapped lines. The floor is set by content language, not aesthetics, and stays latent until the heading wraps to two lines.
165
+ - Verdict: gap(§5.A)
166
+ - Action: folded into §5.A @ v2.3.0 — also added anti-pattern #15.
167
+
168
+ ### 2026-06-29 — Personal-Web / stat-card grid — `whitespace-nowrap` values overflow narrow columns → folded into §9.D
169
+ - Space Read: stat-card grid · packed · STEP 4 · DENSITY 6 · RIGOR 8 · value must fit its column at the *narrowest* breakpoint
170
+ - Did: capped value font `md:text-3xl`(30) → `sm:text-2xl`(24); stat grid `sm:grid-cols-3` → `+ lg:grid-cols-2` (both `MY_CREATED_SKILLS` & `shownAiSkills` blocks sit in `lg:grid-cols-2` cards). Measured 8 values overflowing a 102px column at 1024px (Telegraphic +63, AI→Human +53, SKILL.md +32, Universal +30, …); after fix 0/52 overflow at 375/900/1024/1280.
171
+ - Taught: `whitespace-nowrap` + a multi-col stat grid inside a half-width (2-col) card = guaranteed edge overflow at desktop. It hides at wide viewports (columns wide) and bites at the `lg` breakpoint where cards halve but font/columns don't. Always verify at the narrowest column, not the widest.
172
+ - Verdict: gap(§9.D)
173
+ - Action: folded into §9.D @ v2.2.1 — a clear, visible bug → hardened on first occurrence; it's a failure-mode warning, not a baseline-default change, so §15.D's "wait for a repeat" doesn't gate it.
174
+
175
+ ### 2026-06-29 — Personal-Web / project stat-card grid — bottom-align captions with `flex-1` value, not bare `mt-auto`
176
+ - Space Read: stat-card grid · balanced · STEP 4 · DENSITY 6 · RIGOR 8 · captions must baseline-align across the row; tile inset on-scale & uncramped
177
+ - Did: per-card `px-2 py-3` → `p-3` (kept `sm:p-4`); added `flex flex-col` to the card and `flex-1` to the value block; value↔label `mt-1` → `mt-2` (4 → 8px). Applied to both stat-grid blocks in `src/App.tsx`.
178
+ - Taught: to bottom-align captions across a row of unequal-length values, `flex-col` card + `flex-1` on the value keeps a fixed value↔label min-gap, whereas the bare §4.C `mt-auto`-on-footer collapses that gap to 0 when a row's cards are already equal height (no free space for the auto-margin to absorb).
179
+ - Verdict: refinement(§4.C)
180
+ - Action: pending pattern — single datapoint; per §15.D, harden into §4.C/§10 only on a repeat.
181
+
182
+ ### 2026-09-05 — PCC4SH / Mẫu 02-VT slip grid — read-only cells were the ones breaking the row's shared box
183
+ - Space Read: data-entry grid row · packed · STEP 3 · DENSITY 7 · RIGOR 9 · every cell in a row must sit on one baseline; gloved outdoor use sets a 44px floor
184
+ - Did: converted the 3 computed cells (ĐVT, Thực xuất, Thành tiền) from bare `<td>{text}` to `<input class="mau02vt-input">`, the same class the row's other 4 cells already used (`min-height: 44px`). Changed **no** spacing value — the 44px floor and cell padding were already there.
185
+ - Taught: in a grid that mixes input cells and read-only cells, the **read-only cells are the defect**, not the inputs. Each input carries its own 44px min-height box; a text-only `<td>` is one line tall and rides on the cell's vertical centring instead, so its content sits on a different optical line from its neighbours — visible as a wobble that reads like a random rendering bug rather than a spacing decision. Making the row homogeneous (every cell the same box) fixes alignment as a side effect of fixing the affordance. Second datapoint for the same rule as the 2026-09-05 signature-row case: **the touch-target floor only aligns a row if every cell in that row is subject to it.**
186
+ - Verdict: covered(§4.C + §9)
187
+ - Action: no skill change — §4.C already says the shared box governs the row, and §9's 44px floor already applies per control. Logged as the second occurrence in a different surface (grid row, not signature row); if a third appears, promote to an explicit anti-pattern "read-only cell inside an input row".
188
+ - CORRECTION, same day: the inputs were reverted — the warehouse model forbids editing those three cells (a DB `CHECK (issued_quantity = 1)`, and the issue/cancel flow moves exactly one slot). Final fix is the *general* form of the lesson, and it is the better one: the cells stay read-only but get a `min-height:44px` flex box of their own (`.mau02vt-computed`), so the row is homogeneous **without** making anything editable. Turning them into inputs was never the point — **matching the box was**. Second-order win: with the input fill colour now removed from exactly the non-editable cells, the on-screen instruction "type into the coloured cells" became literally true again, so the affordance and the alignment were one fix, not two.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bùi Đăng Minh (buidangminh23)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.