reviewzy 0.2.0 → 0.4.0

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.
@@ -1,317 +1,340 @@
1
1
  /* ============================================================
2
2
  reviewzy — dashboard page styles
3
+ The page layer: the sidebar shell, the list's own rhythm, and the
4
+ htmx behavior hooks. Every component surface is a shipped class;
5
+ nothing here repaints one.
3
6
  ============================================================ */
4
7
 
5
- /* ---- Page shell (navbar shell per cloudy-ui) ---- */
6
- .app-shell { min-height: 100vh; display: flex; flex-direction: column; }
7
- .main { padding: var(--space-10) var(--space-12); max-width: 1400px; margin: 0 auto; width: 100%; }
8
+ /* ---- The shell ----
9
+ The sidebar is a grid track, so the page column follows the animated
10
+ column width. A fixed 252px track would leave a 186px gap around the
11
+ 66px rail. */
12
+ .layout { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 100vh; }
8
13
 
9
- /* ---- Page header: the eyebrow / title / lede triplet (login/editor pages) ---- */
10
- .page-header { margin-bottom: var(--space-8); padding-bottom: var(--space-5); border-bottom: 1px solid var(--line); }
11
- .page-eyebrow { color: var(--accent); margin-bottom: var(--space-2); }
12
- .page-title { font: 550 var(--text-2xl)/1.15 var(--font-sans); color: var(--text); letter-spacing: -0.02em; margin-bottom: var(--space-3); }
13
- .page-lede { font-size: var(--text-base); color: var(--text-dim); max-width: 540px; }
14
- /* The entries header is just the title plus the total count, no eyebrow or lede. */
15
- .page-count { font-size: var(--text-sm); color: var(--text-dim); }
14
+ /* The page column takes the main track's width: the design drops the cap beside a sidebar,
15
+ because a capped column leaves a dead strip beside the content once the track outgrows
16
+ --page-w (268px on each side at 1920). The track is pinned rather than auto-placed: under
17
+ 768 the sidebar is the fixed drawer and leaves the grid, so auto-placement would land the
18
+ page in the `auto` track, which sizes to the content's own intrinsic width — zero inside a
19
+ `container-type: inline-size` wrapper, collapsing the editor page to its padding. min-width
20
+ keeps the grid item from growing to its widest table's min-content width, which would widen
21
+ the document at 390. */
22
+ .layout > .page { grid-column: 2; min-width: 0; }
16
23
 
17
- /* ---- Filter form ---- */
18
- .filters { display: grid; grid-template-columns: minmax(0, 2fr) 200px 200px; gap: var(--space-3); align-items: end; margin-bottom: var(--space-8); }
19
- @media (max-width: 768px) { .filters { grid-template-columns: 1fr; } }
24
+ /* One spacing ladder: the page opens at --space-8 and closes at --space-16. */
25
+ .page { padding-block: var(--space-8) var(--space-16); }
26
+
27
+ /* The rail's footer holds one control: the theme toggle keeps the slot, and
28
+ sign-out returns with the expanded column. */
29
+ @media (min-width: 769px) {
30
+ .sidebar.rail .sidebar-user form { display: none; }
31
+ }
32
+
33
+ /* ---- List page rhythm: header, banner, tabs, then the queue section ---- */
34
+ .page-header { margin-bottom: var(--space-8); }
35
+
36
+ /* The banner is the component's card form here, not the bleed form: it sits on
37
+ the page column under the header. */
38
+ #page-banner > .banner { margin-bottom: var(--space-4); }
39
+
40
+ /* The status strip is a row of submit buttons inside the filters form; the count
41
+ line rides the toolbar's end edge and the filter-bar's own block margin closes
42
+ the pair. Five tabs with counts never fit a 390px column, so the strip scrolls
43
+ in its own box the way the navbar's link band does; the row keeps its hairline
44
+ across the scrolled content. */
45
+ .status-scroll { margin-bottom: var(--space-4); overflow-x: auto; }
46
+ .status-scroll .tabs,
47
+ .style-guide-projects .tabs { width: max-content; min-width: 100%; }
48
+ .list-meta {
49
+ margin-left: auto;
50
+ display: flex;
51
+ align-items: center;
52
+ gap: var(--space-3);
53
+ }
20
54
 
