///|
/// Inline style parser - parses CSS property declarations
/// e.g., "width: 100px; height: 50px; display: flex"

// Default viewport size for inline parser vh/vw resolution
let default_viewport_width : Double = 1000.0

///|
let default_viewport_height : Double = 1000.0

///|
fn is_viewport_width_unit(unit : String) -> Bool {
  unit == "vw" || unit == "dvw" || unit == "svw" || unit == "lvw"
}

///|
fn is_viewport_height_unit(unit : String) -> Bool {
  unit == "vh" || unit == "dvh" || unit == "svh" || unit == "lvh"
}

///|
/// Parse a calc() expression and try to simplify it to a Dimension
/// Returns Some(dimension) if simplification is possible, None otherwise.
/// Mixed length+percentage calc() returns None (the inline parser falls back to
/// Auto); min()/max()/clamp() use parse_calc_linear directly to keep the mixed
/// form.
fn parse_calc_expression(
  tokens : ArrayView[@token.Token],
) -> (@types.Dimension, ArrayView[@token.Token])? {
  match parse_calc_linear(tokens) {
    Some(((px, pct), rest)) =>
      if pct.abs() < 0.0001 {
        Some((@types.Length(px), rest))
      } else if px.abs() < 0.0001 {
        Some((@types.Percent(pct), rest))
      } else {
        // Mixed - cannot simplify (fall back to Auto)
        None
      }
    None => None
  }
}

///|
/// Parse a calc() body into a linear (px, percent_ratio) form. Starts right
/// after the Function("calc") token and consumes through the matching
/// RightParen, returning the remaining tokens. Returns None if no RightParen.
fn parse_calc_linear(
  tokens : ArrayView[@token.Token],
) -> ((Double, Double), ArrayView[@token.Token])? {
  let mut remaining = tokens
  let mut result_px : Double = 0.0
  let mut result_pct : Double = 0.0
  let mut current_op : Char = '+'
  // A unitless number before the first dimensioned term (e.g. `calc(2 * 10px)`)
  // multiplies that term; stash it here until the first value lands.
  let mut have_value = false
  let mut leading_factor : Double = 1.0
  while remaining.length() > 0 {
    remaining = skip_whitespace(remaining)
    if remaining.length() == 0 {
      break
    }
    match remaining[0] {
      @token.Token::RightParen => {
        // End of calc expression: return the linear form (pct as a ratio).
        return Some(((result_px, result_pct / 100.0), remaining[1:]))
      }
      @token.Token::Dimension(value, raw_unit) => {
        let unit = raw_unit.to_lower()
        if unit == "%" {
          if !have_value {
            result_pct = value * leading_factor
            have_value = true
            leading_factor = 1.0
          } else {
            match current_op {
              '+' => result_pct = result_pct + value
              '-' => result_pct = result_pct - value
              '*' => result_pct = result_pct * value
              '/' => result_pct = result_pct / value
              _ => ()
            }
          }
        } else {
          // px and other length units (rem/em/pt/cm/in/...) accumulate in px.
          // Unknown units fall back to the legacy px interpretation.
          let px = match dimension_unit_to_px(value, unit) {
            Some(p) => p
            None => value
          }
          if !have_value {
            result_px = px * leading_factor
            have_value = true
            leading_factor = 1.0
          } else {
            match current_op {
              '+' => result_px = result_px + px
              '-' => result_px = result_px - px
              '*' => result_px = result_px * px
              '/' => result_px = result_px / px
              _ => ()
            }
          }
        }
        remaining = remaining[1:]
      }
      @token.Token::Percentage(value) => {
        if !have_value {
          result_pct = value * leading_factor
          have_value = true
          leading_factor = 1.0
        } else {
          match current_op {
            '+' => result_pct = result_pct + value
            '-' => result_pct = result_pct - value
            '*' => result_pct = result_pct * value
            '/' => result_pct = result_pct / value
            _ => ()
          }
        }
        remaining = remaining[1:]
      }
      @token.Token::Number(value, _) => {
        // Unitless number. After a dimensioned term it scales the running
        // result (`10px * 2`); before the first term it is a leading factor for
        // the next term (`2 * 10px`). Zero has no effect.
        if value != 0.0 {
          match current_op {
            '*' =>
              if have_value {
                result_px = result_px * value
                result_pct = result_pct * value
              } else {
                leading_factor = leading_factor * value
              }
            '/' =>
              if have_value {
                result_px = result_px / value
                result_pct = result_pct / value
              } else {
                leading_factor = leading_factor / value
              }
            _ => if !have_value { leading_factor = leading_factor * value }
          }
        }
        remaining = remaining[1:]
      }
      @token.Token::Delim(c) =>
        if c == '+' || c == '-' || c == '*' || c == '/' {
          current_op = c
          remaining = remaining[1:]
        } else {
          remaining = remaining[1:]
        }
      _ => remaining = remaining[1:]
    }
  }
  None
}

///|
/// Collapse a linear (px, percent_ratio) form to the most specific Dimension.
/// Mirrors the computed engine's lp_to_dimension.
fn lp_to_dim(px : Double, pct : Double) -> @types.Dimension {
  if pct.abs() < 0.0001 {
    @types.Length(px)
  } else if px.abs() < 0.0001 {
    @types.Percent(pct)
  } else {
    @types.Calc(px, pct)
  }
}

///|
/// Reduce a resolvable Dimension to a linear (px, percent_ratio) form. Returns
/// None for forms that cannot be expressed as a single linear term (MathFn,
/// intrinsic keywords, Auto) — matching the computed engine's math_arg_lp.
fn dimension_to_lp(dim : @types.Dimension) -> (Double, Double)? {
  match dim {
    @types.Length(px) => Some((px, 0.0))
    @types.Percent(p) => Some((0.0, p))
    @types.Calc(px, pct) => Some((px, pct))
    _ => None
  }
}

