@dogsbay/render-html 0.0.0-stage → 0.2.0-beta.117

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.
package/README.md CHANGED
@@ -1,3 +1,51 @@
1
- # Temporary Holding Version
1
+ # @dogsbay/render-html
2
+
3
+ Dogsbay MD (`TreeNode[]` from `@dogsbay/format-dogsbay-md`) to HTML, with the
4
+ markup and Tailwind classes of the Dogsbay Astro components.
5
+
6
+ - **Works without a highlighter, math or a CSS compiler.** With no `highlighter`, code
7
+ renders as plain text in the same markup. With no `math`, LaTeX stays as source.
8
+ - **Worker- and browser-safe:** the main entry imports no Node built-ins.
9
+
10
+ ```ts
11
+ import { dogsbayMdToTree } from "@dogsbay/format-dogsbay-md/browser";
12
+ import { renderBlocks, type RenderContext } from "@dogsbay/render-html";
13
+
14
+ const ctx: RenderContext = {
15
+ link: (href) => href,
16
+ asset: (src) => src,
17
+ headings: [], ids: new Set(), unknown: new Set(), seq: 0,
18
+ t: (key) => key,
19
+ // highlighter: await createHighlighter(), // @dogsbay/highlight
20
+ // math: katexMath, // @dogsbay/render-html/math (needs katex)
21
+ };
22
+ const html = renderBlocks(dogsbayMdToTree(markdown).tree, ctx);
23
+ ```
24
+
25
+ The output uses Tailwind utility classes and the theme tokens. Styling it needs the Dogsbay
26
+ theme CSS, compiled for the classes the page uses.
27
+
28
+ ## Stylesheets
29
+
30
+ `css/` holds the stylesheets for this markup: the theme presets (`theme-default.css`,
31
+ `theme-mintlify.css`), `docs.css`, `steps.css` and `code-rich.css`. Import them as
32
+ `@dogsbay/render-html/css/<file>`. They are Tailwind 4 sources (`@apply`, `@utility`,
33
+ tokens), so compile them with the page's classes.
34
+
35
+ ## Copied sources
36
+
37
+ `alert-icons.ts`, `badge-variants.ts`, `kbd-variants.ts`, `file-tree.ts`,
38
+ `code-markers.ts`, `tone-classes.ts` and everything in `css/` are copies from `@dogsbay/ui` and
39
+ `@dogsbay/format-astro`. Copying them keeps this package free of Astro.
40
+
41
+ After changing them upstream, run `pnpm --filter @dogsbay/render-html sync-sources`.
42
+ `tests/sources.test.ts` fails while a copy is stale.
43
+
44
+ ## Favicon
45
+
46
+ `@dogsbay/render-html/favicon` exports the Dogsbay paw (`FAVICON_SVG`, and
47
+ `FAVICON_DATA_URI` for a self-contained page); the source with its design notes
48
+ is `assets/favicon.svg`. The code in this package is MIT licensed; the Dogsbay
49
+ name and paw mark are not licensed for use as your own branding. Pages Dogsbay
50
+ renders use it as their default favicon.
2
51
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
@@ -0,0 +1,28 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <!--
3
+ Drawn, not traced. The photographic source was pale mint on white and
4
+ its glass texture survived any threshold as speckle.
5
+
6
+ FILLED, not stroked. An outlined version read as a face at size: the
7
+ negative space between the toes and the pad became two eyes and a
8
+ mouth once adjacent strokes merged.
9
+
10
+ Toes sit slightly high and the pad slightly low so the gaps survive
11
+ downsampling — at 16px a tighter arrangement fuses the toes into a
12
+ single bar.
13
+
14
+ Theme-aware: the browser composites a favicon against the tab strip,
15
+ so one fixed colour disappears on either a light or a dark UI.
16
+ -->
17
+ <style>
18
+ .paw { fill: #1e6e7d; }
19
+ @media (prefers-color-scheme: dark) { .paw { fill: #7fd3e0; } }
20
+ </style>
21
+ <g class="paw">
22
+ <ellipse cx="14.5" cy="29.5" rx="6.3" ry="8.6" transform="rotate(-22 14.5 29.5)"/>
23
+ <ellipse cx="26" cy="17.5" rx="6.3" ry="9.2" transform="rotate(-8 26 17.5)"/>
24
+ <ellipse cx="40" cy="17.5" rx="6.3" ry="9.2" transform="rotate(8 40 17.5)"/>
25
+ <ellipse cx="51.5" cy="29.5" rx="6.3" ry="8.6" transform="rotate(22 51.5 29.5)"/>
26
+ <path d="M32 36c9.8 0 16.8 6.8 16.8 13.8S41.8 59.5 32 59.5 15.2 54.3 15.2 47.3 22.2 36 32 36Z"/>
27
+ </g>
28
+ </svg>
@@ -0,0 +1,88 @@
1
+ /* Generated by packages/render-html/scripts/sync-sources.mjs from @dogsbay/ui code-rich.css. Do not edit. */
2
+ /* CodeRich annotation styles — applied via CSS classes from Shiki transformers */
3
+
4
+ /* ── All lines ────────────────────────────────────────── */
5
+ .code-rich-body .line {
6
+ /* inline-block, not inline-flex: a flex line makes each token span a flex item, and
7
+ browsers then copy a hand-made selection with a line break between tokens. */
8
+ display: inline-block;
9
+ width: 100%;
10
+ }
11
+
12
+ /* ── Gutter (single column: line number + diff marker) ── */
13
+ .code-rich-body .gutter {
14
+ display: inline-block;
15
+ min-width: 4ch;
16
+ margin-right: 1.5ch;
17
+ text-align: right;
18
+ /* Theme-aware, and NO alpha. This was a fixed dark grey at 70%
19
+ opacity: fine on a light code background, 1.3:1 against the dark
20
+ one — line numbers were effectively invisible in dark mode.
21
+ Opacity compounds it, since it blends toward whatever is behind.
22
+ --muted-foreground already flips per theme (L 0.45 / 0.708). */
23
+ color: var(--muted-foreground);
24
+ user-select: none;
25
+ flex-shrink: 0;
26
+ }
27
+ .code-rich-body .gutter.add {
28
+ color: oklch(0.55 0.2 145);
29
+ }
30
+ .code-rich-body .gutter.remove {
31
+ color: oklch(0.55 0.2 25);
32
+ }
33
+
34
+ /* Dark mode: the diff gutters sit on TINTED line backgrounds, not on
35
+ the plain code background, and at L0.55 they measure 2.89:1 (add)
36
+ and 2.63:1 (remove) there — both well under AA. Light mode is
37
+ unaffected, so these are overrides rather than a changed base.
38
+ Raised to L0.72 for headroom over the 4.5:1 floor. */
39
+ .dark .code-rich-body .gutter.add {
40
+ color: oklch(0.72 0.2 145);
41
+ }
42
+ .dark .code-rich-body .gutter.remove {
43
+ color: oklch(0.72 0.14 25);
44
+ }
45
+
46
+ /* ── Diff line backgrounds ────────────────────────────── */
47
+ .code-rich-body .line.diff.add {
48
+ background-color: oklch(0.75 0.18 145 / 0.15);
49
+ }
50
+ .code-rich-body .line.diff.remove {
51
+ background-color: oklch(0.65 0.2 25 / 0.15);
52
+ }
53
+
54
+ /* ── Focus ─────────────────────────────────────────────── */
55
+ .code-rich-body pre.has-focused .line:not(.focused) {
56
+ opacity: 0.7;
57
+ transition: opacity 0.3s;
58
+ }
59
+ .code-rich-body pre.has-focused:hover .line:not(.focused) {
60
+ opacity: 0.9;
61
+ }
62
+ .code-rich-body pre.has-focused .line.focused {
63
+ opacity: 1;
64
+ }
65
+
66
+ /* ── Highlight (background only) ──────────────────────── */
67
+ .code-rich-body .line.highlighted {
68
+ background-color: oklch(0.7 0.1 250 / 0.08);
69
+ }
70
+
71
+ /* ── Error / Warning / Info levels ─────────────────────── */
72
+ .code-rich-body .line.highlighted.error {
73
+ background-color: oklch(0.65 0.2 25 / 0.08);
74
+ }
75
+ .code-rich-body .line.highlighted.warning {
76
+ background-color: oklch(0.75 0.18 65 / 0.08);
77
+ }
78
+ .code-rich-body .line.highlighted.info {
79
+ background-color: oklch(0.7 0.15 250 / 0.08);
80
+ }
81
+
82
+ /* ── Word highlight ────────────────────────────────────── */
83
+ .code-rich-body .highlighted-word {
84
+ background-color: oklch(0.7 0.1 250 / 0.25);
85
+ border-radius: 3px;
86
+ padding: 1px 4px;
87
+ margin: 0 -1px;
88
+ }
package/css/docs.css ADDED
@@ -0,0 +1,207 @@
1
+ /* Generated by packages/render-html/scripts/sync-sources.mjs from @dogsbay/format-astro (generateGlobalCss). Do not edit. */
2
+
3
+ /* Custom elements have no user-agent style, so an unstyled one is
4
+ display:inline — on which w-full does nothing and a border paints
5
+ as detached fragments rather than around the content. @dogsbay/elements
6
+ injects these same defaults at runtime, which is what reaches EXISTING
7
+ sites (global.css is scaffolded once and never rewritten). Having them
8
+ here too means a new site is correct at CSS time, with no flash before
9
+ the module script runs.
10
+
11
+ :where() keeps specificity at zero, so any utility class wins. */
12
+ :where(db-tabs, db-accordion, db-collapsible, db-card, db-steps, db-code-block) {
13
+ display: block;
14
+ }
15
+ :where(db-link-button) {
16
+ display: inline-block;
17
+ }
18
+
19
+ /* Scan @dogsbay packages for Tailwind classes */
20
+
21
+ /* Pin the grid-tone palette. These classes are emitted into
22
+ markdown-generated .astro pages by the grid-item serializer
23
+ (TONE_CLASSES in @dogsbay/format-astro). Without inlining,
24
+ opacity-modified utilities like bg-primary/10 get purged when
25
+ Tailwind doesn't see them in the scanned globs, leaving grid
26
+ demo cells with no visible background. */
27
+ @source inline("bg-amber-500/15 bg-blue-500/15 bg-green-500/15 bg-muted bg-primary bg-primary/10 bg-primary/20 bg-primary/50 bg-primary/60 bg-primary/80 bg-purple-500/15 bg-red-500/15 dark:text-amber-300 dark:text-blue-300 dark:text-green-300 dark:text-purple-300 dark:text-red-300 text-amber-700 text-blue-700 text-green-700 text-muted-foreground text-primary text-primary-foreground text-purple-700 text-red-700");
28
+
29
+ /* Prose typography for rendered content.
30
+ Every element rule is guarded with :not(.dba-api *): API-reference
31
+ regions (ApiLayout root carries the dba-api marker) compose their own
32
+ typography from utility classes, and unguarded .docs-prose rules would
33
+ out-specify them. The guard lets mixed prose+endpoint pages (MDX
34
+ imports) keep full typography outside the cards. */
35
+ .docs-prose {
36
+ line-height: 1.7;
37
+
38
+ & h1:not(.dba-api *) { font-family: var(--font-heading); font-size: 2rem; font-weight: 700; margin-top: 0; margin-bottom: 1rem; line-height: 1.2; letter-spacing: -0.025em; }
39
+ & h2:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.5rem; font-weight: 600; margin-top: 2.5rem; margin-bottom: 0.75rem; line-height: 1.3; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; letter-spacing: -0.015em; }
40
+ & h3:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; line-height: 1.4; }
41
+ & h4:not(.dba-api *) { font-family: var(--font-heading); font-size: 1.1rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; }
42
+
43
+ & p:not(.dba-api *) { margin-top: 0.75rem; margin-bottom: 0.75rem; }
44
+
45
+ /* The :not([data-variant]) guard exempts anchors that ARE components. A
46
+ link-button renders as an <a> carrying bg-primary + text-primary-foreground
47
+ and a data-variant attribute, and this rule repainted its text with
48
+ var(--primary) — primary on primary: an unreadable solid block in light
49
+ mode, a blank one in dark. The secondary variant survived only because its
50
+ background is light enough to read primary-coloured text, which is why it
51
+ looked like a colour bug in one button rather than a cascade bug in both.
52
+ (No backticks in this comment: the whole stylesheet is a template
53
+ literal.) */
54
+ & a:not(.dba-api *):not([data-variant]) { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }
55
+ & a:hover:not(.dba-api *):not([data-variant]) { opacity: 0.8; }
56
+
57
+ & strong:not(.dba-api *) { font-weight: 600; }
58
+ & code:not(.dba-api *) { font-size: 0.875em; padding: 0.15em 0.35em; border-radius: 0.25rem; background: var(--muted); font-family: var(--font-code, ui-monospace, monospace); }
59
+ & pre code:not(.dba-api *) { padding: 0; background: none; font-size: 1em; }
60
+
61
+ /* Spacing between consecutive block elements (code blocks, alerts, etc.) */
62
+ & > * + * { margin-top: 0.75rem; }
63
+ & li > * + *:not(.dba-api *) { margin-top: 0.75rem; }
64
+
65
+ & ul:not(.dba-api *) { list-style: disc; padding-left: 1.5rem; margin: 0.75rem 0; }
66
+ & ol:not(.dba-api *) { list-style: decimal; padding-left: 1.5rem; margin: 0.75rem 0; }
67
+ & li:not(.dba-api *) { margin: 0.25rem 0; }
68
+ & li > ul:not(.dba-api *), & li > ol:not(.dba-api *) { margin: 0.25rem 0; }
69
+
70
+ & blockquote:not(.dba-api *) { border-left: 4px solid var(--border); padding-left: 1rem; color: var(--muted-foreground); font-style: italic; margin: 1rem 0; }
71
+
72
+ & hr:not(.dba-api *) { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
73
+
74
+ & table:not(.dba-api *) { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.875rem; }
75
+ & th:not(.dba-api *) { text-align: left; vertical-align: bottom; font-weight: 600; padding: 0.5rem; border-bottom: 2px solid var(--border); }
76
+ & td:not(.dba-api *) { padding: 0.5rem; vertical-align: top; border-bottom: 1px solid var(--border); }
77
+ /* A table's caption is its title, above it; the same for Table components and raw HTML tables */
78
+ & caption:not(.dba-api *) { caption-side: top; text-align: left; font-weight: 500; color: var(--foreground); padding-bottom: 0.5rem; }
79
+
80
+ /* Tailwind Preflight sets display: block on every img, which turns an
81
+ INLINE icon into its own line and splits the sentence around it —
82
+ AsciiDoc UI macros render mid-step and were breaking every procedure
83
+ that used one. inline-block restores the flow and leaves a standalone
84
+ image (the only child of its paragraph) looking exactly as before. */
85
+ & img:not(.dba-api *) { display: inline-block; max-width: 100%; border-radius: 0.5rem; }
86
+ /* A standalone image — the only child of its paragraph — keeps block
87
+ layout, so the vertical rhythm and the absence of baseline descender
88
+ space are exactly what they were before inline icons were fixed. */
89
+ & p > img:only-child:not(.dba-api *) { display: block; }
90
+
91
+ & .heading-anchor { text-decoration: none; opacity: 0; margin-right: 0.25rem; transition: opacity 0.2s; }
92
+ & h1:hover .heading-anchor, & h2:hover .heading-anchor, & h3:hover .heading-anchor, & h4:hover .heading-anchor { opacity: 0.4; }
93
+
94
+ & details:not(.dba-api *) { border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0; }
95
+ & summary:not(.dba-api *) { cursor: pointer; font-weight: 600; }
96
+
97
+ /* mark is emitted by the highlight directive AND can arrive as raw HTML
98
+ from an importer. Without a rule here it renders as the browser default
99
+ yellow, which clashes in light mode and is unreadable on a dark page
100
+ (nothing declares color-scheme). Token-based so it follows the theme. */
101
+ & mark:not(.dba-api *) { background: var(--dsb-mark-bg, rgb(254 240 138 / 0.6)); color: inherit; border-radius: 0.125rem; padding: 0 0.125rem; }
102
+ &:where(.dark *) mark:not(.dba-api *), .dark & mark:not(.dba-api *) { background: var(--dsb-mark-bg-dark, rgb(234 179 8 / 0.3)); }
103
+
104
+ & dl:not(.dba-api *) { margin: 1rem 0; }
105
+ & dt:not(.dba-api *) { font-weight: 600; margin-top: 0.75rem; }
106
+ & dd:not(.dba-api *) { margin-left: 1.5rem; color: var(--muted-foreground); }
107
+
108
+ /* In-cell admonitions. Inside a raw HTML table cell nothing
109
+ markdown-native works, so the AsciiDoc engine emits a neutral
110
+ dl.db-admonition shape (DD-010) and this rule renders it as a compact
111
+ callout box — same contract the Obsidian plugin styles. Per-type
112
+ accent from theme tokens; overrides the generic dl/dt/dd rules above. */
113
+ & dl.db-admonition {
114
+ margin: 0.75rem 0;
115
+ padding: 0.5rem 0.75rem;
116
+ border-left: 3px solid var(--info);
117
+ border-radius: 0.25rem;
118
+ background: color-mix(in oklab, var(--info) 8%, transparent);
119
+ }
120
+ /* The LABEL blends the accent toward the foreground; the border and the
121
+ tint keep the accent at full strength, so the callout still reads as
122
+ its colour while the text stays legible.
123
+
124
+ Measured from painted pixels on a real page, light mode, 13px/600 —
125
+ the accent used raw is a genuine AA failure, not one of axe's oklch
126
+ misreads:
127
+
128
+ --info raw 2.18:1 mixed 5.66:1
129
+ --warning raw 2.25:1 mixed 5.74:1
130
+ --destructive raw 5.97:1 mixed 10.98:1
131
+
132
+ Mixed with var(--foreground), not black, on purpose. Black fixes
133
+ light and BREAKS dark: 70% black measures 5.21:1 light but 3.25:1
134
+ dark, because the tint it sits on is dark too. --foreground is
135
+ near-black in light and near-white in dark, so one rule serves both
136
+ (dark measures 10.96 / 10.87 / 9.79). A fixed colour used in both
137
+ themes is a bug waiting. */
138
+ & dl.db-admonition > dt {
139
+ margin-top: 0;
140
+ font-weight: 600;
141
+ font-size: 0.8125rem;
142
+ text-transform: uppercase;
143
+ letter-spacing: 0.03em;
144
+ color: color-mix(in oklab, var(--info) 60%, var(--foreground));
145
+ }
146
+ & dl.db-admonition > dd { margin-left: 0; margin-top: 0.25rem; color: var(--foreground); }
147
+ & dl.db-admonition-warning, & dl.db-admonition-caution {
148
+ border-left-color: var(--warning);
149
+ background: color-mix(in oklab, var(--warning) 8%, transparent);
150
+ }
151
+ & dl.db-admonition-warning > dt, & dl.db-admonition-caution > dt { color: color-mix(in oklab, var(--warning) 60%, var(--foreground)); }
152
+ & dl.db-admonition-important {
153
+ border-left-color: var(--destructive);
154
+ background: color-mix(in oklab, var(--destructive) 8%, transparent);
155
+ }
156
+ & dl.db-admonition-important > dt { color: color-mix(in oklab, var(--destructive) 60%, var(--foreground)); }
157
+
158
+ /* Granularity book-view affordances (plans/granularity-views.md):
159
+ "Read as single page →" on topics, "Open as page ↗" on book sections.
160
+ Deliberately subtle/muted — the book has one per section. */
161
+ & .dsb-read-single-page {
162
+ display: inline-block;
163
+ margin: 0 0 1.25rem;
164
+ font-size: 0.8125rem;
165
+ color: var(--muted-foreground);
166
+ text-decoration: none;
167
+ }
168
+ & .dsb-read-single-page:hover { color: var(--foreground); text-decoration: underline; }
169
+ & .dsb-open-as-page { margin: -0.25rem 0 0.5rem; font-size: 0.75rem; }
170
+ & .dsb-open-as-page a { color: var(--muted-foreground); text-decoration: none; }
171
+ & .dsb-open-as-page a:hover { color: var(--foreground); text-decoration: underline; }
172
+
173
+ /* AsciiDoc block roles ([role="x"] / [.x]) preserved as classes (#384).
174
+ _abstract marks a module's lead/intro paragraph; .lead/.small are
175
+ Asciidoctor built-ins; _additional-resources marks the trailing
176
+ "Additional resources" section heading. */
177
+ & ._abstract, & .abstract, & .lead {
178
+ font-size: 1.05rem;
179
+ line-height: 1.6;
180
+ color: var(--muted-foreground);
181
+ }
182
+ & .small { font-size: 0.875rem; }
183
+ & h2._additional-resources, & h3._additional-resources {
184
+ font-size: 1.15rem;
185
+ margin-top: 2rem;
186
+ }
187
+
188
+ /* Related resources (#384 R3): [role="_additional-resources"] + .Title +
189
+ list folds to a <section class="related-resources"> (→ DITA related-links). */
190
+ & .related-resources {
191
+ margin-top: 2rem;
192
+ padding-top: 1rem;
193
+ border-top: 1px solid var(--border);
194
+ }
195
+ & .related-resources-title {
196
+ font-weight: 600;
197
+ margin: 0 0 0.5rem;
198
+ }
199
+ }
200
+
201
+ @utility scrollbar-none {
202
+ -ms-overflow-style: none;
203
+ scrollbar-width: none;
204
+ &::-webkit-scrollbar {
205
+ display: none;
206
+ }
207
+ }
package/css/steps.css ADDED
@@ -0,0 +1,149 @@
1
+ /* Generated by packages/render-html/scripts/sync-sources.mjs from @dogsbay/ui Steps.astro, SubSteps.astro. Do not edit. */
2
+
3
+ .steps-list {
4
+ position: relative;
5
+ padding-left: 1.75rem;
6
+ counter-reset: step;
7
+ list-style: none;
8
+ margin: 0;
9
+ }
10
+
11
+ .steps-list > li {
12
+ position: relative;
13
+ padding-bottom: 1.5rem;
14
+ counter-increment: step;
15
+ }
16
+
17
+ .steps-list > li:last-child {
18
+ padding-bottom: 0;
19
+ }
20
+
21
+ /* Numbered circle */
22
+ .steps-list > li::before {
23
+ content: counter(step);
24
+ position: absolute;
25
+ left: -1.75rem;
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ width: 1.5rem;
30
+ height: 1.5rem;
31
+ border-radius: 9999px;
32
+ border: 2px solid var(--color-border);
33
+ background: var(--color-background);
34
+ color: var(--color-foreground);
35
+ font-size: 0.75rem;
36
+ font-weight: 600;
37
+ line-height: 1;
38
+ }
39
+
40
+ /* Connector line */
41
+ .steps-list > li:not(:last-child)::after {
42
+ content: "";
43
+ position: absolute;
44
+ left: calc(-1.75rem + 0.6875rem);
45
+ top: 1.75rem;
46
+ bottom: 0;
47
+ width: 1px;
48
+ background: var(--color-border);
49
+ }
50
+
51
+ /* Sub-steps: nested ol inside li */
52
+ .steps-list > li ol {
53
+ position: relative;
54
+ padding-left: 1.5rem;
55
+ counter-reset: substep;
56
+ list-style: none;
57
+ margin: 0.75rem 0 0 0;
58
+ }
59
+
60
+ .steps-list > li ol > li {
61
+ position: relative;
62
+ padding-bottom: 1rem;
63
+ counter-increment: substep;
64
+ }
65
+
66
+ .steps-list > li ol > li:last-child {
67
+ padding-bottom: 0;
68
+ }
69
+
70
+ /* Lettered circle (a, b, c) */
71
+ .steps-list > li ol > li::before {
72
+ content: counter(substep, lower-alpha);
73
+ position: absolute;
74
+ left: -1.5rem;
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ width: 1.25rem;
79
+ height: 1.25rem;
80
+ border-radius: 9999px;
81
+ border: 1.5px solid var(--color-border);
82
+ background: var(--color-background);
83
+ color: var(--color-muted-foreground, #737373);
84
+ font-size: 0.625rem;
85
+ font-weight: 600;
86
+ line-height: 1;
87
+ }
88
+
89
+ /* Sub-step connector line */
90
+ .steps-list > li ol > li:not(:last-child)::after {
91
+ content: "";
92
+ position: absolute;
93
+ left: calc(-1.5rem + 0.5625rem);
94
+ top: 1.5rem;
95
+ bottom: 0;
96
+ width: 1px;
97
+ background: var(--color-border);
98
+ opacity: 0.6;
99
+ }
100
+
101
+
102
+ .sub-steps-list {
103
+ position: relative;
104
+ padding-left: 1.5rem;
105
+ counter-reset: substep;
106
+ list-style: none;
107
+ margin: 0.75rem 0 0 0;
108
+ }
109
+
110
+ .sub-steps-list > li {
111
+ position: relative;
112
+ padding-bottom: 1rem;
113
+ counter-increment: substep;
114
+ }
115
+
116
+ .sub-steps-list > li:last-child {
117
+ padding-bottom: 0;
118
+ }
119
+
120
+ /* Lettered circle (a, b, c) */
121
+ .sub-steps-list > li::before {
122
+ content: counter(substep, lower-alpha);
123
+ position: absolute;
124
+ left: -1.5rem;
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ width: 1.25rem;
129
+ height: 1.25rem;
130
+ border-radius: 9999px;
131
+ border: 1.5px solid var(--color-border);
132
+ background: var(--color-background);
133
+ color: var(--color-muted-foreground, #737373);
134
+ font-size: 0.625rem;
135
+ font-weight: 600;
136
+ line-height: 1;
137
+ }
138
+
139
+ /* Connector line (thinner) */
140
+ .sub-steps-list > li:not(:last-child)::after {
141
+ content: "";
142
+ position: absolute;
143
+ left: calc(-1.5rem + 0.5625rem);
144
+ top: 1.5rem;
145
+ bottom: 0;
146
+ width: 1px;
147
+ background: var(--color-border);
148
+ opacity: 0.6;
149
+ }