21
55
  /* ---- Entries region ---- */
22
56
  /* The loading strip is hidden until htmx marks the request in flight (hx-indicator adds the
23
57
  .htmx-request class); it sits beside the swap target, so a swap never removes it. */
24
- #entries-loading { display: none; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--text-dim); padding: var(--space-2) 0; }
58
+ #entries-loading { display: none; align-items: center; gap: var(--space-2); font-size: var(--t-2); color: var(--text-3); padding: var(--space-2) 0; }
25
59
  #entries-loading.htmx-request { display: flex; }
26
- /* The spinner reuses the @keyframes spin components.css ships for .btn-loading. */
27
- .spinner {
28
- width: 12px; height: 12px;
29
- border: 2px solid var(--line-strong);
30
- border-top-color: var(--accent);
31
- border-radius: 50%;
32
- animation: spin 0.6s linear infinite;
33
- flex-shrink: 0;
34
- }
60
+ /* A control mid-request ignores further activations: htmx marks the requesting element with
61
+ .htmx-request while the request is in flight, and pointer-events drops the duplicate click a
62
+ double-click would otherwise fire. */
63
+ .htmx-request { pointer-events: none; }
35
64
 
36
- /* ---- Partial state ---- */
37
- .partial-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-4); }
38
- .partial-count { font-size: var(--text-sm); color: var(--text-dim); }
65
+ /* ---- Bulk approve ----
66
+ The pinned batch action is the shipped section-sticky bar: transparent in flow so the sky
67
+ shows through, toned and hairlined only while it is stuck. Only its spacing is the page's. */
68
+ .batch-bar { margin-bottom: var(--space-4); }
39
69
 
40
- /* ---- Screen-reader-only ---- */
41
- /* Present in the DOM and the accessibility tree, absent from layout, and never focusable. */
42
- .visually-hidden {
43
- position: absolute;
44
- width: 1px;
45
- height: 1px;
46
- margin: -1px;
47
- padding: 0;
48
- overflow: hidden;
49
- clip: rect(0 0 0 0);
50
- white-space: nowrap;
51
- border: 0;
70
+ /* ---- Project sections: one heading and one table each ----
71
+ Batches stay a data concept behind the row selection and the one approve
72
+ action; nothing on the page discloses or groups by them. */
73
+ .project-section { margin-bottom: var(--space-8); }
74
+ .project-head {
75
+ display: flex;
76
+ flex-direction: column;
77
+ align-items: flex-start;
78
+ gap: var(--space-1);
79
+ margin-bottom: var(--space-3);
80
+ }
81
+ .project-name {
82
+ font: 500 var(--t-5)/1.2 var(--font-sans);
83
+ letter-spacing: -0.01em;
84
+ color: var(--text);
85
+ margin: 0;
86
+ min-width: 0;
87
+ overflow-wrap: anywhere;
88
+ }
89
+ /* The project's count line: the drafts and entries of the rows shown in this group. */
90
+ .project-meta {
91
+ margin-top: 0;
92
+ font-size: var(--t-2);
93
+ max-width: none;
52
94
  }
53
-
54
- /* ---- Project groups ---- */
55
- .project-group { margin-bottom: var(--space-10); }
56
- .project-header { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-4); }
57
- .project-name { font: 500 var(--text-lg)/1.2 var(--font-mono); color: var(--text); letter-spacing: -0.01em; }
58
- .batch { margin-bottom: var(--space-6); }
59
- .batch-header { display: flex; align-items: baseline; gap: var(--space-3); margin-bottom: var(--space-2); }
60
- .batch-meta { font: 400 var(--text-xs)/1 var(--font-sans); color: var(--text-faint); }
61
95
 
62
96
  /* ---- Entry table cells ----
63
- The table lays out fixed with a colgroup so the title column takes the remainder and the row
64
- never overflows; select, status, file, and actions are the fixed columns. The status pill has
65
- its own column, so the title truncates at one consistent point and the pill stays tight. */
66
- .col-select { width: 48px; }
67
- .col-status { width: 106px; }
68
- .col-file { width: 200px; }
97
+ The table lays out with a fixed colgroup so the entry column takes the remainder and a long
98
+ title or file path truncates instead of widening the table; select, status, filed and actions
99
+ are the fixed columns. The table never compresses below 560px: the fixed columns sum to 422
100
+ and the entry column needs room to read, so a narrower wrap scrolls instead of collapsing it.
101
+ Truncation itself is the shipped .truncate utility. */
102
+ #entries-list table { table-layout: fixed; min-width: 560px; }
103
+ .col-select { width: 44px; }
104
+ .col-status { width: 108px; }
105
+ .col-filed { width: 150px; }
69
106
  .col-actions { width: 120px; }