///|
/// Parse a single min()/max()/clamp() argument (one primary: a value token, a
/// calc(), or a nested math function) into a linear (px, percent_ratio) form.
/// Bare arithmetic without a calc() wrapper is not supported (parity with the
/// computed engine). Returns the remaining tokens positioned at the following
/// Comma or RightParen.
fn parse_math_arg_lp(
  tokens : ArrayView[@token.Token],
) -> ((Double, Double), ArrayView[@token.Token])? {
  let toks = skip_whitespace(tokens)
  if toks.length() == 0 {
    return None
  }
  match toks[0] {
    @token.Token::Function(name) => {
      let lname = name.to_lower()
      if lname == "calc" {
        parse_calc_linear(toks[1:])
      } else if lname == "min" || lname == "max" || lname == "clamp" {
        match parse_math_function(lname, toks[1:]) {
          Some((dim, rest)) =>
            match dimension_to_lp(dim) {
              Some(lp) => Some((lp, rest))
              None => None
            }
          None => None
        }
      } else {
        None
      }
    }
    @token.Token::Dimension(value, raw_unit) => {
      let unit = raw_unit.to_lower()
      if unit == "%" {
        Some(((0.0, value / 100.0), toks[1:]))
      } else {
        match dimension_unit_to_px(value, unit) {
          Some(px) => Some(((px, 0.0), toks[1:]))
          // Unknown unit is an invalid length argument; reject it so the result
          // matches the computed engine (rather than the legacy px fallback).
          None => None
        }
      }
    }
    @token.Token::Percentage(value) => Some(((0.0, value / 100.0), toks[1:]))
    @token.Token::Number(value, _) =>
      if value == 0.0 {
        Some(((0.0, 0.0), toks[1:]))
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse min()/max()/clamp() arguments starting right after the Function token,
/// through the matching RightParen. Builds a collapsed Dimension when the
/// arguments share a unit (or there is a single argument) and a MathFn when
/// length and percentage are mixed and need a layout-time basis.
fn parse_math_function(
  op_name : String,
  tokens : ArrayView[@token.Token],
) -> (@types.Dimension, ArrayView[@token.Token])? {
  let op = match op_name {
    "min" => @types.MathOp::Min
    "max" => @types.MathOp::Max
    _ => @types.MathOp::Clamp
  }
  let lps : Array[(Double, Double)] = []
  let mut remaining = tokens
  let mut closed = false
  while !closed {
    remaining = skip_whitespace(remaining)
    if remaining.length() == 0 {
      return None // unterminated function
    }
    // Allow an empty argument list to terminate cleanly (e.g. malformed input).
    match remaining[0] {
      @token.Token::RightParen => {
        remaining = remaining[1:]
        closed = true
        continue
      }
      _ => ()
    }
    match parse_math_arg_lp(remaining) {
      Some((lp, rest)) => {
        lps.push(lp)
        remaining = skip_whitespace(rest)
        if remaining.length() == 0 {
          return None
        }
        match remaining[0] {
          @token.Token::Comma => remaining = remaining[1:]
          @token.Token::RightParen => {
            remaining = remaining[1:]
            closed = true
          }
          _ => return None // unexpected token
        }
      }
      None => return None
    }
  }
  if lps.is_empty() {
    return None
  }
  if op == @types.MathOp::Clamp && lps.length() != 3 {
    return None
  }
  // Single min()/max() argument resolves to that argument verbatim.
  if lps.length() == 1 {
    let (px, pct) = lps[0]
    return Some((lp_to_dim(px, pct), remaining))
  }
  let mut all_px = true
  let mut all_pct = true
  for lp in lps {
    if lp.1 != 0.0 {
      all_px = false
    }
    if lp.0 != 0.0 {
      all_pct = false
    }
  }
  let dim = if all_px {
    @types.Dimension::Length(@types.apply_math_op(op, lps.map(fn(lp) { lp.0 })))
  } else if all_pct {
    @types.Dimension::Percent(@types.apply_math_op(op, lps.map(fn(lp) { lp.1 })))
  } else {
    @types.Dimension::MathFn(op, lps)
  }
  Some((dim, remaining))
}

///|
/// Parse a dimension value (length, percent, auto)
/// viewport units are converted to Length using default viewport (1000x1000)

///|
/// Convert a length unit to pixels using the inline parser's context-free
/// defaults (16px font, 1000×1000 viewport). Mirrors the unit coverage of the
/// computed engine's resolve_dimension. Returns None for non-length units.
fn dimension_unit_to_px(value : Double, unit : String) -> Double? {
  match unit {
    "px" => Some(value)
    "em" | "rem" => Some(value * 16.0)
    // ex/ch approximated as 0.5em, matching the computed engine's default.
    "ex" | "ch" => Some(value * 16.0 * 0.5)
    "vw" | "dvw" | "svw" | "lvw" | "vi" =>
      Some(value * default_viewport_width / 100.0)
    "vh" | "dvh" | "svh" | "lvh" | "vb" =>
      Some(value * default_viewport_height / 100.0)
    "vmin" => {
      let base = if default_viewport_width < default_viewport_height {
        default_viewport_width
      } else {
        default_viewport_height
      }
      Some(value * base / 100.0)
    }
    "vmax" => {
      let base = if default_viewport_width > default_viewport_height {
        default_viewport_width
      } else {
        default_viewport_height
      }
      Some(value * base / 100.0)
    }
    "pt" => Some(value * 96.0 / 72.0)
    "pc" => Some(value * 16.0)
    "in" => Some(value * 96.0)
    "cm" => Some(value * 96.0 / 2.54)
    "mm" => Some(value * 96.0 / 25.4)
    "q" => Some(value * 96.0 / 101.6)
    _ => None
  }
}

///|
fn parse_dimension(tokens : ArrayView[@token.Token]) -> @types.Dimension? {
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Function(name) => {
      let lname = name.to_lower()
      if lname == "calc" {
        // Try to parse and simplify calc expression
        match parse_calc_expression(tokens[1:]) {
          Some((dim, _)) => return Some(dim)
          None => return Some(@types.Auto) // Fallback for complex calc
        }
      } else if lname == "min" || lname == "max" || lname == "clamp" {
        match parse_math_function(lname, tokens[1:]) {
          Some((dim, _)) => return Some(dim)
          None => return Some(@types.Auto) // Fallback for complex/mixed-calc args
        }
      } else {
        None
      }
    }
    @token.Token::Dimension(value, raw_unit) => {
      // CSS units are case-insensitive.
      let unit = raw_unit.to_lower()
      if unit == "%" {
        Some(@types.Percent(value / 100.0))
      } else {
        match dimension_unit_to_px(value, unit) {
          Some(px) => Some(@types.Length(px))
          // Unknown unit: fall back to treating the number as pixels (legacy).
          None => Some(@types.Length(value))
        }
      }
    }
    @token.Token::Percentage(value) => Some(@types.Percent(value / 100.0))
    @token.Token::Number(value, _) =>
      // Unitless 0 is valid
      if value == 0.0 {
        Some(@types.Length(0.0))
      } else {
        None
      }
    @token.Token::Ident(s) =>
      if s == "auto" {
        Some(@types.Auto)
      } else if s == "min-content" {
        Some(@types.MinContent)
      } else if s == "max-content" {
        Some(@types.MaxContent)
      } else if s == "fit-content" {
        // fit-content without argument is equivalent to fit-content(max-content)
        // Using infinity to represent unbounded
        Some(@types.FitContent(1.0e10))
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse multiple dimension values (for shorthand properties like margin, padding)
fn parse_multi_dimensions(
  tokens : ArrayView[@token.Token],
  max_count : Int,
) -> Array[@types.Dimension] {
  let result : Array[@types.Dimension] = []
  let mut rest = tokens
  while result.length() < max_count && rest.length() > 0 {
    // Skip whitespace
    rest = skip_whitespace(rest)
    if rest.length() == 0 {
      break
    }
    // Try to parse a dimension
    match parse_dimension(rest) {
      Some(dim) => {
        result.push(dim)
        rest = rest[1:]
      }
      None => break
    }
  }
  result
}

///|
fn parse_display_keyword(keyword : String) -> @types.Display? {
  if keyword == "block" {
    Some(@types.Block)
  } else if keyword == "inline" {
    Some(@types.Inline)
  } else if keyword == "inline-block" {
    Some(@types.InlineBlock)
  } else if keyword == "flex" {
    Some(@types.Flex)
  } else if keyword == "inline-flex" {
    Some(@types.InlineFlex)
  } else if keyword == "grid" {
    Some(@types.Grid)
  } else if keyword == "inline-grid" {
    Some(@types.InlineGrid)
  } else if keyword == "table" {
    Some(@types.Table)
  } else if keyword == "inline-table" {
    Some(@types.InlineTable)
  } else if keyword == "table-row" {
    Some(@types.TableRow)
  } else if keyword == "table-cell" {
    Some(@types.TableCell)
  } else if keyword == "table-caption" {
    Some(@types.TableCaption)
  } else if keyword == "table-row-group" {
    Some(@types.TableRowGroup)
  } else if keyword == "table-header-group" {
    Some(@types.TableHeaderGroup)
  } else if keyword == "table-footer-group" {
    Some(@types.TableFooterGroup)
  } else if keyword == "table-column" {
    Some(@types.TableColumn)
  } else if keyword == "table-column-group" {
    Some(@types.TableColumnGroup)
  } else if keyword == "none" {
    Some(@types.Display::None)
  } else if keyword == "contents" {
    Some(@types.Contents)
  } else if keyword == "flow-root" {
    Some(@types.FlowRoot)
  } else if keyword == "math" {
    Some(@types.Inline)
  } else {
    None
  }
}

///|
/// Parse display property
fn parse_display(tokens : ArrayView[@token.Token]) -> @types.Display? {
  if tokens.length() == 0 {
    return None
  }
  let keywords : Array[String] = []
  for token in tokens {
    match token {
      @token.Token::Ident(s) => keywords.push(s)
      @token.Token::Whitespace => ()
      _ => return None
    }
  }
  if keywords.length() == 0 {
    return None
  }
  if keywords.length() == 2 {
    let first = keywords[0]
    let second = keywords[1]
    if (first == "math" && second == "inline") ||
      (first == "inline" && second == "math") {
      return Some(@types.Inline)
    }
    if (first == "math" && second == "block") ||
      (first == "block" && second == "math") {
      return Some(@types.Block)
    }
    if (first == "flow-root" && second == "list-item") ||
      (first == "list-item" && second == "flow-root") {
      return Some(@types.FlowRoot)
    }
  }
  if keywords.length() != 1 {
    return None
  }
  parse_display_keyword(keywords[0])
}

///|
/// Parse position property
fn parse_position(tokens : ArrayView[@token.Token]) -> @types.Position? {
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Ident(s) =>
      if s == "relative" {
        Some(@types.Relative)
      } else if s == "absolute" {
        Some(@types.Absolute)
      } else if s == "fixed" {
        Some(@types.Fixed)
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse float property
fn parse_float(
  tokens : ArrayView[@token.Token],
  direction : @style.Direction,
) -> @types.CssFloat? {
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Ident(s) =>
      if s == "left" {
        Some(@types.CssFloat::Left)
      } else if s == "right" {
        Some(@types.CssFloat::Right)
      } else if s == "inline-start" {
        Some(
          if direction.is_rtl() {
            @types.CssFloat::Right
          } else {
            @types.CssFloat::Left
          },
        )
      } else if s == "inline-end" {
        Some(
          if direction.is_rtl() {
            @types.CssFloat::Left
          } else {
            @types.CssFloat::Right
          },
        )
      } else if s == "none" {
        Some(@types.CssFloat::None)
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse clear property
fn parse_clear(
  tokens : ArrayView[@token.Token],
  direction : @style.Direction,
) -> @types.Clear? {
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Ident(s) =>
      if s == "left" {
        Some(@types.Clear::Left)
      } else if s == "right" {
        Some(@types.Clear::Right)
      } else if s == "inline-start" {
        Some(
          if direction.is_rtl() {
            @types.Clear::Right
          } else {
            @types.Clear::Left
          },
        )
      } else if s == "inline-end" {
        Some(
          if direction.is_rtl() {
            @types.Clear::Left
          } else {
            @types.Clear::Right
          },
        )
      } else if s == "both" {
        Some(@types.Clear::Both)
      } else if s == "none" {
        Some(@types.Clear::None)
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse flex-direction property
fn parse_flex_direction(
  tokens : ArrayView[@token.Token],
) -> @types.FlexDirection? {
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Ident(s) =>
      if s == "row" {
        Some(@types.Row)
      } else if s == "row-reverse" {
        Some(@types.RowReverse)
      } else if s == "column" {
        Some(@types.Column)
      } else if s == "column-reverse" {
        Some(@types.ColumnReverse)
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse flex-wrap property
fn parse_flex_wrap(tokens : ArrayView[@token.Token]) -> @types.FlexWrap? {
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Ident(s) =>
      if s == "nowrap" {
        Some(@types.NoWrap)
      } else if s == "wrap" {
        Some(@types.FlexWrap::Wrap)
      } else if s == "wrap-reverse" {
        Some(@types.WrapReverse)
      } else {
        None
      }
    _ => None
  }
}

///|
fn parse_alignment_keyword(value : String) -> @types.Alignment? {
  if value == "flex-start" {
    Some(@types.Alignment::FlexStart)
  } else if value == "start" {
    Some(@types.Alignment::Start)
  } else if value == "flex-end" {
    Some(@types.Alignment::FlexEnd)
  } else if value == "end" {
    Some(@types.Alignment::End)
  } else if value == "left" {
    Some(@types.Alignment::Left)
  } else if value == "right" {
    Some(@types.Alignment::Right)
  } else if value == "center" {
    Some(@types.Alignment::Center)
  } else if value == "space-between" {
    Some(@types.SpaceBetween)
  } else if value == "space-around" {
    Some(@types.SpaceAround)
  } else if value == "space-evenly" {
    Some(@types.SpaceEvenly)
  } else if value == "stretch" {
    Some(@types.Alignment::Stretch)
  } else if value == "baseline" || value == "first baseline" {
    Some(@types.Alignment::Baseline)
  } else if value == "last baseline" {
    Some(@types.Alignment::End)
  } else {
    None
  }
}

///|
fn collect_alignment_parts(tokens : ArrayView[@token.Token]) -> Array[String] {
  let parts : Array[String] = []
  for i = 0; i < tokens.length(); i = i + 1 {
    match tokens[i] {
      @token.Token::Ident(s) => parts.push(s)
      _ => ()
    }
  }
  parts
}

///|
fn parse_alignment_segment_with_overflow(
  parts : Array[String],
  start : Int,
) -> ((@types.Alignment, Bool), Int)? {
  if start >= parts.length() {
    return None
  }

  let mut idx = start
  let mut is_safe = false
  let mut is_unsafe = false
  if parts[idx] == "safe" {
    is_safe = true
    idx = idx + 1
  } else if parts[idx] == "unsafe" {
    is_unsafe = true
    idx = idx + 1
  }
  if idx >= parts.length() {
    return None
  }

  let keyword = if idx + 1 < parts.length() &&
    parts[idx + 1] == "baseline" &&
    (parts[idx] == "first" || parts[idx] == "last") {
    parts[idx] + " baseline"
  } else {
    parts[idx]
  }
  let next_index = if keyword == parts[idx] { idx + 1 } else { idx + 2 }

  match parse_alignment_keyword(keyword) {
    Some(parsed) => {
      let alignment = if is_safe {
        match parsed {
          @types.Alignment::Center
          | @types.Alignment::End
          | @types.Alignment::FlexEnd => @types.Alignment::Start
          _ => parsed
        }
      } else {
        parsed
      }
      Some(((alignment, is_unsafe), next_index))
    }
    None => None
  }
}

///|
fn parse_alignment_segment(
  parts : Array[String],
  start : Int,
) -> (@types.Alignment, Int)? {
  match parse_alignment_segment_with_overflow(parts, start) {
    Some(((alignment, _), next_index)) => Some((alignment, next_index))
    None => None
  }
}

///|
fn parse_alignment_with_overflow(
  tokens : ArrayView[@token.Token],
) -> (@types.Alignment, Bool)? {
  let parts = collect_alignment_parts(tokens)
  match parse_alignment_segment_with_overflow(parts, 0) {
    Some(((alignment, is_unsafe), _)) => Some((alignment, is_unsafe))
    None => None
  }
}

///|
/// Parse alignment property (justify-content, align-items, etc.)
fn parse_alignment(tokens : ArrayView[@token.Token]) -> @types.Alignment? {
  let parts = collect_alignment_parts(tokens)
  match parse_alignment_segment(parts, 0) {
    Some((alignment, _)) => Some(alignment)
    None => None
  }
}

///|
fn parse_place_content_with_overflow(
  tokens : ArrayView[@token.Token],
) -> ((@types.Alignment, Bool), (@types.Alignment, Bool))? {
  let parts = collect_alignment_parts(tokens)
  match parse_alignment_segment_with_overflow(parts, 0) {
    Some((align_content, next_index)) =>
      if next_index >= parts.length() {
        Some((align_content, align_content))
      } else {
        match parse_alignment_segment_with_overflow(parts, next_index) {
          Some((justify_content, _)) => Some((align_content, justify_content))
          None => Some((align_content, align_content))
        }
      }
    None => None
  }
}

///|
/// Parse self-alignment property (align-self, justify-self)
fn parse_self_alignment(tokens : ArrayView[@token.Token]) -> @types.AlignSelf? {
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Ident(s) =>
      if s == "auto" {
        Some(@types.AlignSelf::Auto)
      } else if s == "flex-start" || s == "start" {
        Some(@types.AlignSelf::Start)
      } else if s == "flex-end" || s == "end" {
        Some(@types.AlignSelf::End)
      } else if s == "center" {
        Some(@types.AlignSelf::Center)
      } else if s == "stretch" {
        Some(@types.AlignSelf::Stretch)
      } else if s == "baseline" {
        Some(@types.AlignSelf::Baseline)
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse a numeric value
fn parse_number(tokens : ArrayView[@token.Token]) -> Double? {
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Number(value, _) => Some(value)
    _ => None
  }
}

///|
/// Parse an integer value
fn parse_integer(tokens : ArrayView[@token.Token]) -> Int? {
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Number(value, _) => Some(value.to_int())
    _ => None
  }
}

///|
/// Parse flex shorthand property
/// flex: none | auto |  [] []
/// The values can appear in any order, with these rules:
/// - Numbers are flex-grow (first) and flex-shrink (second)
/// - Dimensions/percentages/keywords are flex-basis
fn parse_flex_shorthand(
  tokens : ArrayView[@token.Token],
) -> (Double, Double, @types.Dimension)? {
  let tokens = skip_whitespace(tokens)
  if tokens.length() == 0 {
    return None
  }

  // Check for keyword values
  match tokens[0] {
    @token.Token::Ident(s) => {
      if s == "none" {
        // flex: none = 0 0 auto
        return Some((0.0, 0.0, @types.Auto))
      }
      if s == "auto" {
        // flex: auto = 1 1 auto
        return Some((1.0, 1.0, @types.Auto))
      }
      if s == "initial" {
        // flex: initial = 0 1 auto
        return Some((0.0, 1.0, @types.Auto))
      }
    }
    _ => ()
  }

  // CSS flex shorthand order:  [] []
  // Numbers must come first, then optionally a basis value.
  // Invalid orderings like "1 0% 1" (basis in middle) must return None.
  let numbers : Array[Double] = []
  let mut basis : @types.Dimension? = None
  let mut tokens_view = tokens
  let mut seen_basis = false

  // First, parse all leading numbers (grow and optionally shrink)
  while tokens_view.length() > 0 {
    tokens_view = skip_whitespace(tokens_view)
    if tokens_view.length() == 0 {
      break
    }
    match tokens_view[0] {
      @token.Token::Number(v, _) => {
        if seen_basis {
          // Number after basis is invalid (e.g., "1 0% 1")
          return None
        }
        numbers.push(v)
        tokens_view = tokens_view[1:]
      }
      @token.Token::Dimension(_, _) | @token.Token::Percentage(_) => {
        // This is flex-basis
        if seen_basis {
          // Second basis value is invalid
          return None
        }
        match parse_dimension(tokens_view) {
          Some(dim) => {
            basis = Some(dim)
            seen_basis = true
            tokens_view = tokens_view[1:]
          }
          None => break
        }
      }
      @token.Token::Ident(s) =>
        // Could be flex-basis keyword (auto, content, etc.)
        if s == "auto" || s == "content" {
          if seen_basis {
            return None
          }
          basis = Some(@types.Auto)
          seen_basis = true
          tokens_view = tokens_view[1:]
        } else {
          break
        }
      _ => break
    }
  }

  // Check for trailing tokens (anything after basis is invalid)
  tokens_view = skip_whitespace(tokens_view)
  if tokens_view.length() > 0 {
    // Still have tokens left - could be invalid like "1 0% 1"
    match tokens_view[0] {
      @token.Token::Number(_, _) =>
        // Number after basis is invalid
        if seen_basis {
          return None
        }
      _ => ()
    }
  }

  // Determine values based on what we found
  let grow = if numbers.length() >= 1 { numbers[0] } else { 1.0 }
  let shrink = if numbers.length() >= 2 { numbers[1] } else { 1.0 }
  let final_basis = match basis {
    Some(b) => b
    None =>
      // CSS spec: when only numbers are specified, basis defaults to 0%.
      if numbers.length() >= 1 {
        @types.Percent(0.0)
      } else {
        @types.Auto
      }
  }
  if numbers.length() == 0 && basis is None {
    return None
  }
  Some((grow, shrink, final_basis))
}

///|
/// Parse flex-flow shorthand property
/// flex-flow:  || 
fn parse_flex_flow_shorthand(
  tokens : ArrayView[@token.Token],
) -> (@types.FlexDirection, @types.FlexWrap)? {
  let tokens = skip_whitespace(tokens)
  if tokens.length() == 0 {
    return None
  }
  let mut direction : @types.FlexDirection? = None
  let mut wrap : @types.FlexWrap? = None

  // Parse up to 2 tokens
  let mut i = 0
  while i < tokens.length() {
    match tokens[i] {
      @token.Token::Ident(s) =>
        // Check if it's a flex-direction value
        if s == "row" {
          direction = Some(@types.FlexDirection::Row)
        } else if s == "row-reverse" {
          direction = Some(@types.FlexDirection::RowReverse)
        } else if s == "column" {
          direction = Some(@types.FlexDirection::Column)
        } else if s == "column-reverse" {
          direction = Some(@types.FlexDirection::ColumnReverse)
          // Check if it's a flex-wrap value
        } else if s == "nowrap" {
          wrap = Some(@types.FlexWrap::NoWrap)
        } else if s == "wrap" {
          wrap = Some(@types.FlexWrap::Wrap)
        } else if s == "wrap-reverse" {
          wrap = Some(@types.FlexWrap::WrapReverse)
        }
      @token.Token::Whitespace => ()
      _ => ()
    }
    i += 1
  }

  // If we found at least one value, return with defaults for the other
  if direction is Some(_) || wrap is Some(_) {
    Some(
      (
        direction.unwrap_or(@types.FlexDirection::Row),
        wrap.unwrap_or(@types.FlexWrap::NoWrap),
      ),
    )
  } else {
    None
  }
}

///|
/// Parse aspect-ratio property (e.g., "16 / 9" or "1.5")
fn parse_aspect_ratio(tokens : ArrayView[@token.Token]) -> Double? {
  if tokens.length() == 0 {
    return None
  }
  let tokens = skip_whitespace(tokens)
  match tokens[0] {
    @token.Token::Number(value, _) => {
      // Check for "x / y" format
      let rest = skip_whitespace(tokens[1:])
      if rest.length() >= 2 {
        match (rest[0], skip_whitespace(rest[1:])) {
          (@token.Token::Delim('/'), rest2) =>
            if rest2.length() > 0 {
              match rest2[0] {
                @token.Token::Number(divisor, _) =>
                  if divisor != 0.0 {
                    return Some(value / divisor)
                  } else {
                    return Some(value)
                  }
                _ => return Some(value)
              }
            } else {
              return Some(value)
            }
          _ => return Some(value)
        }
      }
      Some(value)
    }
    _ => None
  }
}

///|
/// Parse grid-auto-flow property
fn parse_grid_auto_flow(
  tokens : ArrayView[@token.Token],
) -> @types.GridAutoFlow? {
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Ident(s) =>
      if s == "row" {
        Some(@types.GridAutoFlow::Row)
      } else if s == "column" {
        Some(@types.GridAutoFlow::Column)
      } else if s == "row dense" {
        Some(@types.RowDense)
      } else if s == "column dense" {
        Some(@types.ColumnDense)
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse a single track value for MinTrackSizing
fn parse_min_track_sizing(
  tokens : ArrayView[@token.Token],
) -> (@types.MinTrackSizing, ArrayView[@token.Token])? {
  let tokens = skip_whitespace(tokens)
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Dimension(value, unit) =>
      if unit == "px" {
        Some((@types.MinTrackSizing::Length(value), tokens[1:]))
      } else if unit == "%" {
        Some((@types.MinTrackSizing::Percent(value / 100.0), tokens[1:]))
      } else if is_viewport_width_unit(unit) {
        Some(
          (
            @types.MinTrackSizing::Length(
              value * default_viewport_width / 100.0,
            ),
            tokens[1:],
          ),
        )
      } else if is_viewport_height_unit(unit) {
        Some(
          (
            @types.MinTrackSizing::Length(
              value * default_viewport_height / 100.0,
            ),
            tokens[1:],
          ),
        )
      } else {
        // rem/em and physical units (pt/cm/in/...) are converted to px.
        let px = match dimension_unit_to_px(value, unit.to_lower()) {
          Some(p) => p
          None => value
        }
        Some((@types.MinTrackSizing::Length(px), tokens[1:]))
      }
    @token.Token::Percentage(value) =>
      Some((@types.MinTrackSizing::Percent(value / 100.0), tokens[1:]))
    @token.Token::Number(value, _) =>
      if value == 0.0 {
        Some((@types.MinTrackSizing::Length(0.0), tokens[1:]))
      } else {
        None
      }
    @token.Token::Ident(s) =>
      if s == "auto" {
        Some((@types.MinTrackSizing::Auto, tokens[1:]))
      } else if s == "min-content" {
        Some((@types.MinTrackSizing::MinContent, tokens[1:]))
      } else if s == "max-content" {
        Some((@types.MinTrackSizing::MaxContent, tokens[1:]))
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse a single track value for MaxTrackSizing
fn parse_max_track_sizing(
  tokens : ArrayView[@token.Token],
) -> (@types.MaxTrackSizing, ArrayView[@token.Token])? {
  let tokens = skip_whitespace(tokens)
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Dimension(value, unit) =>
      if unit == "px" {
        Some((@types.MaxTrackSizing::Length(value), tokens[1:]))
      } else if unit == "%" {
        Some((@types.MaxTrackSizing::Percent(value / 100.0), tokens[1:]))
      } else if unit == "fr" {
        Some((@types.MaxTrackSizing::Fr(value), tokens[1:]))
      } else if is_viewport_width_unit(unit) {
        Some(
          (
            @types.MaxTrackSizing::Length(
              value * default_viewport_width / 100.0,
            ),
            tokens[1:],
          ),
        )
      } else if is_viewport_height_unit(unit) {
        Some(
          (
            @types.MaxTrackSizing::Length(
              value * default_viewport_height / 100.0,
            ),
            tokens[1:],
          ),
        )
      } else {
        // rem/em and physical units (pt/cm/in/...) are converted to px.
        let px = match dimension_unit_to_px(value, unit.to_lower()) {
          Some(p) => p
          None => value
        }
        Some((@types.MaxTrackSizing::Length(px), tokens[1:]))
      }
    @token.Token::Percentage(value) =>
      Some((@types.MaxTrackSizing::Percent(value / 100.0), tokens[1:]))
    @token.Token::Number(value, _) =>
      if value == 0.0 {
        Some((@types.MaxTrackSizing::Length(0.0), tokens[1:]))
      } else {
        None
      }
    @token.Token::Ident(s) =>
      if s == "auto" {
        Some((@types.MaxTrackSizing::Auto, tokens[1:]))
      } else if s == "min-content" {
        Some((@types.MaxTrackSizing::MinContent, tokens[1:]))
      } else if s == "max-content" {
        Some((@types.MaxTrackSizing::MaxContent, tokens[1:]))
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse the single `` argument of `fit-content( ... )`,
/// starting right after the `Function("fit-content")` token. Returns the value,
/// whether it is a percentage (ratio) rather than a px length, and the tokens
/// after the matching RightParen. Length units are converted to px.
fn parse_fit_content_arg(
  tokens : ArrayView[@token.Token],
) -> (Double, Bool, ArrayView[@token.Token])? {
  let rest = skip_whitespace(tokens)
  if rest.length() == 0 {
    return None
  }
  let (val, is_pct, rest2) : (Double, Bool, ArrayView[@token.Token]) = match
    rest[0] {
    @token.Token::Dimension(v, raw_unit) => {
      let unit = raw_unit.to_lower()
      if unit == "%" {
        (v / 100.0, true, rest[1:])
      } else {
        let px = match dimension_unit_to_px(v, unit) {
          Some(p) => p
          None => v
        }
        (px, false, rest[1:])
      }
    }
    @token.Token::Percentage(v) => (v / 100.0, true, rest[1:])
    @token.Token::Number(v, _) =>
      if v == 0.0 {
        (0.0, false, rest[1:])
      } else {
        return None
      }
    _ => return None
  }
  let rest3 = skip_whitespace(rest2)
  if rest3.length() == 0 {
    return None
  }
  match rest3[0] {
    @token.Token::RightParen => Some((val, is_pct, rest3[1:]))
    _ => None
  }
}

///|
/// Parse a single track sizing value (without repeat)
fn parse_single_track_sizing(
  tokens : ArrayView[@token.Token],
) -> (@types.SingleTrackSizing, ArrayView[@token.Token])? {
  let tokens = skip_whitespace(tokens)
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Dimension(value, unit) =>
      if unit == "px" {
        Some((@types.SingleTrackSizing::Length(value), tokens[1:]))
      } else if unit == "%" {
        Some((@types.SingleTrackSizing::Percent(value / 100.0), tokens[1:]))
      } else if unit == "fr" {
        Some((@types.SingleTrackSizing::Fr(value), tokens[1:]))
      } else {
        // rem/em and physical units (pt/cm/in/...) are converted to px.
        let px = match dimension_unit_to_px(value, unit.to_lower()) {
          Some(p) => p
          None => value
        }
        Some((@types.SingleTrackSizing::Length(px), tokens[1:]))
      }
    @token.Token::Percentage(value) =>
      Some((@types.SingleTrackSizing::Percent(value / 100.0), tokens[1:]))
    @token.Token::Number(value, _) =>
      if value == 0.0 {
        Some((@types.SingleTrackSizing::Length(0.0), tokens[1:]))
      } else {
        None
      }
    @token.Token::Ident(s) =>
      if s == "auto" {
        Some((@types.SingleTrackSizing::Auto, tokens[1:]))
      } else if s == "min-content" {
        Some((@types.SingleTrackSizing::MinContent, tokens[1:]))
      } else if s == "max-content" {
        Some((@types.SingleTrackSizing::MaxContent, tokens[1:]))
      } else {
        None
      }
    @token.Token::Function(name) =>
      if name == "minmax" {
        // Parse minmax(min, max)
        let rest = tokens[1:] // skip Function token (already consumed '(')
        match parse_min_track_sizing(rest) {
          Some((min_val, rest2)) => {
            // Skip whitespace and comma
            let rest3 = skip_whitespace(rest2)
            if rest3.length() == 0 {
              return None
            }
            let rest4 = match rest3[0] {
              @token.Token::Comma => rest3[1:]
              _ => rest3
            }
            match parse_max_track_sizing(rest4) {
              Some((max_val, rest5)) => {
                // Skip to closing paren
                let rest6 = skip_whitespace(rest5)
                if rest6.length() > 0 {
                  match rest6[0] {
                    @token.Token::RightParen => {
                      let next_tokens = rest6[1:]
                      Some(
                        (
                          @types.SingleTrackSizing::MinMax(min_val, max_val),
                          next_tokens,
                        ),
                      )
                    }
                    _ => None
                  }
                } else {
                  None
                }
              }
              None => None
            }
          }
          None => None
        }
      } else if name == "fit-content" {
        match parse_fit_content_arg(tokens[1:]) {
          Some((v, true, rest)) =>
            Some((@types.SingleTrackSizing::FitContentPercent(v), rest))
          Some((v, false, rest)) =>
            Some((@types.SingleTrackSizing::FitContentLength(v), rest))
          None => None
        }
      } else if name == "calc" {
        match parse_calc_linear(tokens[1:]) {
          Some(((px, pct), rest)) =>
            if pct.abs() < 0.0001 {
              Some((@types.SingleTrackSizing::Length(px), rest))
            } else if px.abs() < 0.0001 {
              Some((@types.SingleTrackSizing::Percent(pct), rest))
            } else {
              // Mixed length+percentage calc cannot be represented as a single
              // track breadth; fall back to auto but keep parsing the rest.
              Some((@types.SingleTrackSizing::Auto, rest))
            }
          None => None
        }
      } else if name == "min" || name == "max" || name == "clamp" {
        match parse_math_function(name, tokens[1:]) {
          Some((dim, rest)) => {
            let tf = match dim {
              @types.Length(v) => @types.SingleTrackSizing::Length(v)
              @types.Percent(v) => @types.SingleTrackSizing::Percent(v)
              _ => @types.SingleTrackSizing::Auto
            }
            Some((tf, rest))
          }
          None => None
        }
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse a track sizing function (including repeat)
fn parse_track_sizing_function(
  tokens : ArrayView[@token.Token],
) -> (@types.TrackSizingFunction, ArrayView[@token.Token])? {
  let tokens = skip_whitespace(tokens)
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Function(name) =>
      if name == "repeat" {
        // Parse repeat(count, tracks...)
        let rest = tokens[1:]
        let rest = skip_whitespace(rest)
        if rest.length() == 0 {
          return None
        }
        // Parse repeat count
        let (count, rest2) : (@types.RepeatCount, ArrayView[@token.Token]) = match
          rest[0] {
          @token.Token::Number(value, _) =>
            (@types.RepeatCount::Count(value.to_int()), rest[1:])
          @token.Token::Ident(s) =>
            if s == "auto-fill" {
              (@types.RepeatCount::AutoFill, rest[1:])
            } else if s == "auto-fit" {
              (@types.RepeatCount::AutoFit, rest[1:])
            } else {
              return None
            }
          _ => return None
        }
        // Skip comma
        let rest3 = skip_whitespace(rest2)
        if rest3.length() == 0 {
          return None
        }
        let rest4 = match rest3[0] {
          @token.Token::Comma => rest3[1:]
          _ => rest3
        }
        // Parse track list until closing paren, preserving the line names
        // declared within one repetition (index = line, length = tracks + 1).
        let tracks : Array[@types.SingleTrackSizing] = []
        let line_names : Array[Array[String]] = []
        let mut remaining = rest4
        let mut done = false
        while !done {
          let (names, after_names) = capture_grid_line_names(remaining)
          remaining = after_names
          if remaining.length() == 0 {
            line_names.push(names)
            done = true
          } else {
            match remaining[0] {
              @token.Token::RightParen => {
                line_names.push(names)
                remaining = remaining[1:]
                done = true
              }
              _ =>
                match parse_single_track_sizing(remaining) {
                  Some((track, rest)) => {
                    line_names.push(names)
                    tracks.push(track)
                    remaining = rest
                  }
                  None => {
                    line_names.push(names)
                    done = true
                  }
                }
            }
          }
        }
        if tracks.length() > 0 {
          Some(
            (
              @types.TrackSizingFunction::Repeat(count, tracks, line_names),
              remaining,
            ),
          )
        } else {
          None
        }
      } else if name == "minmax" {
        // minmax is also valid as TrackSizingFunction
        match parse_single_track_sizing(tokens) {
          Some((@types.SingleTrackSizing::MinMax(min_val, max_val), rest)) =>
            Some((@types.TrackSizingFunction::MinMax(min_val, max_val), rest))
          _ => None
        }
      } else if name == "fit-content" {
        match parse_fit_content_arg(tokens[1:]) {
          Some((v, true, rest)) =>
            Some((@types.TrackSizingFunction::FitContentPercent(v), rest))
          Some((v, false, rest)) =>
            Some((@types.TrackSizingFunction::FitContentLength(v), rest))
          None => None
        }
      } else if name == "calc" {
        match parse_calc_linear(tokens[1:]) {
          Some(((px, pct), rest)) =>
            if pct.abs() < 0.0001 {
              Some((@types.TrackSizingFunction::Length(px), rest))
            } else if px.abs() < 0.0001 {
              Some((@types.TrackSizingFunction::Percent(pct), rest))
            } else {
              // Mixed length+percentage calc cannot be represented as a single
              // track breadth; fall back to auto but keep parsing the rest.
              Some((@types.TrackSizingFunction::Auto, rest))
            }
          None => None
        }
      } else if name == "min" || name == "max" || name == "clamp" {
        match parse_math_function(name, tokens[1:]) {
          Some((dim, rest)) => {
            let tf = match dim {
              @types.Length(v) => @types.TrackSizingFunction::Length(v)
              @types.Percent(v) => @types.TrackSizingFunction::Percent(v)
              _ => @types.TrackSizingFunction::Auto
            }
            Some((tf, rest))
          }
          None => None
        }
      } else {
        None
      }
    @token.Token::Dimension(value, unit) =>
      if unit == "px" {
        Some((@types.TrackSizingFunction::Length(value), tokens[1:]))
      } else if unit == "%" {
        Some((@types.TrackSizingFunction::Percent(value / 100.0), tokens[1:]))
      } else if unit == "fr" {
        Some((@types.TrackSizingFunction::Fr(value), tokens[1:]))
      } else {
        // rem/em and physical units (pt/cm/in/...) are converted to px.
        let px = match dimension_unit_to_px(value, unit.to_lower()) {
          Some(p) => p
          None => value
        }
        Some((@types.TrackSizingFunction::Length(px), tokens[1:]))
      }
    @token.Token::Percentage(value) =>
      Some((@types.TrackSizingFunction::Percent(value / 100.0), tokens[1:]))
    @token.Token::Number(value, _) =>
      if value == 0.0 {
        Some((@types.TrackSizingFunction::Length(0.0), tokens[1:]))
      } else {
        None
      }
    @token.Token::Ident(s) =>
      if s == "auto" {
        Some((@types.TrackSizingFunction::Auto, tokens[1:]))
      } else if s == "min-content" {
        Some((@types.TrackSizingFunction::MinContent, tokens[1:]))
      } else if s == "max-content" {
        Some((@types.TrackSizingFunction::MaxContent, tokens[1:]))
      } else if s == "none" {
        // none means empty template
        None
      } else {
        None
      }
    _ => None
  }
}

///|
/// Capture the grid line names declared in `[name1 name2 ...]` bracket groups at
/// the current position. Returns the collected names (lowercased, possibly
/// empty) and the remaining tokens after the brackets. Consecutive bracket
/// groups at the same line are merged.
fn capture_grid_line_names(
  tokens : ArrayView[@token.Token],
) -> (Array[String], ArrayView[@token.Token]) {
  let names : Array[String] = []
  let mut remaining = skip_whitespace(tokens)
  while remaining.length() > 0 {
    match remaining[0] {
      @token.Token::LeftBracket => {
        let mut i = 1
        while i < remaining.length() {
          match remaining[i] {
            @token.Token::RightBracket => {
              i = i + 1
              break
            }
            @token.Token::Ident(s) => {
              names.push(s.to_lower())
              i = i + 1
            }
            _ => i = i + 1
          }
        }
        remaining = skip_whitespace(remaining[i:])
      }
      _ => return (names, remaining)
    }
  }
  (names, remaining)
}

///|
/// Parse grid-template-columns or grid-template-rows value, preserving named
/// lines. Returns the track sizing functions and the line names (index = grid
/// line, length = tracks + 1).
fn parse_grid_template_track_list(
  tokens : ArrayView[@token.Token],
) -> (Array[@types.TrackSizingFunction], Array[Array[String]]) {
  let result : Array[@types.TrackSizingFunction] = []
  let line_names : Array[Array[String]] = []
  let mut remaining = tokens
  let mut done = false
  while !done {
    let (names, rest) = capture_grid_line_names(remaining)
    remaining = rest
    let stop = if remaining.length() == 0 {
      true
    } else {
      match remaining[0] {
        @token.Token::Semicolon | @token.Token::EOF => true
        @token.Token::Ident("none") =>
          // `none` means empty template: no tracks, no line names.
          return ([], [])
        _ =>
          match parse_track_sizing_function(remaining) {
            Some((track, rest2)) => {
              result.push(track)
              remaining = rest2
              false
            }
            None => true
          }
      }
    }
    if stop {
      // `names` belongs to the final grid line.
      line_names.push(names)
      done = true
    } else {
      // `names` belongs to the line before the track we just pushed.
      line_names.push(names)
    }
  }
  (result, line_names)
}

///|
/// Parse grid-template-columns or grid-template-rows value (tracks only).
fn parse_grid_template_tracks(
  tokens : ArrayView[@token.Token],
) -> Array[@types.TrackSizingFunction] {
  parse_grid_template_track_list(tokens).0
}

///|
/// Parse the `` that follows `subgrid`. Unlike a track list,
/// each `[...]` bracket group is a SEPARATE grid line, so names are not merged.
/// `repeat(...)` inside the list is not yet expanded and is skipped.
fn parse_subgrid_line_names(
  tokens : ArrayView[@token.Token],
) -> Array[Array[String]] {
  let result : Array[Array[String]] = []
  let mut remaining = skip_whitespace(tokens)
  while remaining.length() > 0 {
    match remaining[0] {
      @token.Token::LeftBracket => {
        let names : Array[String] = []
        let mut i = 1
        while i < remaining.length() {
          match remaining[i] {
            @token.Token::RightBracket => {
              i = i + 1
              break
            }
            @token.Token::Ident(s) => {
              names.push(s.to_lower())
              i = i + 1
            }
            _ => i = i + 1
          }
        }
        result.push(names)
        remaining = skip_whitespace(remaining[i:])
      }
      @token.Token::Semicolon | @token.Token::EOF => break
      // Skip anything else (e.g. an unsupported repeat()) without aborting.
      _ => remaining = skip_whitespace(remaining[1:])
    }
  }
  result
}

///|
/// Parse one grid template axis (grid-template-columns / -rows), detecting the
/// `subgrid` and `masonry` keywords. Returns the axis kind, the explicit track
/// sizing functions, and the line names.
fn parse_grid_template_axis(
  tokens : ArrayView[@token.Token],
) -> (
  @types.GridTemplateKind,
  Array[@types.TrackSizingFunction],
  Array[Array[String]],
) {
  let toks = skip_whitespace(tokens)
  if toks.length() > 0 {
    match toks[0] {
      @token.Token::Ident("subgrid") =>
        return (@types.Subgrid, [], parse_subgrid_line_names(toks[1:]))
      @token.Token::Ident("masonry") => return (@types.Masonry, [], [])
      _ => ()
    }
  }
  let (tracks, names) = parse_grid_template_track_list(toks)
  (@types.Explicit, tracks, names)
}

///|
/// Parse grid-template shorthand: ` / `, preserving named lines.
fn parse_grid_template_shorthand(
  tokens : ArrayView[@token.Token],
) -> (
  Array[@types.TrackSizingFunction],
  Array[Array[String]],
  Array[@types.TrackSizingFunction],
  Array[Array[String]],
)? {
  let mut slash_index = -1
  for i = 0; i < tokens.length(); i = i + 1 {
    match tokens[i] {
      @token.Token::Delim('/') => {
        slash_index = i
        break
      }
      _ => ()
    }
  }
  if slash_index < 0 {
    return None
  }
  let (rows, row_names) = parse_grid_template_track_list(tokens[:slash_index])
  let (cols, col_names) = parse_grid_template_track_list(tokens[slash_index + 1:])
  Some((rows, row_names, cols, col_names))
}

///|
/// Parse the `grid-template` / `grid` "areas" form:
///   [ ?  ? ? ]+ [ /  ]?
/// Returns the areas (one string per row), the row track sizes and row line
/// names, and the column track sizes and column line names. Returns None when
/// the value is not the areas form (no  tokens present).
fn parse_grid_template_areas_form(
  tokens : ArrayView[@token.Token],
) -> (
  Array[String],
  Array[@types.TrackSizingFunction],
  Array[Array[String]],
  Array[@types.TrackSizingFunction],
  Array[Array[String]],
)? {
  // Split off the optional `/ ` part at the top-level slash.
  let mut slash_index = -1
  let mut has_string = false
  for i = 0; i < tokens.length(); i = i + 1 {
    match tokens[i] {
      @token.Token::Delim('/') => if slash_index < 0 { slash_index = i }
      @token.Token::String(_) => has_string = true
      _ => ()
    }
  }
  if !has_string {
    return None
  }
  let left = if slash_index < 0 { tokens } else { tokens[:slash_index] }
  let areas : Array[String] = []
  let rows : Array[@types.TrackSizingFunction] = []
  let row_names : Array[Array[String]] = []
  let mut remaining = left
  let mut done = false
  while !done {
    let (names, rest) = capture_grid_line_names(remaining)
    remaining = rest
    if remaining.length() == 0 {
      row_names.push(names)
      done = true
    } else {
      match remaining[0] {
        @token.Token::String(s) => {
          row_names.push(names)
          areas.push(s)
          remaining = remaining[1:]
          // Optional row track size after the string.
          let after = skip_whitespace(remaining)
          match parse_track_sizing_function(after) {
            Some((track, rest2)) => {
              rows.push(track)
              remaining = rest2
            }
            None => rows.push(@types.TrackSizingFunction::Auto)
          }
        }
        _ => {
          row_names.push(names)
          done = true
        }
      }
    }
  }
  let (cols, col_names) = if slash_index < 0 {
    ([], [])
  } else {
    parse_grid_template_track_list(tokens[slash_index + 1:])
  }
  Some((areas, rows, row_names, cols, col_names))
}

///|
/// Whether a side of the `grid` shorthand contains the `auto-flow` keyword.
fn grid_side_has_autoflow(tokens : ArrayView[@token.Token]) -> Bool {
  for i = 0; i < tokens.length(); i = i + 1 {
    match tokens[i] {
      @token.Token::Ident("auto-flow") => return true
      _ => ()
    }
  }
  false
}

///|
/// Parse the `[ auto-flow && dense? ] ?` side of a `grid` shorthand.
/// Returns the resolved auto-flow direction and the implicit (auto) track list.
fn parse_grid_autoflow_side(
  tokens : ArrayView[@token.Token],
  is_column : Bool,
) -> (@types.GridAutoFlow, Array[@types.TrackSizingFunction]) {
  let mut dense = false
  let mut remaining = skip_whitespace(tokens)
  let mut consuming = true
  while consuming && remaining.length() > 0 {
    match remaining[0] {
      @token.Token::Ident("auto-flow") => remaining = skip_whitespace(remaining[1:])
      @token.Token::Ident("dense") => {
        dense = true
        remaining = skip_whitespace(remaining[1:])
      }
      _ => consuming = false
    }
  }
  let flow = if is_column {
    if dense {
      @types.GridAutoFlow::ColumnDense
    } else {
      @types.GridAutoFlow::Column
    }
  } else if dense {
    @types.GridAutoFlow::RowDense
  } else {
    @types.GridAutoFlow::Row
  }
  let (tracks, _) = parse_grid_template_track_list(remaining)
  (flow, tracks)
}

///|
/// Parse the `grid` shorthand auto-flow form:
///    / [ auto-flow && dense? ] ?
///   [ auto-flow && dense? ] ? / 
/// Returns (auto-flow, template-rows, row-names, template-cols, col-names,
/// auto-rows, auto-cols). Returns None when neither side has `auto-flow`.
fn parse_grid_autoflow_form(
  tokens : ArrayView[@token.Token],
) -> (
  @types.GridAutoFlow,
  Array[@types.TrackSizingFunction],
  Array[Array[String]],
  Array[@types.TrackSizingFunction],
  Array[Array[String]],
  Array[@types.TrackSizingFunction],
  Array[@types.TrackSizingFunction],
)? {
  let mut slash = -1
  for i = 0; i < tokens.length(); i = i + 1 {
    match tokens[i] {
      @token.Token::Delim('/') => {
        slash = i
        break
      }
      _ => ()
    }
  }
  if slash < 0 {
    return None
  }
  let left = tokens[:slash]
  let right = tokens[slash + 1:]
  if grid_side_has_autoflow(right) {
    // rows are explicit (left); columns flow automatically (right).
    let (rows, row_names) = parse_grid_template_track_list(left)
    let (flow, auto_cols) = parse_grid_autoflow_side(right, true)
    Some((flow, rows, row_names, [], [], [], auto_cols))
  } else if grid_side_has_autoflow(left) {
    // rows flow automatically (left); columns are explicit (right).
    let (cols, col_names) = parse_grid_template_track_list(right)
    let (flow, auto_rows) = parse_grid_autoflow_side(left, false)
    Some((flow, [], [], cols, col_names, auto_rows, []))
  } else {
    None
  }
}

///|
/// Parse the `grid` shorthand auto-flow form from a raw string.
pub fn parse_grid_autoflow_form_from_string(
  value : String,
) -> (
  @types.GridAutoFlow,
  Array[@types.TrackSizingFunction],
  Array[Array[String]],
  Array[@types.TrackSizingFunction],
  Array[Array[String]],
  Array[@types.TrackSizingFunction],
  Array[@types.TrackSizingFunction],
)? {
  let tokens = @token.tokenize(value)
  parse_grid_autoflow_form(tokens[:])
}

///|
/// Classify one `` token slice into a GridPlacement. Handles
///   auto |  |  && ? |
///   span && ? && ?
/// (order-independent; CSS keywords are case-insensitive).
fn classify_grid_line(tokens : ArrayView[@token.Token]) -> @types.GridPlacement {
  let mut is_span = false
  let mut has_auto = false
  let mut line = 0
  let mut name : String? = None
  for i = 0; i < tokens.length(); i = i + 1 {
    match tokens[i] {
      @token.Token::Ident(s) => {
        let ls = s.to_lower()
        if ls == "auto" {
          has_auto = true
        } else if ls == "span" {
          is_span = true
        } else {
          name = Some(ls)
        }
      }
      @token.Token::Number(v, _) => {
        let n = v.to_int()
        if n != 0 {
          line = n
        }
      }
      _ => ()
    }
  }
  if is_span {
    let count = if line != 0 { line } else { 1 }
    match name {
      Some(nm) => @types.GridPlacement::SpanNamed(count, nm)
      None => @types.GridPlacement::Span(count)
    }
  } else if has_auto {
    @types.GridPlacement::Auto
  } else {
    match name {
      Some(nm) =>
        if line != 0 {
          @types.GridPlacement::NamedLine(line, nm)
        } else {
          @types.GridPlacement::Named(nm)
        }
      None => if line != 0 { @types.GridPlacement::Line(line) } else { @types.GridPlacement::Auto }
    }
  }
}

///|
/// Split a token view into segments separated by top-level `/` delimiters.
fn split_grid_segments(
  tokens : ArrayView[@token.Token],
) -> Array[ArrayView[@token.Token]] {
  let segments : Array[ArrayView[@token.Token]] = []
  let mut start = 0
  for i = 0; i < tokens.length(); i = i + 1 {
    match tokens[i] {
      @token.Token::Delim('/') => {
        segments.push(tokens[start:i])
        start = i + 1
      }
      _ => ()
    }
  }
  segments.push(tokens[start:])
  segments
}

///|
/// Whether a token segment carries any meaningful (non-whitespace/EOF) token.
fn grid_segment_is_empty(tokens : ArrayView[@token.Token]) -> Bool {
  for i = 0; i < tokens.length(); i = i + 1 {
    match tokens[i] {
      @token.Token::Whitespace | @token.Token::EOF => ()
      _ => return false
    }
  }
  true
}

///|
/// Parse a grid-column / grid-row value (` [ /  ]?`) from tokens.
/// A single bare `` is copied to both start and end (implicit
/// named area); any other single value leaves the end as auto.
fn parse_grid_line_shorthand_tokens(
  tokens : ArrayView[@token.Token],
) -> (@types.GridPlacement, @types.GridPlacement) {
  let segments = split_grid_segments(tokens)
  if segments.length() >= 2 {
    (classify_grid_line(segments[0]), classify_grid_line(segments[1]))
  } else {
    let start = classify_grid_line(tokens)
    match start {
      @types.GridPlacement::Named(_) => (start, start)
      _ => (start, @types.GridPlacement::Auto)
    }
  }
}

///|
/// Parse a grid-area value (` [ /  [ / 
/// [ /  ]?]?]?`) from tokens, filling omitted longhands with auto
/// (a single value applies to both start longhands).
fn parse_grid_area_shorthand_tokens(
  tokens : ArrayView[@token.Token],
) -> (
  @types.GridPlacement,
  @types.GridPlacement,
  @types.GridPlacement,
  @types.GridPlacement,
) {
  let parts = split_grid_segments(tokens).filter(fn(s) {
    !grid_segment_is_empty(s)
  })
  let auto = @types.GridPlacement::Auto
  if parts.length() >= 4 {
    (
      classify_grid_line(parts[0]),
      classify_grid_line(parts[1]),
      classify_grid_line(parts[2]),
      classify_grid_line(parts[3]),
    )
  } else if parts.length() == 3 {
    (
      classify_grid_line(parts[0]),
      classify_grid_line(parts[1]),
      classify_grid_line(parts[2]),
      auto,
    )
  } else if parts.length() == 2 {
    (classify_grid_line(parts[0]), classify_grid_line(parts[1]), auto, auto)
  } else if parts.length() == 1 {
    let start = classify_grid_line(parts[0])
    (start, start, auto, auto)
  } else {
    (auto, auto, auto, auto)
  }
}

///|
/// If the value is a single `` (no slash, not a number / `auto` /
/// `span` / global keyword), return that lowercased ident as a grid-area name.
/// Otherwise return None (the value is a line-based placement).
fn grid_area_single_name(tokens : ArrayView[@token.Token]) -> String? {
  let mut found : String? = None
  for i = 0; i < tokens.length(); i = i + 1 {
    match tokens[i] {
      @token.Token::Whitespace | @token.Token::EOF => ()
      @token.Token::Ident(s) => {
        let ls = s.to_lower()
        if ls == "auto" ||
          ls == "span" ||
          ls == "none" ||
          ls == "inherit" ||
          ls == "initial" ||
          ls == "unset" {
          return None
        }
        // More than one meaningful token => not a bare ident.
        match found {
          Some(_) => return None
          None => found = Some(ls)
        }
      }
      // Any number, slash, or other token means this is not a bare ident.
      _ => return None
    }
  }
  found
}

///|
/// Parse a single `` from a raw string.
pub fn parse_grid_placement_from_string(
  value : String,
) -> @types.GridPlacement {
  classify_grid_line(@token.tokenize(value)[:])
}

///|
/// Parse a grid-column / grid-row shorthand from a raw string.
pub fn parse_grid_line_shorthand_from_string(
  value : String,
) -> (@types.GridPlacement, @types.GridPlacement) {
  parse_grid_line_shorthand_tokens(@token.tokenize(value)[:])
}

///|
/// Parse a grid-area shorthand from a raw string.
pub fn parse_grid_area_shorthand_from_string(
  value : String,
) -> (
  @types.GridPlacement,
  @types.GridPlacement,
  @types.GridPlacement,
  @types.GridPlacement,
) {
  parse_grid_area_shorthand_tokens(@token.tokenize(value)[:])
}

///|
/// Parse grid-template-columns or grid-template-rows from a raw string
pub fn parse_grid_template_tracks_from_string(
  value : String,
) -> Array[@types.TrackSizingFunction] {
  let tokens = @token.tokenize(value)
  parse_grid_template_tracks(tokens[:])
}

///|
/// Parse grid-template-columns or grid-template-rows from a raw string,
/// returning both the track sizing functions and the named lines (index = grid
/// line, length = tracks + 1).
pub fn parse_grid_template_track_list_from_string(
  value : String,
) -> (Array[@types.TrackSizingFunction], Array[Array[String]]) {
  let tokens = @token.tokenize(value)
  parse_grid_template_track_list(tokens[:])
}

///|
/// Parse the `grid-template` / `grid` areas form from a raw string.
/// Returns (areas, rows, row_line_names, cols, col_line_names) or None when the
/// value is not the areas form.
pub fn parse_grid_template_areas_form_from_string(
  value : String,
) -> (
  Array[String],
  Array[@types.TrackSizingFunction],
  Array[Array[String]],
  Array[@types.TrackSizingFunction],
  Array[Array[String]],
)? {
  let tokens = @token.tokenize(value)
  parse_grid_template_areas_form(tokens[:])
}

///|
/// Parse one grid template axis from a raw string, detecting subgrid/masonry.
/// Returns the axis kind, the explicit tracks, and the line names.
pub fn parse_grid_template_axis_from_string(
  value : String,
) -> (
  @types.GridTemplateKind,
  Array[@types.TrackSizingFunction],
  Array[Array[String]],
) {
  let tokens = @token.tokenize(value)
  parse_grid_template_axis(tokens[:])
}

///|
/// Parse overflow property
fn parse_overflow(tokens : ArrayView[@token.Token]) -> @types.Overflow? {
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Ident(s) =>
      if s == "visible" {
        Some(@types.Visible)
      } else if s == "hidden" {
        Some(@types.Hidden)
      } else if s == "clip" {
        Some(@types.Clip)
      } else if s == "scroll" {
        Some(@types.Scroll)
      } else if s == "auto" {
        Some(@types.Overflow::Auto)
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse clip property (legacy, for accessibility patterns)
fn parse_clip(tokens : ArrayView[@token.Token]) -> @types.ClipRect {
  if tokens.length() == 0 {
    return @types.ClipRect::Auto
  }
  match tokens[0] {
    @token.Token::Ident(s) =>
      if s == "auto" || s == "initial" || s == "unset" {
        @types.ClipRect::Auto
      } else {
        @types.ClipRect::Auto
      }
    @token.Token::Function(name) =>
      if name == "rect" {
        // Parse rect(top, right, bottom, left)
        // tokens[1:] contains the arguments
        let args = tokens[1:]
        let values : Array[Double] = []
        for token in args {
          match token {
            @token.Token::Dimension(v, unit) => {
              // rem/em and physical length units are converted to px.
              let px = match dimension_unit_to_px(v, unit.to_lower()) {
                Some(p) => p
                None => v
              }
              values.push(px)
            }
            @token.Token::Number(v, _) => values.push(v)
            @token.Token::Ident(s) =>
              if s == "auto" {
                values.push(0.0) // auto in rect is treated as 0
              }
            _ => ()
          }
          if values.length() >= 4 {
            break
          }
        }
        if values.length() >= 4 {
          @types.ClipRect::Rect(
            top=values[0],
            right=values[1],
            bottom=values[2],
            left=values[3],
          )
        } else {
          @types.ClipRect::Auto
        }
      } else {
        @types.ClipRect::Auto
      }
    _ => @types.ClipRect::Auto
  }
}

///|
/// Parse clip-path property subset for paint and hit testing.
fn parse_clip_path(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
  let remaining = skip_whitespace(tokens)
  if remaining.length() == 0 {
    return @style.ClipPath::None
  }
  match remaining[0] {
    @token.Token::Ident(s) =>
      if s == "none" || s == "initial" || s == "unset" {
        @style.ClipPath::None
      } else {
        @style.ClipPath::None
      }
    @token.Token::Function(name) =>
      if name == "inset" {
        parse_clip_path_inset(remaining[1:])
      } else if name == "rect" {
        parse_clip_path_rect(remaining[1:])
      } else if name == "xywh" {
        parse_clip_path_xywh(remaining[1:])
      } else if name == "circle" {
        parse_clip_path_circle(remaining[1:])
      } else if name == "ellipse" {
        parse_clip_path_ellipse(remaining[1:])
      } else if name == "polygon" {
        parse_clip_path_polygon(remaining[1:])
      } else if name == "path" {
        parse_clip_path_path(remaining[1:])
      } else {
        @style.ClipPath::None
      }
    _ => @style.ClipPath::None
  }
}

///|
fn parse_clip_path_inset(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
  let values : Array[Double] = []
  for token in tokens {
    match token {
      @token.Token::Dimension(v, unit) =>
        if unit == "%" {
          values.push(-(v / 100.0))
        } else {
          // rem/em and physical length units are converted to px.
          let px = match dimension_unit_to_px(v, unit.to_lower()) {
            Some(p) => p
            None => v
          }
          values.push(px)
        }
      @token.Token::Percentage(v) => values.push(-(v / 100.0))
      @token.Token::Number(v, _) => values.push(v)
      @token.Token::Ident(s) => if s == "round" { break }
      @token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
        break
      _ => ()
    }
    if values.length() >= 4 {
      break
    }
  }
  match values.length() {
    0 => @style.ClipPath::None
    1 => {
      let v = values[0]
      @style.ClipPath::Inset(v, v, v, v)
    }
    2 => {
      let vertical = values[0]
      let horizontal = values[1]
      @style.ClipPath::Inset(vertical, horizontal, vertical, horizontal)
    }
    3 => @style.ClipPath::Inset(values[0], values[1], values[2], values[1])
    _ => @style.ClipPath::Inset(values[0], values[1], values[2], values[3])
  }
}

///|
fn parse_clip_path_rect(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
  let values : Array[Double] = []
  for token in tokens {
    match token {
      @token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
        break
      @token.Token::Whitespace => ()
      _ =>
        match parse_clip_path_shape_token(token) {
          Some(v) => values.push(v)
          None => ()
        }
    }
    if values.length() >= 4 {
      break
    }
  }
  if values.length() < 4 {
    @style.ClipPath::None
  } else {
    @style.ClipPath::Rect(values[0], values[1], values[2], values[3])
  }
}

///|
fn parse_clip_path_xywh(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
  let values : Array[Double] = []
  for token in tokens {
    match token {
      @token.Token::Ident(s) => if s == "round" { break }
      @token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
        break
      @token.Token::Whitespace => ()
      _ =>
        match parse_clip_path_shape_token(token) {
          Some(v) => values.push(v)
          None => ()
        }
    }
    if values.length() >= 4 {
      break
    }
  }
  if values.length() < 4 {
    @style.ClipPath::None
  } else {
    @style.ClipPath::Xywh(values[0], values[1], values[2], values[3])
  }
}

///|
fn parse_clip_path_circle(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
  let mut radius = -0.5
  let mut center_x = -0.5
  let mut center_y = -0.5
  let center_values : Array[Double] = []
  let mut seen_at = false
  let mut radius_set = false
  for token in tokens {
    match token {
      @token.Token::Ident(s) =>
        if s == "at" {
          seen_at = true
        } else {
          return @style.ClipPath::None
        }
      @token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
        break
      @token.Token::Whitespace => ()
      _ =>
        match parse_clip_path_shape_token(token) {
          Some(v) =>
            if seen_at {
              center_values.push(v)
            } else if !radius_set {
              radius = v
              radius_set = true
            }
          None => ()
        }
    }
  }
  if seen_at {
    if center_values.length() < 2 {
      return @style.ClipPath::None
    }
    center_x = center_values[0]
    center_y = center_values[1]
  }
  @style.ClipPath::Circle(radius, center_x, center_y)
}

///|
fn parse_clip_path_ellipse(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
  let mut radius_x = -0.5
  let mut radius_y = -0.5
  let mut center_x = -0.5
  let mut center_y = -0.5
  let radii : Array[Double] = []
  let center_values : Array[Double] = []
  let mut seen_at = false
  for token in tokens {
    match token {
      @token.Token::Ident(s) =>
        if s == "at" {
          seen_at = true
        } else {
          return @style.ClipPath::None
        }
      @token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
        break
      @token.Token::Whitespace => ()
      _ =>
        match parse_clip_path_shape_token(token) {
          Some(v) => if seen_at { center_values.push(v) } else { radii.push(v) }
          None => ()
        }
    }
  }
  if radii.length() == 0 {
    ()
  } else if radii.length() >= 2 {
    radius_x = radii[0]
    radius_y = radii[1]
  } else {
    return @style.ClipPath::None
  }
  if seen_at {
    if center_values.length() < 2 {
      return @style.ClipPath::None
    }
    center_x = center_values[0]
    center_y = center_values[1]
  }
  @style.ClipPath::Ellipse(radius_x, radius_y, center_x, center_y)
}

///|
fn parse_clip_path_polygon(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
  let values : Array[Double] = []
  for token in tokens {
    match token {
      @token.Token::Ident(s) =>
        if s == "evenodd" || s == "nonzero" {
          ()
        } else {
          return @style.ClipPath::None
        }
      @token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
        break
      @token.Token::Whitespace => ()
      _ =>
        match parse_clip_path_shape_token(token) {
          Some(v) => values.push(v)
          None => ()
        }
    }
  }
  if values.length() < 6 || values.length() % 2 != 0 {
    return @style.ClipPath::None
  }
  let points : Array[(Double, Double)] = []
  let mut i = 0
  while i + 1 < values.length() {
    points.push((values[i], values[i + 1]))
    i = i + 2
  }
  @style.ClipPath::Polygon(points)
}

///|
fn parse_clip_path_path(tokens : ArrayView[@token.Token]) -> @style.ClipPath {
  for token in tokens {
    match token {
      @token.Token::String(path_data) =>
        return parse_clip_path_path_data(path_data)
      @token.Token::RightParen | @token.Token::Semicolon | @token.Token::EOF =>
        break
      _ => ()
    }
  }
  @style.ClipPath::None
}

///|
fn parse_clip_path_path_data(path_data : String) -> @style.ClipPath {
  let tokens = tokenize_clip_path_path_data(path_data)
  let points : Array[(Double, Double)] = []
  let mut i = 0
  let mut command = ""
  let mut current_x = 0.0
  let mut current_y = 0.0
  let mut start_x = 0.0
  let mut start_y = 0.0
  let mut has_quadratic_control = false
  let mut quadratic_control_x = 0.0
  let mut quadratic_control_y = 0.0
  let mut has_cubic_control = false
  let mut cubic_control_x = 0.0
  let mut cubic_control_y = 0.0
  while i < tokens.length() {
    let token = tokens[i]
    if is_clip_path_path_command(token) {
      if token == "Z" || token == "z" {
        current_x = start_x
        current_y = start_y
        has_quadratic_control = false
        has_cubic_control = false
        command = ""
        i += 1
        continue
      }
      command = token
      i += 1
      continue
    }
    match command {
      "M" | "m" => {
        if i + 1 >= tokens.length() {
          return @style.ClipPath::None
        }
        let x = match parse_clip_path_path_number(tokens[i]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let y = match parse_clip_path_path_number(tokens[i + 1]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        if command == "m" {
          current_x += x
          current_y += y
        } else {
          current_x = x
          current_y = y
        }
        start_x = current_x
        start_y = current_y
        points.push((current_x, current_y))
        command = if command == "m" { "l" } else { "L" }
        has_quadratic_control = false
        has_cubic_control = false
        i += 2
      }
      "L" | "l" => {
        if i + 1 >= tokens.length() {
          return @style.ClipPath::None
        }
        let x = match parse_clip_path_path_number(tokens[i]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let y = match parse_clip_path_path_number(tokens[i + 1]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        if command == "l" {
          current_x += x
          current_y += y
        } else {
          current_x = x
          current_y = y
        }
        points.push((current_x, current_y))
        has_quadratic_control = false
        has_cubic_control = false
        i += 2
      }
      "H" | "h" => {
        let x = match parse_clip_path_path_number(tokens[i]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        if command == "h" {
          current_x += x
        } else {
          current_x = x
        }
        points.push((current_x, current_y))
        has_quadratic_control = false
        has_cubic_control = false
        i += 1
      }
      "V" | "v" => {
        let y = match parse_clip_path_path_number(tokens[i]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        if command == "v" {
          current_y += y
        } else {
          current_y = y
        }
        points.push((current_x, current_y))
        has_quadratic_control = false
        has_cubic_control = false
        i += 1
      }
      "Q" | "q" => {
        if i + 3 >= tokens.length() {
          return @style.ClipPath::None
        }
        let control_x = match parse_clip_path_path_number(tokens[i]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let control_y = match parse_clip_path_path_number(tokens[i + 1]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let end_x = match parse_clip_path_path_number(tokens[i + 2]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let end_y = match parse_clip_path_path_number(tokens[i + 3]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let absolute_control_x = if command == "q" {
          current_x + control_x
        } else {
          control_x
        }
        let absolute_control_y = if command == "q" {
          current_y + control_y
        } else {
          control_y
        }
        let absolute_end_x = if command == "q" {
          current_x + end_x
        } else {
          end_x
        }
        let absolute_end_y = if command == "q" {
          current_y + end_y
        } else {
          end_y
        }
        append_quadratic_path_points(
          points, current_x, current_y, absolute_control_x, absolute_control_y, absolute_end_x,
          absolute_end_y,
        )
        current_x = absolute_end_x
        current_y = absolute_end_y
        has_quadratic_control = true
        quadratic_control_x = absolute_control_x
        quadratic_control_y = absolute_control_y
        has_cubic_control = false
        i += 4
      }
      "T" | "t" => {
        if i + 1 >= tokens.length() {
          return @style.ClipPath::None
        }
        let end_x = match parse_clip_path_path_number(tokens[i]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let end_y = match parse_clip_path_path_number(tokens[i + 1]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let absolute_control_x = if has_quadratic_control {
          2.0 * current_x - quadratic_control_x
        } else {
          current_x
        }
        let absolute_control_y = if has_quadratic_control {
          2.0 * current_y - quadratic_control_y
        } else {
          current_y
        }
        let absolute_end_x = if command == "t" {
          current_x + end_x
        } else {
          end_x
        }
        let absolute_end_y = if command == "t" {
          current_y + end_y
        } else {
          end_y
        }
        append_quadratic_path_points(
          points, current_x, current_y, absolute_control_x, absolute_control_y, absolute_end_x,
          absolute_end_y,
        )
        current_x = absolute_end_x
        current_y = absolute_end_y
        has_quadratic_control = true
        quadratic_control_x = absolute_control_x
        quadratic_control_y = absolute_control_y
        has_cubic_control = false
        i += 2
      }
      "C" | "c" => {
        if i + 5 >= tokens.length() {
          return @style.ClipPath::None
        }
        let control1_x = match parse_clip_path_path_number(tokens[i]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let control1_y = match parse_clip_path_path_number(tokens[i + 1]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let control2_x = match parse_clip_path_path_number(tokens[i + 2]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let control2_y = match parse_clip_path_path_number(tokens[i + 3]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let end_x = match parse_clip_path_path_number(tokens[i + 4]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let end_y = match parse_clip_path_path_number(tokens[i + 5]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let absolute_control1_x = if command == "c" {
          current_x + control1_x
        } else {
          control1_x
        }
        let absolute_control1_y = if command == "c" {
          current_y + control1_y
        } else {
          control1_y
        }
        let absolute_control2_x = if command == "c" {
          current_x + control2_x
        } else {
          control2_x
        }
        let absolute_control2_y = if command == "c" {
          current_y + control2_y
        } else {
          control2_y
        }
        let absolute_end_x = if command == "c" {
          current_x + end_x
        } else {
          end_x
        }
        let absolute_end_y = if command == "c" {
          current_y + end_y
        } else {
          end_y
        }
        append_cubic_path_points(
          points, current_x, current_y, absolute_control1_x, absolute_control1_y,
          absolute_control2_x, absolute_control2_y, absolute_end_x, absolute_end_y,
        )
        current_x = absolute_end_x
        current_y = absolute_end_y
        has_cubic_control = true
        cubic_control_x = absolute_control2_x
        cubic_control_y = absolute_control2_y
        has_quadratic_control = false
        i += 6
      }
      "S" | "s" => {
        if i + 3 >= tokens.length() {
          return @style.ClipPath::None
        }
        let control2_x = match parse_clip_path_path_number(tokens[i]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let control2_y = match parse_clip_path_path_number(tokens[i + 1]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let end_x = match parse_clip_path_path_number(tokens[i + 2]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let end_y = match parse_clip_path_path_number(tokens[i + 3]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let absolute_control1_x = if has_cubic_control {
          2.0 * current_x - cubic_control_x
        } else {
          current_x
        }
        let absolute_control1_y = if has_cubic_control {
          2.0 * current_y - cubic_control_y
        } else {
          current_y
        }
        let absolute_control2_x = if command == "s" {
          current_x + control2_x
        } else {
          control2_x
        }
        let absolute_control2_y = if command == "s" {
          current_y + control2_y
        } else {
          control2_y
        }
        let absolute_end_x = if command == "s" {
          current_x + end_x
        } else {
          end_x
        }
        let absolute_end_y = if command == "s" {
          current_y + end_y
        } else {
          end_y
        }
        append_cubic_path_points(
          points, current_x, current_y, absolute_control1_x, absolute_control1_y,
          absolute_control2_x, absolute_control2_y, absolute_end_x, absolute_end_y,
        )
        current_x = absolute_end_x
        current_y = absolute_end_y
        has_cubic_control = true
        cubic_control_x = absolute_control2_x
        cubic_control_y = absolute_control2_y
        has_quadratic_control = false
        i += 4
      }
      "A" | "a" => {
        if i + 6 >= tokens.length() {
          return @style.ClipPath::None
        }
        let radius_x = match parse_clip_path_path_number(tokens[i]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let radius_y = match parse_clip_path_path_number(tokens[i + 1]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let rotation_degrees = match
          parse_clip_path_path_number(tokens[i + 2]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let large_arc_flag = match parse_clip_path_path_number(tokens[i + 3]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let sweep_flag = match parse_clip_path_path_number(tokens[i + 4]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let end_x = match parse_clip_path_path_number(tokens[i + 5]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let end_y = match parse_clip_path_path_number(tokens[i + 6]) {
          Some(v) => v
          None => return @style.ClipPath::None
        }
        let absolute_end_x = if command == "a" {
          current_x + end_x
        } else {
          end_x
        }
        let absolute_end_y = if command == "a" {
          current_y + end_y
        } else {
          end_y
        }
        append_arc_path_points(
          points,
          current_x,
          current_y,
          radius_x,
          radius_y,
          rotation_degrees,
          large_arc_flag != 0.0,
          sweep_flag != 0.0,
          absolute_end_x,
          absolute_end_y,
        )
        current_x = absolute_end_x
        current_y = absolute_end_y
        has_quadratic_control = false
        has_cubic_control = false
        i += 7
      }
      _ => return @style.ClipPath::None
    }
  }
  if points.length() < 3 {
    return @style.ClipPath::None
  }
  @style.ClipPath::Polygon(points)
}

///|
fn tokenize_clip_path_path_data(path_data : String) -> Array[String] {
  let tokens : Array[String] = []
  let current = StringBuilder::new()
  for c in path_data.iter() {
    if is_clip_path_path_command_char(c) {
      flush_clip_path_path_token(tokens, current)
      tokens.push(clip_path_path_char_to_string(c))
    } else if c == ',' || c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      flush_clip_path_path_token(tokens, current)
    } else if c == '-' || c == '+' {
      let current_text = current.to_string()
      if !current_text.is_empty() &&
        !current_text.has_suffix("e") &&
        !current_text.has_suffix("E") {
        flush_clip_path_path_token(tokens, current)
      }
      current.write_char(c)
    } else {
      current.write_char(c)
    }
  }
  flush_clip_path_path_token(tokens, current)
  tokens
}

///|
fn flush_clip_path_path_token(
  tokens : Array[String],
  current : StringBuilder,
) -> Unit {
  let token = current.to_string().trim().to_owned()
  if !token.is_empty() {
    tokens.push(token)
    current.reset()
  }
}

///|
fn append_quadratic_path_points(
  points : Array[(Double, Double)],
  start_x : Double,
  start_y : Double,
  control_x : Double,
  control_y : Double,
  end_x : Double,
  end_y : Double,
) -> Unit {
  let segments = 8
  for step in 1..<(segments + 1) {
    let t = step.to_double() / segments.to_double()
    let one_minus_t = 1.0 - t
    let x = one_minus_t * one_minus_t * start_x +
      2.0 * one_minus_t * t * control_x +
      t * t * end_x
    let y = one_minus_t * one_minus_t * start_y +
      2.0 * one_minus_t * t * control_y +
      t * t * end_y
    points.push((x, y))
  }
}

///|
fn append_cubic_path_points(
  points : Array[(Double, Double)],
  start_x : Double,
  start_y : Double,
  control1_x : Double,
  control1_y : Double,
  control2_x : Double,
  control2_y : Double,
  end_x : Double,
  end_y : Double,
) -> Unit {
  let segments = 8
  for step in 1..<(segments + 1) {
    let t = step.to_double() / segments.to_double()
    let one_minus_t = 1.0 - t
    let x = one_minus_t * one_minus_t * one_minus_t * start_x +
      3.0 * one_minus_t * one_minus_t * t * control1_x +
      3.0 * one_minus_t * t * t * control2_x +
      t * t * t * end_x
    let y = one_minus_t * one_minus_t * one_minus_t * start_y +
      3.0 * one_minus_t * one_minus_t * t * control1_y +
      3.0 * one_minus_t * t * t * control2_y +
      t * t * t * end_y
    points.push((x, y))
  }
}

///|
fn append_arc_path_points(
  points : Array[(Double, Double)],
  start_x : Double,
  start_y : Double,
  radius_x : Double,
  radius_y : Double,
  rotation_degrees : Double,
  large_arc : Bool,
  sweep : Bool,
  end_x : Double,
  end_y : Double,
) -> Unit {
  if (start_x == end_x && start_y == end_y) ||
    radius_x == 0.0 ||
    radius_y == 0.0 {
    points.push((end_x, end_y))
    return
  }
  let mut rx = if radius_x < 0.0 { -radius_x } else { radius_x }
  let mut ry = if radius_y < 0.0 { -radius_y } else { radius_y }
  let pi = 3.14159265358979323846
  let phi = rotation_degrees * pi / 180.0
  let cos_phi = @math.cos(phi)
  let sin_phi = @math.sin(phi)
  let dx = (start_x - end_x) / 2.0
  let dy = (start_y - end_y) / 2.0
  let x1p = cos_phi * dx + sin_phi * dy
  let y1p = -sin_phi * dx + cos_phi * dy
  let lambda = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry)
  if lambda > 1.0 {
    let scale = lambda.sqrt()
    rx = rx * scale
    ry = ry * scale
  }
  let rx2 = rx * rx
  let ry2 = ry * ry
  let x1p2 = x1p * x1p
  let y1p2 = y1p * y1p
  let denominator = rx2 * y1p2 + ry2 * x1p2
  if denominator == 0.0 {
    points.push((end_x, end_y))
    return
  }
  let sq = (rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2) / denominator
  let sq_abs = if sq < 0.0 { 0.0 } else { sq }
  let coef = sq_abs.sqrt() * (if large_arc == sweep { -1.0 } else { 1.0 })
  let cxp = coef * rx * y1p / ry
  let cyp = -coef * ry * x1p / rx
  let center_x = cos_phi * cxp - sin_phi * cyp + (start_x + end_x) / 2.0
  let center_y = sin_phi * cxp + cos_phi * cyp + (start_y + end_y) / 2.0
  let theta1 = clip_path_angle_between(
    1.0,
    0.0,
    (x1p - cxp) / rx,
    (y1p - cyp) / ry,
  )
  let mut dtheta = clip_path_angle_between(
    (x1p - cxp) / rx,
    (y1p - cyp) / ry,
    (-x1p - cxp) / rx,
    (-y1p - cyp) / ry,
  )
  if !sweep && dtheta > 0.0 {
    dtheta = dtheta - 2.0 * pi
  } else if sweep && dtheta < 0.0 {
    dtheta = dtheta + 2.0 * pi
  }
  let segments = clip_path_max_int((dtheta.abs() / pi * 8.0).to_int(), 2)
  for step in 1..<(segments + 1) {
    let t = step.to_double() / segments.to_double()
    let theta = theta1 + t * dtheta
    let cos_t = @math.cos(theta)
    let sin_t = @math.sin(theta)
    let x = cos_phi * rx * cos_t - sin_phi * ry * sin_t + center_x
    let y = sin_phi * rx * cos_t + cos_phi * ry * sin_t + center_y
    points.push((x, y))
  }
}

///|
fn clip_path_angle_between(
  ux : Double,
  uy : Double,
  vx : Double,
  vy : Double,
) -> Double {
  let dot = ux * vx + uy * vy
  let len_product = (ux * ux + uy * uy).sqrt() * (vx * vx + vy * vy).sqrt()
  if len_product == 0.0 {
    return 0.0
  }
  let mut cos_angle = dot / len_product
  if cos_angle > 1.0 {
    cos_angle = 1.0
  }
  if cos_angle < -1.0 {
    cos_angle = -1.0
  }
  let angle = @math.acos(cos_angle)
  let cross = ux * vy - uy * vx
  if cross < 0.0 {
    -angle
  } else {
    angle
  }
}

///|
fn clip_path_max_int(a : Int, b : Int) -> Int {
  if a > b {
    a
  } else {
    b
  }
}

///|
fn is_clip_path_path_command(token : String) -> Bool {
  token == "M" ||
  token == "m" ||
  token == "L" ||
  token == "l" ||
  token == "H" ||
  token == "h" ||
  token == "V" ||
  token == "v" ||
  token == "Q" ||
  token == "q" ||
  token == "T" ||
  token == "t" ||
  token == "C" ||
  token == "c" ||
  token == "S" ||
  token == "s" ||
  token == "A" ||
  token == "a" ||
  token == "Z" ||
  token == "z"
}

///|
fn is_clip_path_path_command_char(c : Char) -> Bool {
  c == 'M' ||
  c == 'm' ||
  c == 'L' ||
  c == 'l' ||
  c == 'H' ||
  c == 'h' ||
  c == 'V' ||
  c == 'v' ||
  c == 'Z' ||
  c == 'z' ||
  c == 'C' ||
  c == 'c' ||
  c == 'S' ||
  c == 's' ||
  c == 'Q' ||
  c == 'q' ||
  c == 'T' ||
  c == 't' ||
  c == 'A' ||
  c == 'a'
}

///|
fn clip_path_path_char_to_string(c : Char) -> String {
  let sb = StringBuilder::new()
  sb.write_char(c)
  sb.to_string()
}

///|
fn parse_clip_path_path_number(token : String) -> Double? {
  let value = @string.parse_double(token) catch { _ => return None }
  Some(value)
}

///|
fn parse_clip_path_shape_token(token : @token.Token) -> Double? {
  match token {
    @token.Token::Dimension(v, unit) =>
      if unit == "%" {
        Some(-(v / 100.0))
      } else {
        // rem/em and physical length units are converted to px.
        match dimension_unit_to_px(v, unit.to_lower()) {
          Some(p) => Some(p)
          None => Some(v)
        }
      }
    @token.Token::Percentage(v) => Some(-(v / 100.0))
    @token.Token::Number(v, _) => Some(v)
    _ => None
  }
}

///|
/// Parse box-sizing property
fn parse_box_sizing(tokens : ArrayView[@token.Token]) -> @types.BoxSizing? {
  if tokens.length() == 0 {
    return None
  }
  match tokens[0] {
    @token.Token::Ident(s) =>
      if s == "content-box" {
        Some(@types.ContentBox)
      } else if s == "border-box" {
        Some(@types.BorderBox)
      } else {
        None
      }
    _ => None
  }
}

///|
/// Skip whitespace tokens and return remaining view
fn skip_whitespace(tokens : ArrayView[@token.Token]) -> ArrayView[@token.Token] {
  let mut i = 0
  while i < tokens.length() {
    match tokens[i] {
      @token.Token::Whitespace => i += 1
      _ => break
    }
  }
  tokens[i:]
}

///|
/// Parse CSS contain property
/// Supports: none, strict, content, size, inline-size, layout, paint, style
/// Multiple keywords can be combined (e.g., "layout paint")
fn parse_contain(tokens : ArrayView[@token.Token]) -> @style.Contain {
  let mut result = @style.Contain::none()
  let mut remaining = skip_whitespace(tokens)
  while remaining.length() > 0 {
    match remaining[0] {
      @token.Token::Ident(s) => {
        if s == "none" {
          return @style.Contain::none()
        } else if s == "strict" {
          return @style.Contain::strict()
        } else if s == "content" {
          return @style.Contain::content()
        } else if s == "size" {
          result = { ..result, size: true }
        } else if s == "inline-size" {
          result = { ..result, inline_size: true }
        } else if s == "layout" {
          result = { ..result, layout: true }
        } else if s == "paint" {
          result = { ..result, paint: true }
        } else if s == "style" {
          result = { ..result, style: true }
        }
        remaining = skip_whitespace(remaining[1:])
      }
      @token.Token::Whitespace => remaining = skip_whitespace(remaining[1:])
      _ => break
    }
  }
  result
}

///|
fn merge_contain(
  base : @style.Contain,
  extra : @style.Contain,
) -> @style.Contain {
  let merged : @style.Contain = {
    size: base.size || extra.size,
    inline_size: base.inline_size || extra.inline_size,
    layout: base.layout || extra.layout,
    paint: base.paint || extra.paint,
    style: base.style || extra.style,
  }
  if merged.size {
    { ..merged, inline_size: false }
  } else {
    merged
  }
}

///|
/// Parse CSS container-type property into implied containment bits.
/// Supported: normal | size | inline-size
fn parse_container_type(tokens : ArrayView[@token.Token]) -> @style.Contain? {
  let remaining = skip_whitespace(tokens)
  if remaining.length() == 0 {
    return None
  }
  match remaining[0] {
    @token.Token::Ident(s) =>
      if s == "normal" {
        Some(@style.Contain::none())
      } else if s == "size" {
        Some({ ..@style.Contain::none(), size: true, style: true })
      } else if s == "inline-size" {
        Some({ ..@style.Contain::none(), inline_size: true, style: true })
      } else {
        None
      }
    _ => None
  }
}

///|
/// Parse contain-intrinsic-inline-size / contain-intrinsic-block-size.
/// Returns a content-box fallback length in px, or None for none/auto.
fn parse_contain_intrinsic_axis(tokens : ArrayView[@token.Token]) -> Double? {
  let remaining = skip_whitespace(tokens)
  if remaining.length() == 0 {
    return None
  }
  match remaining[0] {
    @token.Token::Ident(s) =>
      if s == "none" || s == "auto" {
        None
      } else {
        match parse_dimension(remaining) {
          Some(@types.Length(v)) => if v > 0.0 { Some(v) } else { Some(0.0) }
          _ => None
        }
      }
    _ =>
      match parse_dimension(remaining) {
        Some(@types.Length(v)) => if v > 0.0 { Some(v) } else { Some(0.0) }
        _ => None
      }
  }
}

///|
/// Parse contain-intrinsic-size shorthand.
/// One value applies to both inline/block axes; two values map to inline then block.
fn parse_contain_intrinsic_size(
  tokens : ArrayView[@token.Token],
) -> (Double?, Double?)? {
  let mut remaining = skip_whitespace(tokens)
  let axis_values : Array[Double?] = []
  while remaining.length() > 0 {
    match remaining[0] {
      @token.Token::Whitespace => remaining = skip_whitespace(remaining[1:])
      @token.Token::Ident(s) =>
        if s == "auto" {
          remaining = skip_whitespace(remaining[1:])
        } else if s == "none" {
          axis_values.push(None)
          remaining = skip_whitespace(remaining[1:])
        } else {
          match parse_dimension(remaining) {
            Some(@types.Length(v)) =>
              axis_values.push(Some(if v > 0.0 { v } else { 0.0 }))
            _ => ()
          }
          remaining = skip_whitespace(remaining[1:])
        }
      _ => {
        match parse_dimension(remaining) {
          Some(@types.Length(v)) =>
            axis_values.push(Some(if v > 0.0 { v } else { 0.0 }))
          _ => ()
        }
        remaining = skip_whitespace(remaining[1:])
      }
    }
  }
  if axis_values.length() == 0 {
    None
  } else if axis_values.length() == 1 {
    Some((axis_values[0], axis_values[0]))
  } else {
    Some((axis_values[0], axis_values[1]))
  }
}

///|
/// Parse CSS transform property
/// Supports: translate(x, y), translateX(x), translateY(y), translate3d(x, y, z), none
fn parse_transform(tokens : ArrayView[@token.Token]) -> @style.Transform {
  let remaining = skip_whitespace(tokens)
  if remaining.length() == 0 {
    return @style.Transform::none()
  }
  // Check for "none"
  match remaining[0] {
    @token.Token::Ident(s) => if s == "none" { return @style.Transform::none() }
    _ => ()
  }
  // Reconstruct the value as string for parsing with the full transform parser
  // Since transform values like "translate(10px, 20px)" are complex,
  // we build the string and reuse the computed transform parser
  let sb = StringBuilder::new()
  for i = 0; i < remaining.length(); i = i + 1 {
    match remaining[i] {
      @token.Token::Ident(s) => sb.write_string(s)
      @token.Token::Function(name) => {
        sb.write_string(name)
        sb.write_char('(')
      }
      @token.Token::Dimension(value, unit) => {
        sb.write_string(value.to_string())
        sb.write_string(unit)
      }
      @token.Token::Percentage(value) => {
        sb.write_string(value.to_string())
        sb.write_char('%')
      }
      @token.Token::Number(value, _) => sb.write_string(value.to_string())
      @token.Token::Comma => sb.write_char(',')
      @token.Token::RightParen => sb.write_char(')')
      @token.Token::LeftParen => sb.write_char('(')
      @token.Token::Whitespace => sb.write_char(' ')
      _ => ()
    }
  }
  let value_str = sb.to_string()
  // Parse the reconstructed string
  parse_transform_string(value_str)
}

///|
fn parse_pointer_events(
  tokens : ArrayView[@token.Token],
) -> @style.PointerEvents {
  let remaining = skip_whitespace(tokens)
  if remaining.length() == 0 {
    return @style.PointerEvents::Auto
  }
  match remaining[0] {
    @token.Token::Ident(s) =>
      if s == "none" {
        @style.PointerEvents::None
      } else {
        @style.PointerEvents::Auto
      }
    _ => @style.PointerEvents::Auto
  }
}

///|
/// Parse CSS transform from a string value
fn parse_transform_string(value : String) -> @style.Transform {
  let v = value.trim().to_lower()
  // Handle none
  if v == "none" || v.is_empty() {
    return @style.Transform::none()
  }
  let mut translate_x = @style.TranslateValue::Length(0.0)
  let mut translate_y = @style.TranslateValue::Length(0.0)
  let mut scale_x = 1.0
  let mut scale_y = 1.0
  let mut rotate_degrees = 0.0
  let mut skew_x_degrees = 0.0
  let mut skew_y_degrees = 0.0
  let mut has_matrix = false
  let mut matrix_a = 1.0
  let mut matrix_b = 0.0
  let mut matrix_c = 0.0
  let mut matrix_d = 1.0
  let mut matrix_e = 0.0
  let mut matrix_f = 0.0
  let mut matrix3d : Array[Double] = []
  // Parse transform functions
  let chars = v.to_array()
  let len = chars.length()
  let mut i = 0
  while i < len {
    // Skip whitespace
    while i < len && (chars[i] == ' ' || chars[i] == '\t') {
      i = i + 1
    }
    if i >= len {
      break
    }
    // Find function name
    let func_start = i
    while i < len && chars[i] != '(' {
      i = i + 1
    }
    if i >= len {
      break
    }
    let func_name = transform_chars_to_string(chars[func_start:i])
    i = i + 1 // Skip '('
    // Find function arguments (handle nested parens)
    let args_start = i
    let mut paren_depth = 1
    while i < len && paren_depth > 0 {
      if chars[i] == '(' {
        paren_depth = paren_depth + 1
      } else if chars[i] == ')' {
        paren_depth = paren_depth - 1
      }
      i = i + 1
    }
    let args_str = transform_chars_to_string(chars[args_start:i - 1])
    // Parse transform function
    match func_name.trim() {
      "translate" => {
        // translate(x) or translate(x, y)
        let args = split_transform_args(args_str)
        if args.length() >= 1 {
          translate_x = parse_translate_value(args[0])
        }
        if args.length() >= 2 {
          translate_y = parse_translate_value(args[1])
        }
      }
      "translatex" => translate_x = parse_translate_value(args_str)
      "translatey" => translate_y = parse_translate_value(args_str)
      "translate3d" => {
        // translate3d(x, y, z) - we only use x and y
        let args = split_transform_args(args_str)
        if args.length() >= 1 {
          translate_x = parse_translate_value(args[0])
        }
        if args.length() >= 2 {
          translate_y = parse_translate_value(args[1])
        }
      }
      "scale" => {
        // scale(x) or scale(x, y)
        let args = split_transform_args(args_str)
        if args.length() >= 1 {
          let sx = parse_scale_value(args[0])
          scale_x = scale_x * sx
          scale_y = scale_y * sx
        }
        if args.length() >= 2 {
          scale_y = parse_scale_value(args[1])
        }
      }
      "scalex" => scale_x = scale_x * parse_scale_value(args_str)
      "scaley" => scale_y = scale_y * parse_scale_value(args_str)
      "scale3d" => {
        // scale3d(x, y, z) - we only use x and y
        let args = split_transform_args(args_str)
        if args.length() >= 1 {
          scale_x = scale_x * parse_scale_value(args[0])
        }
        if args.length() >= 2 {
          scale_y = scale_y * parse_scale_value(args[1])
        }
      }
      "rotate" =>
        rotate_degrees = rotate_degrees + parse_rotate_degrees(args_str)
      "skewx" => skew_x_degrees += parse_rotate_degrees(args_str)
      "skewy" => skew_y_degrees += parse_rotate_degrees(args_str)
      "skew" => {
        let args = split_transform_args(args_str)
        if args.length() >= 1 {
          skew_x_degrees += parse_rotate_degrees(args[0])
        }
        if args.length() >= 2 {
          skew_y_degrees += parse_rotate_degrees(args[1])
        }
      }
      "matrix" => {
        let args = split_transform_args(args_str)
        if args.length() >= 6 {
          has_matrix = true
          matrix_a = parse_scale_value(args[0])
          matrix_b = parse_scale_value(args[1])
          matrix_c = parse_scale_value(args[2])
          matrix_d = parse_scale_value(args[3])
          matrix_e = parse_scale_value(args[4])
          matrix_f = parse_scale_value(args[5])
        }
      }
      "matrix3d" => {
        let args = split_transform_args(args_str)
        if args.length() >= 16 {
          // Keep a 2D affine approximation (a,b,c,d,e,f) for 2D consumers and
          // the full 16-element column-major matrix for 3D interpolation.
          has_matrix = true
          matrix_a = parse_scale_value(args[0])
          matrix_b = parse_scale_value(args[1])
          matrix_c = parse_scale_value(args[4])
          matrix_d = parse_scale_value(args[5])
          matrix_e = parse_scale_value(args[12])
          matrix_f = parse_scale_value(args[13])
          matrix3d = []
          for k = 0; k < 16; k = k + 1 {
            matrix3d.push(parse_scale_value(args[k]))
          }
        }
      }
      _ => () // Ignore unsupported functions like perspective.
    }
  }
  {
    translate_x,
    translate_y,
    scale_x,
    scale_y,
    rotate_degrees,
    skew_x_degrees,
    skew_y_degrees,
    has_matrix,
    matrix_a,
    matrix_b,
    matrix_c,
    matrix_d,
    matrix_e,
    matrix_f,
    matrix3d,
  }
}

///|
fn parse_rotate_degrees(value : String) -> Double {
  let v = value.trim().to_owned()
  if v.is_empty() {
    return 0.0
  }
  if v.has_suffix("deg") {
    let num_str = v.unsafe_substring(start=0, end=v.length() - 3)
    return @string.parse_double(num_str.trim()) catch { _ => 0.0 }
  }
  if v.has_suffix("turn") {
    let num_str = v.unsafe_substring(start=0, end=v.length() - 4)
    let turns = @string.parse_double(num_str.trim()) catch { _ => return 0.0 }
    return turns * 360.0
  }
  // `grad` must be checked before `rad`: it also ends with "rad".
  // 400grad = 360deg, so 1grad = 0.9deg.
  if v.has_suffix("grad") {
    let num_str = v.unsafe_substring(start=0, end=v.length() - 4)
    let gradians = @string.parse_double(num_str.trim()) catch {
      _ => return 0.0
    }
    return gradians * 0.9
  }
  if v.has_suffix("rad") {
    let num_str = v.unsafe_substring(start=0, end=v.length() - 3)
    let radians = @string.parse_double(num_str.trim()) catch { _ => return 0.0 }
    return radians * 57.29577951308232
  }
  @string.parse_double(v) catch {
    _ => 0.0
  }
}

///|
/// Parse a scale value (number, no unit)
fn parse_scale_value(value : String) -> Double {
  let v = value.trim()
  if v.is_empty() {
    return 1.0
  }
  @string.parse_double(v) catch {
    _ => 1.0
  }
}

///|
/// Parse a single translate value (can be length or percentage).
/// Tokenizes the value so every length unit (px/rem/em/pt/cm/in/vw/...) is
/// converted to px via dimension_unit_to_px, instead of only px/em/rem.
fn parse_translate_value(value : String) -> @style.TranslateValue {
  let v = value.trim()
  if v.is_empty() {
    return @style.TranslateValue::Length(0.0)
  }
  let tokens = skip_whitespace(@token.tokenize(v.to_owned())[:])
  if tokens.length() == 0 {
    return @style.TranslateValue::Length(0.0)
  }
  match tokens[0] {
    @token.Token::Dimension(n, raw_unit) => {
      let unit = raw_unit.to_lower()
      if unit == "%" {
        @style.TranslateValue::Percent(n / 100.0)
      } else {
        let px = match dimension_unit_to_px(n, unit) {
          Some(p) => p
          None => n
        }
        @style.TranslateValue::Length(px)
      }
    }
    @token.Token::Percentage(n) => @style.TranslateValue::Percent(n / 100.0)
    @token.Token::Number(n, _) => @style.TranslateValue::Length(n)
    _ => @style.TranslateValue::Length(0.0)
  }
}

///|
/// Split a value string into whitespace-separated, non-empty parts.
fn split_ws_parts(value : String) -> Array[String] {
  let parts : Array[String] = []
  for p in value.trim().split(" ") {
    let t = p.trim()
    if t.length() > 0 {
      parts.push(t.to_owned())
    }
  }
  parts
}

///|
/// Parse the `translate` property (`{1,3}`) into a Transform with the
/// translate components set (z is ignored; this engine is 2D).
pub fn parse_translate_property_from_string(value : String) -> @style.Transform {
  let parts = split_ws_parts(value)
  if parts.length() == 0 || value.trim().to_lower() == "none" {
    return @style.Transform::none()
  }
  let x = parse_translate_value(parts[0])
  let y = if parts.length() > 1 {
    parse_translate_value(parts[1])
  } else {
    @style.TranslateValue::Length(0.0)
  }
  { ..@style.Transform::none(), translate_x: x, translate_y: y }
}

///|
/// Parse the `scale` property (`{1,3}`) into a Transform with scale set.
/// A single value applies to both axes.
pub fn parse_scale_property_from_string(value : String) -> @style.Transform {
  let parts = split_ws_parts(value)
  if parts.length() == 0 || value.trim().to_lower() == "none" {
    return @style.Transform::none()
  }
  let sx = parse_scale_value(parts[0])
  let sy = if parts.length() > 1 { parse_scale_value(parts[1]) } else { sx }
  { ..@style.Transform::none(), scale_x: sx, scale_y: sy }
}

///|
/// Parse the `rotate` property (`` or `[x|y|z|] `) into a
/// Transform with rotate set. Only the 2D z-rotation angle is kept.
pub fn parse_rotate_property_from_string(value : String) -> @style.Transform {
  let parts = split_ws_parts(value)
  if parts.length() == 0 || value.trim().to_lower() == "none" {
    return @style.Transform::none()
  }
  // The angle is the last part (an axis keyword / vector may precede it).
  let angle = parse_rotate_degrees(parts[parts.length() - 1])
  { ..@style.Transform::none(), rotate_degrees: angle }
}

///|
/// Split string by comma (for transform arguments) - local version
fn split_transform_args(s : String) -> Array[String] {
  let result : Array[String] = []
  let current = StringBuilder::new()
  for c in s {
    if c == ',' {
      result.push(current.to_string())
      current.reset()
    } else {
      current.write_char(c)
    }
  }
  let last = current.to_string()
  if !last.is_empty() {
    result.push(last)
  }
  result
}

///|
/// Convert char array slice to string
fn transform_chars_to_string(chars : ArrayView[Char]) -> String {
  let sb = StringBuilder::new()
  for c in chars {
    sb.write_char(c)
  }
  sb.to_string()
}

///|
/// Extract property name and value tokens from a declaration
fn parse_declaration(
  tokens : ArrayView[@token.Token],
) -> (String, ArrayView[@token.Token])? {
  let tokens = skip_whitespace(tokens)
  if tokens.length() == 0 {
    return None
  }

  // Get property name
  let property_name = match tokens[0] {
    @token.Token::Ident(name) => name
    _ => return None
  }

  // Skip to colon
  let mut pos = 1
  while pos < tokens.length() {
    match tokens[pos] {
      @token.Token::Whitespace => pos += 1
      @token.Token::Colon => {
        pos += 1
        break
      }
      _ => return None
    }
  }

  // Get value tokens (until semicolon or end)
  let value_start = pos
  while pos < tokens.length() {
    match tokens[pos] {
      @token.Token::Semicolon | @token.Token::EOF => break
      _ => pos += 1
    }
  }
  let value_tokens = skip_whitespace(tokens[value_start:pos])
  Some((property_name, value_tokens))
}

///|
/// Apply a single property to a style
fn apply_property(
  style : @style.Style,
  property : String,
  value_tokens : ArrayView[@token.Token],
) -> @style.Style {
  // display
  if property == "display" {
    match parse_display(value_tokens) {
      Some(v) => return { ..style, display: v }
      None => ()
    }
  }
  // position
  if property == "position" {
    match parse_position(value_tokens) {
      Some(v) => return { ..style, position: v }
      None => ()
    }
  }
  // float
  if property == "float" {
    match parse_float(value_tokens, style.direction) {
      Some(v) => return { ..style, float: v }
      None => ()
    }
  }
  // clear
  if property == "clear" {
    match parse_clear(value_tokens, style.direction) {
      Some(v) => return { ..style, clear: v }
      None => ()
    }
  }
  // box-sizing
  if property == "box-sizing" {
    match parse_box_sizing(value_tokens) {
      Some(v) => return { ..style, box_sizing: v }
      None => ()
    }
  }
  // width
  if property == "width" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, width: v }
      None => ()
    }
  }
  // height
  if property == "height" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, height: v }
      None => ()
    }
  }
  // min-width
  if property == "min-width" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, min_width: v }
      None => ()
    }
  }
  // min-height
  if property == "min-height" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, min_height: v }
      None => ()
    }
  }
  // max-width
  if property == "max-width" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, max_width: v }
      None => ()
    }
  }
  // max-height
  if property == "max-height" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, max_height: v }
      None => ()
    }
  }
  // flex-direction
  if property == "flex-direction" {
    match parse_flex_direction(value_tokens) {
      Some(v) => return { ..style, flex_direction: v }
      None => ()
    }
  }
  // flex-wrap
  if property == "flex-wrap" {
    match parse_flex_wrap(value_tokens) {
      Some(v) => return { ..style, flex_wrap: v }
      None => ()
    }
  }
  // justify-content
  if property == "justify-content" {
    match parse_alignment_with_overflow(value_tokens) {
      Some((v, is_unsafe)) =>
        return {
          ..style,
          justify_content: v,
          justify_content_unsafe: is_unsafe,
        }
      None => ()
    }
  }
  // align-items
  if property == "align-items" {
    match parse_alignment(value_tokens) {
      Some(v) => return { ..style, align_items: v }
      None => ()
    }
  }
  // align-content
  if property == "align-content" {
    match parse_alignment_with_overflow(value_tokens) {
      Some((v, is_unsafe)) =>
        return { ..style, align_content: v, align_content_unsafe: is_unsafe }
      None => ()
    }
  }
  // place-content
  if property == "place-content" {
    match parse_place_content_with_overflow(value_tokens) {
      Some(
        (
          (align_content, align_content_unsafe),
          (justify_content, justify_content_unsafe),
        )
      ) =>
        return {
          ..style,
          align_content,
          align_content_unsafe,
          justify_content,
          justify_content_unsafe,
        }
      None => ()
    }
  }
  // align-self
  if property == "align-self" {
    match parse_self_alignment(value_tokens) {
      Some(v) => return { ..style, align_self: v }
      None => ()
    }
  }
  // justify-self
  if property == "justify-self" {
    match parse_self_alignment(value_tokens) {
      Some(v) => return { ..style, justify_self: v }
      None => ()
    }
  }
  // justify-items
  if property == "justify-items" {
    match parse_alignment(value_tokens) {
      Some(v) => return { ..style, justify_items: v }
      None => ()
    }
  }
  // flex-grow (must be non-negative)
  if property == "flex-grow" {
    match parse_number(value_tokens) {
      Some(v) if v >= 0.0 => return { ..style, flex_grow: v }
      _ => ()
    }
  }
  // flex-shrink (must be non-negative)
  if property == "flex-shrink" {
    match parse_number(value_tokens) {
      Some(v) if v >= 0.0 => return { ..style, flex_shrink: v }
      _ => ()
    }
  }
  // flex-basis
  if property == "flex-basis" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, flex_basis: v }
      None => ()
    }
  }
  // flex shorthand
  if property == "flex" {
    match parse_flex_shorthand(value_tokens) {
      Some((grow, shrink, basis)) =>
        return {
          ..style,
          flex_grow: grow,
          flex_shrink: shrink,
          flex_basis: basis,
        }
      None => ()
    }
  }
  // flex-flow shorthand
  if property == "flex-flow" {
    match parse_flex_flow_shorthand(value_tokens) {
      Some((direction, wrap)) =>
        return { ..style, flex_direction: direction, flex_wrap: wrap }
      None => ()
    }
  }
  // order
  if property == "order" {
    match parse_integer(value_tokens) {
      Some(v) => return { ..style, order: v }
      None => ()
    }
  }
  // margin (supports 1, 2, 3, or 4 values)
  if property == "margin" {
    let dims = parse_multi_dimensions(value_tokens, 4)
    match dims.length() {
      1 => {
        // All sides
        let v = dims[0]
        return { ..style, margin: { top: v, right: v, bottom: v, left: v } }
      }
      2 => {
        // top/bottom, left/right
        let tb = dims[0]
        let lr = dims[1]
        return { ..style, margin: { top: tb, right: lr, bottom: tb, left: lr } }
      }
      3 => {
        // top, left/right, bottom
        let t = dims[0]
        let lr = dims[1]
        let b = dims[2]
        return { ..style, margin: { top: t, right: lr, bottom: b, left: lr } }
      }
      4 =>
        // top, right, bottom, left
        return {
          ..style,
          margin: {
            top: dims[0],
            right: dims[1],
            bottom: dims[2],
            left: dims[3],
          },
        }
      _ => ()
    }
  }
  // margin-left
  if property == "margin-left" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, margin: { ..style.margin, left: v } }
      None => ()
    }
  }
  // margin-right
  if property == "margin-right" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, margin: { ..style.margin, right: v } }
      None => ()
    }
  }
  // margin-top
  if property == "margin-top" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, margin: { ..style.margin, top: v } }
      None => ()
    }
  }
  // margin-bottom
  if property == "margin-bottom" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, margin: { ..style.margin, bottom: v } }
      None => ()
    }
  }
  // padding (supports 1, 2, 3, or 4 values)
  if property == "padding" {
    let dims = parse_multi_dimensions(value_tokens, 4)
    match dims.length() {
      1 => {
        let v = dims[0]
        return { ..style, padding: { top: v, right: v, bottom: v, left: v } }
      }
      2 => {
        let tb = dims[0]
        let lr = dims[1]
        return {
          ..style,
          padding: { top: tb, right: lr, bottom: tb, left: lr },
        }
      }
      3 => {
        let t = dims[0]
        let lr = dims[1]
        let b = dims[2]
        return { ..style, padding: { top: t, right: lr, bottom: b, left: lr } }
      }
      4 =>
        return {
          ..style,
          padding: {
            top: dims[0],
            right: dims[1],
            bottom: dims[2],
            left: dims[3],
          },
        }
      _ => ()
    }
  }
  // padding-left
  if property == "padding-left" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, padding: { ..style.padding, left: v } }
      None => ()
    }
  }
  // padding-right
  if property == "padding-right" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, padding: { ..style.padding, right: v } }
      None => ()
    }
  }
  // padding-top
  if property == "padding-top" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, padding: { ..style.padding, top: v } }
      None => ()
    }
  }
  // padding-bottom
  if property == "padding-bottom" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, padding: { ..style.padding, bottom: v } }
      None => ()
    }
  }
  // padding-block (logical property: sets top and bottom for horizontal writing mode)
  if property == "padding-block" {
    match parse_dimension(value_tokens) {
      Some(v) =>
        return { ..style, padding: { ..style.padding, top: v, bottom: v } }
      None => ()
    }
  }
  // padding-inline (logical property: sets left and right for horizontal writing mode)
  if property == "padding-inline" {
    match parse_dimension(value_tokens) {
      Some(v) =>
        return { ..style, padding: { ..style.padding, left: v, right: v } }
      None => ()
    }
  }
  // gap
  if property == "gap" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, row_gap: v, column_gap: v }
      None => ()
    }
  }
  // row-gap
  if property == "row-gap" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, row_gap: v }
      None => ()
    }
  }
  // column-gap
  if property == "column-gap" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, column_gap: v }
      None => ()
    }
  }
  // overflow
  if property == "overflow" {
    match parse_overflow(value_tokens) {
      Some(v) => return { ..style, overflow_x: v, overflow_y: v }
      None => ()
    }
  }
  // overflow-x
  if property == "overflow-x" {
    match parse_overflow(value_tokens) {
      Some(v) => return { ..style, overflow_x: v }
      None => ()
    }
  }
  // overflow-y
  if property == "overflow-y" {
    match parse_overflow(value_tokens) {
      Some(v) => return { ..style, overflow_y: v }
      None => ()
    }
  }
  // clip (legacy, for accessibility)
  if property == "clip" {
    return { ..style, clip: parse_clip(value_tokens) }
  }
  // clip-path (paint/hit-test subset)
  if property == "clip-path" {
    return { ..style, clip_path: parse_clip_path(value_tokens) }
  }
  // border (single value)
  if property == "border-width" {
    match parse_dimension(value_tokens) {
      Some(v) =>
        return { ..style, border: { left: v, right: v, top: v, bottom: v } }
      None => ()
    }
  }
  // border-left-width
  if property == "border-left-width" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, border: { ..style.border, left: v } }
      None => ()
    }
  }
  // border-right-width
  if property == "border-right-width" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, border: { ..style.border, right: v } }
      None => ()
    }
  }
  // border-top-width
  if property == "border-top-width" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, border: { ..style.border, top: v } }
      None => ()
    }
  }
  // border-bottom-width
  if property == "border-bottom-width" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, border: { ..style.border, bottom: v } }
      None => ()
    }
  }
  // border-block (logical property: sets top and bottom border width)
  // Shorthand can be: border-block: 10px | border-block: 10px solid | border-block: 10px solid green
  if property == "border-block" {
    // Parse first token as dimension (width)
    match parse_dimension(value_tokens) {
      Some(v) =>
        return { ..style, border: { ..style.border, top: v, bottom: v } }
      None => ()
    }
  }
  // border-block-width
  if property == "border-block-width" {
    match parse_dimension(value_tokens) {
      Some(v) =>
        return { ..style, border: { ..style.border, top: v, bottom: v } }
      None => ()
    }
  }
  // border-inline (logical property: sets left and right border width)
  if property == "border-inline" {
    match parse_dimension(value_tokens) {
      Some(v) =>
        return { ..style, border: { ..style.border, left: v, right: v } }
      None => ()
    }
  }
  // border-inline-width
  if property == "border-inline-width" {
    match parse_dimension(value_tokens) {
      Some(v) =>
        return { ..style, border: { ..style.border, left: v, right: v } }
      None => ()
    }
  }
  // inset (supports 1, 2, 3, or 4 values like margin/padding)
  if property == "inset" {
    let dims = parse_multi_dimensions(value_tokens, 4)
    match dims.length() {
      1 => {
        let v = dims[0]
        return { ..style, inset: { top: v, right: v, bottom: v, left: v } }
      }
      2 => {
        let tb = dims[0]
        let lr = dims[1]
        return { ..style, inset: { top: tb, right: lr, bottom: tb, left: lr } }
      }
      3 => {
        let t = dims[0]
        let lr = dims[1]
        let b = dims[2]
        return { ..style, inset: { top: t, right: lr, bottom: b, left: lr } }
      }
      4 =>
        return {
          ..style,
          inset: {
            top: dims[0],
            right: dims[1],
            bottom: dims[2],
            left: dims[3],
          },
        }
      _ => ()
    }
  }
  // top
  if property == "top" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, inset: { ..style.inset, top: v } }
      None => ()
    }
  }
  // right
  if property == "right" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, inset: { ..style.inset, right: v } }
      None => ()
    }
  }
  // bottom
  if property == "bottom" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, inset: { ..style.inset, bottom: v } }
      None => ()
    }
  }
  // left
  if property == "left" {
    match parse_dimension(value_tokens) {
      Some(v) => return { ..style, inset: { ..style.inset, left: v } }
      None => ()
    }
  }
  // aspect-ratio
  if property == "aspect-ratio" {
    match parse_aspect_ratio(value_tokens) {
      Some(v) => return { ..style, aspect_ratio: Some(v) }
      None => ()
    }
  }
  // grid-auto-flow
  if property == "grid-auto-flow" {
    match parse_grid_auto_flow(value_tokens) {
      Some(v) => return { ..style, grid_auto_flow: v }
      None => ()
    }
  }
  // grid-template-columns
  if property == "grid-template-columns" {
    let (kind, tracks, line_names) = parse_grid_template_axis(value_tokens)
    return {
      ..style,
      grid_template_columns: tracks,
      grid_template_column_line_names: line_names,
      grid_template_columns_kind: kind,
    }
  }
  // grid-template-rows
  if property == "grid-template-rows" {
    let (kind, tracks, line_names) = parse_grid_template_axis(value_tokens)
    return {
      ..style,
      grid_template_rows: tracks,
      grid_template_row_line_names: line_names,
      grid_template_rows_kind: kind,
    }
  }
  // grid-template / grid shorthand. Try the "areas" form first (it carries
  //  rows). The `grid` shorthand additionally supports the auto-flow
  // form. Finally fall back to the ` / ` form.
  if property == "grid-template" || property == "grid" {
    match parse_grid_template_areas_form(value_tokens) {
      Some((areas, rows, row_names, cols, col_names)) =>
        return {
          ..style,
          grid_template_areas: areas,
          grid_template_rows: rows,
          grid_template_row_line_names: row_names,
          grid_template_columns: cols,
          grid_template_column_line_names: col_names,
        }
      None => ()
    }
    if property == "grid" {
      match parse_grid_autoflow_form(value_tokens) {
        Some((flow, rows, row_names, cols, col_names, auto_rows, auto_cols)) =>
          return {
            ..style,
            grid_auto_flow: flow,
            grid_template_rows: rows,
            grid_template_row_line_names: row_names,
            grid_template_columns: cols,
            grid_template_column_line_names: col_names,
            grid_auto_rows: auto_rows,
            grid_auto_columns: auto_cols,
          }
        None => ()
      }
    }
    match parse_grid_template_shorthand(value_tokens) {
      Some((rows, row_names, cols, col_names)) =>
        return {
          ..style,
          grid_template_rows: rows,
          grid_template_row_line_names: row_names,
          grid_template_columns: cols,
          grid_template_column_line_names: col_names,
        }
      None => ()
    }
  }
  // Grid item placement longhands
  if property == "grid-column-start" {
    return {
      ..style,
      grid_column: { ..style.grid_column, start: classify_grid_line(value_tokens) },
    }
  }
  if property == "grid-column-end" {
    return {
      ..style,
      grid_column: { ..style.grid_column, end: classify_grid_line(value_tokens) },
    }
  }
  if property == "grid-row-start" {
    return {
      ..style,
      grid_row: { ..style.grid_row, start: classify_grid_line(value_tokens) },
    }
  }
  if property == "grid-row-end" {
    return {
      ..style,
      grid_row: { ..style.grid_row, end: classify_grid_line(value_tokens) },
    }
  }
  // grid-column / grid-row shorthand
  if property == "grid-column" {
    let (start, end) = parse_grid_line_shorthand_tokens(value_tokens)
    return { ..style, grid_column: { start, end } }
  }
  if property == "grid-row" {
    let (start, end) = parse_grid_line_shorthand_tokens(value_tokens)
    return { ..style, grid_row: { start, end } }
  }
  // grid-area: either a single named area, or up to four grid lines.
  if property == "grid-area" {
    match grid_area_single_name(value_tokens) {
      // A single  names a grid area rather than placing lines.
      Some(name) => return { ..style, grid_area: Some(name) }
      None => {
        let (rs, cs, re, ce) = parse_grid_area_shorthand_tokens(value_tokens)
        return {
          ..style,
          grid_row: { start: rs, end: re },
          grid_column: { start: cs, end: ce },
        }
      }
    }
  }
  // Individual transform properties (compose into individual_transform).
  if property == "translate" {
    let t = parse_translate_property_from_string(reconstruct_css(value_tokens))
    return {
      ..style,
      individual_transform: {
        ..style.individual_transform,
        translate_x: t.translate_x,
        translate_y: t.translate_y,
      },
    }
  }
  if property == "scale" {
    let t = parse_scale_property_from_string(reconstruct_css(value_tokens))
    return {
      ..style,
      individual_transform: {
        ..style.individual_transform,
        scale_x: t.scale_x,
        scale_y: t.scale_y,
      },
    }
  }
  if property == "rotate" {
    let t = parse_rotate_property_from_string(reconstruct_css(value_tokens))
    return {
      ..style,
      individual_transform: {
        ..style.individual_transform,
        rotate_degrees: t.rotate_degrees,
      },
    }
  }
  // Animation longhands
  if property == "animation-name" {
    return { ..style, animation_name: parse_ident_list(value_tokens) }
  }
  if property == "animation-duration" {
    return { ..style, animation_duration: parse_time_list(value_tokens) }
  }
  if property == "animation-timing-function" {
    return {
      ..style,
      animation_timing_function: parse_easing_list(value_tokens),
    }
  }
  if property == "animation-delay" {
    return { ..style, animation_delay: parse_time_list(value_tokens) }
  }
  if property == "animation-iteration-count" {
    return {
      ..style,
      animation_iteration_count: parse_iteration_count_list(value_tokens),
    }
  }
  if property == "animation-direction" {
    return { ..style, animation_direction: parse_direction_list(value_tokens) }
  }
  if property == "animation-fill-mode" {
    return { ..style, animation_fill_mode: parse_fill_mode_list(value_tokens) }
  }
  if property == "animation-play-state" {
    return {
      ..style,
      animation_play_state: parse_play_state_list(value_tokens),
    }
  }
  // animation shorthand
  if property == "animation" {
    let a = parse_animation_shorthand(value_tokens)
    return {
      ..style,
      animation_name: a.names,
      animation_duration: a.durations,
      animation_timing_function: a.timing_functions,
      animation_delay: a.delays,
      animation_iteration_count: a.iteration_counts,
      animation_direction: a.directions,
      animation_fill_mode: a.fill_modes,
      animation_play_state: a.play_states,
    }
  }
  // Transition longhands
  if property == "transition-property" {
    return { ..style, transition_property: parse_ident_list(value_tokens) }
  }
  if property == "transition-duration" {
    return { ..style, transition_duration: parse_time_list(value_tokens) }
  }
  if property == "transition-timing-function" {
    return {
      ..style,
      transition_timing_function: parse_easing_list(value_tokens),
    }
  }
  if property == "transition-delay" {
    return { ..style, transition_delay: parse_time_list(value_tokens) }
  }
  // transition shorthand
  if property == "transition" {
    let t = parse_transition_shorthand(value_tokens)
    return {
      ..style,
      transition_property: t.properties,
      transition_duration: t.durations,
      transition_timing_function: t.timing_functions,
      transition_delay: t.delays,
    }
  }
  // contain
  if property == "contain" {
    let contain = parse_contain(value_tokens)
    return { ..style, contain, }
  }
  // container-type
  if property == "container-type" {
    match parse_container_type(value_tokens) {
      Some(implied) =>
        if implied.has_containment() {
          return { ..style, contain: merge_contain(style.contain, implied) }
        } else {
          return style
        }
      None => ()
    }
  }
  // contain-intrinsic-inline-size
  if property == "contain-intrinsic-inline-size" {
    let contain_intrinsic_inline_size = parse_contain_intrinsic_axis(
      value_tokens,
    )
    return { ..style, contain_intrinsic_inline_size, }
  }
  // contain-intrinsic-block-size
  if property == "contain-intrinsic-block-size" {
    let contain_intrinsic_block_size = parse_contain_intrinsic_axis(
      value_tokens,
    )
    return { ..style, contain_intrinsic_block_size, }
  }
  // contain-intrinsic-size
  if property == "contain-intrinsic-size" {
    match parse_contain_intrinsic_size(value_tokens) {
      Some((contain_intrinsic_inline_size, contain_intrinsic_block_size)) =>
        return {
          ..style,
          contain_intrinsic_inline_size,
          contain_intrinsic_block_size,
        }
      None => ()
    }
  }
  // transform
  if property == "transform" {
    let transform = parse_transform(value_tokens)
    return { ..style, transform, }
  }
  // pointer-events
  if property == "pointer-events" {
    let pointer_events = parse_pointer_events(value_tokens)
    return { ..style, pointer_events, }
  }
  // Unknown property - ignore
  style
}

