@@ -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// ---------------------------------------------------------------------------