70
107
  .entry-head {
71
108
  display: flex;
72
- align-items: center;
73
- gap: var(--space-2);
74
- }
75
- .entry-title {
76
- flex: 1 1 auto;
109
+ flex-direction: column;
110
+ gap: var(--space-1);
77
111
  min-width: 0;
78
- font: 450 var(--text-base)/1.4 var(--font-sans);
112
+ }
113
+ /* The title is the row's link into the editor; the hover underline is the affordance. */
114
+ .entry-link {
115
+ display: block;
116
+ font: 400 var(--t-3)/1.4 var(--font-sans);
79
117
  color: var(--text);
80
- white-space: nowrap;
81
- overflow: hidden;
82
- text-overflow: ellipsis;
118
+ text-decoration: none;
83
119
  }
84
- .entry-filer {
85
- flex-shrink: 0;
86
- font-size: var(--text-xs);
87
- color: var(--text-faint);
88
- white-space: nowrap;
120
+ .entry-link:hover { text-decoration: underline; }
121
+ .entry-file {
122
+ display: block;
123
+ font-size: var(--t-2);
124
+ color: var(--text-4);
89
125
  }
90
- .cell-file {
91
- font-family: var(--font-mono);
92
- font-size: var(--text-xs);
93
- color: var(--text-faint);
126
+ .cell-filed {
127
+ font-size: var(--t-2);
128
+ color: var(--text-4);
94
129
  white-space: nowrap;
95
- overflow: hidden;
96
- text-overflow: ellipsis;
97
130
  }
98
-
99
- /* ---- Row actions and batch approve ----
100
- A control mid-request ignores further activations: htmx marks the requesting element with
101
- .htmx-request while the request is in flight, and pointer-events drops the duplicate click a
102
- double-click would otherwise fire. */
103
- .htmx-request { pointer-events: none; }
104
- /* The cloudy-ui custom checkbox: an appearance:none box on the sunk plane, accent-filled with a
105
- clip-path check when checked. The select-all also takes a dash in its indeterminate state (a
106
- partially selected batch), which the native accent-color box could not express. */
107
- .cell-select input[type="checkbox"],
108
- .batch-header .select-all {
109
- width: 15px;
110
- height: 15px;
111
- appearance: none;
112
- background: var(--bg-sunken);
113
- border: 1px solid var(--line);
114
- border-radius: calc(var(--radius) * 0.5);
115
- cursor: pointer;
116
- flex-shrink: 0;
117
- position: relative;
118
- transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
119
- }
120
- .cell-select input[type="checkbox"]:hover,
121
- .batch-header .select-all:hover { border-color: var(--line-strong); }
122
- .cell-select input[type="checkbox"]:checked,
123
- .batch-header .select-all:checked,
124
- .cell-select input[type="checkbox"]:indeterminate,
125
- .batch-header .select-all:indeterminate {
126
- background: var(--accent);
127
- border-color: var(--accent);
128
- }
129
- .cell-select input[type="checkbox"]:checked::after,
130
- .batch-header .select-all:checked::after {
131
- content: '';
132
- position: absolute;
133
- inset: 3px;
134
- background: var(--text-on-accent);
135
- clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
131
+ .cell-actions { white-space: nowrap; display: inline-flex; align-items: center; gap: var(--space-1); }
132
+ .table-wrap tbody td.cell-actions { padding-left: var(--space-1); padding-right: var(--space-1); }
133
+ /* The row's three icon actions sit at the small control step, so the 120px column holds them
134
+ without a 4px spill; the coarse floor is restated at the override's specificity, or a touch
135
+ row's tap targets would shrink with them. */
136
+ .cell-actions .btn-icon { width: var(--ctl-sm); min-width: var(--ctl-sm); height: var(--ctl-sm); min-height: var(--ctl-sm); }
137
+ @media (pointer: coarse) {
138
+ .cell-actions .btn-icon { width: var(--ctl-lg); min-width: var(--ctl-lg); height: var(--ctl-lg); min-height: var(--ctl-lg); }
136
139
  }
137
- .cell-select input[type="checkbox"]:indeterminate::after,
138
- .batch-header .select-all:indeterminate::after {
139
- content: '';
140
- position: absolute;
141
- left: 3px;
142
- right: 3px;
143
- top: 50%;
144
- height: 2px;
145
- transform: translateY(-50%);
146
- background: var(--text-on-accent);
147
- border-radius: 1px;
140
+ /* The semantic icon actions: the shipped .btn-icon paints the label colour, these give the
141
+ approve and reject glyphs their status colours in every state a disabled slot sits in. */
142
+ .action-approve, .action-approve:hover { color: var(--success); }
143
+ .action-reject, .action-reject:hover { color: var(--danger); }
144
+
145
+ /* Under 900px the filed stamp (metadata, not a decision) leaves the row so the rest of the
146
+ table keeps its columns inside the wrap instead of scrolling wider. The column's width is
147
+ zeroed and its cells are dropped (Chromium ignores `visibility: collapse` on a col), and the
148
+ table's floor steps down with the column it lost. The wrap stays the named scroller for what
149
+ remains. */
150
+ @media (max-width: 900px) {
151
+ #entries-list table { min-width: 440px; }
152
+ #entries-list col.col-filed { width: 0; }
153
+ #entries-list .cell-filed { display: none; }
148
154
  }
149
- .cell-actions { white-space: nowrap; }
150
- .cell-actions .btn + .btn { margin-left: var(--space-2); }
151
- .table-wrap tbody td.cell-actions { padding-left: var(--space-1); padding-right: var(--space-1); }
152
- .batch-bar {
155
+
156
+ /* ---- Notices ----
157
+ The refusal list rides inside the page banner's body, in the banner text's voice. */
158
+ .notice-list {
159
+ margin: var(--space-2) 0 0;
160
+ padding-left: var(--space-5);
153
161
  display: flex;
154
- align-items: center;
155
- margin-bottom: var(--space-4);
156
- /* Sticks just below the 56px navbar so the approve button stays reachable while rows scroll
157
- under it; the background and z-index stop table rows from showing through the bar. */
158
- position: sticky;
159
- top: 56px;
160
- z-index: 10;
161
- padding: var(--space-2) 0;
162
- background: var(--bg);
162
+ flex-direction: column;
163
+ gap: var(--space-1);
164
+ font-size: var(--t-3);
165
+ color: var(--text-2);
163
166
  }
164
- .batch-header .select-all { align-self: center; }
165
- /* ---- Collapsible project sections and batches ----
166
- The chevron points down expanded and turns when collapsed; the state is applied by dashboard.js
167
- from localStorage, so the server ships everything open and the viewer's choice re-applies. */
168
- .collapse-toggle { align-self: center; }
169
- .collapse-toggle svg { transition: transform var(--dur-fast) var(--ease); }
170
- .collapse-toggle[aria-expanded="false"] svg { transform: rotate(-90deg); }
171
- .project-header .collapse-toggle, .batch-header .collapse-toggle { margin-left: auto; }
172
- .notice-list { margin: 0; padding-left: var(--space-5); display: flex; flex-direction: column; gap: var(--space-1); }
173
167
 
174
- /* ---- Diff view: the anchored snippet vs the entry's text, mono on sunken, anchor distinct ---- */
175
- .diff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); margin-bottom: var(--space-8); }
176
- @media (max-width: 900px) { .diff-grid { grid-template-columns: 1fr; } }
168
+ /* ---- Empty-state actions: this page's filter-miss state holds two ways onward ---- */
169
+ .empty-actions { display: flex; gap: var(--space-2); justify-content: center; }
170
+
171
+ /* ---- The change: the anchored snippet as code on the well ----
172
+ The shipped pre owns the surface; the diff wraps instead of scrolling, which is the one
173
+ deviation this view adds. The anchor's annotation bar is the shipped .annot-mark. */
177
174
  .diff-code {
178
- font: 400 var(--text-sm)/1.6 var(--font-mono);
179
- background: var(--bg-sunken);
180
- border: 1px solid var(--line);
181
- border-radius: var(--radius);
182
- padding: var(--space-4);
175
+ margin: 0;
183
176
  white-space: pre-wrap;
184
177
  word-break: break-word;
185
- color: var(--text-muted);
186
- margin: 0;
187
178
  }
188
179
  .diff-line { display: block; }
189
- .diff-anchor {
190
- background: var(--accent-soft);
191
- border-left: 2px solid var(--accent);
192
- color: var(--text);
193
- padding-left: var(--space-2);
180
+ /* A change with both removed and added words renders two rows: the file text with its removals,
181
+ then the proposal with its additions. The rule between them keeps each sentence whole. */
182
+ .diff-rows .diff-proposed {
183
+ margin-top: var(--space-2);
184
+ padding-top: var(--space-2);
185
+ border-top: 1px solid var(--line-soft);
194
186
  }
195
187
 
196
- /* ---- Empty states ---- */
197
- .empty-state { text-align: center; padding: var(--space-12) var(--space-6); }
198
- .empty-icon { margin: 0 auto var(--space-4); width: 32px; height: 32px; color: var(--text-faint); display: flex; align-items: center; justify-content: center; }
199
- .empty-state h3 { font: 500 var(--text-lg)/1.3 var(--font-sans); color: var(--text); margin-bottom: var(--space-2); }
200
- .empty-state p { font-size: var(--text-sm); color: var(--text-dim); max-width: 420px; margin: 0 auto var(--space-4); }
201
- .empty-actions { display: flex; gap: var(--space-2); justify-content: center; }
202
-
203
- /* ---- Error box (cloned from #error-box by dashboard.js) ---- */
204
- .callout-content { min-width: 0; }
205
- .callout-actions { margin-top: var(--space-2); }
206
-
207
- /* ---- Entry editor ---- */
208
- /* The one live region, kept out of sight by the shared .visually-hidden utility above. */
209
- .editor-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-8); align-items: start; }
210
- .editor-main { min-width: 0; }
211
- .editor-rail { display: flex; flex-direction: column; gap: var(--space-6); position: sticky; top: var(--space-8); }
212
- @media (max-width: 900px) {
213
- .editor-grid { grid-template-columns: 1fr; }
214
- .editor-rail { position: static; }
188
+ /* ---- Entry editor: one full-track workspace ----
189
+ The workspace, its header and its support card take the page column's whole track: a cap beside
190
+ the sidebar centres a column away from the nav it belongs to. The page is the query container,
191
+ so the title step, the support card's columns and the pair's order follow the track width, not
192
+ the viewport. Container queries sit inside the browser floor the readme states, so the title step
193
+ and the pair's order keep the query rather than an intrinsic fallback. Each card sizes to its own
194
+ content and its pane scrolls past the cap; one support card spans the workspace below. */
195
+ .editor-page { container-type: inline-size; }
196
+ /* The entry title runs one line on the 1920 and 1440 tracks; a narrow page column steps it down
197
+ so a long machine title does not wrap mid-hyphen. */
198
+ .editor-title { font-size: var(--t-7); }
199
+ @container (max-width: 800px) {
200
+ .editor-title { font-size: var(--t-6); }
201
+ }
202
+ .editor-grid {
203
+ display: flex;
204
+ flex-wrap: wrap;
205
+ gap: var(--space-4);
206
+ /* The pair's cards share one height: the taller card sets the row and the shorter one keeps its
207
+ own content at the top. The inner boxes stay content-sized; the slack is card surface. */
208
+ align-items: stretch;
209
+ }
210
+ /* Each card asks for its own readable measure and the pair wraps when the track cannot hold two,
211
+ so the collapse follows the track's own width instead of a breakpoint: it needs no container
212
+ query and holds in an engine without them. A terminal entry has no change card, so the settled
213
+ pane asks for the whole row, and one support card spans it below the pair. */
214
+ .editor-grid > * { flex: 1 1 320px; min-width: 0; }
215
+ #editor-region.editor-region-wide,
216
+ .editor-support { flex: 1 0 100%; }
217
+ .editor-pane { max-height: 60vh; overflow: auto; }
218
+ /* The proposal box sizes to its text through dashboard.js's autosizer, so a one-line entry gets a
219
+ one-line box and a long one grows to the viewport cap instead of scrolling from the start. */
220
+ /* One support card, two balanced columns: the sections keep their DOM order down each column and
221
+ each takes its own height, so a short Details never stretches to a taller Notes. The section
222
+ labels head their sections at the next type step; the nested "More" keeps the shipped size. The
223
+ 320px floor is the same measure the pair asks for, so an engine without container queries still
224
+ meets one column on a phone instead of two slivers. */
225
+ .editor-support-card .card-content {
226
+ padding: var(--space-2);
227
+ columns: 320px 2;
228
+ column-gap: var(--space-5);
229
+ }
230
+ .editor-support-card .support-section {
231
+ break-inside: avoid;
232
+ margin-bottom: var(--space-3);
215
233
  }
216
- /* The editor's title card header: the title falls back to the anchor text, which can be one long
217
- unbroken string, so it wraps inside the card instead of overflowing or shoving the status pill. */
218
- .editor-title { min-width: 0; overflow-wrap: anywhere; }
219
- /* The entry text ships verbatim, often carrying markdown backticks and significant whitespace;
220
- mono gives every glyph, space included, one full cell so both stay countable while editing. */
234
+ .editor-support-card .support-section > summary { font-size: var(--t-4); }
235
+ @container (max-width: 900px) {
236
+ .editor-support-card .card-content { columns: 1; }
237
+ }
238
+ /* The shipped empty state's block padding is card-scale; one line of it inside a section leaves
239
+ a tall blank band. It also centres its one line across the full card, which reads as a hole;
240
+ inside a section it lines up with the section's rows instead. */
241
+ .support-section .empty-state {
242
+ align-items: flex-start;
243
+ text-align: left;
244
+ padding-block: var(--space-2);
245
+ }
246
+ #editor-notice:empty { display: none; }
247
+ #editor-notice { margin-bottom: var(--space-4); }
248
+ /* The one part of the narrow layout the pair's own measure cannot carry: once it has wrapped, the
249
+ proposal leads, so the reader meets the words before the file's own text. An engine without
250
+ container queries keeps the file's text first. */
251
+ @container (max-width: 640px) {
252
+ #editor-region { order: -1; }
253
+ }
254
+
255
+ /* ---- Textareas: no manual resize ----
256
+ The shipped .input pins a control height and the shipped textarea rule adds a resize
257
+ handle; both fight the page's sizing. The editor box holds a viewport-clamped height and the
258
+ notes box a fixed one; the style-guide fields autosize to their content. */
259
+ main.page textarea.input { height: auto; resize: none; }
221
260
  #editor-text { font-family: var(--font-mono); }
261
+ #editor-notes { height: 120px; min-height: 120px; max-height: 120px; overflow-y: auto; }
262
+ #style-guide-markdown { min-height: 200px; }
263
+ #style-guide-glossary { min-height: 140px; }
222
264
 
223
- /* ---- Editor prev/next rails ----
224
- One full-height fixed strip per side, below the 56px navbar, so the previous and next entries are
225
- a single click away while reading. The strip sits inside the 48px page padding, clear of content. */
226
- .editor-nav-btn {
227
- position: fixed;
228
- top: 56px;
229
- bottom: 0;
230
- width: 44px;
265
+ /* The live metrics row under the proposal's textarea: length, placeholders, tone. */
266
+ .constraint-row {
231
267
  display: flex;
232
268
  align-items: center;
233
- justify-content: center;
234
- color: var(--text-dim);
235
- text-decoration: none;
236
- z-index: 40;
237
- transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
269
+ flex-wrap: wrap;
270
+ gap: var(--space-2) var(--space-4);
271
+ margin-top: var(--space-3);
272
+ font-size: var(--t-2);
273
+ color: var(--text-3);
238
274
  }
239
- .editor-nav-prev { left: 0; }
240
- .editor-nav-next { right: 0; }
241
- .editor-nav-btn:hover { background: var(--bg-hover); color: var(--text); }
242
- .editor-nav-btn.is-disabled { color: var(--text-faint); opacity: 0.4; pointer-events: none; }
243
- .editor-nav-btn svg { width: 18px; height: 18px; }
244
- @media (max-width: 900px) { .editor-nav { display: none; } }
245
- #editor-region > .callout { margin-bottom: var(--space-4); }
246
- #editor-region > .btn { margin-top: var(--space-4); }
247
- .editor-actions { margin-top: var(--space-4); display: flex; align-items: center; gap: var(--space-2); }
248
- /* The in-flight indicator for an approve or reject from the editor: htmx's own rules fade it in. */
249
- .editor-actions .htmx-indicator { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--text-dim); }
275
+ .placeholder-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-3); }
276
+ .placeholder-item { display: flex; align-items: center; gap: var(--space-2); }
277
+ .placeholder-state { font-size: var(--t-2); color: var(--text-3); min-width: 52px; }
278
+ .placeholder-state.is-present { color: var(--success); }
279
+ .char-count.over { color: var(--danger); }
250
280
 
251
- /* The notes card: the agent's row reuses the constraint row shape; the human's textarea and its
281
+ .editor-actions { margin-top: var(--space-4); display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
282
+ /* The in-flight indicators for a reject from the editor and a style-guide save: htmx
283
+ fades the span in via .htmx-request, and display:none until then keeps the laid-out span from
284
+ widening the actions row past the viewport on a narrow screen. */
285
+ .editor-actions .htmx-indicator { display: none; }
286
+ .editor-actions .htmx-indicator.htmx-request { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--t-2); color: var(--text-3); }
287
+
288
+ /* ---- Header prev/next ----
289
+ The neighbour pair lives in the page header's toolbar as secondary buttons; a boundary
290
+ keeps the slot with a disabled button so the pair never shifts. The meta row under the
291
+ title truncates the file path instead of letting a long one widen the page. */
292
+ .page-meta { min-width: 0; }
293
+ .page-meta-item { min-width: 0; }
294
+ .page-file { min-width: 0; max-width: 100%; }
295
+
296
+ /* ---- Notes ----
297
+ The notes card: the agent's row is the info-table readout; the human's textarea and its
252
298
  save button sit below it. */