///|
fn seed_direction(tokens : Array[@token.Token]) -> @style.Direction {
  let mut direction = @style.Direction::Ltr
  let mut pos = 0
  while pos < tokens.length() {
    match tokens[pos] {
      @token.Token::Whitespace | @token.Token::Semicolon => {
        pos += 1
        continue
      }
      @token.Token::EOF => break
      _ => ()
    }

    match parse_declaration(tokens[pos:]) {
      Some((property, value_tokens)) => {
        if property == "direction" {
          match value_tokens.length() {
            0 => ()
            _ =>
              match value_tokens[0] {
                @token.Token::Ident(s) =>
                  if s == "rtl" {
                    direction = @style.Direction::Rtl
                  } else if s == "ltr" {
                    direction = @style.Direction::Ltr
                  }
                _ => ()
              }
          }
        }
        while pos < tokens.length() {
          match tokens[pos] {
            @token.Token::Semicolon => {
              pos += 1
              break
            }
            @token.Token::EOF => break
            _ => pos += 1
          }
        }
      }
      None => pos += 1
    }
  }
  direction
}

///|
/// Parse inline CSS style string and return a Style object
/// e.g., "width: 100px; height: 50px; display: flex"
pub fn parse_inline_style(css : String) -> @style.Style {
  let tokens = @token.tokenize(css)
  let mut style = {
    ..@style.Style::default(),
    direction: seed_direction(tokens),
  }
  let mut pos = 0
  while pos < tokens.length() {
    // Skip leading whitespace and semicolons
    match tokens[pos] {
      @token.Token::Whitespace | @token.Token::Semicolon => {
        pos += 1
        continue
      }
      @token.Token::EOF => break
      _ => ()
    }

    // Try to parse a declaration
    match parse_declaration(tokens[pos:]) {
      Some((property, value_tokens)) => {
        style = apply_property(style, property, value_tokens)
        // Skip to next semicolon or end
        while pos < tokens.length() {
          match tokens[pos] {
            @token.Token::Semicolon => {
              pos += 1
              break
            }
            @token.Token::EOF => break
            _ => pos += 1
          }
        }
      }
      None => pos += 1
    }
  }
  // Convert content-box dimensions to border-box dimensions
  // The layout engine always works with outer (border-box) dimensions
  // For content-box (CSS default), width/height specify content area only,
  // so we need to add padding and border to get the outer dimensions
  adjust_for_box_sizing(style)
}

