Skip to main content

euv_ui/style/class/shell/
fn.rs

1use super::*;
2
3class! {
4    // ═══════════════════════════════════════════════════════════════════════════
5    // Layout Shell
6    // ═══════════════════════════════════════════════════════════════════════════
7
8    pub c_app_root {
9        display: "flex";
10        height: "100%";
11        padding-bottom: var!(safe-area-inset-bottom);
12        font-family: "system-ui, -apple-system, sans-serif";
13        background: var!(background);
14        color: var!(foreground);
15        user-select: "none";
16        -webkit-user-select: "none";
17        -webkit-font-smoothing: "antialiased";
18        -moz-osx-font-smoothing: "grayscale";
19        text-rendering: "optimizeLegibility";
20        scrollbar-color: format!("{} {}", var!(scrollbar-thumb), var!(scrollbar-track));
21        ::-webkit-scrollbar-thumb {
22            background: var!(scrollbar-thumb);
23            border: "none";
24            border-radius: "0px";
25        }
26        ::-webkit-scrollbar-thumb:hover {
27            background: var!(scrollbar-thumb-hover);
28        }
29        ::-webkit-scrollbar-thumb:active {
30            background: var!(scrollbar-thumb-active);
31        }
32
33        // ═══════════════════════════════════════════════════════════════════════════
34        // CSS Reset — Cross-browser normalization
35        // ═══════════════════════════════════════════════════════════════════════════
36
37        *, *::before, *::after {
38            box-sizing: "border-box";
39        }
40        h1, h2, h3, h4, h5, h6, p, ul, ol, dl, dd, figure, blockquote, pre, hr {
41            margin: "0px";
42            padding: "0px";
43        }
44        ul, ol {
45            list-style: "none";
46            padding: "0px";
47            margin: "0px";
48        }
49        a {
50            color: "inherit";
51            text-decoration: "none";
52        }
53        button {
54            appearance: "none";
55            -webkit-appearance: "none";
56            -moz-appearance: "none";
57            background: "transparent";
58            border: "none";
59            padding: "0px";
60            margin: "0px";
61            font: "inherit";
62            color: "inherit";
63            cursor: "pointer";
64            outline: "none";
65            font-family: "inherit";
66            ::-moz-focus-inner {
67                border: "0px";
68                padding: "0px";
69            }
70        }
71        input, textarea, select, button {
72            font: "inherit";
73            font-family: "inherit";
74            font-size: "inherit";
75            line-height: "normal";
76            margin: "0px";
77            padding: "0px";
78            border: "none";
79            outline: "none";
80            background: "transparent";
81            color: "inherit";
82            appearance: "none";
83            -webkit-appearance: "none";
84            -moz-appearance: "none";
85        }
86        input[type="text"], input[type="number"], input[type="email"], input[type="password"], input[type="search"], input[type="tel"], input[type="url"] {
87            appearance: "none";
88            -webkit-appearance: "none";
89            -moz-appearance: "none";
90            border-radius: "0px";
91        }
92        input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {
93            -webkit-appearance: "none";
94            appearance: "none";
95        }
96        input[type="number"] {
97            -moz-appearance: "textfield";
98        }
99        input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button {
100            -webkit-appearance: "none";
101            appearance: "none";
102            margin: "0px";
103        }
104        textarea {
105            appearance: "none";
106            -webkit-appearance: "none";
107            -moz-appearance: "none";
108            resize: "vertical";
109            font-family: "inherit";
110        }
111        select {
112            appearance: "none";
113            -webkit-appearance: "none";
114            -moz-appearance: "none";
115            background-image: "none";
116            border-radius: "0px";
117        }
118        img, svg, video, canvas, audio, iframe, embed, object {
119            display: "block";
120            max-width: "100%";
121        }
122        table {
123            border-collapse: "collapse";
124            border-spacing: "0px";
125        }
126        hr {
127            border: "none";
128            margin: "0px";
129        }
130        :focus-visible {
131            outline: "none";
132        }
133        : focus:not(: focus-visible) {
134            outline: "none";
135        }
136        h1, h2, h3, h4, h5, h6 {
137            font-weight: "inherit";
138            font-size: "inherit";
139        }
140        strong, b {
141            font-weight: "700";
142        }
143        em, i {
144            font-style: "italic";
145        }
146        code, pre, kbd, samp {
147            font-family: "ui-monospace, monospace";
148        }
149        @media ((max-width: 767px)) {
150            input, select, textarea {
151                font-size: "16px";
152            }
153        }
154    }
155
156    pub(crate) c_mobile_app_root {
157        display: "flex";
158        flex-direction: "column";
159        width: "100%";
160        height: "100%";
161        font-family: "system-ui, -apple-system, sans-serif";
162        background: var!(background);
163        color: var!(foreground);
164        padding: format!("0px {} {} {}", var!(safe-area-inset-right), var!(padding-shell-bottom), var!(safe-area-inset-left));
165        scrollbar-color: format!("{} {}", var!(scrollbar-thumb), var!(scrollbar-track));
166        ::-webkit-scrollbar-thumb {
167            background: var!(scrollbar-thumb);
168            border: "none";
169            border-radius: "0px";
170        }
171        ::-webkit-scrollbar-thumb:hover {
172            background: var!(scrollbar-thumb-hover);
173        }
174        ::-webkit-scrollbar-thumb:active {
175            background: var!(scrollbar-thumb-active);
176        }
177    }
178
179    // ═══════════════════════════════════════════════════════════════════════════
180    // Navigation - Desktop Sidebar
181    // ═══════════════════════════════════════════════════════════════════════════
182
183    pub c_app_nav {
184        // Adaptive sidebar width — `clamp()` keeps the column ≥ the historic
185        // 248px floor (no regression on dense sidebar trees) and ≤ 320px on
186        // roomy viewports, so longer English titles like "Internationalization"
187        // stop wrapping at the historic fixed 248px.
188        width: var!(nav-width);
189        background: var!(background);
190        display: "flex";
191        flex-direction: "column";
192        // Pin the sidebar to the viewport while the main column scrolls.
193        // Without this the whole row flex container scrolls as a single
194        // block and the nav appears to slide off-screen with the content.
195        // `align-self: stretch` (the row flex default) is overridden by
196        // `flex-start` so the sticky height is the natural full height and
197        // not stretched to match the (much taller) main column.
198        // `100%` rather than `100vh`: the app root is a `height: 100%`
199        // flex column, so a percentage resolves against the real available
200        // box (viewport minus any safe-area inset) instead of the whole
201        // window — on a device with a notch the viewport unit would make the
202        // nav taller than the screen and push its footer below the fold.
203        position: "sticky";
204        top: "0px";
205        align-self: "flex-start";
206        height: "100%";
207        flex-shrink: "0";
208        padding-top: var!(safe-area-inset-top);
209        @media ((max-width: 767px)) {
210            display: "none";
211        }
212    }
213
214    pub c_nav_header {
215        padding: format!("{} {}", var!(space-xl), var!(edge-gutter-nav));
216        @media ((max-width: 767px)) {
217            padding: format!("{} {}", var!(space-xl), var!(edge-gutter-mobile));
218        }
219        width: "100%";
220        box-sizing: "border-box";
221        font-size: var!(font-xl);
222        font-weight: "700";
223        color: var!(foreground);
224        letter-spacing: "-0.02em";
225        display: "flex";
226        align-items: "center";
227        gap: format!("{}", var!(space-md));
228        flex-shrink: "0";
229        text-decoration: "none";
230        cursor: "pointer";
231        position: "relative";
232        ::after {
233            content: "''";
234            position: "absolute";
235            bottom: "0px";
236            left: var!(edge-gutter-nav);
237            right: var!(edge-gutter-nav);
238            height: "1px";
239            background: format!("linear-gradient(90deg, transparent, {}, transparent)", var!(border));
240        }
241        @media ((max-width: 767px)) {
242            ::after {
243                left: var!(edge-gutter-mobile);
244                right: var!(edge-gutter-mobile);
245            }
246        }
247    }
248
249    pub c_nav_brand_title {
250        font-size: var!(font-xl);
251        font-weight: "700";
252        color: var!(foreground);
253        letter-spacing: "-0.02em";
254    }
255
256    pub(crate) c_euv_logo {
257        display: "flex";
258        background: var!(accent);
259        align-items: "center";
260        justify-content: "center";
261        color: var!(text-on-accent);
262        font-weight: "700";
263        border: "none";
264        cursor: "pointer";
265        padding: "0px";
266        flex-shrink: "0";
267        position: "relative";
268    }
269
270    pub(crate) c_euv_logo_nav {
271        width: "32px";
272        height: "32px";
273        font-size: var!(font-lg);
274    }
275
276    pub(crate) c_euv_logo_fab {
277        width: "36px";
278        height: "36px";
279        font-size: var!(font-xl);
280        @media ((max-width: 767px)) {
281            width: "44px";
282            height: "44px";
283        }
284    }
285
286    pub c_nav_section_label {
287        padding: format!("{} {} {} {}", var!(space-md), var!(edge-gutter-nav), var!(space-xs), var!(edge-gutter-nav));
288        @media ((max-width: 767px)) {
289            padding: format!("{} {} {} {}", var!(space-md), var!(edge-gutter-mobile), var!(space-xs), var!(edge-gutter-mobile));
290        }
291        margin: "0px";
292        font-size: var!(font-xs);
293        font-weight: "700";
294        color: var!(foreground);
295        text-transform: "uppercase";
296        letter-spacing: "0.10em";
297        flex-shrink: "0";
298    }
299
300    pub c_nav_items_scroll {
301        flex: "1";
302        overflow-y: "auto";
303        contain: "content";
304        scrollbar-color: format!("{} {}", var!(scrollbar-thumb), var!(scrollbar-track));
305        ::-webkit-scrollbar-thumb {
306            background: var!(scrollbar-thumb);
307            border: "none";
308            border-radius: "0px";
309        }
310        ::-webkit-scrollbar-thumb:hover {
311            background: var!(scrollbar-thumb-hover);
312        }
313        ::-webkit-scrollbar-thumb:active {
314            background: var!(scrollbar-thumb-active);
315        }
316    }
317
318    pub c_nav_theme_toggle {
319        // The bottom padding here and the top padding of `c_nav_footer` add
320        // up across the boundary between the two rows, while only the
321        // footer's own bottom padding separates the text from the sidebar
322        // edge. Keeping the toggle's vertical padding at `space-sm` puts
323        // the attribution line in the middle of the band it sits in instead
324        // of hugging the bottom, without moving the button itself.
325        padding: format!("{} {}", var!(space-sm), var!(edge-gutter-nav));
326        flex-shrink: "0";
327        margin-top: "auto";
328        @media ((max-width: 767px)) {
329            display: "none";
330        }
331    }
332
333    // Optional row in the nav column / drawer for locale switchers or similar
334    // widgets, placed between the brand header and the section label.
335    pub c_nav_locale_row {
336        padding: format!("{} {}", var!(space-md), var!(edge-gutter-nav));
337        @media ((max-width: 767px)) {
338            padding: format!("{} {}", var!(space-md), var!(edge-gutter-mobile));
339        }
340        flex-shrink: "0";
341    }
342
343    pub c_nav_locale_button {
344        width: "100%";
345        height: "36px";
346        padding: "0px";
347        cursor: "pointer";
348        outline: "none";
349        border: format!("1px dashed {}", var!(border));
350        background: "transparent";
351        color: var!(foreground);
352        display: "flex";
353        align-items: "center";
354        justify-content: "center";
355        // The trigger mirrors the sidebar's hover affordance: the dashed
356        // border thickens to the foreground colour and a translucent wash
357        // derived from the text colour appears behind the label. The wash is
358        // `color-mix` on `foreground` rather than `accent-muted`, because in
359        // the monochrome palette `accent-muted` resolves to the same value as
360        // `background` (white on light, black on dark) and would be invisible.
361        :hover {
362            border-color: var!(foreground);
363            background: format!("color-mix(in srgb, {} 8%, transparent)", var!(foreground));
364        }
365        :focus-visible {
366            outline: "none";
367        }
368        // `:active` must not reset the hover styles: it only fires while the
369        // pointer is held, and zeroing the background there is what made the
370        // click look like nothing happened.
371        :active {
372            border-color: var!(foreground);
373            background: format!("color-mix(in srgb, {} 12%, transparent)", var!(foreground));
374        }
375    }
376
377    // Open state of the locale switcher: the same solid treatment the sidebar
378    // gives its current page, so "menu is open" reads as strongly as
379    // "this is the page you are on".
380    pub c_nav_locale_button_open {
381        width: "100%";
382        height: "36px";
383        padding: "0px";
384        cursor: "pointer";
385        outline: "none";
386        border: format!("1px solid {}", var!(accent));
387        display: "flex";
388        align-items: "center";
389        justify-content: "center";
390        // Background and text colour are a matched pair: the fill is the
391        // accent, so the label must be the colour that reads on top of it.
392        // Setting one without the other is what produces invisible text.
393        background: var!(accent);
394        color: var!(text-on-accent);
395        :hover {
396            background: var!(accent);
397            color: var!(text-on-accent);
398            border-color: var!(accent);
399        }
400        :focus-visible {
401            outline: "none";
402        }
403        :active {
404            background: var!(accent);
405            color: var!(text-on-accent);
406            border-color: var!(accent);
407        }
408    }
409
410    pub c_nav_theme_button {
411        width: "100%";
412        height: "36px";
413        padding: "0px";
414        cursor: "pointer";
415        outline: "none";
416        border: format!("1px dashed {}", var!(border));
417        display: "flex";
418        align-items: "center";
419        justify-content: "center";
420        :focus-visible {
421            outline: "none";
422        }
423        :active {
424            background: "transparent";
425            border-color: var!(border);
426        }
427    }
428
429    pub c_theme_icon_sun {
430        width: "20px";
431        height: "20px";
432        background-repeat: "no-repeat";
433        background-position: "center";
434        background-size: "20px 20px";
435        background-image: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='%23ffffff' stroke='%23ffffff' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='5'/%3E%3Cline x1='12' y1='1' x2='12' y2='4'/%3E%3Cline x1='12' y1='20' x2='12' y2='23'/%3E%3Cline x1='4.22' y1='4.22' x2='6.34' y2='6.34'/%3E%3Cline x1='17.66' y1='17.66' x2='19.78' y2='19.78'/%3E%3Cline x1='1' y1='12' x2='4' y2='12'/%3E%3Cline x1='20' y1='12' x2='23' y2='12'/%3E%3Cline x1='4.22' y1='19.78' x2='6.34' y2='17.66'/%3E%3Cline x1='17.66' y1='6.34' x2='19.78' y2='4.22'/%3E%3C/svg%3E\")";
436        transition: format!("transform {} {}", var!(duration-normal), var!(ease-out));
437    }
438
439    pub c_theme_icon_moon {
440        width: "20px";
441        height: "20px";
442        background-repeat: "no-repeat";
443        background-position: "center";
444        background-size: "20px 20px";
445        background-image: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='%23000000' stroke='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 13A9 9 0 1 1 11 3a7 7 0 0 0 10 10z'/%3E%3C/svg%3E\")";
446        transition: format!("transform {} {}", var!(duration-normal), var!(ease-out));
447    }
448
449    pub c_nav_footer {
450        // The attribution line sits between the divider painted at `top: 0`
451        // and the bottom of the sidebar's visible area, so the two vertical
452        // paddings are what decide whether it looks centred in that band.
453        // `space-md` is the tighter of the two old values and keeps the row
454        // compact against the bottom edge.
455        padding: format!("{} {} {}", var!(space-md), var!(edge-gutter-nav), var!(space-md));
456        // On mobile the top padding also carries the bottom safe-area inset.
457        //
458        // The band this line has to look centred in runs from the divider to
459        // the *page* bottom, not to the bottom of the element that reserves
460        // the safe area. On desktop that element is `c_app_root`, whose
461        // `padding-bottom` lifts the whole sidebar and leaves the inset
462        // region below the footer empty — the symmetric `space-md` pair is
463        // already correct there. The mobile drawer cannot inherit that: it is
464        // `position: fixed`, so it escapes `c_app_root`'s padded box entirely
465        // and reserves `padding-shell-bottom` itself. The inset therefore
466        // lands *below* the footer rather than above it, and a symmetric pair
467        // would leave a gap under the line of `inset` px and `space-md` above
468        // it — a 34px inset turned a 42px band into a visibly lopsided one.
469        //
470        // Adding the inset to the top padding restores the symmetry against
471        // the page bottom at any inset value, which is the invariant that
472        // matters: the band grows upward (the footer is the last row of a
473        // column flex container, so it has room to) and the line stays put
474        // relative to the bottom edge instead of drifting up as the inset
475        // grows.
476        @media ((max-width: 767px)) {
477            padding-top: format!("calc({} + {})", var!(space-md), var!(safe-area-inset-bottom));
478            padding-right: var!(edge-gutter-mobile);
479            padding-bottom: var!(space-md);
480        }
481        position: "relative";
482        font-size: var!(font-xs);
483        color: var!(muted-foreground);
484        flex-shrink: "0";
485        text-decoration: "none";
486        display: "flex";
487        // Both axes: the row is a single line of text, so centring it
488        // vertically inside the padded box is what puts the glyphs in the
489        // middle of the band rather than riding the top edge.
490        align-items: "center";
491        // Center the "Built with X" attribution line inside the sidebar so
492        // it visually balances with the centered brand header above and the
493        // centered theme toggle divider. `justify-content: center` works
494        // here because the link itself is a single row of inline text —
495        // when it wraps to multiple lines on narrow sidebars the lines stay
496        // grouped because `flex-direction` is the default `row`.
497        justify-content: "center";
498        gap: var!(space-xs);
499        cursor: "pointer";
500        text-align: "center";
501        transition: format!("opacity {} {}", var!(duration-fast), var!(ease-out));
502        opacity: "1";
503        :hover {
504            opacity: "1";
505        }
506    }
507
508    pub c_nav_footer_divider {
509        position: "absolute";
510        top: "0";
511        left: var!(edge-gutter-nav);
512        right: var!(edge-gutter-nav);
513        height: "1px";
514        background: format!("linear-gradient(90deg, transparent, {}, transparent)", var!(border));
515        @media ((max-width: 767px)) {
516            left: var!(edge-gutter-mobile);
517            right: var!(edge-gutter-mobile);
518        }
519    }
520
521    pub c_nav_footer_text {
522        font-weight: "400";
523        letter-spacing: "0.02em";
524    }
525
526    pub c_nav_footer_brand {
527        font-weight: "700";
528        color: var!(accent);
529    }
530
531    pub(crate) c_nav_item_active {
532        display: "flex";
533        align-items: "center";
534        gap: var!(space-sm);
535        padding: format!("{} {}", var!(space-md), var!(edge-gutter-nav));
536        @media ((max-width: 767px)) {
537            padding: format!("{} {}", var!(space-md), var!(edge-gutter-mobile));
538        }
539        text-decoration: "none";
540        font-size: var!(font-base);
541        color: var!(text-on-accent);
542        font-weight: "600";
543        background: var!(accent);
544    }
545
546    pub(crate) c_nav_item_inactive {
547        display: "flex";
548        align-items: "center";
549        gap: var!(space-sm);
550        padding: format!("{} {}", var!(space-md), var!(edge-gutter-nav));
551        @media ((max-width: 767px)) {
552            padding: format!("{} {}", var!(space-md), var!(edge-gutter-mobile));
553        }
554        text-decoration: "none";
555        font-size: var!(font-base);
556        color: var!(foreground);
557        font-weight: "400";
558        :hover {
559            background: var!(accent-muted);
560            color: var!(accent);
561            box-shadow: format!("inset 4px 0 0 0 {}", var!(foreground));
562        }
563    }
564
565    pub(crate) c_nav_item_icon {
566        flex-shrink: "0";
567        width: "20px";
568        text-align: "center";
569    }
570
571    pub(crate) c_nav_item_label {
572        flex: "1";
573        overflow: "hidden";
574        text-overflow: "ellipsis";
575        white-space: "nowrap";
576        color: "inherit";
577    }
578
579    // ═══════════════════════════════════════════════════════════════════════════
580    // Main Content Area
581    // ═══════════════════════════════════════════════════════════════════════════
582
583    pub c_app_main {
584        flex: "1";
585        height: "100%";
586        overflow: "auto";
587        padding: format!("{} {} 0 {}", var!(padding-main-top), var!(edge-gutter), var!(edge-gutter));
588        scrollbar-color: format!("{} {}", var!(scrollbar-thumb), var!(scrollbar-track));
589        ::-webkit-scrollbar {
590            width: "6px";
591        }
592        ::-webkit-scrollbar-thumb {
593            background: var!(scrollbar-thumb);
594            border: "none";
595            border-radius: "0px";
596        }
597        ::-webkit-scrollbar-thumb:hover {
598            background: var!(scrollbar-thumb-hover);
599        }
600        ::-webkit-scrollbar-thumb:active {
601            background: var!(scrollbar-thumb-active);
602        }
603        @media ((max-width: 767px)) {
604            padding: format!("{} {} 0 {}", var!(padding-main-top-mobile), var!(edge-gutter-mobile), var!(edge-gutter-mobile));
605            scrollbar-width: "none";
606            ::-webkit-scrollbar {
607                width: "0px";
608            }
609        }
610    }
611
612    pub c_page_router {
613        flex: "1";
614        display: "flex";
615        flex-direction: "column";
616    }
617
618    pub c_page_container {
619        width: "100%";
620        margin: "0px auto";
621        max-width: var!(content-max-width);
622    }
623
624    // ═══════════════════════════════════════════════════════════════════════════
625    // Mobile Layout
626    // ═══════════════════════════════════════════════════════════════════════════
627
628    pub c_mobile_header {
629        display: "flex";
630        align-items: "center";
631        justify-content: "space-between";
632        padding: format!("var(--euv-mobile-safe-top, 0px) {} 0px {}", var!(edge-gutter-mobile), var!(edge-gutter-mobile));
633        height: format!("calc({} + var(--euv-mobile-safe-top, 0px))", var!(mobile-header-height));
634        flex-shrink: "0";
635        position: "sticky";
636        top: "0px";
637        z-index: "100";
638        contain: "content";
639        background: var!(background);
640        border-bottom: format!("1px solid {}", var!(border));
641    }
642
643    pub c_mobile_header_left {
644        display: "flex";
645        align-items: "center";
646        gap: format!("{}", var!(space-md));
647    }
648
649    pub c_mobile_header_logo {
650        display: "flex";
651        align-items: "center";
652        gap: format!("{}", var!(space-sm));
653        text-decoration: "none";
654        color: var!(foreground);
655    }
656
657    pub c_mobile_menu_button {
658        width: "40px";
659        height: "40px";
660        border: "none";
661        cursor: "pointer";
662        font-size: "22px";
663        font-weight: "700";
664        display: "flex";
665        align-items: "center";
666        justify-content: "center";
667        color: var!(foreground);
668        padding: "0px";
669        :active {
670            background: "transparent";
671            color: var!(foreground);
672        }
673    }
674
675    pub c_mobile_theme_button {
676        width: "40px";
677        height: "40px";
678        border: "none";
679        cursor: "pointer";
680        display: "flex";
681        align-items: "center";
682        justify-content: "center";
683        padding: "0px";
684        :active {
685            background: "transparent";
686        }
687    }
688
689    pub c_mobile_menu_button_active {
690        width: "40px";
691        height: "40px";
692        border: "none";
693        cursor: "pointer";
694        font-size: "22px";
695        font-weight: "700";
696        display: "flex";
697        align-items: "center";
698        justify-content: "center";
699        color: var!(accent);
700        padding: "0px";
701        :active {
702            background: var!(background);
703            color: var!(accent);
704        }
705    }
706
707    pub c_mobile_drawer_close_button {
708        width: "32px";
709        height: "32px";
710        border: "none";
711        cursor: "pointer";
712        font-size: "18px";
713        font-weight: "500";
714        display: "flex";
715        align-items: "center";
716        justify-content: "center";
717        color: var!(muted-foreground);
718        padding: "0px";
719        transition: format!("color {} {}", var!(duration-fast), var!(ease-out));
720        :hover {
721            color: var!(foreground);
722        }
723        :active {
724            color: var!(muted-foreground);
725        }
726    }
727
728    pub c_mobile_overlay {
729        position: "fixed";
730        top: "0px";
731        left: "0px";
732        width: "100%";
733        height: "100%";
734        background: var!(bg-overlay);
735        z-index: "200";
736        contain: "layout style paint";
737        transition: format!("opacity {} {}", var!(duration-overlay), var!(ease-out));
738    }
739
740    pub c_mobile_overlay_hidden {
741        opacity: "0";
742        pointer-events: "none";
743    }
744
745    pub c_mobile_nav_drawer {
746        position: "fixed";
747        top: "0px";
748        left: "0px";
749        width: "240px";
750        max-width: "100%";
751        height: "100%";
752        background: var!(background);
753        z-index: "201";
754        display: "flex";
755        flex-direction: "column";
756        padding-top: "var(--euv-mobile-safe-top, 0px)";
757        padding-bottom: var!(padding-shell-bottom);
758        contain: "layout style paint";
759        will-change: "transform";
760        transition: format!("transform {} {}", var!(duration-overlay), var!(ease-out));
761        overflow: "hidden";
762    }
763
764    pub c_mobile_nav_drawer_closed {
765        transform: "translateX(-100%)";
766    }
767
768    pub c_mobile_nav_drawer_header {
769        display: "flex";
770        align-items: "center";
771        justify-content: "space-between";
772        padding: format!("0px {}", var!(edge-gutter-mobile));
773        height: var!(mobile-header-height);
774        flex-shrink: "0";
775        position: "relative";
776        ::after {
777            content: "''";
778            position: "absolute";
779            bottom: "0px";
780            left: var!(edge-gutter-mobile);
781            right: var!(edge-gutter-mobile);
782            height: "1px";
783            background: format!("linear-gradient(90deg, transparent, {}, transparent)", var!(border));
784        }
785    }
786
787    pub c_mobile_main {
788        c_app_main();
789        ::-webkit-scrollbar {
790            width: "0px";
791        }
792    }
793
794    // ═══════════════════════════════════════════════════════════════════════════
795    // euv_navbar
796    // ═══════════════════════════════════════════════════════════════════════════
797
798    pub(crate) c_euv_navbar {
799        position: "fixed";
800        top: "0px";
801        left: "0px";
802        right: "0px";
803        height: "56px";
804        display: "flex";
805        align-items: "center";
806        gap: var!(gap-element);
807        padding: format!("0px {}", var!(edge-gutter));
808        border-bottom: format!("1px solid {}", var!(border));
809        background: var!(background);
810        z-index: "100";
811        @media ((max-width: 767px)) {
812            padding: format!("0px {}", var!(edge-gutter-mobile));
813        }
814    }
815
816    pub(crate) c_euv_navbar_brand {
817        display: "flex";
818        align-items: "center";
819        gap: var!(gap-element);
820        font-size: var!(font-lg);
821        font-weight: "700";
822        letter-spacing: "-0.02em";
823        color: var!(foreground);
824        cursor: "pointer";
825        flex-shrink: "0";
826    }
827
828    pub(crate) c_euv_navbar_logo {
829        width: "32px";
830        height: "32px";
831        display: "flex";
832        align-items: "center";
833        justify-content: "center";
834        background: var!(accent);
835        color: var!(text-on-accent);
836        font-size: var!(font-lg);
837        flex-shrink: "0";
838    }
839
840    pub(crate) c_euv_navbar_links {
841        display: "flex";
842        align-items: "center";
843        gap: var!(gap-section);
844        margin-left: "auto";
845        @media ((max-width: 767px)) {
846            display: "none";
847        }
848    }
849
850    pub(crate) c_euv_navbar_link {
851        font-size: var!(font-sm);
852        font-weight: "500";
853        color: var!(foreground);
854        padding: format!("{} {}", var!(space-xs), var!(space-sm));
855        border-bottom: "2px solid transparent";
856        cursor: "pointer";
857        :hover {
858            color: var!(accent);
859        }
860    }
861
862    pub(crate) c_euv_navbar_link_active {
863        font-size: var!(font-sm);
864        font-weight: "600";
865        color: var!(accent);
866        padding: format!("{} {}", var!(space-xs), var!(space-sm));
867        border-bottom: format!("2px solid {}", var!(accent));
868        cursor: "pointer";
869    }
870
871    pub(crate) c_euv_navbar_actions {
872        display: "flex";
873        align-items: "center";
874        gap: var!(gap-element);
875        margin-left: var!(gap-section);
876        @media ((max-width: 767px)) {
877            margin-left: "auto";
878        }
879    }
880
881    pub c_euv_navbar_icon_button {
882        width: "36px";
883        height: "36px";
884        display: "flex";
885        align-items: "center";
886        justify-content: "center";
887        border: format!("1px dashed {}", var!(border));
888        cursor: "pointer";
889        font-size: var!(font-base);
890        :hover {
891            background: var!(accent-muted);
892        }
893    }
894
895    pub(crate) c_euv_navbar_menu_button {
896        display: "none";
897        width: "40px";
898        height: "40px";
899        align-items: "center";
900        justify-content: "center";
901        font-size: var!(font-xl);
902        cursor: "pointer";
903        transition: format!("background {} {}", var!(duration-fast), var!(ease-out));
904        @media ((max-width: 767px)) {
905            display: "flex";
906        }
907    }
908
909    pub(crate) c_euv_navbar_menu_button_active {
910        display: "none";
911        width: "40px";
912        height: "40px";
913        align-items: "center";
914        justify-content: "center";
915        font-size: var!(font-xl);
916        cursor: "pointer";
917        background: var!(accent-muted);
918        transition: format!("background {} {}", var!(duration-fast), var!(ease-out));
919        @media ((max-width: 767px)) {
920            display: "flex";
921        }
922    }
923
924    // ═══════════════════════════════════════════════════════════════════════════
925    // euv_sidebar
926    // ═══════════════════════════════════════════════════════════════════════════
927
928    pub(crate) c_euv_sidebar_group {
929        margin-bottom: var!(space-xs);
930    }
931
932    pub c_euv_sidebar_group_title {
933        display: "flex";
934        align-items: "center";
935        justify-content: "space-between";
936        width: "100%";
937        // Symmetric horizontal padding so the text and the dashed border of
938        // any nested children line up on the left edge (see sidebar_children
939        // below). Left padding equals `border-left-width + padding-left` of
940        // the children container — that's the "text and the dashed border
941        // share the same x" requirement.
942        padding: format!("{} {}", var!(space-md), var!(space-md));
943        font-size: var!(font-base);
944        font-weight: "400";
945        cursor: "pointer";
946        text-align: "left";
947        // Hover: inset 3px shadow bar between the dashed tree guide and the
948        // title text. Using `box-shadow inset` instead of a real border
949        // avoids any layout change so the text x-position never moves on
950        // hover (see comment on `c_euv_sidebar_link:hover`).
951        :hover {
952            box-shadow: format!("inset 3px 0 0 0 {}", var!(foreground));
953        }
954    }
955
956    // A top-level group title has no `c_euv_sidebar_children` ancestor, so
957    // the base hover bar paints flush against the sidebar's own left edge
958    // and reads as part of the sidebar border rather than as an affordance.
959    // This variant insets the bar by the row's own left padding and doubles
960    // its weight, so a first-level row gets the same "you can click this"
961    // cue a nested row does, just with room around it. The offset lives on
962    // the shadow rather than on `padding`, so the text never moves.
963    pub c_euv_sidebar_group_title_root {
964        display: "flex";
965        align-items: "center";
966        justify-content: "space-between";
967        width: "100%";
968        padding: format!("{} {}", var!(space-md), var!(space-md));
969        font-size: var!(font-base);
970        font-weight: "400";
971        cursor: "pointer";
972        text-align: "left";
973        :hover {
974            box-shadow: format!("inset 4px 0 0 0 {}", var!(foreground));
975            background: var!(muted);
976        }
977    }
978
979    pub c_euv_sidebar_group_title_root_active {
980        c_euv_sidebar_group_title_root();
981        background: var!(accent);
982        color: var!(text-on-accent);
983        font-weight: "600";
984        :hover {
985            box-shadow: "none";
986            background: var!(accent);
987        }
988    }
989
990    pub c_euv_sidebar_group_title_active {
991        c_euv_sidebar_group_title();
992        background: var!(accent);
993        color: var!(text-on-accent);
994        font-weight: "600";
995        // Same flush fill as `c_euv_sidebar_link_active_flush`: a nested
996        // active group title should read as one block with the sidebar, not
997        // as a chip sitting in from the edge. The negative margin below pulls
998        // the fill out to this level's dashed guide; the left padding pays it
999        // straight back so the label keeps the exact x it has while inactive.
1000        margin-left: format!("-{}px", var!(side-indent-num));
1001        width: format!("calc(100% + {}px)", var!(side-indent-num));
1002        padding-left: format!("calc({} + {}px)", var!(space-md), var!(side-indent-num));
1003        :hover {
1004            box-shadow: "none";
1005        }
1006    }
1007
1008    pub(crate) c_euv_sidebar_group_arrow {
1009        font-size: var!(font-xs);
1010        color: var!(muted-foreground);
1011        transition: format!("transform {} {}", var!(duration-fast), var!(ease-out));
1012    }
1013
1014    pub(crate) c_euv_sidebar_group_arrow_open {
1015        transform: "rotate(90deg)";
1016    }
1017
1018    pub(crate) c_euv_sidebar_group_arrow_active {
1019        color: var!(text-on-accent);
1020    }
1021
1022    pub c_euv_sidebar_children {
1023        display: "flex";
1024        flex-direction: "column";
1025        // Compact nesting: keep the dashed tree guide on the left of every
1026        // nesting level, but trim its indent so the child's text sits just
1027        // to the right of the dashed border instead of a full 33px gutter
1028        // away (the old `margin-left: 20px` + `padding-left: 12px` setup).
1029        // The numbers chosen (margin 8 + border 1 + padding 8 = 17px) are
1030        // large enough to read as a distinct level but small enough that
1031        // three-deep trees still fit in a 248–320px sidebar.
1032        margin-left: var!(space-sm);
1033        padding-left: var!(space-sm);
1034        border-left: format!("1px dashed {}", var!(border));
1035        animation: format!("euv-fade-in {} {}", var!(duration-normal), var!(ease-out));
1036    }
1037
1038    pub c_euv_sidebar_link {
1039        display: "block";
1040        padding: format!("{} {}", var!(space-md), var!(space-md));
1041        font-size: var!(font-base);
1042        color: var!(foreground);
1043        font-weight: "400";
1044        cursor: "pointer";
1045        // Hover: paint a 3px solid bar *between* the dashed tree guide and
1046        // the link's text. We use `inset box-shadow` rather than a real
1047        // `border-left` because borders participate in the box's layout
1048        // (they grow the border-edge and require compensating padding to
1049        // keep the text x-position stable — and `calc(12px + 3px - 3px)`
1050        // rounds inconsistently across browsers, causing sub-pixel text
1051        // shifts on hover). An inset shadow is painted *inside* the
1052        // existing padding box without changing layout, so the text never
1053        // moves. The dashed parent border is also untouched.
1054        :hover {
1055            box-shadow: format!("inset 3px 0 0 0 {}", var!(foreground));
1056        }
1057    }
1058
1059    pub c_euv_sidebar_link_active {
1060        display: "block";
1061        padding: format!("{} {}", var!(space-md), var!(space-md));
1062        font-size: var!(font-base);
1063        background: var!(accent);
1064        color: var!(text-on-accent);
1065        font-weight: "600";
1066        cursor: "pointer";
1067        // Active link: when the cursor is over it, the accent background
1068        // must dominate. Drop the inset bar entirely so the accent fill
1069        // carries the visual weight; on dark accent, an inset bar in the
1070        // foreground color is invisible and only competes for the eye's
1071        // focus. The accent fill + bold text already announce "you are
1072        // here".
1073        :hover {
1074            box-shadow: "none";
1075        }
1076    }
1077
1078    // An active child needs the accent fill to read as one continuous block
1079    // against the sidebar, not as a chip floating inside its parent's gutter.
1080    // `c_euv_sidebar_children` insets every nesting level by
1081    // `margin-left + padding-left` and paints a dashed guide on the way in.
1082    // The active row must pull its fill back out only as far as the guide it
1083    // hangs from — `margin-left + border-left-width`, i.e. `side-indent`
1084    // (9px) — so the fill starts exactly at the dashed rail and nothing
1085    // crosses it. Compensating by the *full* inset (margin + padding +
1086    // border = 17px) is wrong: it also cancels the row's own `padding-left`,
1087    // pushing the fill 8px past the guide and past the sidebar's left edge on
1088    // shallower levels. The negative margin is otherwise safe because the row
1089    // is the full width of its container: the fill grows leftward into the
1090    // parent's padding and nothing to its left moves.
1091    pub c_euv_sidebar_link_active_flush {
1092        display: "block";
1093        // Text must not move between the inactive and active states, so the
1094        // negative margin that pulls the fill out to the dashed guide is paid
1095        // back here as extra left padding. The two cancel for the label while
1096        // the background still reaches the guide. Only `padding-left` is
1097        // touched — the shorthand would drop the other three sides.
1098        padding: format!("{} {}", var!(space-md), var!(space-md));
1099        padding-left: format!("calc({} + {}px)", var!(space-md), var!(side-indent-num));
1100        // `var!()` expands to a bare `var(--token)`, so the unit has to live
1101        // inside the token — writing `-{}px` here would emit
1102        // `calc(-var(--side-indent-num)px)`, which is not a length and gets
1103        // dropped by the parser.
1104        margin-left: format!("-{}px", var!(side-indent-num));
1105        width: format!("calc(100% + {}px)", var!(side-indent-num));
1106        font-size: var!(font-base);
1107        background: var!(accent);
1108        color: var!(text-on-accent);
1109        font-weight: "600";
1110        cursor: "pointer";
1111        :hover {
1112            box-shadow: "none";
1113        }
1114    }
1115
1116    // ═══════════════════════════════════════════════════════════════════════════
1117    // euv_toc
1118    // ═══════════════════════════════════════════════════════════════════════════
1119
1120    pub c_euv_toc {
1121        // The outer `c_euv_doc_toc` column is already sticky in
1122        // `c_app_main`'s scroll context — adding another sticky offset here
1123        // would double the offset and the TOC would slide past the top of
1124        // the scrollable area instead of staying pinned. Keep the inner
1125        // toc purely as a layout container.
1126        display: "flex";
1127        flex-direction: "column";
1128        gap: var!(space-xs);
1129        border-left: format!("1px solid {}", var!(border));
1130        padding-left: var!(space-lg);
1131    }
1132
1133    pub(crate) c_euv_toc_title {
1134        font-size: var!(font-xs);
1135        font-weight: "700";
1136        text-transform: "uppercase";
1137        letter-spacing: "0.08em";
1138        color: var!(muted-foreground);
1139        margin-bottom: var!(space-xs);
1140    }
1141
1142    pub c_euv_toc_link {
1143        font-size: var!(font-sm);
1144        color: var!(muted-foreground);
1145        cursor: "pointer";
1146        line-height: "1.5";
1147        // Regular weight by default: only the *active* entry carries weight,
1148        // so hovering an inactive entry must not bold it or the weight would
1149        // read as a false "you are here" cue on mouse-over alone.
1150        font-weight: "400";
1151        :hover {
1152            color: var!(accent);
1153        }
1154    }
1155
1156    pub c_euv_toc_link_active {
1157        c_euv_toc_link();
1158        color: var!(accent);
1159        font-weight: "700";
1160        :hover {
1161            font-weight: "700";
1162        }
1163    }
1164
1165    pub c_euv_toc_link_nested {
1166        c_euv_toc_link();
1167        padding-left: var!(space-lg);
1168    }
1169
1170    pub c_euv_toc_link_nested_active {
1171        c_euv_toc_link_nested();
1172        color: var!(accent);
1173        font-weight: "700";
1174        :hover {
1175            font-weight: "700";
1176        }
1177    }
1178}