253
299
  .notes-field { margin-top: var(--space-2); }
254
300
  .notes-actions { margin-top: var(--space-2); display: flex; justify-content: flex-end; }
255
301
 
256
- /* Constraint panel: live count and placeholder checklist; the server owns the refusal. */
257
- .constraint-row { display: flex; gap: var(--space-3); padding: 6px 0; }
258
- .constraint-row + .constraint-row { border-top: 1px solid var(--line); }
259
- .constraint-label { flex-shrink: 0; font: 450 11px/1.4 var(--font-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-dim); width: 96px; padding-top: 2px; }
260
- .constraint-value { min-width: 0; color: var(--text-muted); font-size: var(--text-sm); }
261
- .char-count { color: var(--text-muted); font-size: var(--text-sm); }
262
- .char-count.over { color: var(--danger); }
263
- .placeholder-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
264
- .placeholder-item { display: flex; align-items: center; gap: var(--space-2); }
265
- .placeholder-state { font-size: var(--text-xs); color: var(--text-dim); min-width: 52px; }
266
- .placeholder-state.is-present { color: var(--success); }
267
-
268
- /* The context card's secondary block: the provenance rows (hash, filer, stale note) collapsed
269
- behind a summary, so the anchor and the agent-supplied context rows lead the card. */
302
+ /* The details card's secondary block: the provenance rows (hash, filer, stale note) collapsed
303
+ behind a disclosure, so the agent-supplied context rows lead the card. */
270
304
  .context-secondary { margin-top: var(--space-3); }