///|
/// Adjust dimensions for box-sizing: content-box
/// When box-sizing is content-box (default), specified width/height are content dimensions.
/// The layout engine expects outer dimensions (border-box), so we adjust here.
fn adjust_for_box_sizing(style : @style.Style) -> @style.Style {
  match style.box_sizing {
    @types.BorderBox => style // No adjustment needed
    @types.ContentBox => {
      // Calculate padding and border sums
      let padding_h = resolve_dimension_to_px(style.padding.left) +
        resolve_dimension_to_px(style.padding.right)
      let padding_v = resolve_dimension_to_px(style.padding.top) +
        resolve_dimension_to_px(style.padding.bottom)
      let border_h = resolve_dimension_to_px(style.border.left) +
        resolve_dimension_to_px(style.border.right)
      let border_v = resolve_dimension_to_px(style.border.top) +
        resolve_dimension_to_px(style.border.bottom)

      // Adjust width and height to include padding+border
      let adjusted_width = match style.width {
        @types.Dimension::Length(w) =>
          @types.Dimension::Length(w + padding_h + border_h)
        other => other
      }
      let adjusted_height = match style.height {
        @types.Dimension::Length(h) =>
          @types.Dimension::Length(h + padding_v + border_v)
        other => other
      }
      // Adjust min/max constraints too
      let adjusted_min_width = match style.min_width {
        @types.Dimension::Length(w) =>
          @types.Dimension::Length(w + padding_h + border_h)
        other => other
      }
      let adjusted_min_height = match style.min_height {
        @types.Dimension::Length(h) =>
          @types.Dimension::Length(h + padding_v + border_v)
        other => other
      }
      let adjusted_max_width = match style.max_width {
        @types.Dimension::Length(w) =>
          @types.Dimension::Length(w + padding_h + border_h)
        other => other
      }
      let adjusted_max_height = match style.max_height {
        @types.Dimension::Length(h) =>
          @types.Dimension::Length(h + padding_v + border_v)
        other => other
      }
      {
        ..style,
        width: adjusted_width,
        height: adjusted_height,
        min_width: adjusted_min_width,
        min_height: adjusted_min_height,
        max_width: adjusted_max_width,
        max_height: adjusted_max_height,
        // Mark as adjusted (now using border-box semantics internally)
        box_sizing: @types.BorderBox,
      }
    }
  }
}

///|
/// Helper to resolve a dimension to pixels (for padding/border calculation)
fn resolve_dimension_to_px(dim : @types.Dimension) -> Double {
  match dim {
    @types.Length(v) => v
    @types.Percent(_) => 0.0 // Percentages are resolved later
    @types.Calc(px, _) => px // Percent component is resolved later
    @types.MathFn(op, args) =>
      // Percent components resolve later; reduce using px parts only.
      if args.is_empty() {
        0.0
      } else {
        @types.apply_math_op(op, args.map(fn(a) { a.0 }))
      }
    @types.Auto => 0.0
    @types.MinContent => 0.0 // Intrinsic sizing resolved during layout
    @types.MaxContent => 0.0
    @types.FitContent(_) => 0.0
  }
}