Skip to main content

euv_ui/style/class/shell/
fn.rs

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