271
- .context-secondary summary { cursor: pointer; font-size: var(--text-xs); color: var(--text-dim); }
272
305
  .context-secondary .info-table { margin-top: var(--space-2); }
273
306
 
274
307
  /* Repo row link: the brand icon beside the shortened owner/repo path, linking out to the host. */
275
- .repo-link { display: inline-flex; align-items: center; gap: 6px; color: var(--text); text-decoration: none; }
276
- .repo-link:hover { color: var(--accent); }
277
- .repo-icon { width: 14px; height: 14px; flex-shrink: 0; fill: currentColor; opacity: 0.7; }
278
- .placeholder-token { font: 400 var(--text-sm)/1 var(--font-mono); color: var(--text); background: var(--bg-sunken); border: 1px solid var(--line); border-radius: var(--radius); padding: 2px 6px; }
308
+ .repo-link { display: inline-flex; align-items: center; gap: var(--space-1); color: var(--text); text-decoration: none; }
309
+ .repo-link:hover { color: var(--accent-text); text-decoration: none; }
310
+ .repo-icon { width: 14px; height: 14px; flex-shrink: 0; fill: currentColor; opacity: var(--icon-dim); }
279
311
 
280
- /* Entry images: one image per row at the card's width, a remove form below each, and the
281
- one-file upload form closing the card. The file input flexes to whatever width remains. */
282
- .image-list { list-style: none; margin: 0 0 var(--space-3); padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
283
- .entry-image { display: block; max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: var(--radius); }
284
- .image-skip { margin: 0; font-size: var(--text-xs); color: var(--text-dim); }
312
+ /* Entry images: the dropzone is the upload control; the arrived images sit under it as the
313
+ shipped dropzone list, and the submit ends the block as its action row. */
314
+ .image-list { gap: var(--space-3); }
315
+ .entry-image { display: block; max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: var(--r-1); }
316
+ .image-skip { margin: 0; font-size: var(--t-2); color: var(--text-3); }
285
317
  .image-remove { margin-top: var(--space-1); }
286
- .image-upload { display: flex; gap: var(--space-2); align-items: center; }
287
- .image-upload .input { flex: 1; min-width: 0; }
288
-
289
- /* Revision history: newest first; the text clamps like the list's entry text. */
290
- .history-empty { font-size: var(--text-sm); color: var(--text-faint); padding: var(--space-2) 0; }
291
- .revision-list { list-style: none; margin: 0; padding: 0; }
292
- .revision { padding: var(--space-3) 0; }
293
- .revision + .revision { border-top: 1px solid var(--line); }
294
- .revision-head { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-2); }
295
- .revision-time { font-size: var(--text-xs); color: var(--text-faint); }
296
- .revision-head .btn { margin-left: auto; }
297
- .revision-text {
298
- display: -webkit-box;
299
- -webkit-line-clamp: 2;
300
- -webkit-box-orient: vertical;
301
- overflow: hidden;
302
- font-size: var(--text-sm);
303
- color: var(--text-muted);
304
- }
318
+ .image-upload { display: flex; justify-content: flex-end; }
305
319
 
