on master help sign in
Keyframes are CSS: remove @keyframes

Its from [opacity 0] syntax was a mini-language used nowhere else. The @keyframes rule goes in @style like any other CSS; htmlang upgrade converts the attribute lines to declarations.

commitffdc0cca94f9859725c2f26caa7663613f9fe086 authoriko <mail@iko.soy> dateSat 26 Sep 2026, 03:03 (2 weeks ago) parent030614e
21 files changed+106−157
DESIGN.md+7−7 crates/htmlang-core/src/ast.rs−1 crates/htmlang-core/src/codegen.rs−8 crates/htmlang-core/src/parser.rs+1−73 crates/htmlang-core/src/syntax.rs+2−3 editors/vscode/snippets/htmlang.json+5−3 examples/demo.hl+6−4 examples/demo.html+1−1 src/bin/htmlang_lsp/analysis.rs+3−5 src/bin/htmlang_lsp/docs.rs−1 src/upgrade.rs+38 tests/regressions.rs+1−7 tests/snapshots.rs+6−22 tests/snapshots/animate_shorthand.hl+10−6 tests/snapshots/animate_shorthand.html+1−1 tests/snapshots/animations.hl+8−4 tests/snapshots/animations.html+1−1 tests/snapshots/scroll_driven_animations.hl+5−3 tests/snapshots/scroll_driven_animations.html+1−1 tests/snapshots/stress_large.hl+9−5 tests/snapshots/stress_large.html+1−1
DESIGN.md+7−7
@@ -433,18 +433,17 @@ link). Translations are a JSON file per locale: `@data $t locales/$lang.json`.
433433
434434## CSS
435435
436−- `@style` holds raw CSS, including at-rules such as `@scope`, `@property` and
437− `@starting-style`.
438−- `@keyframes name` takes htmlang attribute syntax (`from [opacity 0]`) or raw
439− CSS.
436+- `@style` holds raw CSS, including at-rules such as `@keyframes`, `@scope`,
437+ `@property` and `@starting-style`.
440438- Generated rules live in `@layer htmlang` (after a `hl-reset` layer), so any
441439 CSS in `@style` or `@raw` overrides them.
442440
443441```
444−@keyframes fade-in
445− from [opacity 0]
446− to [opacity 1]
447442@style
443+ @keyframes fade-in {
444+ from { opacity: 0; }
445+ to { opacity: 1; }
446+ }
448447 .note { color: gray; }
449448@el [animation fade-in 0.3s ease, class=note] Fades in
450449```
@@ -485,6 +484,7 @@ removed form with its replacement.
485484| `$a ~ " " ~ $b` | `"$a $b"` |
486485| `@each $x in LIST [page N]` | Split the list, or filter it with `@if` |
487486| `...$bundle` | `$bundle` |
487+| `@keyframes NAME` with `from [opacity 0]` | The `@keyframes` rule in `@style` |
488488| `@include lib.hl as ui` | `@include lib.hl`, without the `ui.` prefix |
489489| `@data file.json` (keys as `$key`) | `@data $name file.json` (keys as `$name.key`) |
490490| `[attrs]` alone on a line | `@el [attrs]` |
crates/htmlang-core/src/ast.rs−1
@@ -10,7 +10,6 @@ pub struct Document {
1010 pub head_blocks: Vec<String>,
1111 pub variables: HashMap<String, String>,
1212 pub defines: HashMap<String, Vec<Attribute>>,
13− pub keyframes: Vec<(String, String)>,
1413 pub css_vars: Vec<(String, String)>,
1514 pub custom_css: Vec<String>,
1615 pub og_tags: Vec<(String, String)>,
crates/htmlang-core/src/codegen.rs−8
@@ -889,14 +889,6 @@ fn build_element_css(doc: &Document, styles: &StyleCollector, dev: bool) -> Stri
889889
890890 element_css.push_str(&styles_css);
891891
892− // @keyframes
893− for (name, kf_body) in &doc.keyframes {
894− if dev {
895− element_css.push_str(&format!("@keyframes {} {{\n{}\n}}\n", name, kf_body));
896− } else {
897− element_css.push_str(&format!("@keyframes {}{{{}}}", name, kf_body));
898− }
899− }
900892
901893 // Keyframes for the standard library's `$skeleton` bundle, when used
902894 if element_css.contains("hl-skeleton") {
crates/htmlang-core/src/parser.rs+1−73
@@ -69,7 +69,6 @@ struct ParseContext {
6969 variables: HashMap<String, String>,
7070 defines: HashMap<String, Vec<Attribute>>,
7171 functions: HashMap<String, FnDef>,
72− keyframes: Vec<(String, String)>,
7372 css_vars: Vec<(String, String)>,
7473 custom_css: Vec<String>,
7574 og_tags: Vec<(String, String)>,
@@ -157,7 +156,6 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
157156 variables: HashMap::new(),
158157 defines: HashMap::new(),
159158 functions: HashMap::new(),
160− keyframes: Vec::new(),
161159 css_vars: Vec::new(),
162160 custom_css: Vec::new(),
163161 og_tags: Vec::new(),
@@ -189,7 +187,6 @@ pub fn parse_with_base(input: &str, base_path: Option<&Path>) -> ParseResult {
189187 head_blocks: ctx.head_blocks,
190188 variables: ctx.variables,
191189 defines: ctx.defines,
192− keyframes: ctx.keyframes,
193190 css_vars: ctx.css_vars,
194191 custom_css: ctx.custom_css,
195192 og_tags: ctx.og_tags,
@@ -751,34 +748,6 @@ impl Evaluator {
751748
752749 // --- @each loop ---
753750
754− // --- @keyframes directive ---
755−
756− if let Some(rest) = content.strip_prefix("@keyframes ") {
757− let name = rest.trim().to_string();
758− if name.is_empty() {
759− return Err(ParseError {
760− line: line_num,
761− message: "@keyframes requires a name".to_string(),
762− });
763− }
764− let mut body = String::new();
765− for_each_line(children, &mut |node| {
766− if let Syntax::Line { text, .. } = node {
767− let trimmed = text.trim();
768− // Support htmlang-style: from [opacity 0] / to [opacity 1] / 50% [transform scale(1.5)]
769− if let Some(kf_css) = parse_keyframe_line(trimmed) {
770− body.push_str(&kf_css);
771− } else {
772− body.push_str(trimmed);
773− }
774− }
775− });
776− ctx.keyframes.push((name, body));
777− return Ok(None);
778− }
779−
780−
781−
782751 // --- Function call ---
783752
784753 if content.starts_with('@') {
@@ -1421,7 +1390,6 @@ const KNOWN_DIRECTIVES: &[&str] = &[
14211390 "let",
14221391 "include",
14231392 "raw",
1424− "keyframes",
14251393 "if",
14261394 "else",
14271395 "each",
@@ -1525,6 +1493,7 @@ const REMOVED_SYNTAX: &[(&str, &str)] = &[
15251493 "put each locale's strings in a JSON file and use `@data $t locales/$lang.json`",
15261494 ),
15271495 ("@defer", "remove it: the content is already in the page"),
1496+ ("@keyframes", "write the CSS `@keyframes` rule in `@style`"),
15281497 ("@log", "removed: a layout needs no compile-time messages"),
15291498 ("@warn", "removed: a layout needs no compile-time messages"),
15301499 ("@assert", "removed: a layout needs no compile-time checks"),
@@ -3161,47 +3130,6 @@ fn matching_brace(s: &str) -> Option<usize> {
31613130 None
31623131}
31633132
3164−/// Parse a keyframe line in htmlang syntax: `from [opacity 0]` / `50% [transform scale(1.5)]`
3165−fn parse_keyframe_line(line: &str) -> Option<String> {
3166− let (selector, rest) = if let Some(rest) = line.strip_prefix("from") {
3167− ("from", rest.trim())
3168− } else if let Some(rest) = line.strip_prefix("to") {
3169− ("to", rest.trim())
3170− } else if let Some(pct_end) = line.find('%') {
3171− let rest = line[pct_end + 1..].trim();
3172− let selector = &line[..pct_end + 1];
3173− (selector, rest)
3174− } else {
3175− return None;
3176− };
3177−
3178− if !rest.starts_with('[') || !rest.ends_with(']') {
3179− return None;
3180− }
3181−
3182− let inner = &rest[1..rest.len() - 1];
3183− // Parse comma-separated key-value pairs into CSS
3184− let mut css = String::new();
3185− for part in split_commas(inner) {
3186− let part = part.trim();
3187− if part.is_empty() {
3188− continue;
3189− }
3190− if let Some((key, value)) = part.split_once(' ') {
3191− css.push_str(key.trim());
3192− css.push(':');
3193− css.push_str(value.trim());
3194− css.push(';');
3195− }
3196− }
3197−
3198− if css.is_empty() {
3199− return None;
3200− }
3201−
3202− Some(format!("{}{{{}}}", selector, css))
3203−}
3204−
32053133// ---------------------------------------------------------------------------
32063134// Variable usage tracking
32073135// ---------------------------------------------------------------------------
crates/htmlang-core/src/syntax.rs+2−3
@@ -105,7 +105,7 @@ pub(crate) fn parse(input: &str) -> Vec<Syntax> {
105105}
106106
107107/// Build the nodes for the lines from `pos` indented deeper than `parent`.
108−/// A `plain` block is text (the body of `@style`, `@head` or `@keyframes`),
108+/// A `plain` block is text (the body of `@style` or `@head`),
109109/// so no directive in it is given structure.
110110fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>, plain: bool) -> Vec<Syntax> {
111111 let mut nodes = Vec::new();
@@ -137,8 +137,7 @@ fn build(lines: &[Line], pos: &mut usize, parent: Option<usize>, plain: bool) ->
137137 let text_body = plain
138138 || text == "@style"
139139 || text == "@head"
140− || text.starts_with("@head ")
141− || text.starts_with("@keyframes ");
140+ || text.starts_with("@head ");
142141 let children = build(lines, pos, Some(line.indent), text_body);
143142 if plain {
144143 nodes.push(Syntax::Line {
editors/vscode/snippets/htmlang.json+5−3
@@ -167,9 +167,11 @@
167167 "Keyframes animation": {
168168 "prefix": "@keyframes",
169169 "body": [
170− "@keyframes ${1:fade-in}",
171− " from [opacity 0]",
172− " to [opacity 1]",
170+ "@style",
171+ " @keyframes ${1:fade-in} {",
172+ " from { opacity: 0; }",
173+ " to { opacity: 1; }",
174+ " }",
173175 "",
174176 "@el [animation $1 ${2:0.3s} ${3:ease}]",
175177 " $0"
examples/demo.hl+6−4
@@ -36,10 +36,12 @@
3636 transition all 0.2s ease
3737]
3838
39−-- Keyframes using attribute syntax
40−@keyframes fade-up
41− from [opacity 0, transform translateY(8px)]
42− to [opacity 1, transform translateY(0)]
39+-- Keyframes and other CSS go in @style
40+@style
41+ @keyframes fade-up {
42+ from { opacity: 0; transform: translateY(8px); }
43+ to { opacity: 1; transform: translateY(0); }
44+ }
4345
4446-- Raw HTML escape hatch
4547@raw """
examples/demo.html+1−1
@@ -1,3 +1,3 @@
1−<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>htmlang Demo</title><link rel="preload" href="https://picsum.photos/72" as="image"><link rel="preload" href="https://picsum.photos/640/200" as="image"><meta name="description" content="A one-page tour of htmlang."><meta name="theme-color" content="#3b82f6"><meta property="og:title" content="htmlang"><meta property="og:type" content="website"><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}:root{--primary:#3b82f6;--ink:#0f172a;--muted:#64748b;--subtle:#f8fafc;}@layer htmlang{.a{display:flex;flex-direction:column;max-width:960px;margin-left:auto;margin-right:auto;padding:32px;gap:24px;background:white;}.b{display:flex;flex-direction:column;gap:8px;}.c{display:flex;flex-direction:column;gap:6px;color:var(--muted);font-size:13px;}.d{display:flex;flex-direction:row;gap:6px;align-items:center;}.e{color:var(--muted);}.f{color:var(--ink);}.g{display:flex;flex-direction:row;gap:16px;align-items:center;}.h{display:flex;flex-direction:column;flex:1;min-width:0;gap:6px;}.i{font-weight:bold;font-size:40px;color:var(--ink);}.j{margin:0;line-height:1.6;color:var(--muted);}.k{font-weight:bold;color:var(--primary);}.l{color:var(--primary);text-decoration:underline;}.m{width:72px;height:72px;border-radius:36px;}.n{border-top:1px solid #e5e7eb;}.o{display:flex;flex-direction:column;gap:12px;padding-block:8px;}.p{font-weight:bold;font-size:22px;color:var(--ink);}.q{display:flex;flex-direction:row;flex-wrap:wrap;gap:8px;}.r{display:flex;flex-direction:column;padding:16px;background:white;border-radius:8px;border:1px solid #e5e7eb;box-shadow:0 1px 3px rgba(0,0,0,0.05);cursor:pointer;transition:all 0.2s ease;animation:fade-up 0.3s ease;}.s{display:flex;flex-direction:row;gap:10px;align-items:center;}.t{font-size:20px;color:var(--primary);}.u{font-weight:bold;font-size:16px;color:var(--ink);}.v{margin:0;line-height:1.7;}.w{font-weight:bold;}.x{font-style:italic;}.y{padding:2px 6px;background:#f1f5f9;border-radius:4px;font-family:monospace;font-size:13px;}.z{background:#fef08a;}.aa{display:flex;flex-direction:column;margin:0;padding-inline-start:16px;border-left:3px solid var(--primary);color:var(--muted);font-style:italic;}.ab{margin:0;white-space:pre;background:var(--ink);color:#e2e8f0;padding:16px;border-radius:8px;font-family:monospace;font-size:13px;overflow-x:auto;}.ac{display:flex;flex-direction:column;gap:6px;}.ad{display:flex;flex-direction:row;gap:10px;align-items:baseline;}.ae{font-weight:bold;min-width:80px;}.af{color:var(--muted);font-style:italic;}.ag{display:flex;flex-direction:row;gap:6px;}.ah{display:flex;flex-direction:column;width:18px;height:18px;border-radius:9px;background:var(--primary);opacity:0.4;}.ai{display:flex;flex-direction:row;gap:4px;}.aj{display:flex;flex-direction:column;width:24px;height:24px;border-radius:4px;background:var(--primary);color:white;text-align:center;font-size:12px;font-weight:bold;line-height:24px;}.ak{font-weight:bold;min-width:64px;}.al{color:var(--primary);font-weight:bold;}.am{color:#10b981;font-weight:bold;}.an{display:flex;flex-direction:column;padding:12px;border-radius:6px;background:var(--primary);color:white;}.ao{font-family:ui-monospace,monospace;}.ap{display:flex;flex-direction:column;gap:10px;max-width:420px;}.aq{font-weight:bold;font-size:13px;}.ar{padding:10px;border:1px solid #e5e7eb;border-radius:6px;transition:border 0.15s ease;}.as{padding:10px;border:1px solid #e5e7eb;border-radius:6px;}.at{display:flex;flex-direction:row;gap:8px;}.au{padding:10px 16px;background:var(--primary);color:white;font-weight:bold;border-radius:6px;border:0 solid currentColor;transition:background 0.15s ease;cursor:pointer;}.av{padding:10px 16px;background:white;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;}.aw{display:flex;flex-direction:row;gap:16px;flex-wrap:wrap;}.ax{display:flex;flex-direction:column;flex:1;gap:8px;min-width:240px;}.ay{margin:0;padding-left:0;gap:4px;list-style:disc;padding-inline-start:20px;}.az{flex-direction:column;display:list-item;}.a0{margin:0;padding-left:0;list-style:decimal;padding-inline-start:20px;}.a1{text-align:left;padding:8px;}.a2{text-align:right;padding:8px;}.a3{padding:8px;border-top:1px solid #e5e7eb;}.a4{padding:8px;border-top:1px solid #e5e7eb;text-align:right;}.a5{display:flex;flex-direction:column;margin:0;gap:6px;}.a6{border-radius:8px;}.a7{color:var(--muted);font-size:13px;text-align:center;}.a8{display:flex;flex-direction:column;padding:12px;border:1px solid #e5e7eb;border-radius:6px;background:var(--subtle);}.a9{font-weight:bold;cursor:pointer;}.ba{margin:0;color:var(--muted);line-height:1.6;padding-block-start:6px;}.bb{flex-direction:column;display:grid;grid-template-columns:1fr 2fr;grid-template-areas:"h h" "s m" "f f";gap:8px;padding:8px;background:var(--subtle);border-radius:8px;}.bc{display:flex;flex-direction:column;grid-area:h;padding:10px;background:var(--primary);color:white;border-radius:6px;text-align:center;}.bd{display:flex;flex-direction:column;grid-area:s;padding:10px;background:white;border-radius:6px;border:1px solid #e5e7eb;}.be{display:flex;flex-direction:column;grid-area:m;padding:10px;background:white;border-radius:6px;border:1px solid #e5e7eb;}.bf{display:flex;flex-direction:column;grid-area:f;padding:10px;background:var(--ink);color:white;border-radius:6px;text-align:center;}.bg{gap:4px;font-style:normal;}.bh,.bn{color:var(--primary);}.bi{font-size:13px;color:var(--muted);}.bj{font-size:13px;}.bk{display:flex;flex-direction:column;padding-block-start:24px;margin-block-start:24px;border-top:1px solid #e5e7eb;gap:8px;}.bl{display:flex;flex-direction:row;gap:8px;align-items:center;}.bm{display:flex;flex-direction:column;padding:12px 20px;background:var(--primary);color:white;font-weight:bold;border-radius:8px;transition:all 0.15s ease;cursor:pointer;}.bo{display:flex;flex-direction:column;flex:1;min-width:0;}.bp{color:var(--muted);font-size:13px;}.r:hover{box-shadow:0 4px 12px rgba(0,0,0,0.08);}.au:hover,.bm:hover{background:#3575dd;}.bn:hover{text-decoration:underline;}.an::before{content:"→ ";font-weight:bold;}.ar:focus{border:1px solid var(--primary);}.ar:valid{border:1px solid #10b981;}.ar:invalid{border:1px solid #ef4444;}.au:active,.bm:active{background:#2f68c5;}.bn:visited{color:#8b5cf6;}@media(prefers-color-scheme:dark){.a{background:#0b1220;color:#e2e8f0;}.i{color:white;}}@media print{.bk{display:none;}}}@keyframes fade-up{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:translateY(0);}}</style></head><body><noscript><style>.needs-js{display:none}</style></noscript><div class="a"><header class="b"><nav class="c"><div class="d"><a href="/" class="e">Home</a><span>/</span><a href="/demos" class="e">Demos</a><span>/</span><span class="f">htmlang</span></div></nav><div class="g"><div class="h"><span class="i">Welcome to htmlang</span><p class="j">A minimalist layout language that compiles to <span class="k">static HTML</span> — no JS, no build step. Visit the <a href="https://github.com" rel="noopener noreferrer" target="_blank" class="l">repo</a> for more.</p></div><img src="https://picsum.photos/72" class="m" alt="Avatar" fetchpriority="high"></div></header><hr class="n"><section class="o"><span class="p">Features</span><div class="q"><article class="r"><div class="s"><span class="t">✦</span><span class="u">Simple</span></div><span>Write layouts without CSS classes.</span></article><article class="r"><div class="s"><span class="t">⚡</span><span class="u">Fast</span></div><span>Compiles to a single self-contained HTML file.</span></article><article class="r"><div class="s"><span class="t">○</span><span class="u">Minimal</span></div><span>Zero runtime JavaScript by default.</span></article><article class="r"><div class="s"><span class="t">◆</span><span class="u">Typed</span></div><span>LSP with diagnostics and go-to-definition.</span></article></div></section><section class="o"><span class="p">Typography</span><p class="v">A quick brown fox jumps over <span class="w">the lazy</span> <span class="x">dog</span>. Press <kbd class="y">Ctrl+K</kbd> to search. <mark class="z">Highlighted</mark> uses mark. <abbr title="HyperText Markup Language + language">htmlang</abbr> is an abbreviation.</p><blockquote class="aa"><span>Layout is code. Style is code. Everything should compose.</span><cite>— Anonymous</cite></blockquote><pre class="ab">@let button $label
1+<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>htmlang Demo</title><link rel="preload" href="https://picsum.photos/72" as="image"><link rel="preload" href="https://picsum.photos/640/200" as="image"><meta name="description" content="A one-page tour of htmlang."><meta name="theme-color" content="#3b82f6"><meta property="og:title" content="htmlang"><meta property="og:type" content="website"><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}:root{--primary:#3b82f6;--ink:#0f172a;--muted:#64748b;--subtle:#f8fafc;}@layer htmlang{.a{display:flex;flex-direction:column;max-width:960px;margin-left:auto;margin-right:auto;padding:32px;gap:24px;background:white;}.b{display:flex;flex-direction:column;gap:8px;}.c{display:flex;flex-direction:column;gap:6px;color:var(--muted);font-size:13px;}.d{display:flex;flex-direction:row;gap:6px;align-items:center;}.e{color:var(--muted);}.f{color:var(--ink);}.g{display:flex;flex-direction:row;gap:16px;align-items:center;}.h{display:flex;flex-direction:column;flex:1;min-width:0;gap:6px;}.i{font-weight:bold;font-size:40px;color:var(--ink);}.j{margin:0;line-height:1.6;color:var(--muted);}.k{font-weight:bold;color:var(--primary);}.l{color:var(--primary);text-decoration:underline;}.m{width:72px;height:72px;border-radius:36px;}.n{border-top:1px solid #e5e7eb;}.o{display:flex;flex-direction:column;gap:12px;padding-block:8px;}.p{font-weight:bold;font-size:22px;color:var(--ink);}.q{display:flex;flex-direction:row;flex-wrap:wrap;gap:8px;}.r{display:flex;flex-direction:column;padding:16px;background:white;border-radius:8px;border:1px solid #e5e7eb;box-shadow:0 1px 3px rgba(0,0,0,0.05);cursor:pointer;transition:all 0.2s ease;animation:fade-up 0.3s ease;}.s{display:flex;flex-direction:row;gap:10px;align-items:center;}.t{font-size:20px;color:var(--primary);}.u{font-weight:bold;font-size:16px;color:var(--ink);}.v{margin:0;line-height:1.7;}.w{font-weight:bold;}.x{font-style:italic;}.y{padding:2px 6px;background:#f1f5f9;border-radius:4px;font-family:monospace;font-size:13px;}.z{background:#fef08a;}.aa{display:flex;flex-direction:column;margin:0;padding-inline-start:16px;border-left:3px solid var(--primary);color:var(--muted);font-style:italic;}.ab{margin:0;white-space:pre;background:var(--ink);color:#e2e8f0;padding:16px;border-radius:8px;font-family:monospace;font-size:13px;overflow-x:auto;}.ac{display:flex;flex-direction:column;gap:6px;}.ad{display:flex;flex-direction:row;gap:10px;align-items:baseline;}.ae{font-weight:bold;min-width:80px;}.af{color:var(--muted);font-style:italic;}.ag{display:flex;flex-direction:row;gap:6px;}.ah{display:flex;flex-direction:column;width:18px;height:18px;border-radius:9px;background:var(--primary);opacity:0.4;}.ai{display:flex;flex-direction:row;gap:4px;}.aj{display:flex;flex-direction:column;width:24px;height:24px;border-radius:4px;background:var(--primary);color:white;text-align:center;font-size:12px;font-weight:bold;line-height:24px;}.ak{font-weight:bold;min-width:64px;}.al{color:var(--primary);font-weight:bold;}.am{color:#10b981;font-weight:bold;}.an{display:flex;flex-direction:column;padding:12px;border-radius:6px;background:var(--primary);color:white;}.ao{font-family:ui-monospace,monospace;}.ap{display:flex;flex-direction:column;gap:10px;max-width:420px;}.aq{font-weight:bold;font-size:13px;}.ar{padding:10px;border:1px solid #e5e7eb;border-radius:6px;transition:border 0.15s ease;}.as{padding:10px;border:1px solid #e5e7eb;border-radius:6px;}.at{display:flex;flex-direction:row;gap:8px;}.au{padding:10px 16px;background:var(--primary);color:white;font-weight:bold;border-radius:6px;border:0 solid currentColor;transition:background 0.15s ease;cursor:pointer;}.av{padding:10px 16px;background:white;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;}.aw{display:flex;flex-direction:row;gap:16px;flex-wrap:wrap;}.ax{display:flex;flex-direction:column;flex:1;gap:8px;min-width:240px;}.ay{margin:0;padding-left:0;gap:4px;list-style:disc;padding-inline-start:20px;}.az{flex-direction:column;display:list-item;}.a0{margin:0;padding-left:0;list-style:decimal;padding-inline-start:20px;}.a1{text-align:left;padding:8px;}.a2{text-align:right;padding:8px;}.a3{padding:8px;border-top:1px solid #e5e7eb;}.a4{padding:8px;border-top:1px solid #e5e7eb;text-align:right;}.a5{display:flex;flex-direction:column;margin:0;gap:6px;}.a6{border-radius:8px;}.a7{color:var(--muted);font-size:13px;text-align:center;}.a8{display:flex;flex-direction:column;padding:12px;border:1px solid #e5e7eb;border-radius:6px;background:var(--subtle);}.a9{font-weight:bold;cursor:pointer;}.ba{margin:0;color:var(--muted);line-height:1.6;padding-block-start:6px;}.bb{flex-direction:column;display:grid;grid-template-columns:1fr 2fr;grid-template-areas:"h h" "s m" "f f";gap:8px;padding:8px;background:var(--subtle);border-radius:8px;}.bc{display:flex;flex-direction:column;grid-area:h;padding:10px;background:var(--primary);color:white;border-radius:6px;text-align:center;}.bd{display:flex;flex-direction:column;grid-area:s;padding:10px;background:white;border-radius:6px;border:1px solid #e5e7eb;}.be{display:flex;flex-direction:column;grid-area:m;padding:10px;background:white;border-radius:6px;border:1px solid #e5e7eb;}.bf{display:flex;flex-direction:column;grid-area:f;padding:10px;background:var(--ink);color:white;border-radius:6px;text-align:center;}.bg{gap:4px;font-style:normal;}.bh,.bn{color:var(--primary);}.bi{font-size:13px;color:var(--muted);}.bj{font-size:13px;}.bk{display:flex;flex-direction:column;padding-block-start:24px;margin-block-start:24px;border-top:1px solid #e5e7eb;gap:8px;}.bl{display:flex;flex-direction:row;gap:8px;align-items:center;}.bm{display:flex;flex-direction:column;padding:12px 20px;background:var(--primary);color:white;font-weight:bold;border-radius:8px;transition:all 0.15s ease;cursor:pointer;}.bo{display:flex;flex-direction:column;flex:1;min-width:0;}.bp{color:var(--muted);font-size:13px;}.r:hover{box-shadow:0 4px 12px rgba(0,0,0,0.08);}.au:hover,.bm:hover{background:#3575dd;}.bn:hover{text-decoration:underline;}.an::before{content:"→ ";font-weight:bold;}.ar:focus{border:1px solid var(--primary);}.ar:valid{border:1px solid #10b981;}.ar:invalid{border:1px solid #ef4444;}.au:active,.bm:active{background:#2f68c5;}.bn:visited{color:#8b5cf6;}@media(prefers-color-scheme:dark){.a{background:#0b1220;color:#e2e8f0;}.i{color:white;}}@media print{.bk{display:none;}}}@keyframes fade-up {from { opacity: 0; transform: translateY(8px); }to { opacity: 1; transform: translateY(0); }}</style></head><body><noscript><style>.needs-js{display:none}</style></noscript><div class="a"><header class="b"><nav class="c"><div class="d"><a href="/" class="e">Home</a><span>/</span><a href="/demos" class="e">Demos</a><span>/</span><span class="f">htmlang</span></div></nav><div class="g"><div class="h"><span class="i">Welcome to htmlang</span><p class="j">A minimalist layout language that compiles to <span class="k">static HTML</span> — no JS, no build step. Visit the <a href="https://github.com" rel="noopener noreferrer" target="_blank" class="l">repo</a> for more.</p></div><img src="https://picsum.photos/72" class="m" alt="Avatar" fetchpriority="high"></div></header><hr class="n"><section class="o"><span class="p">Features</span><div class="q"><article class="r"><div class="s"><span class="t">✦</span><span class="u">Simple</span></div><span>Write layouts without CSS classes.</span></article><article class="r"><div class="s"><span class="t">⚡</span><span class="u">Fast</span></div><span>Compiles to a single self-contained HTML file.</span></article><article class="r"><div class="s"><span class="t">○</span><span class="u">Minimal</span></div><span>Zero runtime JavaScript by default.</span></article><article class="r"><div class="s"><span class="t">◆</span><span class="u">Typed</span></div><span>LSP with diagnostics and go-to-definition.</span></article></div></section><section class="o"><span class="p">Typography</span><p class="v">A quick brown fox jumps over <span class="w">the lazy</span> <span class="x">dog</span>. Press <kbd class="y">Ctrl+K</kbd> to search. <mark class="z">Highlighted</mark> uses mark. <abbr title="HyperText Markup Language + language">htmlang</abbr> is an abbreviation.</p><blockquote class="aa"><span>Layout is code. Style is code. Everything should compose.</span><cite>— Anonymous</cite></blockquote><pre class="ab">@let button $label
22 @el [padding 12, background blue]
33 @text $label</pre></section><section class="o"><span class="p">Control flow</span><div class="ac"><div class="ad"><span class="ae">Alice</span><span class="e">Engineer</span></div><div class="ad"><span class="ae">Bob</span><span class="e">Designer</span></div><div class="ad"><span class="ae">Carol</span><span class="e">PM</span></div></div><span class="af" data-state="">(Nothing — @each ... @else fallback)</span><div class="ag"><div class="ah"></div><div class="ah"></div><div class="ah"></div><div class="ah"></div><div class="ah"></div><div class="ah"></div></div><div class="ai"><div class="aj"><span>1</span></div><div class="aj"><span>2</span></div><div class="aj"><span>3</span></div><div class="aj"><span>4</span></div><div class="aj"><span>5</span></div><div class="aj"><span>6</span></div></div><div class="d"><span class="ak">Count:</span><span class="al">3 items</span></div><span class="am">✓ Ready</span><div class="an"><span>Conditional styling via <code class="ao">if(cond, a, b)</code>.</span></div></section><section class="o"><span class="p">Forms</span><form action="/subscribe" class="ap"><label class="aq" for="email">Email</label><input class="ar" id="email" type="email" name="email" placeholder="you@example.com" required><label class="aq" for="role">Role</label><select class="as" id="role" name="role"><option value="dev">Developer</option><option value="designer">Designer</option><option value="other">Other</option></select><label class="aq" for="msg">Message</label><textarea class="as" id="msg" name="msg" rows="3"></textarea><div class="at"><button class="au" type="submit">Subscribe</button><button class="av" type="reset">Reset</button></div><progress value="60" max="100"></progress></form></section><section class="o"><span class="p">Data</span><div class="aw"><div class="ax"><span class="w">Unordered</span><ul class="ay"><li class="az">Composable with @each</li><li class="az">Indent to nest children</li><li class="az">Bold list items work too</li></ul><span class="w">Ordered</span><ol class="a0"><li class="az">First</li><li class="az">Second</li><li class="az">Third</li></ol></div><div class="ax"><span class="w">Table</span><table><thead><tr><th class="a1">Item</th><th class="a2">Qty</th></tr></thead><tbody><tr><td class="a3">Widgets</td><td class="a4">12</td></tr><tr><td class="a3">Gadgets</td><td class="a4">7</td></tr></tbody></table></div></div><figure class="a5"><img src="https://picsum.photos/640/200" class="a6" alt="A random placeholder" fetchpriority="high"><figcaption class="a7">A placeholder image.</figcaption></figure></section><section class="o"><span class="p">Disclosure + grid areas</span><details class="a8"><summary class="a9">What is htmlang?</summary><p class="ba">htmlang is a layout-first language that compiles to a single, self-contained HTML file with embedded CSS.</p></details><div class="bb"><div class="bc"><span>Header</span></div><div class="bd"><span>Sidebar</span></div><div class="be"><span>Main</span></div><div class="bf"><span>Footer</span></div></div></section><section class="o"><span class="p">Misc</span><address class="bg"><span class="w">htmlang HQ</span><span>123 Flexbox Avenue</span><a href="mailto:hi@htmlang.dev" class="bh">hi@htmlang.dev</a></address><div class="s"><span class="bi">Published</span><time class="bj" datetime="2026-04-16">April 16 2026</time></div><div class="s"><span class="bi">Confidence</span><meter value="8" min="0" max="10" low="3" high="7">8/10</meter></div></section><footer class="bk"><div class="bl"><div class="bm"><a href="https://github.com" rel="noopener noreferrer" target="_blank">Get Started</a></div><a href="https://github.com" rel="noopener noreferrer" target="_blank" class="bn">Browse source</a><div class="bo"></div><span class="bp">© 2026 htmlang</span></div></footer></div></body></html>
src/bin/htmlang_lsp/analysis.rs+3−5
@@ -87,9 +87,9 @@ pub(crate) fn document_symbols(text: &str) -> Vec<SymbolInformation> {
8787 }
8888 }
8989
90− // @keyframes definitions
90+ // @keyframes rules (in @style blocks)
9191 if let Some(rest) = trimmed.strip_prefix("@keyframes ") {
92− let name = rest.trim();
92+ let name = rest.split(['{', ' ']).next().unwrap_or("");
9393 if !name.is_empty() {
9494 symbols.push(SymbolInformation {
9595 name: format!("@keyframes {}", name),
@@ -866,16 +866,14 @@ pub(crate) fn folding_ranges(text: &str) -> Vec<FoldingRange> {
866866 let mut i = 0;
867867 while i < lines.len() {
868868 let trimmed = lines[i].trim();
869− // Fold blocks that start with @let (with body), @if, @else, @each, @match, @style, @head, @keyframes
869+ // Fold blocks that start with @let (with body), @if, @else, @each, @style, @head
870870 if trimmed.starts_with("@let ")
871871 || trimmed.starts_with("@if ")
872872 || trimmed == "@else"
873873 || trimmed.starts_with("@else if ")
874874 || trimmed.starts_with("@each ")
875− || trimmed.starts_with("@match ")
876875 || trimmed == "@style"
877876 || trimmed == "@head"
878− || trimmed.starts_with("@keyframes ")
879877 {
880878 let start_indent = lines[i].len() - lines[i].trim_start().len();
881879 let start_line = i;
src/bin/htmlang_lsp/docs.rs−1
@@ -98,7 +98,6 @@ pub(crate) const DIRECTIVES: &[Doc] = &[
9898 doc("raw", "Pastes HTML into the output verbatim.", "@raw \"\"\"<div></div>\"\"\""),
9999 doc("markdown", "Markdown (indented body or file), converted to HTML.", "@markdown\n # Title"),
100100 doc("style", "Raw CSS; overrides generated styles.", "@style\n .note { color: gray; }"),
101− doc("keyframes", "CSS animation, in attribute syntax or raw CSS.", "@keyframes fade\n from [opacity 0]\n to [opacity 1]"),
102101 doc("head", "Raw HTML added to `<head>`.", "@head\n <link rel=\"icon\" href=\"f.ico\">"),
103102 doc("meta", "A `<meta>` tag; `og:` names become Open Graph tags.", "@meta description A small site"),
104103 doc("if", "Renders its body when the condition holds; `@else if` / `@else` follow.", "@if $count > 2 and not $hidden\n @text Many"),
src/upgrade.rs+38
@@ -293,6 +293,19 @@ fn rewrite_block(
293293 let end = block_end(lines, i);
294294 let body = &lines[i + 1..end];
295295
296+ // @keyframes NAME with `from [opacity 0]` lines → the CSS rule in @style
297+ if let Some(name) = trimmed.strip_prefix("@keyframes ") {
298+ let mut out = vec![
299+ format!("{pad}@style"),
300+ format!("{pad} @keyframes {} {{", name.trim()),
301+ ];
302+ for line in body.iter().filter(|l| !l.trim().is_empty()) {
303+ out.push(format!("{pad} {}", keyframe_css(line.trim())));
304+ }
305+ out.push(format!("{pad} }}"));
306+ return Some(Block { lines: out, end });
307+ }
308+
296309 // @tooltip TEXT (text shown and used as the hover tip) →
297310 // @tooltip [tip TEXT] TEXT
298311 if let Some(rest) = trimmed.strip_prefix("@tooltip ")
@@ -704,6 +717,23 @@ fn fold_head_directives(input: &str, manual: &mut Vec<(usize, String)>) -> (Stri
704717 (text, attrs.len())
705718}
706719
720+/// `from [opacity 0, transform none]` → `from { opacity: 0; transform: none; }`;
721+/// raw CSS is returned unchanged.
722+fn keyframe_css(line: &str) -> String {
723+ let Some((selector, list)) = line.split_once('[') else {
724+ return line.to_string();
725+ };
726+ let Some(list) = list.trim_end().strip_suffix(']') else {
727+ return line.to_string();
728+ };
729+ let declarations: Vec<String> = split_top_level_commas(list)
730+ .into_iter()
731+ .filter_map(|part| part.trim().split_once(' '))
732+ .map(|(key, value)| format!("{}: {};", key.trim(), value.trim()))
733+ .collect();
734+ format!("{} {{ {} }}", selector.trim(), declarations.join(" "))
735+}
736+
707737/// Single-line directive rewrites.
708738fn rewrite_directive_line(
709739 line: &str,
@@ -1618,6 +1648,14 @@ mod tests {
16181648 );
16191649 }
16201650
1651+ #[test]
1652+ fn keyframes_move_into_style() {
1653+ assert_eq!(
1654+ up("@keyframes fade\n from [opacity 0, transform translate(0, 4px)]\n 50% { opacity: 0.5; }\n to [opacity 1]\n@text x\n"),
1655+ "@style\n @keyframes fade {\n from { opacity: 0; transform: translate(0, 4px); }\n 50% { opacity: 0.5; }\n to { opacity: 1; }\n }\n@text x\n"
1656+ );
1657+ }
1658+
16211659 #[test]
16221660 fn unnamed_data_needs_a_manual_name() {
16231661 let result = super::upgrade("@data site.json\n@data $ok ok.json\n");
tests/regressions.rs+1−7
@@ -115,7 +115,7 @@ fn css_vars_are_not_substituted_into_media_queries() {
115115#[test]
116116fn partial_output_includes_style_and_keyframes() {
117117 let result = parser::parse(
118− "@style\n @scope (.card) { .title { font-weight: bold; } }\n@keyframes k\n from [opacity 0]\n@text hi",
118+ "@style\n @scope (.card) { .title { font-weight: bold; } }\n @keyframes k { from { opacity: 0 } }\n@text hi",
119119 );
120120 let out = codegen::generate_partial(&result.document);
121121 assert!(out.contains(".title"), "{}", out);
@@ -196,12 +196,6 @@ fn named_argument_is_not_used_positionally_for_another_param() {
196196 assert!(!out.contains("t=danger"), "{}", out);
197197}
198198
199−#[test]
200−fn keyframe_values_keep_commas_inside_parens() {
201− let out = compile("@keyframes k\n from [transform translate(0, 0)]\n to [opacity 1]\n@text x");
202− assert!(out.contains("transform:translate(0, 0)"), "{}", out);
203−}
204−
205199#[test]
206200fn svg_width_does_not_touch_stroke_width() {
207201 let dir = scratch_dir("svg_attr");
tests/snapshots.rs+6−22
@@ -3011,28 +3011,12 @@ fn test_import_missing_file() {
30113011}
30123012
30133013#[test]
3014−fn test_enhanced_keyframes() {
3015− let result = htmlang::parser::parse(
3016− "@keyframes fade-in\n from [opacity 0]\n to [opacity 1]\n@el [animation fade-in 0.3s] Content",
3017− );
3018− let html = htmlang::codegen::generate(&result.document);
3014+fn keyframes_was_removed() {
3015+ let diags = parse_diagnostics("@keyframes fade\n from [opacity 0]\n");
30193016 assert!(
3020− html.contains("@keyframes fade-in{from{opacity:0;}to{opacity:1;}}"),
3021− "keyframes should parse htmlang syntax, got: {}",
3022− html
3023− );
3024−}
3025−
3026−#[test]
3027−fn test_keyframes_percentage() {
3028− let result = htmlang::parser::parse(
3029− "@keyframes slide\n 0% [transform translateX(-100%)]\n 100% [transform translateX(0)]",
3030− );
3031− let html = htmlang::codegen::generate(&result.document);
3032− assert!(
3033− html.contains("0%{transform:translateX(-100%);}"),
3034− "keyframe percentage should work, got: {}",
3035− html
3017+ diags.iter().any(|d| d.message.contains("`@keyframes` rule in `@style`")),
3018+ "{:?}",
3019+ diags
30363020 );
30373021}
30383022
@@ -3260,7 +3244,7 @@ fn view_transition_name_passthrough() {
32603244#[test]
32613245fn animate_generates_animation_css() {
32623246 let output = compile(
3263− "@page T\n@keyframes fade\n from [opacity 0]\n to [opacity 1]\n@el [animation fade 0.3s ease]\n Content",
3247+ "@page T\n@el [animation fade 0.3s ease]\n Content",
32643248 );
32653249 assert!(
32663250 output.contains("animation:fade 0.3s ease"),
tests/snapshots/animate_shorthand.hl+10−6
@@ -1,10 +1,14 @@
11@page Animate
2−@keyframes fade-in
3− from [opacity 0]
4− to [opacity 1]
5−@keyframes slide-up
6− from [transform translateY(20px), opacity 0]
7− to [transform translateY(0), opacity 1]
2+@style
3+ @keyframes fade-in {
4+ from { opacity: 0; }
5+ to { opacity: 1; }
6+ }
7+@style
8+ @keyframes slide-up {
9+ from { transform: translateY(20px); opacity: 0; }
10+ to { transform: translateY(0); opacity: 1; }
11+ }
812@column [spacing 20, padding 20]
913 @el [animation fade-in 0.3s ease, padding 20]
1014 Faded in
tests/snapshots/animate_shorthand.html+1−1
@@ -1 +1 @@
1−<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Animate</title><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}}@layer htmlang{.a{display:flex;flex-direction:column;gap:20px;padding:20px;}.b{display:flex;flex-direction:column;animation:fade-in 0.3s ease;padding:20px;}.c{display:flex;flex-direction:column;animation:slide-up 0.5s ease-out;padding:20px;background:#e5e7eb;}}@keyframes fade-in{from{opacity:0;}to{opacity:1;}}@keyframes slide-up{from{transform:translateY(20px);opacity:0;}to{transform:translateY(0);opacity:1;}}</style></head><body><div class="a"><div class="b"><span>Faded in</span></div><div class="c"><span>Slid up</span></div></div></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Animate</title><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}}@layer htmlang{.a{display:flex;flex-direction:column;gap:20px;padding:20px;}.b{display:flex;flex-direction:column;animation:fade-in 0.3s ease;padding:20px;}.c{display:flex;flex-direction:column;animation:slide-up 0.5s ease-out;padding:20px;background:#e5e7eb;}}@keyframes fade-in {from { opacity: 0; }to { opacity: 1; }}@keyframes slide-up {from { transform: translateY(20px); opacity: 0; }to { transform: translateY(0); opacity: 1; }}</style></head><body><div class="a"><div class="b"><span>Faded in</span></div><div class="c"><span>Slid up</span></div></div></body></html>
tests/snapshots/animations.hl+8−4
@@ -1,10 +1,14 @@
11@page Animations
22
3−@keyframes fade-in
4− from{opacity:0}to{opacity:1}
3+@style
4+ @keyframes fade-in {
5+ from{opacity:0}to{opacity:1}
6+ }
57
6−@keyframes slide-up
7− 0%{transform:translateY(20px);opacity:0}100%{transform:translateY(0);opacity:1}
8+@style
9+ @keyframes slide-up {
10+ 0%{transform:translateY(20px);opacity:0}100%{transform:translateY(0);opacity:1}
11+ }
812
913@column [spacing 20]
1014 @el [animation fade-in 0.3s ease]
tests/snapshots/animations.html+1−1
@@ -1 +1 @@
1−<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Animations</title><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}}@layer htmlang{.a{display:flex;flex-direction:column;gap:20px;}.b{display:flex;flex-direction:column;animation:fade-in 0.3s ease;}.c{display:flex;flex-direction:column;animation:slide-up 0.5s ease-out;}}@keyframes fade-in{from{opacity:0}to{opacity:1}}@keyframes slide-up{0%{transform:translateY(20px);opacity:0}100%{transform:translateY(0);opacity:1}}</style></head><body><div class="a"><div class="b"><span>Fading in</span></div><div class="c"><span>Sliding up</span></div></div></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Animations</title><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}}@layer htmlang{.a{display:flex;flex-direction:column;gap:20px;}.b{display:flex;flex-direction:column;animation:fade-in 0.3s ease;}.c{display:flex;flex-direction:column;animation:slide-up 0.5s ease-out;}}@keyframes fade-in {from{opacity:0}to{opacity:1}}@keyframes slide-up {0%{transform:translateY(20px);opacity:0}100%{transform:translateY(0);opacity:1}}</style></head><body><div class="a"><div class="b"><span>Fading in</span></div><div class="c"><span>Sliding up</span></div></div></body></html>
tests/snapshots/scroll_driven_animations.hl+5−3
@@ -1,8 +1,10 @@
11@page Scroll Animation Test
22
3−@keyframes fade-in
4− from [opacity 0]
5− to [opacity 1]
3+@style
4+ @keyframes fade-in {
5+ from { opacity: 0; }
6+ to { opacity: 1; }
7+ }
68
79@column [padding 20]
810 @el [animation fade-in linear, animation-timeline scroll(), animation-range entry 0% cover 50%]
tests/snapshots/scroll_driven_animations.html+1−1
@@ -1 +1 @@
1−<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Scroll Animation Test</title><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}}@layer htmlang{.a{display:flex;flex-direction:column;padding:20px;}.b{display:flex;flex-direction:column;animation:fade-in linear;animation-timeline:scroll();animation-range:entry 0% cover 50%;}}@keyframes fade-in{from{opacity:0;}to{opacity:1;}}</style></head><body><div class="a"><div class="b"><span>Animated on scroll</span></div></div></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Scroll Animation Test</title><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}}@layer htmlang{.a{display:flex;flex-direction:column;padding:20px;}.b{display:flex;flex-direction:column;animation:fade-in linear;animation-timeline:scroll();animation-range:entry 0% cover 50%;}}@keyframes fade-in {from { opacity: 0; }to { opacity: 1; }}</style></head><body><div class="a"><div class="b"><span>Animated on scroll</span></div></div></body></html>
tests/snapshots/stress_large.hl+9−5
@@ -13,11 +13,15 @@
1313@let --gap-sm 8
1414@let --gap-lg 24
1515
16−@keyframes fade-in
17− from{opacity:0}to{opacity:1}
18−
19−@keyframes slide-down
20− 0%{transform:translateY(-10px);opacity:0}100%{transform:translateY(0);opacity:1}
16+@style
17+ @keyframes fade-in {
18+ from{opacity:0}to{opacity:1}
19+ }
20+
21+@style
22+ @keyframes slide-down {
23+ 0%{transform:translateY(-10px);opacity:0}100%{transform:translateY(0);opacity:1}
24+ }
2125
2226@let icon-button $label $color=var(--primary)
2327 @button [padding 8 16, background $color, hover:background $--dark, color white, rounded 6, border none, cursor pointer, transition all 0.2s, font-size 14]
tests/snapshots/stress_large.html+1−1
@@ -1 +1 @@
1−<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Stress Test Large Document</title><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}:root{--primary:#3b82f6;--secondary:#10b981;--accent:#f59e0b;--danger:#ef4444;--dark:#1e293b;--light:#f8fafc;--gray:#64748b;--border:#e2e8f0;--radius:8;--gap:16;--gap-sm:8;--gap-lg:24;}@layer htmlang{.a{display:flex;flex-direction:row;gap:0;min-height:100vh;}.b{display:flex;flex-direction:column;width:260px;background:white;border-right:1px solid var(--border);padding:16px;gap:8px;}.c{display:flex;flex-direction:row;gap:12px;padding:8px;align-items:center;}.d{flex-direction:column;width:36px;height:36px;border-radius:50%;background:var(--primary);color:white;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;}.e{display:flex;flex-direction:column;gap:2px;}.f{font-weight:bold;font-size:16px;color:var(--dark);}.g{font-size:12px;color:var(--gray);}.h{display:flex;flex-direction:column;width:100%;height:1px;background:var(--border);margin:8px 0;}.i{display:flex;flex-direction:column;margin-bottom:24px;}.j{font-weight:bold;font-size:12px;color:var(--gray);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;}.k{color:var(--primary);font-weight:bold;padding:8px 12px;background:#eff6ff;border-radius:6px;}.l{color:var(--gray);padding:8px 12px;border-radius:6px;transition:all 0.15s;}.m{display:flex;flex-direction:column;gap:4px;}.n{display:flex;flex-direction:row;gap:8px;align-items:center;}.o{display:flex;flex-direction:column;width:6px;height:6px;border-radius:50%;background:var(--secondary);}.p{font-size:13px;color:var(--gray);}.q{display:flex;flex-direction:column;width:6px;height:6px;border-radius:50%;background:var(--accent);}.r{display:flex;flex-direction:column;width:6px;height:6px;border-radius:50%;background:#cbd5e1;}.s{display:flex;flex-direction:column;margin-top:auto;padding:12px;background:#f1f5f9;border-radius:8px;}.t{flex-direction:column;width:28px;height:28px;border-radius:50%;background:var(--primary);color:white;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;}.u{display:flex;flex-direction:column;gap:0;}.v{font-size:13px;font-weight:bold;color:var(--dark);}.w{font-size:11px;color:var(--gray);}.x{display:flex;flex-direction:column;flex:1;min-width:0;background:var(--light);}.y{display:flex;flex-direction:column;padding:12px 24px;background:white;border-bottom:1px solid var(--border);}.z{display:flex;flex-direction:row;gap:16px;align-items:center;}.aa{display:flex;flex-direction:row;gap:8px;}.ab{color:var(--gray);font-size:14px;}.ac{color:var(--border);font-size:14px;}.ad{color:var(--dark);font-size:14px;}.ae{font-weight:bold;font-size:22px;color:var(--dark);}.af{display:flex;flex-direction:column;flex:1;min-width:0;}.ag{display:flex;flex-direction:row;gap:12px;align-items:center;}.ah{padding:8px 12px;border-radius:6px;border:1px solid var(--border);width:240px;transition:all 0.2s;}.ai{display:flex;flex-direction:column;position:relative;}.aj{padding:8px 16px;background:var(--primary);color:white;border-radius:6px;border:none solid currentColor;cursor:pointer;transition:all 0.2s;font-size:14px;}.ak{display:flex;flex-direction:column;position:absolute;top:-2px;right:-2px;}.al{display:flex;flex-direction:column;width:8px;height:8px;border-radius:50%;background:var(--danger);}.am{padding:8px 16px;background:var(--gray);color:white;border-radius:6px;border:none solid currentColor;cursor:pointer;transition:all 0.2s;font-size:14px;}.an{display:flex;flex-direction:column;padding:24px;overflow-y:auto;}.ao{display:flex;flex-direction:column;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:12px 16px;}.ap{color:#1e40af;font-size:14px;}.aq,.av{display:flex;flex-direction:row;gap:16px;margin-top:24px;}.ar{display:flex;flex-direction:column;background:white;border-radius:8px;padding:16px;border:1px solid var(--border);}.as{font-size:14px;color:var(--gray);}.at{font-weight:bold;font-size:28px;color:var(--dark);margin-top:4px;}.au{font-size:12px;color:var(--secondary);margin-top:2px;}.aw{display:flex;flex-direction:column;background:white;border-radius:8px;padding:16px;border:1px solid var(--border);transition:all 0.2s;animation:fade-in 0.3s ease;}.ax{font-weight:bold;font-size:18px;color:var(--dark);}.ay{display:flex;flex-direction:column;margin-top:8px;}.az{flex-direction:column;height:200px;background:var(--light);border-radius:6px;margin-top:12px;display:flex;align-items:flex-end;padding:16px;gap:8px;}.a0{display:flex;flex-direction:column;width:100%;height:40%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a1{display:flex;flex-direction:column;width:100%;height:65%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a2{display:flex;flex-direction:column;width:100%;height:45%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a3{display:flex;flex-direction:column;width:100%;height:80%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a4{display:flex;flex-direction:column;width:100%;height:55%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a5{display:flex;flex-direction:column;width:100%;height:70%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a6{display:flex;flex-direction:column;width:100%;height:90%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a7{display:flex;flex-direction:column;width:100%;height:60%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a8{display:flex;flex-direction:column;width:100%;height:75%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a9{display:flex;flex-direction:column;width:100%;height:85%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.ba{display:flex;flex-direction:column;width:100%;height:50%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.bb{display:flex;flex-direction:column;width:100%;height:95%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.bc{display:flex;flex-direction:column;gap:12px;margin-top:12px;}.bd{display:flex;flex-direction:row;gap:12px;align-items:center;padding:8px;border-radius:6px;}.be{flex-direction:column;width:32px;height:32px;border-radius:50%;background:var(--primary);color:white;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;}.bf{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0;}.bg{font-size:14px;font-weight:bold;color:var(--dark);}.bh{display:flex;flex-direction:column;flex:1;min-width:0;gap:16px;}.bi{display:flex;flex-direction:column;gap:0;margin-top:12px;}.bj{display:flex;flex-direction:row;gap:0;padding:8px 16px;background:var(--light);border-bottom:1px solid var(--border);}.bk{display:flex;flex-direction:column;width:200px;}.bl{font-weight:bold;font-size:12px;color:var(--gray);text-transform:uppercase;}.bm{display:flex;flex-direction:column;width:150px;}.bn{display:flex;flex-direction:column;width:120px;}.bo{display:flex;flex-direction:row;gap:0;padding:12px 16px;border-bottom:1px solid var(--border);}.bp{font-size:14px;}.bq{display:flex;flex-direction:row;gap:0;}.br{font-size:13px;color:var(--dark);flex:1;min-width:0;}.bs{display:flex;flex-direction:column;width:100%;height:8px;background:var(--border);border-radius:4px;overflow:hidden;}.bt{display:flex;flex-direction:column;width:67%;height:100%;background:var(--primary);border-radius:4px;}.bu{display:flex;flex-direction:column;gap:8px;margin-top:12px;}.bv{display:flex;flex-direction:row;gap:12px;padding:8px 12px;align-items:center;}.bw{flex-direction:column;width:24px;height:24px;border-radius:50%;background:#eff6ff;display:flex;align-items:center;justify-content:center;}.bx{font-size:10px;color:var(--primary);font-weight:bold;}.by{font-size:14px;color:var(--dark);flex:1;min-width:0;}.bz{display:flex;flex-direction:column;width:340px;gap:16px;}.b0{padding:8px 16px;background:var(--secondary);color:white;border-radius:6px;border:none solid currentColor;cursor:pointer;transition:all 0.2s;font-size:14px;}.b1{padding:8px 16px;background:var(--accent);color:white;border-radius:6px;border:none solid currentColor;cursor:pointer;transition:all 0.2s;font-size:14px;}.b2{padding:8px 16px;background:var(--gray);color:white;border-radius:6px;border:none solid currentColor;cursor:pointer;transition:all 0.2s;font-size:14px;}.b3{display:flex;flex-direction:row;gap:10px;align-items:center;padding:6px 8px;border-radius:6px;transition:all 0.15s;}.b4{display:flex;flex-direction:column;gap:0;flex:1;min-width:0;}.b5{display:flex;flex-direction:column;width:8px;height:8px;border-radius:50%;background:var(--secondary);}.b6{display:flex;flex-direction:row;gap:0;flex-wrap:wrap;margin-top:8px;}.b7{flex-direction:column;display:inline-block;padding:4px 10px;background:#f1f5f9;color:var(--gray);border-radius:4px;font-size:12px;margin:2px;}.b8{display:flex;flex-direction:row;gap:10px;padding:10px 12px;border-bottom:1px solid var(--border);align-items:center;}.b9{display:flex;flex-direction:row;gap:0;align-items:center;}.ca{display:flex;flex-direction:column;width:40px;height:22px;border-radius:11px;background:var(--primary);padding:2px;cursor:pointer;transition:all 0.2s;}.cb{display:flex;flex-direction:column;width:18px;height:18px;border-radius:50%;background:white;margin-left:18px;transition:all 0.2s;}.cc{display:flex;flex-direction:column;width:40px;height:22px;border-radius:11px;background:#cbd5e1;padding:2px;cursor:pointer;transition:all 0.2s;}.cd{display:flex;flex-direction:column;width:18px;height:18px;border-radius:50%;background:white;transition:all 0.2s;}.ce{display:flex;flex-direction:column;margin-top:24px;}.cf{display:flex;flex-direction:row;gap:16px;margin-bottom:12px;align-items:center;}.cg{padding:8px 12px;border-radius:6px;border:1px solid var(--border);width:240px;}.ch{padding:6px 12px;border-radius:4px;border:1px solid var(--border);background:white;color:var(--gray);font-size:13px;cursor:pointer;transition:all 0.15s;}.ci{display:flex;flex-direction:row;gap:0;padding:8px 16px;background:var(--light);border-bottom:2px solid var(--border);border-radius:6 6 0 0;}.cj{display:flex;flex-direction:column;width:60px;}.ck{display:flex;flex-direction:row;gap:0;padding:10px 16px;border-bottom:1px solid var(--border);cursor:pointer;transition:all 0.1s;}.cl{flex-direction:column;width:24px;height:24px;border-radius:50%;background:var(--primary);color:white;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;}.cm{font-size:14px;color:var(--dark);}.cn{flex-direction:column;display:inline-block;padding:2px 8px;background:var(--secondary);color:white;border-radius:12px;font-size:12px;}.co{display:flex;flex-direction:row;gap:16px;padding:12px 16px;align-items:center;border-top:1px solid var(--border);}.cp{display:flex;flex-direction:row;gap:4px;}.cq{padding:6px 10px;border-radius:4px;border:1px solid var(--border);background:white;color:var(--gray);font-size:13px;cursor:pointer;}.cr{padding:6px 10px;border-radius:4px;border:1px solid var(--primary);background:var(--primary);color:white;font-size:13px;cursor:pointer;}.cs{display:flex;flex-direction:column;padding:24px 24px;background:white;border-top:1px solid var(--border);}.ct{display:flex;flex-direction:row;gap:24px;}.cu{display:flex;flex-direction:column;gap:8px;flex:1;min-width:0;}.cv{font-weight:bold;font-size:14px;color:var(--dark);}.cw{font-weight:bold;font-size:12px;color:var(--gray);text-transform:uppercase;margin-bottom:4px;}.cx{color:var(--gray);font-size:13px;}.l:hover{color:var(--primary);background:#f1f5f9;}.ab:hover,.cx:hover{color:var(--primary);}.aj:hover,.am:hover,.b0:hover,.b1:hover,.b2:hover{background:var(--dark);}.aw:hover{box-shadow:0 4 12 rgba(0,0,0,0.1);}.a0:hover,.a1:hover,.a2:hover,.a3:hover,.a4:hover,.a5:hover,.a6:hover,.a7:hover,.a8:hover,.a9:hover,.ba:hover,.bb:hover{background:#2563eb;}.bd:hover,.bo:hover,.ck:hover{background:var(--light);}.b3:hover{background:#f1f5f9;}.ch:hover{background:#f1f5f9;color:var(--dark);}.ah:focus{border:1px solid var(--primary);}@media(min-width:640px){.aq,.ct{flex-direction:column;}.bz{flex:1;min-width:0;}}@media(min-width:1024px){.aq,.ct{flex-direction:row;}}@media(prefers-color-scheme:dark){.b{background:#0f172a;border-right:1px solid var(--dark);}.f,.v,.ad,.ae,.at,.ax,.bg,.br,.by,.cm,.cv{color:white;}.h,.bs{background:#334155;}.k,.bw{background:#1e3a5f;}.s,.ch,.cq{background:var(--dark);}.x,.az,.bj,.ci{background:#0f172a;}.y{background:#0f172a;border-bottom:1px solid var(--dark);}.ah{border:1px solid #334155;background:var(--dark);color:white;}.ar,.aw{background:var(--dark);border:1px solid #334155;}.bo,.b8,.ck{border-bottom:1px solid #334155;}.b7{background:#334155;color:#94a3b8;}.cc{background:#475569;}.cs{background:#0f172a;border-top:1px solid var(--dark);}}}@keyframes fade-in{from{opacity:0}to{opacity:1}}@keyframes slide-down{0%{transform:translateY(-10px);opacity:0}100%{transform:translateY(0);opacity:1}}</style></head><body><div class="a"><div class="b"><div class="c"><div class="d"><span>HL</span></div><div class="e"><span class="f">htmlang</span><span class="g">Dashboard v2.0</span></div></div><div class="h"></div><div class="i"><span class="j">Navigation</span><div class="e"><a href="/dashboard" class="k">Dashboard</a><a href="/analytics" class="l">Analytics</a><a href="/projects" class="l">Projects</a><a href="/team" class="l">Team</a><a href="/messages" class="l">Messages</a><a href="/calendar" class="l">Calendar</a><a href="/documents" class="l">Documents</a><a href="/settings" class="l">Settings</a></div></div><div class="i"><span class="j">Favorites</span><div class="e"><a href="/project/alpha" class="l">Project Alpha</a><a href="/project/beta" class="l">Project Beta</a><a href="/reports/q4" class="l">Q4 Reports</a></div></div><div class="i"><span class="j">Shortcuts</span><div class="m"><div class="n"><div class="o"></div><span class="p">Online (12)</span></div><div class="n"><div class="q"></div><span class="p">Away (3)</span></div><div class="n"><div class="r"></div><span class="p">Offline (5)</span></div></div></div><div class="s"><div class="n"><div class="t"><span>JD</span></div><div class="u"><span class="v">Jane Doe</span><span class="w">jane@example.com</span></div></div></div></div><div class="x"><header class="y"><div class="z"><div class="e"><div class="aa"><div class="aa"><a href="/" class="ab">Home</a><span class="ac">/</span></div><span class="ad">Dashboard</span></div><span class="ae">Dashboard Overview</span></div><div class="af"></div><div class="ag"><input class="ah" type="search" placeholder="Search..."><div class="ai"><button class="aj">Alerts</button><div class="ak"><div class="al"></div></div></div><button class="am">Settings</button></div></div></header><div class="an"><div class="ao"><span class="ap">Welcome back! You have 3 new notifications and 2 pending tasks.</span></div><div class="aq"><div class="ar"><span class="as">Total Revenue</span><span class="at">$48</span><span class="au">+12.5% from last month</span></div><div class="ar"><span class="as">Active Users</span><span class="at">2</span><span class="au">+8.2% from last week</span></div><div class="ar"><span class="as">Conversion Rate</span><span class="at">3.24%</span><span class="au">+0.4% from yesterday</span></div><div class="ar"><span class="as">Open Tickets</span><span class="at">142</span><span class="au">-5.1% from last month</span></div></div><div class="av"><div class="aw"><span class="ax">Revenue Overview</span><div class="ay"><div class="az"><div class="a0"></div><div class="a1"></div><div class="a2"></div><div class="a3"></div><div class="a4"></div><div class="a5"></div><div class="a6"></div><div class="a7"></div><div class="a8"></div><div class="a9"></div><div class="ba"></div><div class="bb"></div></div></div></div><div class="aw"><span class="ax">Activity Feed</span><div class="ay"><div class="bc"><div class="bd"><div class="be"><span>Alice</span></div><div class="bf"><span class="bg">Alice</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Bob</span></div><div class="bf"><span class="bg">Bob</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Carol</span></div><div class="bf"><span class="bg">Carol</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Dave</span></div><div class="bf"><span class="bg">Dave</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Eve</span></div><div class="bf"><span class="bg">Eve</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Frank</span></div><div class="bf"><span class="bg">Frank</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Grace</span></div><div class="bf"><span class="bg">Grace</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Hank</span></div><div class="bf"><span class="bg">Hank</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div></div></div></div></div><div class="aq"><div class="bh"><div class="aw"><span class="ax">Project Status</span><div class="ay"><div class="bi"><div class="bj"><div class="bk"><span class="bl">Project</span></div><div class="bm"><span class="bl">Status</span></div><div class="bn"><span class="bl">Progress</span></div><div class="af"><span class="bl">Due Date</span></div></div><div class="bo"><div class="bk"><span class="bp">Project Alpha</span></div><div class="bm"><span class="as">In Progress</span></div><div class="bn"><span class="bp">75%</span></div><div class="af"><span class="as">Dec 15 2025</span></div></div><div class="bo"><div class="bk"><span class="bp">Project Beta</span></div><div class="bm"><span class="as">Review</span></div><div class="bn"><span class="bp">90%</span></div><div class="af"><span class="as">Nov 30 2025</span></div></div><div class="bo"><div class="bk"><span class="bp">Project Gamma</span></div><div class="bm"><span class="as">Planning</span></div><div class="bn"><span class="bp">20%</span></div><div class="af"><span class="as">Jan 20 2026</span></div></div><div class="bo"><div class="bk"><span class="bp">Project Delta</span></div><div class="bm"><span class="as">Completed</span></div><div class="bn"><span class="bp">100%</span></div><div class="af"><span class="as">Oct 10 2025</span></div></div><div class="bo"><div class="bk"><span class="bp">Project Epsilon</span></div><div class="bm"><span class="as">On Hold</span></div><div class="bn"><span class="bp">45%</span></div><div class="af"><span class="as">Feb 28 2026</span></div></div></div></div></div><div class="aw"><span class="ax">Progress Breakdown</span><div class="ay"><div class="bc"><div class="m"><div class="bq"><span class="br">Alpha</span><span class="p">67%</span></div><div class="bs"><div class="bt"></div></div></div><div class="m"><div class="bq"><span class="br">Beta</span><span class="p">67%</span></div><div class="bs"><div class="bt"></div></div></div><div class="m"><div class="bq"><span class="br">Gamma</span><span class="p">67%</span></div><div class="bs"><div class="bt"></div></div></div><div class="m"><div class="bq"><span class="br">Delta</span><span class="p">67%</span></div><div class="bs"><div class="bt"></div></div></div><div class="m"><div class="bq"><span class="br">Epsilon</span><span class="p">67%</span></div><div class="bs"><div class="bt"></div></div></div></div></div></div><div class="aw"><span class="ax">Recent Activity Log</span><div class="ay"><div class="bu"><div class="bv"><div class="bw"><span class="bx">v2.3.1 to production</span></div><span class="by">Deployed</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">feature/auth-redesign</span></div><span class="by">Merged</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">issue #482: Login timeout</span></div><span class="by">Closed</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">CI/CD pipeline config</span></div><span class="by">Updated</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">unit tests for payment module</span></div><span class="by">Added</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">PR #156: API refactor</span></div><span class="by">Reviewed</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">memory leak in worker pool</span></div><span class="by">Fixed</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">hotfix 2.3.2</span></div><span class="by">Released</span><span class="w">just now</span></div></div></div></div></div><div class="bz"><div class="aw"><span class="ax">Quick Actions</span><div class="ay"><div class="bu"><button class="aj">New Project</button><button class="b0">Create Report</button><button class="b1">Invite Member</button><button class="b2">View Backlog</button></div></div></div><div class="aw"><span class="ax">Team Members</span><div class="ay"><div class="bu"><div class="b3"><div class="t"><span>Alice</span></div><div class="b4"><span class="v">Alice</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Bob</span></div><div class="b4"><span class="v">Bob</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Carol</span></div><div class="b4"><span class="v">Carol</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Dave</span></div><div class="b4"><span class="v">Dave</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Eve</span></div><div class="b4"><span class="v">Eve</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Frank</span></div><div class="b4"><span class="v">Frank</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Grace</span></div><div class="b4"><span class="v">Grace</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Hank</span></div><div class="b4"><span class="v">Hank</span><span class="w">Developer</span></div><div class="b5"></div></div></div></div></div><div class="aw"><span class="ax">Tags</span><div class="ay"><div class="b6"><div class="b7"><span>frontend</span></div><div class="b7"><span>backend</span></div><div class="b7"><span>devops</span></div><div class="b7"><span>design</span></div><div class="b7"><span>mobile</span></div><div class="b7"><span>security</span></div><div class="b7"><span>performance</span></div><div class="b7"><span>testing</span></div><div class="b7"><span>docs</span></div><div class="b7"><span>infra</span></div><div class="b7"><span>ux</span></div><div class="b7"><span>api</span></div></div></div></div><div class="aw"><span class="ax">Notifications</span><div class="ay"><div class="bi"><div class="b8"><div class="al"></div><span class="br">New deployment completed</span><span class="w">now</span></div><div class="b8"><div class="al"></div><span class="br">PR review requested</span><span class="w">now</span></div><div class="b8"><div class="al"></div><span class="br">Build failed on staging</span><span class="w">now</span></div><div class="b8"><div class="al"></div><span class="br">Meeting in 30 minutes</span><span class="w">now</span></div><div class="b8"><div class="al"></div><span class="br">Subscription renewal due</span><span class="w">now</span></div></div></div></div><div class="aw"><span class="ax">Settings</span><div class="ay"><div class="bc"><div class="b9"><span class="by">Dark Mode</span><div class="ca"><div class="cb"></div></div></div><div class="b9"><span class="by">Notifications</span><div class="ca"><div class="cb"></div></div></div><div class="b9"><span class="by">Auto-save</span><div class="cc"><div class="cd"></div></div></div><div class="b9"><span class="by">Compact View</span><div class="cc"><div class="cd"></div></div></div></div></div></div></div></div><div class="ce"><div class="aw"><span class="ax">Data Table</span><div class="ay"><div class="bi"><div class="cf"><input class="cg" type="search" placeholder="Filter results..."><div class="af"></div><div class="aa"><button class="ch">all</button><button class="ch">design</button><button class="ch">development</button><button class="ch">marketing</button><button class="ch">analytics</button></div></div><div class="ci"><div class="cj"><span class="bl">ID</span></div><div class="bk"><span class="bl">Name</span></div><div class="bm"><span class="bl">Category</span></div><div class="bn"><span class="bl">Status</span></div><div class="af"><span class="bl">Updated</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Alpha</span></div><span class="cm">Alpha</span></div></div><div class="bm"><span class="p">active</span></div><div class="bn"><div class="cn"><span>active</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Beta</span></div><span class="cm">Beta</span></div></div><div class="bm"><span class="p">active</span></div><div class="bn"><div class="cn"><span>active</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Gamma</span></div><span class="cm">Gamma</span></div></div><div class="bm"><span class="p">active</span></div><div class="bn"><div class="cn"><span>active</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Delta</span></div><span class="cm">Delta</span></div></div><div class="bm"><span class="p">active</span></div><div class="bn"><div class="cn"><span>active</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Epsilon</span></div><span class="cm">Epsilon</span></div></div><div class="bm"><span class="p">active</span></div><div class="bn"><div class="cn"><span>active</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Alpha</span></div><span class="cm">Alpha</span></div></div><div class="bm"><span class="p">pending</span></div><div class="bn"><div class="cn"><span>pending</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Beta</span></div><span class="cm">Beta</span></div></div><div class="bm"><span class="p">pending</span></div><div class="bn"><div class="cn"><span>pending</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Gamma</span></div><span class="cm">Gamma</span></div></div><div class="bm"><span class="p">pending</span></div><div class="bn"><div class="cn"><span>pending</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Delta</span></div><span class="cm">Delta</span></div></div><div class="bm"><span class="p">pending</span></div><div class="bn"><div class="cn"><span>pending</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Epsilon</span></div><span class="cm">Epsilon</span></div></div><div class="bm"><span class="p">pending</span></div><div class="bn"><div class="cn"><span>pending</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Alpha</span></div><span class="cm">Alpha</span></div></div><div class="bm"><span class="p">completed</span></div><div class="bn"><div class="cn"><span>completed</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Beta</span></div><span class="cm">Beta</span></div></div><div class="bm"><span class="p">completed</span></div><div class="bn"><div class="cn"><span>completed</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Gamma</span></div><span class="cm">Gamma</span></div></div><div class="bm"><span class="p">completed</span></div><div class="bn"><div class="cn"><span>completed</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Delta</span></div><span class="cm">Delta</span></div></div><div class="bm"><span class="p">completed</span></div><div class="bn"><div class="cn"><span>completed</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Epsilon</span></div><span class="cm">Epsilon</span></div></div><div class="bm"><span class="p">completed</span></div><div class="bn"><div class="cn"><span>completed</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Alpha</span></div><span class="cm">Alpha</span></div></div><div class="bm"><span class="p">archived</span></div><div class="bn"><div class="cn"><span>archived</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Beta</span></div><span class="cm">Beta</span></div></div><div class="bm"><span class="p">archived</span></div><div class="bn"><div class="cn"><span>archived</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Gamma</span></div><span class="cm">Gamma</span></div></div><div class="bm"><span class="p">archived</span></div><div class="bn"><div class="cn"><span>archived</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Delta</span></div><span class="cm">Delta</span></div></div><div class="bm"><span class="p">archived</span></div><div class="bn"><div class="cn"><span>archived</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Epsilon</span></div><span class="cm">Epsilon</span></div></div><div class="bm"><span class="p">archived</span></div><div class="bn"><div class="cn"><span>archived</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="co"><span class="p">Showing 1-20 of 142 results</span><div class="af"></div><div class="cp"><button class="cq">Prev</button><button class="cr">1</button><button class="cq">2</button><button class="cq">3</button><button class="cq">Next</button></div></div></div></div></div></div></div><footer class="cs"><div class="ct"><div class="cu"><span class="cv">htmlang Dashboard</span><span class="g">Built with htmlang - a declarative HTML language</span><span class="w">Copyright 2025 htmlang contributors</span></div><div class="m"><span class="cw">Product</span><a href="/features" class="cx">Features</a><a href="/pricing" class="cx">Pricing</a><a href="/changelog" class="cx">Changelog</a><a href="/roadmap" class="cx">Roadmap</a></div><div class="m"><span class="cw">Resources</span><a href="/docs" class="cx">Documentation</a><a href="/api" class="cx">API Reference</a><a href="/guides" class="cx">Guides</a><a href="/examples" class="cx">Examples</a></div><div class="m"><span class="cw">Company</span><a href="/about" class="cx">About</a><a href="/blog" class="cx">Blog</a><a href="/careers" class="cx">Careers</a><a href="/contact" class="cx">Contact</a></div><div class="m"><span class="cw">Legal</span><a href="/privacy" class="cx">Privacy Policy</a><a href="/terms" class="cx">Terms of Service</a><a href="/cookies" class="cx">Cookie Policy</a></div></div></footer></div></div></body></html>
1+<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Stress Test Large Document</title><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}:root{--primary:#3b82f6;--secondary:#10b981;--accent:#f59e0b;--danger:#ef4444;--dark:#1e293b;--light:#f8fafc;--gray:#64748b;--border:#e2e8f0;--radius:8;--gap:16;--gap-sm:8;--gap-lg:24;}@layer htmlang{.a{display:flex;flex-direction:row;gap:0;min-height:100vh;}.b{display:flex;flex-direction:column;width:260px;background:white;border-right:1px solid var(--border);padding:16px;gap:8px;}.c{display:flex;flex-direction:row;gap:12px;padding:8px;align-items:center;}.d{flex-direction:column;width:36px;height:36px;border-radius:50%;background:var(--primary);color:white;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;}.e{display:flex;flex-direction:column;gap:2px;}.f{font-weight:bold;font-size:16px;color:var(--dark);}.g{font-size:12px;color:var(--gray);}.h{display:flex;flex-direction:column;width:100%;height:1px;background:var(--border);margin:8px 0;}.i{display:flex;flex-direction:column;margin-bottom:24px;}.j{font-weight:bold;font-size:12px;color:var(--gray);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;}.k{color:var(--primary);font-weight:bold;padding:8px 12px;background:#eff6ff;border-radius:6px;}.l{color:var(--gray);padding:8px 12px;border-radius:6px;transition:all 0.15s;}.m{display:flex;flex-direction:column;gap:4px;}.n{display:flex;flex-direction:row;gap:8px;align-items:center;}.o{display:flex;flex-direction:column;width:6px;height:6px;border-radius:50%;background:var(--secondary);}.p{font-size:13px;color:var(--gray);}.q{display:flex;flex-direction:column;width:6px;height:6px;border-radius:50%;background:var(--accent);}.r{display:flex;flex-direction:column;width:6px;height:6px;border-radius:50%;background:#cbd5e1;}.s{display:flex;flex-direction:column;margin-top:auto;padding:12px;background:#f1f5f9;border-radius:8px;}.t{flex-direction:column;width:28px;height:28px;border-radius:50%;background:var(--primary);color:white;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;}.u{display:flex;flex-direction:column;gap:0;}.v{font-size:13px;font-weight:bold;color:var(--dark);}.w{font-size:11px;color:var(--gray);}.x{display:flex;flex-direction:column;flex:1;min-width:0;background:var(--light);}.y{display:flex;flex-direction:column;padding:12px 24px;background:white;border-bottom:1px solid var(--border);}.z{display:flex;flex-direction:row;gap:16px;align-items:center;}.aa{display:flex;flex-direction:row;gap:8px;}.ab{color:var(--gray);font-size:14px;}.ac{color:var(--border);font-size:14px;}.ad{color:var(--dark);font-size:14px;}.ae{font-weight:bold;font-size:22px;color:var(--dark);}.af{display:flex;flex-direction:column;flex:1;min-width:0;}.ag{display:flex;flex-direction:row;gap:12px;align-items:center;}.ah{padding:8px 12px;border-radius:6px;border:1px solid var(--border);width:240px;transition:all 0.2s;}.ai{display:flex;flex-direction:column;position:relative;}.aj{padding:8px 16px;background:var(--primary);color:white;border-radius:6px;border:none solid currentColor;cursor:pointer;transition:all 0.2s;font-size:14px;}.ak{display:flex;flex-direction:column;position:absolute;top:-2px;right:-2px;}.al{display:flex;flex-direction:column;width:8px;height:8px;border-radius:50%;background:var(--danger);}.am{padding:8px 16px;background:var(--gray);color:white;border-radius:6px;border:none solid currentColor;cursor:pointer;transition:all 0.2s;font-size:14px;}.an{display:flex;flex-direction:column;padding:24px;overflow-y:auto;}.ao{display:flex;flex-direction:column;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:12px 16px;}.ap{color:#1e40af;font-size:14px;}.aq,.av{display:flex;flex-direction:row;gap:16px;margin-top:24px;}.ar{display:flex;flex-direction:column;background:white;border-radius:8px;padding:16px;border:1px solid var(--border);}.as{font-size:14px;color:var(--gray);}.at{font-weight:bold;font-size:28px;color:var(--dark);margin-top:4px;}.au{font-size:12px;color:var(--secondary);margin-top:2px;}.aw{display:flex;flex-direction:column;background:white;border-radius:8px;padding:16px;border:1px solid var(--border);transition:all 0.2s;animation:fade-in 0.3s ease;}.ax{font-weight:bold;font-size:18px;color:var(--dark);}.ay{display:flex;flex-direction:column;margin-top:8px;}.az{flex-direction:column;height:200px;background:var(--light);border-radius:6px;margin-top:12px;display:flex;align-items:flex-end;padding:16px;gap:8px;}.a0{display:flex;flex-direction:column;width:100%;height:40%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a1{display:flex;flex-direction:column;width:100%;height:65%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a2{display:flex;flex-direction:column;width:100%;height:45%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a3{display:flex;flex-direction:column;width:100%;height:80%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a4{display:flex;flex-direction:column;width:100%;height:55%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a5{display:flex;flex-direction:column;width:100%;height:70%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a6{display:flex;flex-direction:column;width:100%;height:90%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a7{display:flex;flex-direction:column;width:100%;height:60%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a8{display:flex;flex-direction:column;width:100%;height:75%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.a9{display:flex;flex-direction:column;width:100%;height:85%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.ba{display:flex;flex-direction:column;width:100%;height:50%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.bb{display:flex;flex-direction:column;width:100%;height:95%;background:var(--primary);border-radius:4 4 0 0;transition:all 0.2s;min-width:20px;}.bc{display:flex;flex-direction:column;gap:12px;margin-top:12px;}.bd{display:flex;flex-direction:row;gap:12px;align-items:center;padding:8px;border-radius:6px;}.be{flex-direction:column;width:32px;height:32px;border-radius:50%;background:var(--primary);color:white;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;}.bf{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0;}.bg{font-size:14px;font-weight:bold;color:var(--dark);}.bh{display:flex;flex-direction:column;flex:1;min-width:0;gap:16px;}.bi{display:flex;flex-direction:column;gap:0;margin-top:12px;}.bj{display:flex;flex-direction:row;gap:0;padding:8px 16px;background:var(--light);border-bottom:1px solid var(--border);}.bk{display:flex;flex-direction:column;width:200px;}.bl{font-weight:bold;font-size:12px;color:var(--gray);text-transform:uppercase;}.bm{display:flex;flex-direction:column;width:150px;}.bn{display:flex;flex-direction:column;width:120px;}.bo{display:flex;flex-direction:row;gap:0;padding:12px 16px;border-bottom:1px solid var(--border);}.bp{font-size:14px;}.bq{display:flex;flex-direction:row;gap:0;}.br{font-size:13px;color:var(--dark);flex:1;min-width:0;}.bs{display:flex;flex-direction:column;width:100%;height:8px;background:var(--border);border-radius:4px;overflow:hidden;}.bt{display:flex;flex-direction:column;width:67%;height:100%;background:var(--primary);border-radius:4px;}.bu{display:flex;flex-direction:column;gap:8px;margin-top:12px;}.bv{display:flex;flex-direction:row;gap:12px;padding:8px 12px;align-items:center;}.bw{flex-direction:column;width:24px;height:24px;border-radius:50%;background:#eff6ff;display:flex;align-items:center;justify-content:center;}.bx{font-size:10px;color:var(--primary);font-weight:bold;}.by{font-size:14px;color:var(--dark);flex:1;min-width:0;}.bz{display:flex;flex-direction:column;width:340px;gap:16px;}.b0{padding:8px 16px;background:var(--secondary);color:white;border-radius:6px;border:none solid currentColor;cursor:pointer;transition:all 0.2s;font-size:14px;}.b1{padding:8px 16px;background:var(--accent);color:white;border-radius:6px;border:none solid currentColor;cursor:pointer;transition:all 0.2s;font-size:14px;}.b2{padding:8px 16px;background:var(--gray);color:white;border-radius:6px;border:none solid currentColor;cursor:pointer;transition:all 0.2s;font-size:14px;}.b3{display:flex;flex-direction:row;gap:10px;align-items:center;padding:6px 8px;border-radius:6px;transition:all 0.15s;}.b4{display:flex;flex-direction:column;gap:0;flex:1;min-width:0;}.b5{display:flex;flex-direction:column;width:8px;height:8px;border-radius:50%;background:var(--secondary);}.b6{display:flex;flex-direction:row;gap:0;flex-wrap:wrap;margin-top:8px;}.b7{flex-direction:column;display:inline-block;padding:4px 10px;background:#f1f5f9;color:var(--gray);border-radius:4px;font-size:12px;margin:2px;}.b8{display:flex;flex-direction:row;gap:10px;padding:10px 12px;border-bottom:1px solid var(--border);align-items:center;}.b9{display:flex;flex-direction:row;gap:0;align-items:center;}.ca{display:flex;flex-direction:column;width:40px;height:22px;border-radius:11px;background:var(--primary);padding:2px;cursor:pointer;transition:all 0.2s;}.cb{display:flex;flex-direction:column;width:18px;height:18px;border-radius:50%;background:white;margin-left:18px;transition:all 0.2s;}.cc{display:flex;flex-direction:column;width:40px;height:22px;border-radius:11px;background:#cbd5e1;padding:2px;cursor:pointer;transition:all 0.2s;}.cd{display:flex;flex-direction:column;width:18px;height:18px;border-radius:50%;background:white;transition:all 0.2s;}.ce{display:flex;flex-direction:column;margin-top:24px;}.cf{display:flex;flex-direction:row;gap:16px;margin-bottom:12px;align-items:center;}.cg{padding:8px 12px;border-radius:6px;border:1px solid var(--border);width:240px;}.ch{padding:6px 12px;border-radius:4px;border:1px solid var(--border);background:white;color:var(--gray);font-size:13px;cursor:pointer;transition:all 0.15s;}.ci{display:flex;flex-direction:row;gap:0;padding:8px 16px;background:var(--light);border-bottom:2px solid var(--border);border-radius:6 6 0 0;}.cj{display:flex;flex-direction:column;width:60px;}.ck{display:flex;flex-direction:row;gap:0;padding:10px 16px;border-bottom:1px solid var(--border);cursor:pointer;transition:all 0.1s;}.cl{flex-direction:column;width:24px;height:24px;border-radius:50%;background:var(--primary);color:white;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;}.cm{font-size:14px;color:var(--dark);}.cn{flex-direction:column;display:inline-block;padding:2px 8px;background:var(--secondary);color:white;border-radius:12px;font-size:12px;}.co{display:flex;flex-direction:row;gap:16px;padding:12px 16px;align-items:center;border-top:1px solid var(--border);}.cp{display:flex;flex-direction:row;gap:4px;}.cq{padding:6px 10px;border-radius:4px;border:1px solid var(--border);background:white;color:var(--gray);font-size:13px;cursor:pointer;}.cr{padding:6px 10px;border-radius:4px;border:1px solid var(--primary);background:var(--primary);color:white;font-size:13px;cursor:pointer;}.cs{display:flex;flex-direction:column;padding:24px 24px;background:white;border-top:1px solid var(--border);}.ct{display:flex;flex-direction:row;gap:24px;}.cu{display:flex;flex-direction:column;gap:8px;flex:1;min-width:0;}.cv{font-weight:bold;font-size:14px;color:var(--dark);}.cw{font-weight:bold;font-size:12px;color:var(--gray);text-transform:uppercase;margin-bottom:4px;}.cx{color:var(--gray);font-size:13px;}.l:hover{color:var(--primary);background:#f1f5f9;}.ab:hover,.cx:hover{color:var(--primary);}.aj:hover,.am:hover,.b0:hover,.b1:hover,.b2:hover{background:var(--dark);}.aw:hover{box-shadow:0 4 12 rgba(0,0,0,0.1);}.a0:hover,.a1:hover,.a2:hover,.a3:hover,.a4:hover,.a5:hover,.a6:hover,.a7:hover,.a8:hover,.a9:hover,.ba:hover,.bb:hover{background:#2563eb;}.bd:hover,.bo:hover,.ck:hover{background:var(--light);}.b3:hover{background:#f1f5f9;}.ch:hover{background:#f1f5f9;color:var(--dark);}.ah:focus{border:1px solid var(--primary);}@media(min-width:640px){.aq,.ct{flex-direction:column;}.bz{flex:1;min-width:0;}}@media(min-width:1024px){.aq,.ct{flex-direction:row;}}@media(prefers-color-scheme:dark){.b{background:#0f172a;border-right:1px solid var(--dark);}.f,.v,.ad,.ae,.at,.ax,.bg,.br,.by,.cm,.cv{color:white;}.h,.bs{background:#334155;}.k,.bw{background:#1e3a5f;}.s,.ch,.cq{background:var(--dark);}.x,.az,.bj,.ci{background:#0f172a;}.y{background:#0f172a;border-bottom:1px solid var(--dark);}.ah{border:1px solid #334155;background:var(--dark);color:white;}.ar,.aw{background:var(--dark);border:1px solid #334155;}.bo,.b8,.ck{border-bottom:1px solid #334155;}.b7{background:#334155;color:#94a3b8;}.cc{background:#475569;}.cs{background:#0f172a;border-top:1px solid var(--dark);}}}@keyframes fade-in {from{opacity:0}to{opacity:1}}@keyframes slide-down {0%{transform:translateY(-10px);opacity:0}100%{transform:translateY(0);opacity:1}}</style></head><body><div class="a"><div class="b"><div class="c"><div class="d"><span>HL</span></div><div class="e"><span class="f">htmlang</span><span class="g">Dashboard v2.0</span></div></div><div class="h"></div><div class="i"><span class="j">Navigation</span><div class="e"><a href="/dashboard" class="k">Dashboard</a><a href="/analytics" class="l">Analytics</a><a href="/projects" class="l">Projects</a><a href="/team" class="l">Team</a><a href="/messages" class="l">Messages</a><a href="/calendar" class="l">Calendar</a><a href="/documents" class="l">Documents</a><a href="/settings" class="l">Settings</a></div></div><div class="i"><span class="j">Favorites</span><div class="e"><a href="/project/alpha" class="l">Project Alpha</a><a href="/project/beta" class="l">Project Beta</a><a href="/reports/q4" class="l">Q4 Reports</a></div></div><div class="i"><span class="j">Shortcuts</span><div class="m"><div class="n"><div class="o"></div><span class="p">Online (12)</span></div><div class="n"><div class="q"></div><span class="p">Away (3)</span></div><div class="n"><div class="r"></div><span class="p">Offline (5)</span></div></div></div><div class="s"><div class="n"><div class="t"><span>JD</span></div><div class="u"><span class="v">Jane Doe</span><span class="w">jane@example.com</span></div></div></div></div><div class="x"><header class="y"><div class="z"><div class="e"><div class="aa"><div class="aa"><a href="/" class="ab">Home</a><span class="ac">/</span></div><span class="ad">Dashboard</span></div><span class="ae">Dashboard Overview</span></div><div class="af"></div><div class="ag"><input class="ah" type="search" placeholder="Search..."><div class="ai"><button class="aj">Alerts</button><div class="ak"><div class="al"></div></div></div><button class="am">Settings</button></div></div></header><div class="an"><div class="ao"><span class="ap">Welcome back! You have 3 new notifications and 2 pending tasks.</span></div><div class="aq"><div class="ar"><span class="as">Total Revenue</span><span class="at">$48</span><span class="au">+12.5% from last month</span></div><div class="ar"><span class="as">Active Users</span><span class="at">2</span><span class="au">+8.2% from last week</span></div><div class="ar"><span class="as">Conversion Rate</span><span class="at">3.24%</span><span class="au">+0.4% from yesterday</span></div><div class="ar"><span class="as">Open Tickets</span><span class="at">142</span><span class="au">-5.1% from last month</span></div></div><div class="av"><div class="aw"><span class="ax">Revenue Overview</span><div class="ay"><div class="az"><div class="a0"></div><div class="a1"></div><div class="a2"></div><div class="a3"></div><div class="a4"></div><div class="a5"></div><div class="a6"></div><div class="a7"></div><div class="a8"></div><div class="a9"></div><div class="ba"></div><div class="bb"></div></div></div></div><div class="aw"><span class="ax">Activity Feed</span><div class="ay"><div class="bc"><div class="bd"><div class="be"><span>Alice</span></div><div class="bf"><span class="bg">Alice</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Bob</span></div><div class="bf"><span class="bg">Bob</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Carol</span></div><div class="bf"><span class="bg">Carol</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Dave</span></div><div class="bf"><span class="bg">Dave</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Eve</span></div><div class="bf"><span class="bg">Eve</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Frank</span></div><div class="bf"><span class="bg">Frank</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Grace</span></div><div class="bf"><span class="bg">Grace</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div><div class="bd"><div class="be"><span>Hank</span></div><div class="bf"><span class="bg">Hank</span><span class="g">Updated project files</span></div><span class="w">2h ago</span></div></div></div></div></div><div class="aq"><div class="bh"><div class="aw"><span class="ax">Project Status</span><div class="ay"><div class="bi"><div class="bj"><div class="bk"><span class="bl">Project</span></div><div class="bm"><span class="bl">Status</span></div><div class="bn"><span class="bl">Progress</span></div><div class="af"><span class="bl">Due Date</span></div></div><div class="bo"><div class="bk"><span class="bp">Project Alpha</span></div><div class="bm"><span class="as">In Progress</span></div><div class="bn"><span class="bp">75%</span></div><div class="af"><span class="as">Dec 15 2025</span></div></div><div class="bo"><div class="bk"><span class="bp">Project Beta</span></div><div class="bm"><span class="as">Review</span></div><div class="bn"><span class="bp">90%</span></div><div class="af"><span class="as">Nov 30 2025</span></div></div><div class="bo"><div class="bk"><span class="bp">Project Gamma</span></div><div class="bm"><span class="as">Planning</span></div><div class="bn"><span class="bp">20%</span></div><div class="af"><span class="as">Jan 20 2026</span></div></div><div class="bo"><div class="bk"><span class="bp">Project Delta</span></div><div class="bm"><span class="as">Completed</span></div><div class="bn"><span class="bp">100%</span></div><div class="af"><span class="as">Oct 10 2025</span></div></div><div class="bo"><div class="bk"><span class="bp">Project Epsilon</span></div><div class="bm"><span class="as">On Hold</span></div><div class="bn"><span class="bp">45%</span></div><div class="af"><span class="as">Feb 28 2026</span></div></div></div></div></div><div class="aw"><span class="ax">Progress Breakdown</span><div class="ay"><div class="bc"><div class="m"><div class="bq"><span class="br">Alpha</span><span class="p">67%</span></div><div class="bs"><div class="bt"></div></div></div><div class="m"><div class="bq"><span class="br">Beta</span><span class="p">67%</span></div><div class="bs"><div class="bt"></div></div></div><div class="m"><div class="bq"><span class="br">Gamma</span><span class="p">67%</span></div><div class="bs"><div class="bt"></div></div></div><div class="m"><div class="bq"><span class="br">Delta</span><span class="p">67%</span></div><div class="bs"><div class="bt"></div></div></div><div class="m"><div class="bq"><span class="br">Epsilon</span><span class="p">67%</span></div><div class="bs"><div class="bt"></div></div></div></div></div></div><div class="aw"><span class="ax">Recent Activity Log</span><div class="ay"><div class="bu"><div class="bv"><div class="bw"><span class="bx">v2.3.1 to production</span></div><span class="by">Deployed</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">feature/auth-redesign</span></div><span class="by">Merged</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">issue #482: Login timeout</span></div><span class="by">Closed</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">CI/CD pipeline config</span></div><span class="by">Updated</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">unit tests for payment module</span></div><span class="by">Added</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">PR #156: API refactor</span></div><span class="by">Reviewed</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">memory leak in worker pool</span></div><span class="by">Fixed</span><span class="w">just now</span></div><div class="bv"><div class="bw"><span class="bx">hotfix 2.3.2</span></div><span class="by">Released</span><span class="w">just now</span></div></div></div></div></div><div class="bz"><div class="aw"><span class="ax">Quick Actions</span><div class="ay"><div class="bu"><button class="aj">New Project</button><button class="b0">Create Report</button><button class="b1">Invite Member</button><button class="b2">View Backlog</button></div></div></div><div class="aw"><span class="ax">Team Members</span><div class="ay"><div class="bu"><div class="b3"><div class="t"><span>Alice</span></div><div class="b4"><span class="v">Alice</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Bob</span></div><div class="b4"><span class="v">Bob</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Carol</span></div><div class="b4"><span class="v">Carol</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Dave</span></div><div class="b4"><span class="v">Dave</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Eve</span></div><div class="b4"><span class="v">Eve</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Frank</span></div><div class="b4"><span class="v">Frank</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Grace</span></div><div class="b4"><span class="v">Grace</span><span class="w">Developer</span></div><div class="b5"></div></div><div class="b3"><div class="t"><span>Hank</span></div><div class="b4"><span class="v">Hank</span><span class="w">Developer</span></div><div class="b5"></div></div></div></div></div><div class="aw"><span class="ax">Tags</span><div class="ay"><div class="b6"><div class="b7"><span>frontend</span></div><div class="b7"><span>backend</span></div><div class="b7"><span>devops</span></div><div class="b7"><span>design</span></div><div class="b7"><span>mobile</span></div><div class="b7"><span>security</span></div><div class="b7"><span>performance</span></div><div class="b7"><span>testing</span></div><div class="b7"><span>docs</span></div><div class="b7"><span>infra</span></div><div class="b7"><span>ux</span></div><div class="b7"><span>api</span></div></div></div></div><div class="aw"><span class="ax">Notifications</span><div class="ay"><div class="bi"><div class="b8"><div class="al"></div><span class="br">New deployment completed</span><span class="w">now</span></div><div class="b8"><div class="al"></div><span class="br">PR review requested</span><span class="w">now</span></div><div class="b8"><div class="al"></div><span class="br">Build failed on staging</span><span class="w">now</span></div><div class="b8"><div class="al"></div><span class="br">Meeting in 30 minutes</span><span class="w">now</span></div><div class="b8"><div class="al"></div><span class="br">Subscription renewal due</span><span class="w">now</span></div></div></div></div><div class="aw"><span class="ax">Settings</span><div class="ay"><div class="bc"><div class="b9"><span class="by">Dark Mode</span><div class="ca"><div class="cb"></div></div></div><div class="b9"><span class="by">Notifications</span><div class="ca"><div class="cb"></div></div></div><div class="b9"><span class="by">Auto-save</span><div class="cc"><div class="cd"></div></div></div><div class="b9"><span class="by">Compact View</span><div class="cc"><div class="cd"></div></div></div></div></div></div></div></div><div class="ce"><div class="aw"><span class="ax">Data Table</span><div class="ay"><div class="bi"><div class="cf"><input class="cg" type="search" placeholder="Filter results..."><div class="af"></div><div class="aa"><button class="ch">all</button><button class="ch">design</button><button class="ch">development</button><button class="ch">marketing</button><button class="ch">analytics</button></div></div><div class="ci"><div class="cj"><span class="bl">ID</span></div><div class="bk"><span class="bl">Name</span></div><div class="bm"><span class="bl">Category</span></div><div class="bn"><span class="bl">Status</span></div><div class="af"><span class="bl">Updated</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Alpha</span></div><span class="cm">Alpha</span></div></div><div class="bm"><span class="p">active</span></div><div class="bn"><div class="cn"><span>active</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Beta</span></div><span class="cm">Beta</span></div></div><div class="bm"><span class="p">active</span></div><div class="bn"><div class="cn"><span>active</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Gamma</span></div><span class="cm">Gamma</span></div></div><div class="bm"><span class="p">active</span></div><div class="bn"><div class="cn"><span>active</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Delta</span></div><span class="cm">Delta</span></div></div><div class="bm"><span class="p">active</span></div><div class="bn"><div class="cn"><span>active</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Epsilon</span></div><span class="cm">Epsilon</span></div></div><div class="bm"><span class="p">active</span></div><div class="bn"><div class="cn"><span>active</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Alpha</span></div><span class="cm">Alpha</span></div></div><div class="bm"><span class="p">pending</span></div><div class="bn"><div class="cn"><span>pending</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Beta</span></div><span class="cm">Beta</span></div></div><div class="bm"><span class="p">pending</span></div><div class="bn"><div class="cn"><span>pending</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Gamma</span></div><span class="cm">Gamma</span></div></div><div class="bm"><span class="p">pending</span></div><div class="bn"><div class="cn"><span>pending</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Delta</span></div><span class="cm">Delta</span></div></div><div class="bm"><span class="p">pending</span></div><div class="bn"><div class="cn"><span>pending</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Epsilon</span></div><span class="cm">Epsilon</span></div></div><div class="bm"><span class="p">pending</span></div><div class="bn"><div class="cn"><span>pending</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Alpha</span></div><span class="cm">Alpha</span></div></div><div class="bm"><span class="p">completed</span></div><div class="bn"><div class="cn"><span>completed</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Beta</span></div><span class="cm">Beta</span></div></div><div class="bm"><span class="p">completed</span></div><div class="bn"><div class="cn"><span>completed</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Gamma</span></div><span class="cm">Gamma</span></div></div><div class="bm"><span class="p">completed</span></div><div class="bn"><div class="cn"><span>completed</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Delta</span></div><span class="cm">Delta</span></div></div><div class="bm"><span class="p">completed</span></div><div class="bn"><div class="cn"><span>completed</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Epsilon</span></div><span class="cm">Epsilon</span></div></div><div class="bm"><span class="p">completed</span></div><div class="bn"><div class="cn"><span>completed</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Alpha</span></div><span class="cm">Alpha</span></div></div><div class="bm"><span class="p">archived</span></div><div class="bn"><div class="cn"><span>archived</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Beta</span></div><span class="cm">Beta</span></div></div><div class="bm"><span class="p">archived</span></div><div class="bn"><div class="cn"><span>archived</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Gamma</span></div><span class="cm">Gamma</span></div></div><div class="bm"><span class="p">archived</span></div><div class="bn"><div class="cn"><span>archived</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Delta</span></div><span class="cm">Delta</span></div></div><div class="bm"><span class="p">archived</span></div><div class="bn"><div class="cn"><span>archived</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="ck"><div class="cj"><span class="p">#42</span></div><div class="bk"><div class="n"><div class="cl"><span>Epsilon</span></div><span class="cm">Epsilon</span></div></div><div class="bm"><span class="p">archived</span></div><div class="bn"><div class="cn"><span>archived</span></div></div><div class="af"><span class="p">2 hours ago</span></div></div><div class="co"><span class="p">Showing 1-20 of 142 results</span><div class="af"></div><div class="cp"><button class="cq">Prev</button><button class="cr">1</button><button class="cq">2</button><button class="cq">3</button><button class="cq">Next</button></div></div></div></div></div></div></div><footer class="cs"><div class="ct"><div class="cu"><span class="cv">htmlang Dashboard</span><span class="g">Built with htmlang - a declarative HTML language</span><span class="w">Copyright 2025 htmlang contributors</span></div><div class="m"><span class="cw">Product</span><a href="/features" class="cx">Features</a><a href="/pricing" class="cx">Pricing</a><a href="/changelog" class="cx">Changelog</a><a href="/roadmap" class="cx">Roadmap</a></div><div class="m"><span class="cw">Resources</span><a href="/docs" class="cx">Documentation</a><a href="/api" class="cx">API Reference</a><a href="/guides" class="cx">Guides</a><a href="/examples" class="cx">Examples</a></div><div class="m"><span class="cw">Company</span><a href="/about" class="cx">About</a><a href="/blog" class="cx">Blog</a><a href="/careers" class="cx">Careers</a><a href="/contact" class="cx">Contact</a></div><div class="m"><span class="cw">Legal</span><a href="/privacy" class="cx">Privacy Policy</a><a href="/terms" class="cx">Terms of Service</a><a href="/cookies" class="cx">Cookie Policy</a></div></div></footer></div></div></body></html>