-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathinput.css
More file actions
4744 lines (4317 loc) · 193 KB
/
Copy pathinput.css
File metadata and controls
4744 lines (4317 loc) · 193 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
@import "tailwindcss";
/* Inter Variable, self-hosted under /static/fonts/ to eliminate the
cross-origin font swap on hard-reload. base.html and the wizard layout
both load styles.css, so the @font-face lives here once. The roman file
is preloaded from each layout's <head>. */
@font-face {
font-family: 'Inter var';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/static/fonts/InterVariable.woff2') format('woff2');
}
@font-face {
font-family: 'Inter var';
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url('/static/fonts/InterVariable-Italic.woff2') format('woff2');
}
/* ─── EXPERIMENT (theme-strip branch) ─────────────────────────────────
* Drop the bespoke breadbox-light / breadbox-dark OKLCH palettes and
* fall back to daisyUI's stock light + dark themes. Goal: see whether
* the v1 admin "drift" feel comes from custom theme tokens diverging
* from canonical daisy. Decide keep/revert after visual comparison
* against the pre-strip screenshots under
* .claude/sandbox-screens/theme-before/.
*
* Known dependents that hardcode oklch values against the old palette
* (kept untouched in this PR — flagged here for the follow-up if we
* decide to keep the strip):
* - .bb-card dark-mode lift (color-mix on var(--color-base-100))
* - .bb-comment-bubble dark-mode background mix
* - .bb-tx-avatar--uncategorized
* - .bb-tx-amount--income (hardcoded green)
* - body bg, focus rings, ::selection, scrollbar (oklch hex tied to
* the old palette — may look slightly off under daisy stock)
* - .select::picker(select) dark-mode block (daisyUI v5 bug fix)
* ─────────────────────────────────────────────────────────────────── */
@plugin "daisyui" {
themes: light --default, dark --prefersdark;
}
/* ─── shadcn-flavored daisyUI themes ──────────────────────────────────
* Faithful port of the shadcn/ui new-york default palette (oklch source
* values from shadcn-ui/ui@apps/v4/app/globals.css) onto daisyUI 5's
* token model. Near-zero-chroma grays, inverted primary (dark stamp in
* light, light stamp in dark), red destructive.
*
* Theme names are kept as `light` / `dark` so they override daisyUI's
* built-ins — the existing theme-toggle in base.html writes
* data-theme="light"/"dark" verbatim, no markup changes needed.
*
* Semantic colors that shadcn doesn't define (warning/success/info) are
* aliased to Tailwind palette tokens — same shades shadcn users compose
* inline (amber/emerald/sky 500 → 400 in dark). This keeps daisy
* components like alert-warning / badge-success rendering without
* inventing a new tone vocabulary.
* ──────────────────────────────────────────────────────────────── */
@plugin "daisyui/theme" {
name: "light";
default: true;
prefersdark: false;
color-scheme: light;
/* Mintlify-style tinted-neutral palette, warmed at the brand egg-yolk
hue (gold ~95). The base grays carry a faint WARM cast (chroma
~0.004–0.008 at hue ~95) so the UI reads as a warm off-white; base-100
stays pure white to keep the content surface crisp. Primary is a
near-black warm neutral, NOT gold: a solid gold CTA collided with the
amber warning color, so gold is reserved for brand identity (the logo)
plus the warm chrome tint, and actions use a crisp dark button. */
--color-base-100: oklch(1 0 0);
--color-base-200: oklch(0.971 0.004 95);
--color-base-300: oklch(0.923 0.008 95);
--color-base-content: oklch(0.16 0.01 95);
--color-primary: oklch(0.205 0.012 95);
--color-primary-content: oklch(0.985 0 0);
--color-secondary: oklch(0.971 0.004 95);
--color-secondary-content: oklch(0.205 0.012 95);
--color-accent: oklch(0.971 0.004 95);
--color-accent-content: oklch(0.205 0.012 95);
--color-neutral: oklch(0.205 0.012 95);
--color-neutral-content: oklch(0.985 0 0);
--color-info: var(--color-sky-500);
--color-info-content: oklch(0.985 0 0);
--color-success: var(--color-emerald-500);
--color-success-content: oklch(0.145 0 0);
--color-warning: var(--color-amber-500);
--color-warning-content: oklch(0.145 0 0);
--color-error: oklch(0.577 0.245 27.325);
--color-error-content: oklch(0.985 0 0);
/* Radii matched to the Mintlify dashboard/docs (measured from
app.mintlify.com → buttons 12px, and docs.breadbox.sh → .card 16px,
small elements 6px): small controls (checkbox/toggle/badge) 6px,
fields (button/input/select/tab) 12px, boxes (card/modal/alert) 16px. */
--radius-selector: 0.375rem;
--radius-field: 0.75rem;
--radius-box: 1rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 0;
--noise: 1;
}
@plugin "daisyui/theme" {
name: "dark";
default: false;
prefersdark: true;
color-scheme: dark;
/* Dark counterpart: same faint WARM cast on the dark grays. Primary is a
warm near-white (shadcn-style inverted dark primary, with dark text) —
gold stays brand-only, not the action color. */
--color-base-100: oklch(0.15 0.006 95);
--color-base-200: oklch(0.205 0.008 95);
--color-base-300: oklch(0.269 0.01 95);
--color-base-content: oklch(0.985 0.003 95);
--color-primary: oklch(0.922 0.008 95);
--color-primary-content: oklch(0.205 0.012 95);
--color-secondary: oklch(0.269 0.01 95);
--color-secondary-content: oklch(0.985 0.003 95);
--color-accent: oklch(0.371 0.012 95);
--color-accent-content: oklch(0.985 0 0);
--color-neutral: oklch(0.97 0.004 95);
--color-neutral-content: oklch(0.205 0.012 95);
--color-info: var(--color-sky-400);
--color-info-content: oklch(0.145 0 0);
--color-success: var(--color-emerald-400);
--color-success-content: oklch(0.145 0 0);
--color-warning: var(--color-amber-400);
--color-warning-content: oklch(0.145 0 0);
--color-error: oklch(0.704 0.191 22.216);
--color-error-content: oklch(0.145 0 0);
/* Radii matched to the Mintlify dashboard/docs (measured from
app.mintlify.com → buttons 12px, and docs.breadbox.sh → .card 16px,
small elements 6px): small controls (checkbox/toggle/badge) 6px,
fields (button/input/select/tab) 12px, boxes (card/modal/alert) 16px. */
--radius-selector: 0.375rem;
--radius-field: 0.75rem;
--radius-box: 1rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 0;
--noise: 1;
}
/* Force-include classes that Tailwind's content scanner can't see
because they live inside dynamic strings (Alpine :class, runtime
templ expressions, JS className builders).
Add new entries here whenever a class only exists at runtime —
prefer this over chasing arbitrary-value scanner bugs. */
@source inline("max-w-[768px] max-w-[390px]");
/* Developer Mode reporter — arbitrary z-index/size only present in base.html. */
@source inline("z-[9998] z-[9999] w-[min(92vw,24rem)] max-h-[85vh] max-h-32");
/* Global prompt modal (base.html) — heights, expand transition, discard
overlay, overscroll: arbitrary/uncommon classes Tailwind can't see in
base.html or behind Alpine :class bindings. */
@source inline("h-[55vh] h-[78vh] max-h-[60vh] max-h-[82vh] transition-[max-width] backdrop-blur-sm bg-base-100/85 rounded-[inherit] z-20");
/* Utility: hide Alpine.js elements until initialized */
[x-cloak] { display: none !important; }
/* ─── Native browser polish ──────────────────────────────────────────
* Two top-level hooks the platform now offers for free:
*
* color-scheme: light dark
* Tells the browser to render native widgets (scrollbars, form
* controls, <dialog> backdrops, input UA styles) with dark-mode
* awareness driven by prefers-color-scheme. Without this they
* stay light against our dark theme. Safe everywhere.
*
* @view-transition { navigation: auto }
* Opt-in to cross-document View Transitions on same-origin nav.
* Browsers that support it (Chrome 126+, Safari 18.2+) cross-fade
* between full page loads automatically; others do regular nav.
* Doesn't disable bfcache or the iOS swipe-back gesture — the spec
* layers transitions on top of native navigation.
*
* Reduced-motion guard zeros out the animations rather than turning
* the at-rule off, which is the pattern recommended by the spec.
* ──────────────────────────────────────────────────────────────── */
:root { color-scheme: light dark; }
@view-transition { navigation: auto; }
/* ─── View Transitions chrome polish ────────────────────────────────
* Default cross-document transition fades the WHOLE viewport, which
* makes the persistent sidebar/navbar look like they "flashed" too.
* Giving them a view-transition-name tells the browser "this is the
* same element on both pages" and confines the fade to <main>.
*
* Tighter duration + iOS-standard ease so the fade feels coincident
* with a server-rendered MPA's <100ms TTFB rather than a slow 250ms
* default that reads as artificial delay.
*
* NOTE: We deliberately do NOT name the active sidebar link as its
* own group. With a name, the browser interpolates the old and new
* bounding boxes, which makes the highlight visibly glide across the
* column from the previous link to the new one. Sounds delightful in
* theory; in practice it's distracting, especially when the two
* active links are far apart or the glide crosses category headers.
* Without a name, the active state cross-fades inside the sidebar
* group — old highlight fades out in place, new one fades in in
* place. Calmer.
* ──────────────────────────────────────────────────────────── */
.bb-sidebar { view-transition-name: app-sidebar; }
.bb-mobile-navbar { view-transition-name: app-navbar; }
.bb-progress-bar { view-transition-name: app-progress; }
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 180ms;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
/* Universal reduced-motion guard. Honors the OS-level "Reduce Motion"
* toggle (iOS Accessibility, macOS Accessibility, Windows Animations,
* Linux GNOME Animations) by collapsing every transition/animation to
* a near-zero duration and disabling smooth scroll. Recommended pattern
* from web.dev / Eric Bailey — scoped to *::before/::after to catch
* pseudo-elements, !important to override Tailwind/daisy/our own rules.
*
* The view-transition pseudo-elements need their own zero-out below
* because they're not matched by the universal selector. */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
/* ─── shadcn-inspired global overrides ─── */
@layer base {
/* Ensure Lucide SVG icons don't steal clicks from interactive parents. */
svg.lucide, svg.lucide * { pointer-events: none; }
/*
* Lucide icons are composed of multiple sibling <path> elements that share
* `stroke="currentColor"`. If the inherited color carries alpha (Tailwind
* v4's `text-COLOR/NN` syntax), every path strokes at that alpha
* independently and overlapping strokes — the curl-arrows of `repeat`, the
* stem of `circle-dollar-sign`, the check on `shield-check`, etc — stack
* into visibly darker pixels at intersections.
*
* Strip alpha at the SVG so each stroke renders fully opaque. To dim a
* Lucide icon, use `opacity-NN` on the <i> (Lucide preserves the class on
* the generated <svg>), which composites the whole icon as a single layer
* before applying transparency. `text-COLOR/NN` on Lucide icons is treated
* the same as `text-COLOR` here — call sites that wanted dimming should
* use `opacity-NN`.
*
* The explicit `/ 1` matters: in CSS relative color syntax, an omitted
* alpha defaults to the *origin* color's alpha (not 100%), so
* `rgb(from currentColor r g b)` alone passes the parent's alpha straight
* through — making this rule a no-op whenever the parent sets
* `text-COLOR/NN` (e.g. the sidebar's `.bb-sidebar-link`
* `text-base-content/60`). `/ 1` forces opaque output.
*/
svg.lucide { color: rgb(from currentColor r g b / 1); }
/* Brand icons follow the same pointer-events rule as lucide — they
* always sit inside an interactive parent (link, button, card
* header) and shouldn't intercept clicks. */
svg.brand, svg.brand * { pointer-events: none; }
/* Brand marks that fill with currentColor (Plaid, MCP) must mirror the
* svg.lucide alpha-strip above. Without it, a muted container color —
* text-base-content/45 (settings section headers) or /65 (settings-rail
* tabs) — bleeds its alpha into the fill, so the mark renders a softer
* gray than the Lucide icons sitting right next to it. The baked-fill
* marks (GitHub, Anthropic, Claude, Teller) use explicit fills and
* ignore `color`, so this is a no-op for them. */
svg.brand { color: rgb(from currentColor r g b / 1); }
/* Two kinds of brand mark live here:
*
* 1. Monochrome marks meant to read as UI icons (Plaid, MCP). Their
* <path>s use fill="currentColor", so they inherit whatever color
* the container sets (muted section-header text, the warning tile,
* the active settings-rail tab) and track light/dark automatically
* — exactly like the Lucide icons sitting next to them. Nothing to
* flip here; currentColor already follows the theme.
*
* 2. Marks that keep their official baked-in fill because the fill IS
* the brand. The black-on-light ones (GitHub, Anthropic) need a
* flip to white in dark mode so they don't disappear on a base-100
* surface. Claude orange already contrasts in both modes; Teller is
* multi-color and stays.
*
* The flip must follow the *resolved* daisy theme, not the raw OS
* preference — the theme toggle ($store.theme) can force
* data-theme="light"/"dark" independent of prefers-color-scheme. Keying
* only off @media (prefers-color-scheme: dark) makes the mark turn white
* on a forced-light page when the OS is dark (white-on-white, invisible),
* and stay black on a forced-dark page when the OS is light. So pair the
* media query (guarded against a forced-light override) with an explicit
* [data-theme="dark"] rule — the same shape the .select / .bb-tx-row /
* .bb-tx-avatar dark overrides use below. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) svg.brand-github path,
:root:not([data-theme="light"]) svg.brand-anthropic path { fill: #fff; }
}
[data-theme="dark"] svg.brand-github path,
[data-theme="dark"] svg.brand-anthropic path { fill: #fff; }
/* `brand-onbtn`: the GitHub mark sitting on a solid-tone button
* (btn-primary) must track the *button's* text color, not the page
* background. primary inverts against the surface in both themes
* (dark button / light text in light mode, and the reverse in dark),
* while the baked-fill flip above keys off the page theme — so the
* unmodified mark renders black-on-black in light mode and
* white-on-white in dark. Opt into currentColor to follow
* primary-content instead. Mirrors the base / media / data-theme
* shape above so it wins on specificity + source order, no !important. */
svg.brand-github.brand-onbtn path { fill: currentColor; }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) svg.brand-github.brand-onbtn path { fill: currentColor; }
}
[data-theme="dark"] svg.brand-github.brand-onbtn path { fill: currentColor; }
/*
* Pre-hydration size for Lucide placeholders.
*
* Before lucide.createIcons() swaps `<i data-lucide="x">` for `<svg>`, the
* <i> renders as `display:inline` with no intrinsic dimensions, so Tailwind
* sizing utilities (`w-4 h-4`, etc.) on the <i> do not produce a real box —
* the placeholder collapses to 0×0 and reflows when the SVG arrives. Make
* the <i> behave like an inline-block 1em×1em box so the row is the right
* height immediately and there is no layout shift on hydration.
*
* Once lucide replaces the <i> with `<svg class="lucide">`, this rule no
* longer applies (svg is not <i>), so SVG sizing remains controlled by the
* size utilities migrated onto the SVG by lucide's clone behavior.
*/
i[data-lucide] {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: -0.125em;
}
html { scroll-behavior: smooth; }
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Match safe area bars to content background */
background-color: var(--color-base-200);
}
/* ── Themed text selection ── */
::selection {
background: oklch(from var(--color-base-content) l c h / 0.15);
color: var(--color-base-content);
}
/* ── Focus rings: accessible, consistent, shadcn-style ── */
:focus-visible {
outline: 2px solid oklch(from var(--color-base-content) l c h / 0.4);
outline-offset: 2px;
border-radius: inherit;
}
/* ── Thin scrollbars ── */
* {
scrollbar-width: thin;
scrollbar-color: oklch(from var(--color-base-content) l c h / 0.25) transparent;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: oklch(from var(--color-base-content) l c h / 0.25);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: oklch(from var(--color-base-content) l c h / 0.4);
}
/* ── Global link transitions ── */
a { transition: color 0.15s ease; }
}
/* ─── App-specific component classes ─── */
@layer components {
/* Daisy `breadcrumbs` is the source of truth (see breadcrumb.templ).
Only override: hide the scrollbar to match the prior shadcn-style
trail, daisy keeps the system scrollbar visible by default. */
.breadcrumbs { scrollbar-width: none; -ms-overflow-style: none; }
.breadcrumbs::-webkit-scrollbar { display: none; }
/* ── Cards: Mintlify-style — flat 1px border, no drop shadow ──
* Mintlify's surfaces are border-only: a hairline defines the card
* against the bright content column, with no shadow lift. We dropped
* the shadcn `shadow-sm` (it read as an "off" halo on the new white
* content surface). Radius is rounded-2xl (16px) to match Mintlify's
* .card radius (measured on docs.breadbox.sh). Dark mode keeps the
* color-mix surface lift below; the faint border carries elevation. */
.bb-card {
@apply bg-base-100 rounded-2xl border border-base-300;
transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.2s ease;
}
.bb-card--interactive {
@apply cursor-pointer;
}
.bb-card--interactive:hover {
@apply bg-base-200/50;
border-color: var(--color-base-300);
}
/* ── Tray — a recessed, grouped surface ──
* A muted base-200 "well" that holds regular-bg `.bb-card` panels floating
* inside it (the tray's own padding is the gray gutter around them). Models
* Mintlify's "Finish setting up" card: the outer surface reads gray, while
* the grouped rows sit on the regular base-100 fill — an inversion of the
* usual flat card. Reusable across the design system: wrap grouped content
* in `.bb-tray`, keep a header / section labels directly on the tray, and
* nest one or more `.bb-card` panels for the rows. The gray↔regular contrast
* is just the base-200/base-100 relationship, so it holds in both themes.
* `.bb-tray__label` is the quiet uppercase section label between panels. */
.bb-tray {
@apply rounded-2xl bg-base-200/70 border border-base-300/60 p-1.5;
}
.bb-tray__label {
@apply px-2.5 pt-2 pb-1 text-[0.7rem] font-medium uppercase tracking-wider text-base-content/40;
}
/* ── List-row keyboard focus ──
* The redesigned list surfaces (/reports, /rules, /recurring,
* /accounts, /tags, /categories, /connections, /household, agent-run
* rows) wrap each row's whole-row link in an `<a class="contents">`.
* `display:contents` produces no box, so `:focus-visible` on the
* anchor has nothing to paint — keyboard users tabbing through rows
* got no visible focus indicator (the `hover:bg-base-200/40` mouse
* state already worked). Promote the keyboard-focus ring onto the
* `.list-row` <li> itself.
* - `:has(> a.contents:focus-visible)` scopes it to rows whose
* DIRECT child row-link is focused. An inner control (the
* overflow kebab) is NOT a `> a.contents`, so focusing the kebab
* keeps its own focus ring and never double-rings the whole row.
* - `:focus-visible` keeps it keyboard-only — a mouse click on the
* row doesn't trigger the ring.
* - Inset `box-shadow` (not outline) so the ring never shifts
* layout, never overlaps the hairline divider, and never clips
* against the bb-card's rounded first/last-row corners.
* `--color-primary` at 0.4 alpha matches the app's `ring-primary/40`
* focus idiom (principle #8 / StatTile, CollapsibleSection). */
.list-row:has(> a.contents:focus-visible),
.list-row:has(> button.contents:focus-visible) {
box-shadow: inset 0 0 0 2px oklch(from var(--color-primary) l c h / 0.4);
border-radius: inherit;
}
/* ── Form card primitives ── */
/* Icon-header: colored tile + title + optional subtitle.
Used at the top of sectioned form cards (bb-card p-0 overflow-hidden). */
.bb-icon-header { @apply flex items-center gap-3 mb-5; }
.bb-icon-header__tile { @apply flex items-center justify-center w-10 h-10 rounded-xl shrink-0; }
.bb-icon-header__text { @apply min-w-0; }
/* Color modifiers for the icon tile. Pair with .bb-icon-header__tile
(40×40 inside form cards) or .bb-entity-header__tile (48×48 at the
top of detail pages — see EntityHeader templ component).
`primary` uses /8 for a slightly softer hero tint (the page-defining
accent); the semantic tones use /10 so info/success/warning/error
read uniformly against each other in nearby cards. */
.bb-icon-tile--primary { @apply bg-primary/8 text-primary; }
.bb-icon-tile--accent { @apply bg-accent/10 text-accent; }
.bb-icon-tile--info { @apply bg-info/10 text-info; }
.bb-icon-tile--success { @apply bg-success/10 text-success; }
.bb-icon-tile--warning { @apply bg-warning/10 text-warning; }
.bb-icon-tile--error { @apply bg-error/10 text-error; }
.bb-icon-tile--neutral { @apply bg-base-200 text-base-content/60; }
/* Entity-avatar gradient monogram — the deterministic-color fallback tile for
an entity with no logo (a counterparty without a logo_url). The hue is
pinned per-name via an inline `--bb-avatar-hue` (0–360), the same way
.bb-tx-avatar reads an inline `--avatar-color`. OKLCH keeps the two stops
legible at every hue (fixed lightness/chroma, hue-only spin) so the white
monogram letters always read; the slight diagonal + darker second stop give
it the calm depth of the Workflows custom-avatar tiles without shouting.
Theme-independent on purpose: an identity color, like a DiceBear avatar. */
.bb-entity-avatar--mono {
background-image: linear-gradient(
135deg,
oklch(0.64 0.13 var(--bb-avatar-hue)),
oklch(0.52 0.15 var(--bb-avatar-hue))
);
color: oklch(0.99 0 0);
}
/* Detail-page entity header — 40×40 tile on mobile, 48×48 at sm+
for the top-of-page row that pairs an icon with the entity name +
badges + meta line. The mobile shrink reclaims horizontal space
for the title once the meta row and secondary action are hidden
(see EntityHeader templ component). */
.bb-entity-header__tile { @apply flex items-center justify-center w-10 h-10 sm:w-12 sm:h-12 rounded-lg sm:rounded-xl shrink-0; }
/* Action row at the bottom of a sectioned form card.
Left-aligned on mobile (thumb-reach), right-aligned on sm+. */
.bb-action-row { @apply p-4 sm:p-5 border-t border-base-300 bg-base-200/20 flex items-center justify-start sm:justify-end gap-2; }
/* ── Settings surface — see .claude/rules/settings.md ──
The shared shape for every /settings/* tab. SettingsSection wraps
SettingsRow children; auto-save selects and one-Save-per-section
forms both reuse these classes.
We chose a slightly heavier outline (border-base-300) than .bb-card
and a flush, full-bleed header so the section reads as a discrete
"group" the way macOS / Linear settings do — not as another card.
The header is intentionally quiet (small icon, sm/font-semibold
title) — the row labels carry the visual hierarchy. */
/* === Settings: borderless gutter header + carded right content ===
Modeled on Mintlify settings. A section is a two-column block: the
left gutter is the section's identity (icon + title + caption), the
right column is the working surface — usually a subtle CARD that
groups the controls. The section itself is borderless; sections are
separated by a full-width hairline + even vertical rhythm.
`--full` drops the gutter so the content spans the whole width
(overview cards, stats grids, page-level alerts). Separator + rhythm
are applied UNCONDITIONALLY (top border + symmetric padding) rather
than via :first-child, because sections are frequently wrapped in a
`<div id="anchor">` for hash deep-links, which defeats sibling rules.
The page header carries no bottom border, so the first section's top
border reads as the line under the title. */
/* Each section is its own inline-size container so the gutter/content
split flips on the *available* width, not the viewport. The live
Settings page sits in a narrow column (viewport − app sidebar −
settings rail), so a viewport `lg:` breakpoint flipped to two columns
while the content column was still cramped (~1030px viewport → a
~290px control column). A container query measures the real width, so
the same section behaves correctly on the live page and in the
wide-open /design sandbox. */
.bb-settings-section {
@apply border-t border-base-200 py-7;
container: settings-section / inline-size;
}
.bb-settings-section__grid {
@apply grid grid-cols-1 gap-x-10 gap-y-3;
}
/* Two-column (gutter + content) only once the section is wide enough to
give the content column a comfortable minimum: 14rem gutter + 2.5rem
gap + ~10rem leaves the content ≥ ~24rem before flipping. `--full`
sections stay single-column at every width. */
@container settings-section (min-width: 40rem) {
.bb-settings-section:not(.bb-settings-section--full) .bb-settings-section__grid {
grid-template-columns: 14rem minmax(0, 1fr);
}
}
/* Left gutter — section identity. Quiet by design; the card on the
right carries the working hierarchy. Top-aligned with the content. */
.bb-settings-section__meta {
@apply min-w-0;
}
.bb-settings-section__heading {
@apply flex items-center gap-2 min-w-0;
}
.bb-settings-section__title {
@apply text-sm font-semibold leading-tight;
}
.bb-settings-section__caption {
@apply text-xs text-base-content/50 mt-1 leading-relaxed;
}
.bb-settings-section__content {
@apply min-w-0;
}
/* Section primary action (e.g. "Create key") — sits above the card,
flush-right, its top edge aligned with the gutter title. */
.bb-settings-section__action {
@apply flex justify-start lg:justify-end gap-2 mb-3 empty:hidden;
}
.bb-settings-section--danger .bb-settings-section__title {
@apply text-error;
}
/* The content card — subtle elevation over the base-200 page surface.
Rows live inside it, divided by hairlines; an optional header/footer
brackets them. Flatter than .bb-card (no shadow) so it reads as a
quiet container, not a raised panel. */
.bb-settings-card {
@apply rounded-xl border border-base-300 bg-base-100;
}
.bb-settings-card--danger {
@apply border-error/25 bg-error/[0.025];
}
.bb-settings-card__header {
@apply px-4 sm:px-5 py-3.5 border-b border-base-200;
}
.bb-settings-card__title {
@apply text-sm font-semibold leading-tight flex items-center gap-2 flex-wrap;
}
.bb-settings-card__caption {
@apply text-xs text-base-content/50 mt-1 leading-snug;
}
.bb-settings-card__footer {
@apply px-4 sm:px-5 py-3 border-t border-base-200 bg-base-200/30 flex items-center justify-end gap-2;
}
/* Rows. Inside a card they carry horizontal padding + hairline dividers;
`--stack` flips to full-width-below for textareas / file inputs /
reveals. */
.bb-settings-row {
@apply flex items-center justify-between gap-4 px-4 sm:px-5 py-3.5 min-h-[3rem];
}
/* The card no longer clips with overflow-hidden (so row tooltips can escape
its bounds), so round whichever child sits at the top/bottom edge — keeps a
`--link` row's hover background, the footer's tint, and the hairline
dividers flush with the card's rounded border instead of poking past it. */
.bb-settings-card > :first-child {
@apply rounded-t-xl;
}
.bb-settings-card > :last-child {
@apply rounded-b-xl;
}
.bb-settings-card > .bb-settings-row + .bb-settings-row,
.bb-settings-card__body > .bb-settings-row + .bb-settings-row,
.bb-settings-section__body > .bb-settings-row + .bb-settings-row {
@apply border-t border-base-200;
}
.bb-settings-row--stack {
@apply flex-col items-stretch gap-2;
}
.bb-settings-row__main {
@apply min-w-0 flex-1;
}
.bb-settings-row__label {
@apply text-sm font-medium leading-tight flex items-center gap-2 flex-wrap;
}
.bb-settings-row__caption {
@apply text-xs text-base-content/50 mt-1 leading-snug;
}
.bb-settings-row__control {
@apply shrink-0 flex items-center;
}
.bb-settings-row--stack .bb-settings-row__control {
@apply w-full;
}
/* When a row IS a link (action list rows), the whole row hovers. */
.bb-settings-row--link {
@apply hover:bg-base-200/50 transition-colors no-underline cursor-pointer;
}
/* Footer used inside Bare/full-width content (no card wrapper). */
.bb-settings-section__footer {
@apply flex items-center justify-end gap-2 mt-4 pt-3 border-t border-base-200;
}
/* === Settings shell: the rail is a true extension of the app sidebar ===
On lg+ the rail is a STICKY, full-height base-200 column sitting flush
against the app sidebar's right edge — same recessed surface + link
language as .bb-sidebar, so Settings reads as the sidebar's own
sub-navigation rather than page content. It's in flow (a sticky peer of
the sidebar, not a fixed overlay) so it rides the page's overscroll the
SAME way .bb-sidebar does — the two never drift apart on a rubber-band
bounce. The topbar is offset to its right (the :has() offset at the end
of this block). Below lg it collapses to an in-flow horizontal scroll
strip at the top of the tab. */
.bb-settings-layout {
/* lg:gap-x-6 restores the 1.5rem gutter between the rail and the content
column. The rail's -1.5rem left margin (below) makes it flush against
the sidebar, which also pulls the content flush against the rail's
right edge; the column-gap puts the breathing room back. It's a
column-gap, so it only applies to the lg+ row layout — on the mobile
flex-col stack it's the cross axis and has no effect. */
/* min-h fills the viewport so the sticky rail has a full-height column and
short settings pages don't float the rail short. Subtract BOTH main's
3rem (sm:p-6 top+bottom) AND the 3.5rem in-flow sticky topbar (.bb-topbar
is h-14) — omitting the topbar makes main a full 100dvh that stacks below
the topbar and overscrolls the page by 3.5rem. The 6.5rem total mirrors
the rail's -6.5rem of negative margin (-(3.5rem+1.5rem) top, -1.5rem
bottom) so rail height (100dvh) minus its margins equals this box. */
@apply flex flex-col lg:flex-row lg:gap-x-6 lg:min-h-[calc(100dvh-6.5rem)];
}
/* Mobile default: an in-flow strip above the content. The lg+ block
below lifts it out into the fixed sidebar-extension column. */
.bb-settings-rail {
@apply shrink-0 mb-5;
}
.bb-settings-rail__list {
@apply flex flex-row gap-1 overflow-x-auto pb-1;
}
.bb-settings-rail__group-label {
@apply hidden lg:block text-[0.65rem] font-medium uppercase tracking-wide text-base-content/40 px-3 pt-5 pb-1.5;
}
/* Items mirror .bb-sidebar-link: muted by default, base-300 on hover,
and a base-300 fill (no border) when active — the active background
matches the hover background, so the row reads as "settled here"
rather than as a separate pill. Weight (font-semibold) + a
full-opacity icon keep the active item distinct from a transient
hover. The transparent default border is kept so the box-model
matches the inactive state (no layout shift). */
.bb-settings-rail__item {
@apply flex items-center gap-3 px-3 py-2.5 lg:py-1.5 min-h-11 lg:min-h-8 shrink-0
rounded-lg text-sm font-medium whitespace-nowrap no-underline border border-transparent
text-base-content/60 hover:bg-base-300 hover:text-base-content;
transition: background-color 0.15s ease, color 0.15s ease;
}
.bb-settings-rail__item[data-active="true"] {
@apply bg-base-300 text-base-content font-semibold;
}
.bb-settings-rail__item i, .bb-settings-rail__item svg {
@apply w-4 h-4 opacity-50;
transition: opacity 0.15s ease;
}
.bb-settings-rail__item:hover i, .bb-settings-rail__item:hover svg {
@apply opacity-70;
}
.bb-settings-rail__item[data-active="true"] i, .bb-settings-rail__item[data-active="true"] svg {
@apply opacity-100;
}
.bb-settings-content {
@apply flex-1 min-w-0;
}
/* lg+: the rail is a sticky, full-height column — the in-flow peer of the
app sidebar (which is itself a sticky .drawer-side). Being in flow, not
fixed, is the whole point: it rides the page's overscroll exactly like
.bb-sidebar, so a rubber-band bounce moves them together instead of
freezing the rail against a bouncing page. */
@media (min-width: 1024px) {
.bb-settings-rail {
position: sticky;
top: 0;
align-self: flex-start;
/* Pull flush to the viewport top — cancel the 3.5rem topbar (.bb-topbar
is h-14) + main's 1.5rem (sm:p-6) top padding so the rail's first tab
lines up with the sidebar's brand row — and flush against the
sidebar's right edge — cancel main's 1.5rem left padding. Lands
exactly where the old fixed rail sat, but now in normal flow.
The -1.5rem BOTTOM margin is the symmetric counterpart: the rail's
sticky-constraint rect is its containing block (.bb-settings-layout),
whose bottom edge sits 1.5rem above the document bottom because of
main's sm:p-6 bottom padding. Pulling the margin box up by 1.5rem lets
the border box extend through that padding to the document edge, so at
full scroll the rail reaches the bottom in lockstep with .drawer-side
(whose constraint rect IS the whole document) instead of leaving a
1.5rem strip of page background below it. */
margin: calc((3.5rem + 1.5rem) * -1) 0 -1.5rem -1.5rem;
height: 100dvh;
width: 14rem;
flex: none;
z-index: 31; /* above the sticky topbar (z-30) */
display: flex;
flex-direction: column;
/* Same vertical + horizontal rhythm as .bb-sidebar (py-5 / px-3). */
padding: 1.25rem 0.75rem;
background-color: var(--color-base-200);
border-right: 1px solid var(--color-base-300);
}
/* The list fills the rail and scrolls on its own if the sections ever
overflow a short viewport. */
.bb-settings-rail__list {
flex: 1 1 auto;
min-height: 0;
flex-direction: column;
gap: 0.125rem;
overflow-x: visible;
overflow-y: auto;
padding-bottom: 0;
}
/* Offset only the topbar by the rail width so it begins to the rail's
right. <main> is NOT offset — the rail now lives inside it in flow, and
the daisy drawer grid already starts <main> at the sidebar's right
edge, so the rail lands flush against the sidebar and the
sidebar-collapse shift is handled for free (no manual `left` tracking).
Scoped to Settings via :has() — the rail only exists there. */
.drawer-content:has(.bb-settings-layout) .bb-topbar {
margin-left: 14rem;
}
}
/* Form inputs/selects with subtle base-200 bg that clears on focus. */
.bb-form-input { @apply input w-full rounded-xl bg-base-200/50 focus:bg-base-100 transition-colors; }
.bb-form-select { @apply select w-full rounded-xl bg-base-200; }
/* Inline error alert inside a form card (pairs with alert-circle icon). */
.bb-form-error { @apply flex items-center gap-3 rounded-xl bg-error/10 text-error px-4 py-3 text-sm; }
/* Danger-zone card tint — combine with .bb-card for destructive-action cards. */
.bb-danger-card { @apply border-error/20 bg-error/[0.02]; }
/* ── Mobile Navbar — sticky so it anchors at the top while scrolling ── */
.bb-mobile-navbar {
@apply navbar px-2 bg-base-100;
position: sticky;
top: 0;
z-index: 30;
border-bottom: 1px solid oklch(from var(--color-base-content) l c h / 0.06);
/* Subtle backdrop blur for depth when content scrolls underneath */
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background-color: color-mix(in oklch, var(--color-base-100) 92%, transparent);
}
.bb-mobile-search-btn {
@apply flex items-center gap-1.5 h-10 px-3 rounded-full
text-sm text-base-content/50 bg-base-200/60 border border-base-300/50
cursor-pointer select-none;
transition: background-color 0.15s ease, border-color 0.15s ease;
min-width: 44px;
min-height: 44px;
}
.bb-mobile-search-btn:active {
@apply bg-base-200 border-base-300;
transform: scale(0.96);
}
.bb-mobile-search-btn i, .bb-mobile-search-btn svg {
@apply w-4 h-4 opacity-50;
}
.bb-mobile-search-label {
@apply text-xs text-base-content/35 hidden;
}
@media (min-width: 480px) {
.bb-mobile-search-label {
@apply inline;
}
}
/* ── Desktop topbar ── static across pages; the lg+ counterpart of
.bb-mobile-navbar. Sticky, hairline-bordered, lightly blurred. Holds
the breadcrumb (left) + search / theme / profile (right). */
.bb-topbar {
@apply hidden lg:flex items-center justify-between gap-3 h-14 px-4 sm:px-6 bg-base-100;
position: sticky;
top: 0;
z-index: 30;
border-bottom: 1px solid oklch(from var(--color-base-content) l c h / 0.06);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background-color: color-mix(in oklch, var(--color-base-100) 92%, transparent);
view-transition-name: app-topbar;
}
.bb-topbar-breadcrumb {
@apply flex items-center gap-1.5 min-w-0 text-sm;
}
/* ⌘K search trigger — shared chrome for the topbar trigger and the
sidebar trigger (the latter now only in the /design sandbox). The
surface + hover + transition live here once so re-theming the trigger
can't make the two drift; each variant's sizing/spacing/icon-size are
deltas (.bb-topbar-search below, .bb-sidebar-search further down). */
.bb-topbar-search,
.bb-sidebar-search {
@apply flex items-center rounded-lg text-sm
bg-base-200/50 border border-base-300/50 cursor-pointer select-none;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.bb-topbar-search:hover,
.bb-sidebar-search:hover {
@apply bg-base-200 border-base-300;
}
/* Topbar variant: navbar-row sizing; hidden < md so the breadcrumb keeps
its width (⌘K + the palette still work there). */
.bb-topbar-search {
@apply hidden md:flex h-9 w-56 px-3 gap-2 text-base-content/40;
}
.bb-topbar-search svg, .bb-topbar-search i {
@apply w-4 h-4;
}
/* ── Sidebar ── */
/* Recessed surface (base-200) — the content column is the bright one
(base-100). This is the Mintlify-style flip of the previous
white-sidebar / gray-content arrangement. */
.bb-sidebar {
@apply bg-base-200 h-full w-[85vw] max-w-80 sm:w-72 lg:w-64 px-3 py-5 flex flex-col border-r border-base-300;
}
.bb-sidebar-brand {
@apply text-lg font-semibold px-3 py-1 mb-5 no-underline text-base-content flex items-center gap-2;
/* When used inside the brand+close flex row we override mb and px via !mb-0 !px-0 !py-0 */
}
.bb-sidebar-nav {
@apply flex flex-col gap-0.5 flex-1 overflow-y-auto pb-4;
scrollbar-width: thin;
scrollbar-color: transparent transparent;
/* Contain the iOS rubber-band — without this, dragging past either end
of the nav list pulls the page underneath the drawer along with it. */
overscroll-behavior: contain;
}
.bb-sidebar-nav:hover,
.bb-sidebar-nav:active {
scrollbar-color: oklch(var(--bc) / 0.15) transparent;
}
/* ── Mobile: iOS 26-style scroll fade gradients ── */
@media (max-width: 1023px) {
.bb-sidebar-nav {
scrollbar-width: none;
--fade-size: 2rem;
--fade-top: 0;
--fade-bottom: 0;
mask-image: linear-gradient(
to bottom,
transparent,
black calc(var(--fade-size) * var(--fade-top)),
black calc(100% - var(--fade-size) * var(--fade-bottom)),
transparent
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent,
black calc(var(--fade-size) * var(--fade-top)),
black calc(100% - var(--fade-size) * var(--fade-bottom)),
transparent
);
}
}
.bb-sidebar-section {
@apply text-[0.7rem] lg:text-[0.65rem] font-medium text-base-content/40 px-3 pt-5 pb-2 lg:pb-1.5;
}
/* Hover/active recalibrated for the recessed (base-200) rail: hover
darkens one step to base-300; active is a bright base-100 pill that
pops against the gray rail and visually links to the content column
(Mintlify pattern). Were both base-200 — invisible on a base-200 rail. */
.bb-sidebar-link {
@apply flex items-center gap-3 px-3 py-2.5 lg:py-1.5 min-h-11 lg:min-h-8
shrink-0 rounded-lg text-sm font-medium
text-base-content/60 no-underline border border-transparent
hover:bg-base-300 hover:text-base-content;
transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease;
}
.bb-sidebar-link:active {
transform: scale(0.98);
}
/* Active item: a base-300 fill (no border) — the same background as
hover — so the row reads as "settled here" rather than as a separate
pill. Weight (font-semibold) + a full-opacity icon distinguish
active from a transient hover. (Label/icon stay base-content rather
than text-primary — the brand gold is too light to read as a
text/thin-stroke icon. The accent surfaces on buttons, filter tabs,
focus rings and links instead.) The base .bb-sidebar-link keeps its
transparent border so the borderless active state doesn't shift
layout. */
.bb-sidebar-link--active {
@apply bg-base-300 text-base-content font-semibold;
}
.bb-sidebar-link i, .bb-sidebar-link svg {
@apply w-[1.125rem] h-[1.125rem] lg:w-4 lg:h-4 opacity-50;
transition: opacity 0.15s ease;
/* Hint the browser to keep icons on their own compositing layer so
the sub-pixel position stays locked when the parent
.bb-sidebar-link triggers its `transform: scale(0.98)` :active
state — without this, the browser re-rounds child positions and