306
- /* ---- Login page: the navbar shell, then one centered card ---- */
307
- .login-main { display: flex; flex-direction: column; align-items: center; }
308
- .login-main .page-header, .login-card { width: 100%; max-width: 400px; }
309
- .login-card .card-content { display: flex; flex-direction: column; gap: var(--space-4); }
320
+ /* ---- Login: the page-state card's content column ---- */
321
+ .login-content { display: flex; flex-direction: column; gap: var(--space-4); }
310
322
  .login-actions { display: flex; justify-content: flex-end; }
311
323
 
312
- /* ---- Style guide editor: the section selector, then the one card ---- */
313
- .style-guide-projects { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-8); }
314
- #style-guide-region > .callout { margin-bottom: var(--space-4); }
324
+ /* ---- Style guide editor: the section tabs, then the one card ---- */
325
+ /* The project strip can hold every project's tab, so it scrolls in its own box like the status
326
+ strip does; the document never widens for it. */
327
+ .style-guide-projects { margin-bottom: var(--space-4); overflow-x: auto; }
328
+ /* The document default underlines every link on hover; the tab costume is a button's, so the
329
+ reset rides the anchor here. */
330
+ .style-guide-projects a.tab,
331
+ .style-guide-projects a.tab:hover { text-decoration: none; }
332
+ /* The settled notices sit on the same measure as the card below them; a full-width callout
333
+ over a half-width form read as two unrelated blocks. */
334
+ #style-guide-region > .callout { max-width: var(--page-narrow); margin-bottom: var(--space-4); }
315
335
  #style-guide-region > .btn { margin-top: var(--space-4); }
316
- .style-guide-card .card-content { display: flex; flex-direction: column; gap: var(--space-5); }
336
+ /* The fields stack in one column on the narrow measure, so the guide's long inputs never
337
+ stretch the whole page column. */
338
+ .style-guide-card { max-width: var(--page-narrow); }
339
+ .style-guide-fields { display: flex; flex-direction: column; gap: var(--space-4); }
317
340
  .style-guide-card .editor-actions { margin-top: 0; }