@aiaiai-pt/design-system 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ <!--
2
+ @component AppFrame
3
+
4
+ The locked public site-shell scaffold: skip-link → header → a single `<main>`
5
+ landmark → footer, in fixed DOM (= reading) order. This is the
6
+ accessibility-by-construction frame for the citizen portal — every page is
7
+ wrapped in exactly one AppFrame, guaranteeing one main landmark, a working
8
+ skip target, and linear reading order (ARTE Silver / WCAG 1.3.2, 2.4.1).
9
+ Structure is frozen and conformance-checkable; only brand tokens vary per
10
+ tenant via `data-theme`.
11
+
12
+ `header` and `footer` are slots so a tenant composes `SiteHeader` / `SiteFooter`
13
+ with its own brand + nav; the page content is the default `children`.
14
+
15
+ @example
16
+ <AppFrame>
17
+ {#snippet header()}<SiteHeader>…</SiteHeader>{/snippet}
18
+ {#snippet footer()}<SiteFooter>…</SiteFooter>{/snippet}
19
+ <PageContainer>{@render page()}</PageContainer>
20
+ </AppFrame>
21
+ -->
22
+ <script>
23
+ import SkipLink from "./SkipLink.svelte";
24
+
25
+ let {
26
+ /** @type {string} id of the main landmark (skip-link target). */
27
+ mainId = "main",
28
+ /** @type {string} */
29
+ class: className = "",
30
+ /** @type {import('svelte').Snippet | undefined} Override the default skip link (e.g. localized). */
31
+ skipLink = undefined,
32
+ /** @type {import('svelte').Snippet | undefined} Site banner (compose `SiteHeader`). */
33
+ header = undefined,
34
+ /** @type {import('svelte').Snippet | undefined} Site footer (compose `SiteFooter`). */
35
+ footer = undefined,
36
+ /** @type {import('svelte').Snippet | undefined} Page content (rendered inside `<main>`). */
37
+ children = undefined,
38
+ ...rest
39
+ } = $props();
40
+ </script>
41
+
42
+ <div class="app-frame {className}" {...rest}>
43
+ {#if skipLink}{@render skipLink()}{:else}<SkipLink href={`#${mainId}`} />{/if}
44
+
45
+ {#if header}{@render header()}{/if}
46
+
47
+ <!-- Single main landmark + focus target for the skip link. tabindex=-1 so
48
+ the skip link can move focus here without making it tabbable otherwise. -->
49
+ <main id={mainId} tabindex="-1" class="app-frame-main">
50
+ {#if children}{@render children()}{/if}
51
+ </main>
52
+
53
+ {#if footer}{@render footer()}{/if}
54
+ </div>
55
+
56
+ <style>
57
+ .app-frame {
58
+ display: flex;
59
+ flex-direction: column;
60
+ min-height: 100vh;
61
+ min-height: 100dvh;
62
+ background: var(--color-surface);
63
+ color: var(--color-text);
64
+ }
65
+
66
+ /* Main grows to push the footer down; the focus target shows no outline box
67
+ (focus is programmatic via the skip link, not a visible widget). */
68
+ .app-frame-main {
69
+ flex: 1 0 auto;
70
+ }
71
+
72
+ .app-frame-main:focus {
73
+ outline: none;
74
+ }
75
+ </style>
@@ -0,0 +1,42 @@
1
+ export default AppFrame;
2
+ type AppFrame = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * AppFrame
8
+ *
9
+ * The locked public site-shell scaffold: skip-link → header → a single `<main>`
10
+ * landmark → footer, in fixed DOM (= reading) order. This is the
11
+ * accessibility-by-construction frame for the citizen portal — every page is
12
+ * wrapped in exactly one AppFrame, guaranteeing one main landmark, a working
13
+ * skip target, and linear reading order (ARTE Silver / WCAG 1.3.2, 2.4.1).
14
+ * Structure is frozen and conformance-checkable; only brand tokens vary per
15
+ * tenant via `data-theme`.
16
+ *
17
+ * `header` and `footer` are slots so a tenant composes `SiteHeader` / `SiteFooter`
18
+ * with its own brand + nav; the page content is the default `children`.
19
+ *
20
+ * @example
21
+ * <AppFrame>
22
+ * {#snippet header()}<SiteHeader>…</SiteHeader>{/snippet}
23
+ * {#snippet footer()}<SiteFooter>…</SiteFooter>{/snippet}
24
+ * <PageContainer>{@render page()}</PageContainer>
25
+ * </AppFrame>
26
+ */
27
+ declare const AppFrame: import("svelte").Component<{
28
+ mainId?: string;
29
+ class?: string;
30
+ skipLink?: any;
31
+ header?: any;
32
+ footer?: any;
33
+ children?: any;
34
+ } & Record<string, any>, {}, "">;
35
+ type $$ComponentProps = {
36
+ mainId?: string;
37
+ class?: string;
38
+ skipLink?: any;
39
+ header?: any;
40
+ footer?: any;
41
+ children?: any;
42
+ } & Record<string, any>;
@@ -0,0 +1,100 @@
1
+ <!--
2
+ @component ContentBlock
3
+
4
+ Prose container for documentation / legal / content pages (privacy, terms,
5
+ accessibility, regulamento — the `content` page template). Applies readable
6
+ measure + typographic rhythm to long-form content, with an optional title
7
+ and a version/date badge slot.
8
+
9
+ SECURITY: ContentBlock only STYLES its children — it does not parse or
10
+ sanitise. A consumer rendering operator-authored markdown/HTML MUST sanitise
11
+ before injecting it (XSS); pass the sanitised result as `children`.
12
+
13
+ @example
14
+ <ContentBlock title="Privacy Policy">
15
+ {#snippet badge()}<Badge>v3 · updated 2026-05-01</Badge>{/snippet}
16
+ {@html sanitizedHtml}
17
+ </ContentBlock>
18
+ -->
19
+ <script>
20
+ let {
21
+ /** @type {string} Page title (rendered as h1 — keep one per page). */
22
+ title = "",
23
+ /** @type {1 | 2} Heading level for `title`. */
24
+ headingLevel = 1,
25
+ /** @type {string} */
26
+ class: className = "",
27
+ /** @type {import('svelte').Snippet | undefined} Version / date badge. */
28
+ badge = undefined,
29
+ /** @type {import('svelte').Snippet | undefined} The (pre-sanitised) prose. */
30
+ children = undefined,
31
+ ...rest
32
+ } = $props();
33
+ </script>
34
+
35
+ <article class="content-block {className}" {...rest}>
36
+ {#if title}
37
+ <header class="content-block-head">
38
+ <svelte:element this={`h${headingLevel}`} class="content-block-title">{title}</svelte:element>
39
+ {#if badge}<div class="content-block-badge">{@render badge()}</div>{/if}
40
+ </header>
41
+ {/if}
42
+ <div class="content-block-prose">
43
+ {#if children}{@render children()}{/if}
44
+ </div>
45
+ </article>
46
+
47
+ <style>
48
+ .content-block {
49
+ width: 100%;
50
+ max-width: var(--content-width-narrow);
51
+ margin-inline: auto;
52
+ padding: var(--space-2xl) var(--content-padding-x);
53
+ }
54
+
55
+ .content-block-head {
56
+ display: flex;
57
+ flex-wrap: wrap;
58
+ align-items: baseline;
59
+ gap: var(--space-sm);
60
+ margin-bottom: var(--space-lg);
61
+ }
62
+
63
+ .content-block-title {
64
+ margin: 0;
65
+ font-family: var(--type-heading-lg-font);
66
+ font-size: var(--type-heading-lg-size);
67
+ color: var(--color-text);
68
+ }
69
+
70
+ /* Long-form rhythm. Scoped to descendants since prose is consumer-injected
71
+ (:global needed — these elements aren't in this component's own markup). */
72
+ .content-block-prose {
73
+ font-family: var(--type-body-font);
74
+ font-size: var(--type-body-size);
75
+ color: var(--color-text);
76
+ line-height: 1.6;
77
+ }
78
+
79
+ .content-block-prose :global(h2) {
80
+ font-family: var(--type-heading-font);
81
+ font-size: var(--type-heading-size);
82
+ margin: var(--space-xl) 0 var(--space-sm);
83
+ }
84
+
85
+ .content-block-prose :global(h3) {
86
+ font-family: var(--type-heading-sm-font);
87
+ font-size: var(--type-heading-sm-size);
88
+ margin: var(--space-lg) 0 var(--space-xs);
89
+ }
90
+
91
+ .content-block-prose :global(p),
92
+ .content-block-prose :global(ul),
93
+ .content-block-prose :global(ol) {
94
+ margin: 0 0 var(--space-md);
95
+ }
96
+
97
+ .content-block-prose :global(a) {
98
+ color: var(--color-accent);
99
+ }
100
+ </style>
@@ -0,0 +1,37 @@
1
+ export default ContentBlock;
2
+ type ContentBlock = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * ContentBlock
8
+ *
9
+ * Prose container for documentation / legal / content pages (privacy, terms,
10
+ * accessibility, regulamento — the `content` page template). Applies readable
11
+ * measure + typographic rhythm to long-form content, with an optional title
12
+ * and a version/date badge slot.
13
+ *
14
+ * SECURITY: ContentBlock only STYLES its children — it does not parse or
15
+ * sanitise. A consumer rendering operator-authored markdown/HTML MUST sanitise
16
+ * before injecting it (XSS); pass the sanitised result as `children`.
17
+ *
18
+ * @example
19
+ * <ContentBlock title="Privacy Policy">
20
+ * {#snippet badge()}<Badge>v3 · updated 2026-05-01</Badge>{/snippet}
21
+ * {@html sanitizedHtml}
22
+ * </ContentBlock>
23
+ */
24
+ declare const ContentBlock: import("svelte").Component<{
25
+ title?: string;
26
+ headingLevel?: number;
27
+ class?: string;
28
+ badge?: any;
29
+ children?: any;
30
+ } & Record<string, any>, {}, "">;
31
+ type $$ComponentProps = {
32
+ title?: string;
33
+ headingLevel?: number;
34
+ class?: string;
35
+ badge?: any;
36
+ children?: any;
37
+ } & Record<string, any>;
@@ -0,0 +1,87 @@
1
+ <!--
2
+ @component Hero
3
+
4
+ Landing / page hero. Renders the page's primary heading (an `<h1>` by
5
+ default — keep exactly one per page for the single-H1 a11y contract), an
6
+ optional subtitle, and an actions slot. Use at the top of `landing` and
7
+ `service-flow` page templates.
8
+
9
+ @example
10
+ <Hero title="Report a problem in Valongo" subtitle="Potholes, lighting, waste — in two minutes.">
11
+ {#snippet actions()}<Button href="/report">Start a report</Button>{/snippet}
12
+ </Hero>
13
+
14
+ @example Custom heading level (when the hero is not the page's H1)
15
+ <Hero title="Latest consultations" headingLevel={2} />
16
+ -->
17
+ <script>
18
+ let {
19
+ /** @type {string} */
20
+ title = "",
21
+ /** @type {string} */
22
+ subtitle = "",
23
+ /** @type {1 | 2 | 3} Heading level for `title` — keep one h1 per page. */
24
+ headingLevel = 1,
25
+ /** @type {string} */
26
+ class: className = "",
27
+ /** @type {import('svelte').Snippet | undefined} Title override (rich content). */
28
+ children = undefined,
29
+ /** @type {import('svelte').Snippet | undefined} Call-to-action buttons. */
30
+ actions = undefined,
31
+ ...rest
32
+ } = $props();
33
+ </script>
34
+
35
+ <section class="hero {className}" {...rest}>
36
+ <div class="hero-inner">
37
+ {#if children}
38
+ {@render children()}
39
+ {:else if title}
40
+ <svelte:element this={`h${headingLevel}`} class="hero-title">{title}</svelte:element>
41
+ {/if}
42
+ {#if subtitle}
43
+ <p class="hero-subtitle">{subtitle}</p>
44
+ {/if}
45
+ {#if actions}
46
+ <div class="hero-actions">{@render actions()}</div>
47
+ {/if}
48
+ </div>
49
+ </section>
50
+
51
+ <style>
52
+ .hero {
53
+ background: var(--color-surface);
54
+ }
55
+
56
+ .hero-inner {
57
+ width: 100%;
58
+ max-width: var(--content-width-wide);
59
+ margin-inline: auto;
60
+ padding: var(--space-3xl) var(--content-padding-x);
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: var(--space-md);
64
+ }
65
+
66
+ .hero-title {
67
+ margin: 0;
68
+ font-family: var(--type-display-font);
69
+ font-size: var(--type-display-size);
70
+ color: var(--color-text);
71
+ }
72
+
73
+ .hero-subtitle {
74
+ margin: 0;
75
+ max-width: var(--content-width-narrow);
76
+ font-family: var(--type-body-font);
77
+ font-size: var(--type-body-size);
78
+ color: var(--color-text-secondary);
79
+ }
80
+
81
+ .hero-actions {
82
+ display: flex;
83
+ flex-wrap: wrap;
84
+ gap: var(--space-sm);
85
+ margin-top: var(--space-sm);
86
+ }
87
+ </style>
@@ -0,0 +1,37 @@
1
+ export default Hero;
2
+ type Hero = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * Hero
8
+ *
9
+ * Landing / page hero. Renders the page's primary heading (an `<h1>` by
10
+ * default — keep exactly one per page for the single-H1 a11y contract), an
11
+ * optional subtitle, and an actions slot. Use at the top of `landing` and
12
+ * `service-flow` page templates.
13
+ *
14
+ * @example
15
+ * <Hero title="Report a problem in Valongo" subtitle="Potholes, lighting, waste — in two minutes.">
16
+ * {#snippet actions()}<Button href="/report">Start a report</Button>{/snippet}
17
+ * </Hero>
18
+ *
19
+ * @example Custom heading level (when the hero is not the page's H1)
20
+ * <Hero title="Latest consultations" headingLevel={2} />
21
+ */
22
+ declare const Hero: import("svelte").Component<{
23
+ title?: string;
24
+ subtitle?: string;
25
+ headingLevel?: number;
26
+ class?: string;
27
+ children?: any;
28
+ actions?: any;
29
+ } & Record<string, any>, {}, "">;
30
+ type $$ComponentProps = {
31
+ title?: string;
32
+ subtitle?: string;
33
+ headingLevel?: number;
34
+ class?: string;
35
+ children?: any;
36
+ actions?: any;
37
+ } & Record<string, any>;
@@ -0,0 +1,100 @@
1
+ <!--
2
+ @component Link
3
+
4
+ A text hyperlink. Two variants, following the Carbon convention:
5
+ - `inline` (default): used inside sentences/prose — always underlined so it's
6
+ distinguishable from surrounding text.
7
+ - `standalone`: used on its own after content (lists, CTAs, footers) —
8
+ underlined only on hover/focus.
9
+
10
+ Accessibility:
11
+ - A link with no (or empty) `href` is NOT a link — it renders a
12
+ non-interactive `<span aria-current="page">` (the canonical "you are here"
13
+ treatment) instead of a keyboard-trap `<a>` with no destination.
14
+ - `external` adds `target="_blank"` + `rel="noopener noreferrer"`.
15
+ - Visible focus ring on `:focus-visible`.
16
+
17
+ @example Inline (prose)
18
+ Read our <Link href="/info/privacy">privacy policy</Link>.
19
+
20
+ @example Standalone CTA
21
+ <Link href="/report" variant="standalone">Report a problem</Link>
22
+
23
+ @example Current page (no href → rendered as non-interactive)
24
+ <Link current>Home</Link>
25
+ -->
26
+ <script>
27
+ let {
28
+ /** @type {string | undefined} Destination. Falsy → rendered as a non-interactive span. */
29
+ href = undefined,
30
+ /** @type {'inline' | 'standalone'} */
31
+ variant = "inline",
32
+ /** @type {boolean} Marks the current page (`aria-current="page"`). */
33
+ current = false,
34
+ /** @type {boolean} Open in a new tab with safe rel. */
35
+ external = false,
36
+ /** @type {string} */
37
+ class: className = "",
38
+ /** @type {import('svelte').Snippet | undefined} */
39
+ children = undefined,
40
+ ...rest
41
+ } = $props();
42
+
43
+ const isLink = $derived(typeof href === "string" && href.length > 0);
44
+ const externalAttrs = $derived(
45
+ external ? { target: "_blank", rel: "noopener noreferrer" } : {},
46
+ );
47
+ </script>
48
+
49
+ {#if isLink}
50
+ <a
51
+ {href}
52
+ class="link link-{variant} {className}"
53
+ aria-current={current ? "page" : undefined}
54
+ {...externalAttrs}
55
+ {...rest}
56
+ >
57
+ {#if children}{@render children()}{/if}
58
+ </a>
59
+ {:else}
60
+ <!-- No destination → not a link. Non-interactive, still announces "current". -->
61
+ <span class="link link-{variant} link-static {className}" aria-current={current ? "page" : undefined} {...rest}>
62
+ {#if children}{@render children()}{/if}
63
+ </span>
64
+ {/if}
65
+
66
+ <style>
67
+ .link {
68
+ color: var(--color-accent);
69
+ border-radius: var(--radius-sm);
70
+ }
71
+
72
+ .link-inline {
73
+ text-decoration: underline;
74
+ text-underline-offset: 0.15em;
75
+ }
76
+
77
+ .link-standalone {
78
+ text-decoration: none;
79
+ }
80
+ .link-standalone:hover {
81
+ text-decoration: underline;
82
+ text-underline-offset: 0.15em;
83
+ }
84
+
85
+ a.link:hover {
86
+ color: var(--color-accent-hover);
87
+ }
88
+
89
+ a.link:focus-visible {
90
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
91
+ outline-offset: var(--focus-ring-offset);
92
+ }
93
+
94
+ /* Non-interactive (current/no-href): inherit text colour, no affordance. */
95
+ .link-static {
96
+ color: var(--color-text);
97
+ text-decoration: none;
98
+ cursor: default;
99
+ }
100
+ </style>
@@ -0,0 +1,46 @@
1
+ export default Link;
2
+ type Link = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * Link
8
+ *
9
+ * A text hyperlink. Two variants, following the Carbon convention:
10
+ * - `inline` (default): used inside sentences/prose — always underlined so it's
11
+ * distinguishable from surrounding text.
12
+ * - `standalone`: used on its own after content (lists, CTAs, footers) —
13
+ * underlined only on hover/focus.
14
+ *
15
+ * Accessibility:
16
+ * - A link with no (or empty) `href` is NOT a link — it renders a
17
+ * non-interactive `<span aria-current="page">` (the canonical "you are here"
18
+ * treatment) instead of a keyboard-trap `<a>` with no destination.
19
+ * - `external` adds `target="_blank"` + `rel="noopener noreferrer"`.
20
+ * - Visible focus ring on `:focus-visible`.
21
+ *
22
+ * @example Inline (prose)
23
+ * Read our <Link href="/info/privacy">privacy policy</Link>.
24
+ *
25
+ * @example Standalone CTA
26
+ * <Link href="/report" variant="standalone">Report a problem</Link>
27
+ *
28
+ * @example Current page (no href → rendered as non-interactive)
29
+ * <Link current>Home</Link>
30
+ */
31
+ declare const Link: import("svelte").Component<{
32
+ href?: any;
33
+ variant?: string;
34
+ current?: boolean;
35
+ external?: boolean;
36
+ class?: string;
37
+ children?: any;
38
+ } & Record<string, any>, {}, "">;
39
+ type $$ComponentProps = {
40
+ href?: any;
41
+ variant?: string;
42
+ current?: boolean;
43
+ external?: boolean;
44
+ class?: string;
45
+ children?: any;
46
+ } & Record<string, any>;
@@ -0,0 +1,160 @@
1
+ <!--
2
+ @component ServiceNavigation
3
+
4
+ Primary site navigation for the public portal, modelled on the GOV.UK
5
+ "Service navigation" and Designers Italia header-nav patterns (the citizen /
6
+ PA gold standard). Data-driven: it owns its `<a>` markup (rendered from
7
+ `items`), so it styles its own links — no magic descendant styling of
8
+ consumer anchors.
9
+
10
+ Accessibility:
11
+ - `<nav aria-label>` landmark; the current item carries `aria-current="page"`.
12
+ - Below `--breakpoint` the list collapses behind a toggle button with
13
+ `aria-expanded` + `aria-controls` (WCAG-friendly disclosure).
14
+ - Empty `items` renders NOTHING — never an empty nav landmark.
15
+ - Toggle + links have visible focus rings.
16
+
17
+ @example
18
+ <ServiceNavigation
19
+ label="Primary"
20
+ items={[
21
+ { href: "/report", label: "Report a problem" },
22
+ { href: "/track", label: "Track a report", current: true },
23
+ { href: "/info/privacy", label: "Privacy" },
24
+ ]}
25
+ >
26
+ {#snippet menuIcon()}<MenuIcon />{/snippet}
27
+ </ServiceNavigation>
28
+ -->
29
+ <script>
30
+ let {
31
+ /** @type {Array<{ href: string, label: string, current?: boolean }>} */
32
+ items = [],
33
+ /** @type {string} aria-label for the nav landmark (localize it). */
34
+ label = "Primary",
35
+ /** @type {string} id linking the toggle button to the menu (unique per page). */
36
+ menuId = "service-navigation-menu",
37
+ /** @type {string} Visible label for the mobile toggle (localize it). */
38
+ toggleLabel = "Menu",
39
+ /** @type {string} */
40
+ class: className = "",
41
+ /** @type {import('svelte').Snippet | undefined} Icon for the mobile toggle. */
42
+ menuIcon = undefined,
43
+ ...rest
44
+ } = $props();
45
+
46
+ let open = $state(false);
47
+ </script>
48
+
49
+ {#if items.length > 0}
50
+ <nav class="service-nav {className}" aria-label={label} {...rest}>
51
+ <button
52
+ type="button"
53
+ class="service-nav-toggle"
54
+ aria-expanded={open}
55
+ aria-controls={menuId}
56
+ onclick={() => (open = !open)}
57
+ >
58
+ {#if menuIcon}{@render menuIcon()}{/if}
59
+ <span class="service-nav-toggle-label">{toggleLabel}</span>
60
+ </button>
61
+
62
+ <ul id={menuId} class="service-nav-list" class:open>
63
+ {#each items as item (item.href)}
64
+ <li class="service-nav-item">
65
+ <a
66
+ href={item.href}
67
+ class="service-nav-link"
68
+ aria-current={item.current ? "page" : undefined}
69
+ >
70
+ {item.label}
71
+ </a>
72
+ </li>
73
+ {/each}
74
+ </ul>
75
+ </nav>
76
+ {/if}
77
+
78
+ <style>
79
+ .service-nav {
80
+ display: flex;
81
+ align-items: center;
82
+ }
83
+
84
+ /* Toggle hidden on wide viewports; the list is a horizontal row. */
85
+ .service-nav-toggle {
86
+ display: none;
87
+ align-items: center;
88
+ gap: var(--space-2xs);
89
+ padding: var(--space-2xs) var(--space-sm);
90
+ border: 1px solid var(--color-border);
91
+ border-radius: var(--radius-md);
92
+ background: var(--color-surface);
93
+ color: var(--color-text);
94
+ font-family: var(--type-label-font);
95
+ font-size: var(--type-label-size);
96
+ cursor: pointer;
97
+ }
98
+ .service-nav-toggle:focus-visible,
99
+ .service-nav-link:focus-visible {
100
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
101
+ outline-offset: var(--focus-ring-offset);
102
+ }
103
+
104
+ .service-nav-list {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: var(--space-2xs);
108
+ margin: 0;
109
+ padding: 0;
110
+ list-style: none;
111
+ }
112
+
113
+ .service-nav-link {
114
+ display: block;
115
+ padding: var(--space-2xs) var(--space-sm);
116
+ border-radius: var(--radius-md);
117
+ color: var(--color-text-secondary);
118
+ text-decoration: none;
119
+ font-family: var(--type-label-font);
120
+ font-size: var(--type-label-size);
121
+ }
122
+ .service-nav-link:hover {
123
+ color: var(--color-text);
124
+ background: var(--color-surface-tertiary);
125
+ }
126
+ /* Current page: emphasized text + an accent underline bar (GOV.UK pattern). */
127
+ .service-nav-link[aria-current="page"] {
128
+ color: var(--color-text);
129
+ font-weight: var(--raw-font-weight-semibold);
130
+ box-shadow: inset 0 -3px 0 0 var(--color-accent);
131
+ }
132
+
133
+ /* Mobile: button shows, list collapses behind it. */
134
+ @media (max-width: 47.99rem) {
135
+ .service-nav {
136
+ position: relative;
137
+ }
138
+ .service-nav-toggle {
139
+ display: inline-flex;
140
+ }
141
+ .service-nav-list {
142
+ display: none;
143
+ position: absolute;
144
+ top: calc(100% + var(--space-2xs));
145
+ inset-inline-end: 0;
146
+ flex-direction: column;
147
+ align-items: stretch;
148
+ min-width: 12rem;
149
+ padding: var(--space-2xs);
150
+ background: var(--color-surface);
151
+ border: 1px solid var(--color-border);
152
+ border-radius: var(--radius-md);
153
+ box-shadow: var(--shadow-md);
154
+ z-index: 50;
155
+ }
156
+ .service-nav-list.open {
157
+ display: flex;
158
+ }
159
+ }
160
+ </style>