#ifndef FREEDOM_CSS_H
#define FREEDOM_CSS_H

#include <stddef.h>
#include "css_color.h"
#include "css_counter.h"

#ifdef __cplusplus
#error "Freedom is pure C (C11). C++ is not supported."
#endif

/*
 * css — pure parser + simple cascade for AUTHOR CSS (<style> blocks and inline
 * style= attributes). Renders the page closer to what the webmaster intended,
 * with a deliberately simpler subset.
 *
 * CSS is hostile remote content, so this module is an auditable surface:
 *   - It NEVER phones home: any value containing url( is dropped, and every
 *     @-rule (@import/@font-face/@media/...) is skipped.
 *   - It is bounded (anti-DoS): rules/selectors/declarations are capped.
 *   - It fails closed: unparseable selectors/declarations are dropped, not guessed.
 *   - It executes nothing (expression()/var()/calc()/JS URLs are unknown -> ignored).
 *
 * It produces no pixels and opens no sockets: it resolves a small css_style value
 * for an element. page_view consults it; render_doc still gates the result behind
 * caps.css (Privacy by Default). See spec/css.md for the full contract.
 */

typedef enum css_status {
    CSS_OK = 0,
    CSS_ERR_NULL_ARG, /* out was NULL */
    CSS_ERR_OOM       /* allocation failed */
} css_status;

typedef enum css_align {
    CSS_ALIGN_UNSET = 0,
    CSS_ALIGN_LEFT,
    CSS_ALIGN_CENTER,
    CSS_ALIGN_RIGHT,
    CSS_ALIGN_JUSTIFY
} css_align;

typedef enum css_display {
    CSS_DISP_UNSET = 0,
    CSS_DISP_NONE,
    CSS_DISP_BLOCK,
    CSS_DISP_INLINE,
    CSS_DISP_INLINE_BLOCK,
    CSS_DISP_LIST_ITEM,
    CSS_DISP_FLEX,
    CSS_DISP_GRID,
    CSS_DISP_OTHER,
    /* The table roles of CSS 2.1 17.2. APPEND only: these codes ride the render IPC.
     * `inline-table` shares CSS_DISP_TABLE and the header/footer group spellings
     * share CSS_DISP_TABLE_ROW_GROUP -- the engine draws no distinction between
     * them, so giving each its own code would be a difference it cannot honour. */
    CSS_DISP_TABLE,
    CSS_DISP_TABLE_ROW_GROUP,
    CSS_DISP_TABLE_ROW,
    CSS_DISP_TABLE_CELL,
    CSS_DISP_TABLE_CAPTION,
    CSS_DISP_TABLE_COLUMN,
    /* NOT a CSS value -- the parser never produces it. page_view stamps it on the
     * pv_box_def of a display:inline element whose box model changes the line
     * (padding/border/margin/radius): an inline-level box that flows in its line
     * and is painted around its fragments, never opened as a block. */
    CSS_DISP_INLINE_BOX
} css_display;

typedef enum css_justify {  /* justify-content (flex/grid main axis) */
    CSS_JUSTIFY_UNSET = 0,
    CSS_JUSTIFY_START,
    CSS_JUSTIFY_END,
    CSS_JUSTIFY_CENTER,
    CSS_JUSTIFY_SPACE_BETWEEN,
    CSS_JUSTIFY_SPACE_AROUND,
    CSS_JUSTIFY_SPACE_EVENLY
} css_justify;

/* Anti-DoS bounds for the flex/grid container params. */
#define CSS_GAP_MAX       4096   /* px cap on gap */
#define CSS_GRID_COLS_MAX 64     /* cap on grid-template-columns track count */
#define CSS_GRID_TRACKS_MAX 8    /* cap on SIZED tracks (tracks past it lay out as auto) */
/* Cap on the raw `grid-template-areas` text kept on a resolved style. It holds
 * the quoted row strings verbatim (flex_layout parses them); a template longer
 * than this is DROPPED, never truncated -- half a template would place items
 * against a grid the author never wrote. Sized for the real thing: the largest in
 * the measured corpus (Wikipedia's Vector page container) is 66 bytes. */
#define CSS_GRID_AREAS_MAX 256
#define CSS_GRAD_STOPS_MAX 4     /* cap on linear-gradient color stops (extra kept out) */
#define CSS_LINE_MIN      50     /* line-height clamp floor (percent) */
#define CSS_LINE_MAX      400    /* line-height clamp ceiling (percent, anti-DoS) */
/* A line-height that is an absolute <length> (px, pt, rem, ...) is NOT a
 * multiple of the font-size (CSS 2.1 10.8.1) and inherits as that length: it
 * travels as CSS_LINE_ABS_BASE + tenths of px, up to CSS_LINE_ABS_MAX_PX.
 * bx_line_height_px is the one place that reads either encoding. */
#define CSS_LINE_ABS_BASE   100000
#define CSS_LINE_ABS_MAX_PX 4000
#define CSS_SVG_PAINT_NONE (-5)  /* fill/stroke: none (distinct from every CC_COLOR_* sentinel) */
#define CSS_FONT_LIST_MAX 192    /* bytes of a normalized font-family list (truncated at a name boundary) */
#define CSS_URL_MAX       1024   /* cap on a captured background-image url() text (anti-DoS; longer -> dropped, fail closed, never truncated-and-fetched). Sized to comfortably hold the RESOLVED absolute URL too (render_doc.c resolves in place against the page origin), not just the raw author-CSS token -- real CDN asset URLs with hashed filenames/query strings routinely exceed a bare 256-byte cap. */

/* mask layer geometry slots (css_style.mask_geo / pv_box_def.mask_geo), the same
 * vocabulary as the background layer: repeat (css_bg_repeat), size keyword
 * (css_bg_size) + w/h px + w/h per-mille, position x/y px + x/y per-mille. */
enum { CSS_MG_REPEAT = 0, CSS_MG_SIZE, CSS_MG_SIZE_W, CSS_MG_SIZE_H, CSS_MG_SIZE_WP,
       CSS_MG_SIZE_HP, CSS_MG_POS_X, CSS_MG_POS_Y, CSS_MG_POS_XP, CSS_MG_POS_YP,
       CSS_MASK_GEO_N };

/* text-decoration line bits, OR-combined into css_style.text_decoration. The field
 * is -1 when unset and 0 for an explicit `none` (so `a { text-decoration: none }`
 * can drop the default link underline). */
#define CSS_DECO_UNDERLINE    0x1
#define CSS_DECO_LINE_THROUGH 0x2
#define CSS_DECO_OVERLINE     0x4

/* Generic font family bucket (font-family). A specific family name is mapped to its
 * generic group; an unrecognised name leaves the field unset (UA sans-serif). */
typedef enum css_font_family {
    CSS_FF_UNSET = 0, CSS_FF_SERIF, CSS_FF_SANS, CSS_FF_MONO,
    CSS_FF_CURSIVE, CSS_FF_FANTASY
} css_font_family;

/* text-transform. 0 is unset; CSS_TT_NONE is an explicit `none`. */
typedef enum css_text_transform {
    CSS_TT_UNSET = 0, CSS_TT_NONE, CSS_TT_UPPERCASE, CSS_TT_LOWERCASE, CSS_TT_CAPITALIZE
} css_text_transform;

/* vertical-align (subset: only the inline shifts). 0 unset. */
typedef enum css_valign {
    CSS_VA_UNSET = 0, CSS_VA_BASELINE, CSS_VA_SUB, CSS_VA_SUPER,
    CSS_VA_MIDDLE, CSS_VA_TOP, CSS_VA_BOTTOM
} css_valign;

/* white-space (subset: only the wrap/keep distinction is consumed). 0 unset. */
typedef enum css_white_space {
    CSS_WS_UNSET = 0, CSS_WS_NORMAL, CSS_WS_NOWRAP, CSS_WS_PRE,
    CSS_WS_PRE_WRAP, CSS_WS_PRE_LINE
} css_white_space;

/* list-style-type (and the type token of the list-style shorthand). 0 unset. */
typedef enum css_list_style {
    CSS_LS_UNSET = 0, CSS_LS_NONE, CSS_LS_DISC, CSS_LS_CIRCLE, CSS_LS_SQUARE,
    CSS_LS_DECIMAL, CSS_LS_LOWER_ALPHA, CSS_LS_UPPER_ALPHA,
    CSS_LS_LOWER_ROMAN, CSS_LS_UPPER_ROMAN
} css_list_style;

/* --- Layout / box decoration (Hito 23b-7) ---------------------------------- */

/* position. 0 unset; STATIC is the explicit in-flow default. RELATIVE offsets the
 * box from its in-flow spot; ABSOLUTE/FIXED take it out of flow; STICKY is the
 * scroll-pinned hybrid. The engine resolves the keyword here; how far each is
 * honoured at paint time is the presentation layer's call (a later milestone). */
typedef enum css_position {
    CSS_POS_UNSET = 0, CSS_POS_STATIC, CSS_POS_RELATIVE,
    CSS_POS_ABSOLUTE, CSS_POS_FIXED, CSS_POS_STICKY
} css_position;

/* box-sizing. 0 unset; CONTENT is content-box (width excludes padding/border),
 * BORDER is border-box (width includes them). */
typedef enum css_box_sizing {
    CSS_BOXS_UNSET = 0, CSS_BOXS_CONTENT, CSS_BOXS_BORDER
} css_box_sizing;

/* border-style / outline-style (subset). 0 unset; NONE/HIDDEN paint nothing. The
 * decorative variants are recognised so the cascade keeps them; the painter may
 * collapse the fancier ones (groove/ridge/inset/outset) to solid. */
typedef enum css_border_style {
    CSS_BST_UNSET = 0, CSS_BST_NONE, CSS_BST_HIDDEN, CSS_BST_SOLID,
    CSS_BST_DASHED, CSS_BST_DOTTED, CSS_BST_DOUBLE,
    CSS_BST_GROOVE, CSS_BST_RIDGE, CSS_BST_INSET, CSS_BST_OUTSET
} css_border_style;

/* flex-direction. 0 unset. */
typedef enum css_flex_direction {
    CSS_FD_UNSET = 0, CSS_FD_ROW, CSS_FD_ROW_REVERSE,
    CSS_FD_COLUMN, CSS_FD_COLUMN_REVERSE
} css_flex_direction;

/* flex-wrap. 0 unset. */
typedef enum css_flex_wrap {
    CSS_FW_UNSET = 0, CSS_FW_NOWRAP, CSS_FW_WRAP, CSS_FW_WRAP_REVERSE
} css_flex_wrap;

/* align-items / align-self / align-content / justify-items (cross-axis alignment).
 * 0 unset. AUTO only applies to align-self (it inherits the parent's align-items). */
typedef enum css_align_kw {
    CSS_AK_UNSET = 0, CSS_AK_AUTO, CSS_AK_STRETCH, CSS_AK_START, CSS_AK_END,
    CSS_AK_CENTER, CSS_AK_BASELINE,
    CSS_AK_SPACE_BETWEEN, CSS_AK_SPACE_AROUND, CSS_AK_SPACE_EVENLY
} css_align_kw;

/* grid-auto-flow (subset: the row/column axis only; `dense` is ignored). 0 unset. */
typedef enum css_grid_flow {
    CSS_GF_UNSET = 0, CSS_GF_ROW, CSS_GF_COLUMN
} css_grid_flow;

/* float. 0 unset; NONE is the explicit in-flow default. LEFT/RIGHT take the box out of
 * normal flow into a side-by-side float band (see spec/float.md). */
typedef enum css_float {
    CSS_FLOAT_UNSET = 0, CSS_FLOAT_NONE, CSS_FLOAT_LEFT, CSS_FLOAT_RIGHT
} css_float;

/* clear. 0 unset; NONE is the default. LEFT/RIGHT/BOTH drop the box below preceding
 * floats (v1 collapses the three to "end the float band"). */
typedef enum css_clear {
    CSS_CLEAR_UNSET = 0, CSS_CLEAR_NONE, CSS_CLEAR_LEFT, CSS_CLEAR_RIGHT, CSS_CLEAR_BOTH
} css_clear;

/* visibility. 0 unset; VISIBLE is the explicit default. HIDDEN paints nothing for the
 * box and its content but keeps its layout space (unlike display:none, which removes
 * it). COLLAPSE (meant for table rows/columns) has no real table-row model here, so it
 * collapses to the same HIDDEN behaviour -- a documented simplification. */
typedef enum css_visibility {
    CSS_VIS_UNSET = 0, CSS_VIS_VISIBLE, CSS_VIS_HIDDEN, CSS_VIS_COLLAPSE
} css_visibility;

/* overflow / overflow-x / overflow-y. 0 unset; VISIBLE is the explicit default. This
 * static, non-interactive renderer has no scrollbar machinery, so SCROLL/AUTO/HIDDEN
 * are resolved distinctly (for debug_dom / a future scrolling milestone) but the v1
 * presentation layer only distinguishes "clips" (HIDDEN/SCROLL/AUTO) from VISIBLE --
 * see spec/css.md for the exact staged scope. */
typedef enum css_overflow {
    CSS_OF_UNSET = 0, CSS_OF_VISIBLE, CSS_OF_HIDDEN, CSS_OF_SCROLL, CSS_OF_AUTO
} css_overflow;

/* cursor (subset of the CSS keyword list). 0 unset. v1 paint only distinguishes
 * POINTER (shows the hand cursor already used for links) from every other value
 * (shows the default arrow); the rest are resolved for completeness/debug_dom and a
 * future milestone that loads more cursor theme shapes -- see spec/css.md.
 * APPEND only: the codes ride the render IPC as ints (pv_box_def), so existing
 * codes never renumber. */
typedef enum css_cursor {
    CSS_CUR_UNSET = 0, CSS_CUR_AUTO, CSS_CUR_DEFAULT, CSS_CUR_POINTER, CSS_CUR_TEXT,
    CSS_CUR_MOVE, CSS_CUR_NOT_ALLOWED, CSS_CUR_HELP, CSS_CUR_WAIT, CSS_CUR_CROSSHAIR,
    CSS_CUR_GRAB, CSS_CUR_ZOOM_IN, CSS_CUR_NONE,
    CSS_CUR_EW_RESIZE, CSS_CUR_NS_RESIZE, CSS_CUR_NESW_RESIZE, CSS_CUR_NWSE_RESIZE,
    CSS_CUR_COL_RESIZE, CSS_CUR_ROW_RESIZE, CSS_CUR_ALL_SCROLL, CSS_CUR_CELL,
    CSS_CUR_COPY, CSS_CUR_ALIAS, CSS_CUR_CONTEXT_MENU, CSS_CUR_PROGRESS,
    CSS_CUR_NO_DROP, CSS_CUR_VERTICAL_TEXT, CSS_CUR_ZOOM_OUT, CSS_CUR_GRABBING
} css_cursor;

/* text-overflow. 0 unset; CLIP is the explicit default. ELLIPSIS only has a visible
 * effect where a line cannot wrap (white-space:nowrap) and would otherwise overflow
 * its box -- see spec/css.md. */
typedef enum css_text_overflow {
    CSS_TO_UNSET = 0, CSS_TO_CLIP, CSS_TO_ELLIPSIS
} css_text_overflow;

/* word-break / overflow-wrap / word-wrap. 0 unset; NORMAL is the explicit UA default
 * (no mid-word break). The two breaking modes carry the distinct CSS semantics:
 *   BREAK      -- `word-break: break-all`: break greedily between any two clusters to
 *                 fill each line, even when the whole word would fit on the next line.
 *   BREAK_WORD -- `overflow-wrap: break-word`/`anywhere` and the deprecated aliases
 *                 `word-wrap: break-word`/`word-break: break-word`: break a word ONLY
 *                 as a last resort, i.e. when it cannot fit on a line by itself. A word
 *                 that would fit on the next line wraps whole (never mid-cluster).
 * Collapsing these two into one value made every `word-wrap:break-word` page (a near
 * universal defensive rule) chop its last word on every line -- see spec/css.md. */
typedef enum css_word_break {
    CSS_WB_UNSET = 0, CSS_WB_NORMAL, CSS_WB_BREAK, CSS_WB_BREAK_WORD
} css_word_break;

/* text-decoration-style (Hito 23b-6 extension). 0 unset; the line drawing style
 * for underline/overline/line-through. The painter may collapse the fancier ones to
 * solid (v1 fallback). */
typedef enum css_text_decoration_style {
    CSS_TDS_UNSET = 0, CSS_TDS_SOLID, CSS_TDS_DOUBLE, CSS_TDS_DOTTED,
    CSS_TDS_DASHED, CSS_TDS_WAVY
} css_text_decoration_style;

/* direction (Hito 23b-6 extension). 0 unset; LTR is the explicit default. RTL
 * flips the inline flow direction. */
typedef enum css_direction {
    CSS_DIR_UNSET = 0, CSS_DIR_LTR, CSS_DIR_RTL
} css_direction;

/* border-collapse. 0 unset; SEPARATE is the explicit default (separate cell
 * borders); COLLAPSE merges adjacent borders into one. */
typedef enum css_border_collapse {
    CSS_BCOL_UNSET = 0, CSS_BCOL_COLLAPSE, CSS_BCOL_SEPARATE
} css_border_collapse;

/* empty-cells. 0 unset; SHOW draws cell background/border for empty <td>/<th>;
 * HIDE suppresses them (only when border-collapse is separate). */
typedef enum css_empty_cells {
    CSS_EC_UNSET = 0, CSS_EC_SHOW, CSS_EC_HIDE
} css_empty_cells;

/* caption-side. 0 unset; TOP is the explicit default; BOTTOM places the
 * <caption> below the table. */
typedef enum css_caption_side {
    CSS_CS_UNSET = 0, CSS_CS_TOP, CSS_CS_BOTTOM
} css_caption_side;

/* table-layout. 0 unset; AUTO is the explicit default (automatic table layout);
 * FIXED uses the declared column widths first. */
typedef enum css_table_layout {
    CSS_TL_UNSET = 0, CSS_TL_AUTO, CSS_TL_FIXED
} css_table_layout;

/* font-variant (subset: only small-caps). 0 unset; NONE is the explicit default;
 * SMALL_CAPS renders lowercase as small capitals. */
typedef enum css_font_variant {
    CSS_FV_UNSET = 0, CSS_FV_NORMAL, CSS_FV_SMALL_CAPS
} css_font_variant;

/* hyphens. 0 unset; MANUAL is the explicit default (hyphen only at &shy;);
 * AUTO allows the UA to hyphenate; NONE disables hyphenation. */
typedef enum css_hyphens {
    CSS_HY_UNSET = 0, CSS_HY_NONE, CSS_HY_MANUAL, CSS_HY_AUTO
} css_hyphens;

/* user-select. 0 unset; AUTO is the explicit default; NONE prevents selection;
 * TEXT allows; ALL selects all on one click. */
typedef enum css_user_select {
    CSS_US_UNSET = 0, CSS_US_NONE, CSS_US_TEXT, CSS_US_ALL, CSS_US_AUTO
} css_user_select;

/* appearance. 0 unset; AUTO is the explicit default (native-looking);
 * NONE removes native styling (common CSS reset). */
typedef enum css_appearance {
    CSS_AP_UNSET = 0, CSS_AP_AUTO, CSS_AP_NONE
} css_appearance;

/* pointer-events. 0 unset; AUTO is the explicit default; NONE makes the
 * element not a hit-test target. */
typedef enum css_pointer_events {
    CSS_PE_UNSET = 0, CSS_PE_AUTO, CSS_PE_NONE
} css_pointer_events;

/* background-repeat. 0 unset. */
typedef enum css_bg_repeat {
    CSS_BGR_UNSET = 0, CSS_BGR_REPEAT, CSS_BGR_NO_REPEAT,
    CSS_BGR_REPEAT_X, CSS_BGR_REPEAT_Y, CSS_BGR_SPACE, CSS_BGR_ROUND
} css_bg_repeat;
/* background-size. 0 unset. */
typedef enum css_bg_size {
    CSS_BGS_UNSET = 0, CSS_BGS_AUTO, CSS_BGS_COVER, CSS_BGS_CONTAIN
} css_bg_size;
/* background-clip. 0 unset. */
typedef enum css_bg_clip {
    CSS_BGC_UNSET = 0, CSS_BGC_BORDER_BOX, CSS_BGC_PADDING_BOX,
    CSS_BGC_CONTENT_BOX, CSS_BGC_TEXT
} css_bg_clip;
/* background-origin. 0 unset. */
typedef enum css_bg_origin {
    CSS_BGO_UNSET = 0, CSS_BGO_PADDING_BOX, CSS_BGO_BORDER_BOX, CSS_BGO_CONTENT_BOX
} css_bg_origin;
/* background-attachment. 0 unset. */
typedef enum css_bg_attachment {
    CSS_BGA_UNSET = 0, CSS_BGA_SCROLL, CSS_BGA_FIXED, CSS_BGA_LOCAL
} css_bg_attachment;
/* isolation. 0 unset. */
typedef enum css_isolation {
    CSS_ISO_UNSET = 0, CSS_ISO_AUTO, CSS_ISO_ISOLATE
} css_isolation;
/* contain bitmask values. 0 = none/unset. */
#define CSS_CONTAIN_SIZE   1
#define CSS_CONTAIN_LAYOUT 2
#define CSS_CONTAIN_STYLE  4
#define CSS_CONTAIN_PAINT  8
/* content-visibility. 0 unset. */
typedef enum css_content_visibility {
    CSS_CV_UNSET = 0, CSS_CV_VISIBLE, CSS_CV_AUTO, CSS_CV_HIDDEN
} css_content_visibility;
/* image-rendering. 0 unset. */
typedef enum css_image_rendering {
    CSS_IR_UNSET = 0, CSS_IR_AUTO, CSS_IR_PIXELATED, CSS_IR_CRISP_EDGES
} css_image_rendering;
/* color-scheme. 0 unset. */
typedef enum css_color_scheme {
    CSS_CSH_UNSET = 0, CSS_CSH_NORMAL, CSS_CSH_LIGHT, CSS_CSH_DARK
} css_color_scheme;
/* print-color-adjust. 0 unset. */
typedef enum css_print_color_adjust {
    CSS_PCA_UNSET = 0, CSS_PCA_ECONOMY, CSS_PCA_EXACT
} css_print_color_adjust;
/* forced-color-adjust. 0 unset. */
typedef enum css_forced_color_adjust {
    CSS_FCA_UNSET = 0, CSS_FCA_AUTO, CSS_FCA_NONE
} css_forced_color_adjust;

/* mix-blend-mode. 0 unset. */
typedef enum css_mix_blend {
    CSS_MB_UNSET = 0, CSS_MB_NORMAL, CSS_MB_MULTIPLY, CSS_MB_SCREEN,
    CSS_MB_OVERLAY, CSS_MB_DARKEN, CSS_MB_LIGHTEN, CSS_MB_COLOR_DODGE,
    CSS_MB_COLOR_BURN, CSS_MB_DIFFERENCE, CSS_MB_EXCLUSION, CSS_MB_HUE,
    CSS_MB_SATURATION, CSS_MB_COLOR, CSS_MB_LUMINOSITY,
    CSS_MB_PLUS_LIGHTER   /* Compositing 1: additive (CAIRO_OPERATOR_ADD) */
} css_mix_blend;
/* object-fit. 0 unset. */
typedef enum css_object_fit {
    CSS_OFI_UNSET = 0, CSS_OFI_FILL, CSS_OFI_CONTAIN, CSS_OFI_COVER,
    CSS_OFI_NONE, CSS_OFI_SCALE_DOWN
} css_object_fit;

/* object-position packed in one int so it crosses the IPC codec as ONE field:
 * ((x_pm + 1) << 16) | (y_pm + 1), each axis in per-mille of the free space
 * (0 = start edge, 500 = centre, 1000 = end edge). 0 = not declared = centred. */
#define CSS_OBJPOS_PACK(x_pm, y_pm) ((((x_pm) + 1) << 16) | ((y_pm) + 1))
#define CSS_OBJPOS_X(p) ((((p) >> 16) & 0xFFFF) - 1)
#define CSS_OBJPOS_Y(p) (((p) & 0xFFFF) - 1)
/* list-style-position. 0 unset. */
typedef enum css_list_pos {
    CSS_LP_UNSET = 0, CSS_LP_OUTSIDE, CSS_LP_INSIDE
} css_list_pos;
/* font-kerning. 0 unset. */
typedef enum css_font_kerning {
    CSS_FK_UNSET = 0, CSS_FK_AUTO, CSS_FK_NORMAL, CSS_FK_NONE
} css_font_kerning;
/* text-rendering. 0 unset. */
typedef enum css_text_rendering {
    CSS_TR_UNSET = 0, CSS_TR_AUTO, CSS_TR_OPTIMIZE_SPEED,
    CSS_TR_OPTIMIZE_LEGIBILITY, CSS_TR_GEOMETRIC_PRECISION
} css_text_rendering;
/* font-stretch. 0 unset. Maps keyword to a percentage value. */
typedef enum css_font_stretch {
    CSS_FS_UNSET = 0, CSS_FS_NORMAL, CSS_FS_CONDENSED, CSS_FS_EXPANDED,
    CSS_FS_ULTRA_CONDENSED, CSS_FS_EXTRA_CONDENSED, CSS_FS_SEMI_CONDENSED,
    CSS_FS_SEMI_EXPANDED, CSS_FS_EXTRA_EXPANDED, CSS_FS_ULTRA_EXPANDED
} css_font_stretch;
/* resize. 0 unset. */
typedef enum css_resize {
    CSS_RS_UNSET = 0, CSS_RS_NONE, CSS_RS_BOTH, CSS_RS_HORIZONTAL, CSS_RS_VERTICAL
} css_resize;
/* scroll-behavior. 0 unset. */
typedef enum css_scroll_behavior {
    CSS_SB_UNSET = 0, CSS_SB_AUTO, CSS_SB_SMOOTH
} css_scroll_behavior;
/* touch-action. 0 unset. */
typedef enum css_touch_action {
    CSS_TA_UNSET = 0, CSS_TA_AUTO, CSS_TA_NONE, CSS_TA_MANIPULATION
} css_touch_action;
/* overscroll-behavior. 0 unset. */
typedef enum css_overscroll {
    CSS_OS_UNSET = 0, CSS_OS_AUTO, CSS_OS_CONTAIN, CSS_OS_NONE
} css_overscroll;
/* column-fill. 0 unset; BALANCE is the CSS initial value (columns come out as
 * even as possible); AUTO fills each column to the container height in turn. */
typedef enum css_column_fill {
    CSS_CF_UNSET = 0, CSS_CF_BALANCE, CSS_CF_AUTO
} css_column_fill;
/* column-span. 0 unset; NONE is the initial value; ALL makes the element span
 * every column, splitting the multi-column flow around it. */
typedef enum css_column_span {
    CSS_CSP_UNSET = 0, CSS_CSP_NONE, CSS_CSP_ALL
} css_column_span;
/* backface-visibility. 0 unset. */
typedef enum css_backface {
    CSS_BF_UNSET = 0, CSS_BF_VISIBLE, CSS_BF_HIDDEN
} css_backface;
/* Anti-DoS clamps for the layout properties. Insets/z-index/order reuse CSS_LEN_*.
 * Border/outline widths and radius are non-negative px clamped to CSS_LEN_MAX.
 * border-spacing is clamped to [0, CSS_SPACING_MAX] like letter-spacing (a table
 * with 200+ px cell spacing is layout-poison, not a legitimate value). */
#define CSS_BORDER_W_MAX  CSS_LEN_MAX
#define CSS_BORDER_SPACING_MAX 200
#define CSS_FLEX_FACTOR_MAX 100000  /* flex-grow/-shrink stored x100; ~1000.0 cap */
#define CSS_GRID_SPAN_MAX 1000      /* grid-column/-row `span N` cap */

/* Anti-DoS clamps for the new metric properties (px, absolute value). text-indent
 * reuses the box-model CSS_LEN_MAX clamp (below). */
#define CSS_SPACING_MAX 200      /* letter-spacing / word-spacing */
#define CSS_SHADOW_MAX  100      /* text-shadow offsets */
#define CSS_BSHADOW_LAYERS 4     /* box-shadow layers kept (CSS Backgrounds 3 7.1); more are dropped */

/* Box-model lengths (margin/padding/width/max-width), in px. Two out-of-band
 * sentinels distinguish "not declared" and the 'auto' keyword from a real length;
 * a real length is clamped to [-CSS_LEN_MAX, CSS_LEN_MAX] (anti-DoS). */
#define CSS_LEN_MAX       100000
#define CSS_LEN_UNSET     (-2147483647 - 1) /* INT_MIN: property not set */
#define CSS_LEN_AUTO      (-2147483647)     /* INT_MIN+1: the 'auto' keyword */
#define CSS_LEN_END       (-2147483646)     /* INT_MIN+2: 100% offset (right/bottom edge) */

/* Intrinsic sizing keywords (CSS Sizing 3 5.1). Like `auto` these are not lengths
 * -- they name a measurement only layout can take -- so they ride the same
 * out-of-band sentinel channel rather than a parallel per-property flag. A
 * consumer that does not know them must treat them as `auto`, which is what the
 * spec's fallback behaviour already is for a box that cannot measure itself. */
#define CSS_LEN_MIN_CONTENT (-2147483645)   /* INT_MIN+3 */
#define CSS_LEN_MAX_CONTENT (-2147483644)   /* INT_MIN+4 */
#define CSS_LEN_FIT_CONTENT (-2147483643)   /* INT_MIN+5 */

/* True when `v` is one of the intrinsic keywords above. */
#define CSS_LEN_IS_INTRINSIC(v) \
    ((v) == CSS_LEN_MIN_CONTENT || (v) == CSS_LEN_MAX_CONTENT || \
     (v) == CSS_LEN_FIT_CONTENT)

/* --- <length-percentage>: the percentage channel (spec/css_length.md 7) -----
 *
 * A percentage cannot be resolved by the parser: it needs the containing block,
 * which only layout knows. So it travels SYMBOLICALLY, in per-mille of that
 * block's relevant dimension (50% -> 500, 33.333% -> 333), beside the px value
 * of the same property, and the two are combined exactly once, at layout, by
 * cl_lp_used / bx_lp_px.
 *
 * Per-mille integers rather than a double because the whole render IPC is
 * integer; a tenth of a percent is finer than any real stylesheet asks for.
 *
 * 0 means "no percentage component", which is not a lie: contributing 0% of a
 * containing block contributes nothing. What makes that safe is that EVERY
 * emitter of a <length-percentage> writes both slots, so a more specific
 * `width: 200px` clears a less specific `width: 50%` in the cascade instead of
 * silently combining with it.
 *
 * This replaces the ad-hoc width_pct/max_width_pct pair: a percentage is a
 * property of the VALUE TYPE, not of `width`. */
#define CSS_PCT_MAX       10000    /* +-1000%, saturating (anti-DoS, like CSS_LEN_MAX) */

/* Anti-DoS cap on the font-relative component of a length, in thousandths of an
 * em (+-1000em). Saturating like CSS_PCT_MAX and for the same reason: the value
 * is an author-supplied multiplier of the font-size, and a page asking for
 * 1e9em must be clamped rather than allowed to overflow the fold. */
#define CSS_EM_MILLI_MAX  1000000

/* Anti-DoS ceiling on a computed font-size, in px. Bounds the value every
 * font-relative length is multiplied by, so a page nesting `font-size:2em`
 * hundreds deep saturates instead of running the geometry away. Generous: no
 * real stylesheet asks for text taller than this. */
#define CSS_FONT_SIZE_MAX 1600

/* Anti-DoS cap on `column-count`. Real stylesheets ask for two to four; a page
 * demanding thousands of columns is layout poison, not a legitimate value. */
#define CSS_COLUMN_COUNT_MAX 16

typedef enum css_pct_slot {
    /* Resolved against the containing block WIDTH -- all four sides, including
     * top/bottom, per CSS 2.1 8.3 (margin) and 8.4 (padding). */
    CSS_PCT_MARGIN_TOP = 0, CSS_PCT_MARGIN_RIGHT,
    CSS_PCT_MARGIN_BOTTOM, CSS_PCT_MARGIN_LEFT,
    CSS_PCT_PAD_TOP, CSS_PCT_PAD_RIGHT, CSS_PCT_PAD_BOTTOM, CSS_PCT_PAD_LEFT,
    CSS_PCT_WIDTH, CSS_PCT_MAX_WIDTH, CSS_PCT_MIN_WIDTH,
    /* Resolved against the containing block HEIGHT, and only when that height is
     * definite; against an auto height these compute to auto (CSS 2.1 10.5). */
    CSS_PCT_HEIGHT, CSS_PCT_MIN_HEIGHT, CSS_PCT_MAX_HEIGHT,
    /* Insets: left/right against width, top/bottom against height (9.3.2). */
    CSS_PCT_INSET_TOP, CSS_PCT_INSET_RIGHT,
    CSS_PCT_INSET_BOTTOM, CSS_PCT_INSET_LEFT,
    CSS_PCT_TEXT_INDENT,   /* containing block width (16.1) */
    CSS_PCT_FLEX_BASIS,    /* container main size (Flexbox 7.2.3) */
    /* border-radius corners. CSS Backgrounds 3 5.1 resolves the horizontal
     * radius against the box's own WIDTH and the vertical one against its
     * HEIGHT; this engine draws a circular corner, so it resolves against the
     * smaller of the two -- which is also exactly what the existing
     * min(w,h)/2 corner clamp enforces, and what makes `border-radius: 50%`
     * on a square produce a circle. */
    CSS_PCT_RADIUS_TL, CSS_PCT_RADIUS_TR, CSS_PCT_RADIUS_BR, CSS_PCT_RADIUS_BL,
    /* translate() arguments. The odd one out: CSS Transforms 1 section 8
     * resolves these against the element's OWN border box (x against its width,
     * y against its height), not against any containing block. That is what
     * makes `left:50%; top:50%; transform:translate(-50%,-50%)` centre a box --
     * the universal centring idiom, and dead without both halves. */
    CSS_PCT_TRANSLATE_X, CSS_PCT_TRANSLATE_Y,
    /* background-size components, against the background POSITIONING AREA
     * (Backgrounds 3 3.9), and the vertical-align baseline shift, against the
     * element's own line-height (CSS 2.1 10.8.1). Both are the element's own box
     * rather than a containing block, like the translate() pair above. */
    CSS_PCT_BG_SIZE_W, CSS_PCT_BG_SIZE_H,
    /* background-position. Not a fraction of the area: a percentage aligns that
     * fraction of the IMAGE with the same fraction of the area (Backgrounds 3
     * 3.6), so the used offset is (area - image) * pct -- which is also exactly
     * what left/center/right mean, making keyword and percentage one channel. */
    CSS_PCT_BG_POS_X, CSS_PCT_BG_POS_Y,
    CSS_PCT_VALIGN,
    CSS_PCT_N
} css_pct_slot;

/* Max compounds in one complex selector (subject + ancestor/parent constraints),
 * i.e. the number of space/`>`/`+`/`~`-separated parts. A deeper chain is dropped
 * (fail closed). Real sites nest well past four: a bound of 4 silently dropped
 * slashdot's `#firehose article header h2 a` (5) so its story titles rendered
 * black-on-teal instead of white, and its layout rules on `article header ...`
 * never applied. Eight covers the overwhelming majority of author selectors
 * (measured against slashdot's ~3400-selector sheet: depth<=6 is 99 percent;
 * depth 7-9 are a handful of comment-moderation rules) while staying bounded
 * against a CSS DoS.
 * Cost is O(CSS_MAX_COMPOUNDS) per compiled selector (sizeof(css_sel) ~= 1.4 KiB per
 * compound), so keep this modest. */
#define CSS_MAX_COMPOUNDS 8

/* Max attribute selectors ([attr], [attr=v], ...) in one compound. More are dropped
 * (the whole selector fails closed). */
#define CSS_MAX_ATTR_SEL  4

/* Max pseudo-classes (:link, :nth-child(...), ...) in one compound. More are
 * dropped (the whole selector fails closed). */
#define CSS_MAX_PSEUDO_SEL 4

/* Bound on the A/B coefficients of :nth-child(An+B) (anti-DoS; larger drops the
 * selector, fail closed). */
#define CSS_NTH_MAX 100000

/* A resolved presentation. Each field uses a sentinel for "unset" so the caller
 * can layer inheritance (take the first ancestor that sets each inheriting one).
 * The flex/grid container fields (gap/justify/grid_cols) are NOT inherited: they
 * describe the container element itself, so the caller reads them from that
 * element's resolved style, not up the ancestor chain. */
typedef struct css_style {
    int         color;       /* 0xRRGGBB or -1 (unset) */
    int         svg_fill;    /* SVG `fill` presentation property: 0xRRGGBB, -1 unset,
                              * CC_COLOR_CURRENT for currentColor. Colours inline-<svg>
                              * shapes that declare no fill of their own.
                              * CSS_SVG_PAINT_NONE for `fill:none`. */
    /* SVG `stroke` (same encoding as svg_fill, CSS_SVG_PAINT_NONE = none) and
     * `stroke-width` in hundredths of a px (-1 unset). Both inherit (SVG 2 13.2). */
    int         svg_stroke;
    int         svg_stroke_w;
    int         background;  /* 0xRRGGBB or -1 (unset) */
    /* Background alpha (2026-07-19): the 4th component of an rgba()/hsla()
     * background color as a percent 0..100; CSS_LEN_UNSET = opaque. The color
     * channel above resolves unchanged. The box background fill multiplies by
     * it (glassmorphism needs translucent panels). #RRGGBBAA hex is out of
     * scope v1. */
    int         bg_alpha;
    /* linear-gradient background (2026-07-11). NOT inherited, like background.
     * bg_grad_n: stop count (0 = no gradient, 2..CSS_GRAD_STOPS_MAX). bg_grad_angle:
     * CSS degrees normalized [0,359] (0 = to top, 90 = to right; 180 = to bottom is
     * the default). bg_grad_c: packed 0xRRGGBB stops, painted evenly spaced (stop
     * positions are parsed but ignored in v1). The accepted grammar has no URL form:
     * a gradient can never fetch. */
    int         bg_grad_n;
    int         bg_grad_angle;
    int         bg_grad_c[CSS_GRAD_STOPS_MAX];
    int         bg_grad_pos[CSS_GRAD_STOPS_MAX]; /* R5d: stop positions 0-1000 */
    int         bg_grad_radial;   /* gradient KIND: 0=linear, 1=radial, 2=conic */
    /* radial geometry (2026-10-20): bg_rad_size = shape*16 + size (cg_radial_shape /
     * cg_radial_size); centre and explicit radii in the cg_radial encoding (>=0 per-mille
     * of the box, <0 is -(px)-1, CG_LEN_UNSET = not given). Meaningful only for kind 1. */
    int         bg_rad_size, bg_rad_cx, bg_rad_cy, bg_rad_rx, bg_rad_ry;
    /* mask (spec/css_mask.md): layer count (0 = none), mask-composite (cm_comp), the clip pair
     * (clip0 | clip1<<2), and the three compact words of each of the two layers. */
    int         mask_n, mask_comp, mask_clip;
    int         mask_w[6];
    /* mask-image: url() (tanda 83): the raw URL of the first mask layer ("" = none; gated and
     * resolved downstream exactly like background-image), and its geometry (CSS_MG_*). */
    char        mask_url[CSS_URL_MAX];
    int         mask_geo[CSS_MASK_GEO_N];
    char        content_str[CSS_URL_MAX];                   /* R8: ::before content (backward compat; synced from content_before_str or content_after_str) */
    char        content_before_str[CSS_URL_MAX];            /* R8: ::before{content:"X"} value */
    /* The winning `content` GENERATES a box (any string, `""` included) rather
     * than `none`/`normal`/unset: content_on for the element/pseudo itself,
     * content_before_on/content_after_on for its ::before/::after. An empty string
     * is how almost every decorative generated box is written, and it is the one
     * the *_str fields cannot tell from `none`. 0 by default. */
    int         content_on, content_before_on, content_after_on;
    char        content_after_str[CSS_URL_MAX];             /* R8: ::after{content:"X"} value */
    css_align   text_align;  /* CSS_ALIGN_UNSET if absent */
    int         font_scale;  /* percent (e.g. 150), or 0 (unset) */
    /* Whether font_scale is a percent OF THE 16px ROOT (1: px/pt/rem/viewport units
     * and the absolute keywords) or of the INHERITED size (0: em/%/smaller/larger).
     * Meaningless when font_scale is 0. An absolute size REPLACES the user-agent
     * heading scale instead of multiplying it -- without this every author-styled
     * <h1> rendered at double size. spec/css.md "font-size: absolute vs relative". */
    int         font_abs;
    int         line_scale;  /* line-height percent of the natural line box, or 0 (unset) */
    int         text_decoration; /* OR of CSS_DECO_*; 0 = none; -1 = unset */
    int         text_decoration_color; /* 0xRRGGBB or -1 (unset) */
    int         text_decoration_style; /* css_text_decoration_style, 0 (unset) */
    int         text_underline_offset;     /* px, CSS_LEN_AUTO = auto, CSS_LEN_UNSET = not declared */
    int         text_decoration_thickness; /* px for underline/strike/overline line width,
                                              or -1 unset, or 0 for from-font */
    int         bold;        /* 1, 0, or -1 (unset) */
    int         italic;      /* 1, 0, or -1 (unset) */
    css_display display;     /* CSS_DISP_UNSET if absent */
    int         gap;         /* px between flex/grid items, or -1 (unset) */
    css_justify justify;     /* justify-content; CSS_JUSTIFY_UNSET if absent */
    int         grid_cols;   /* grid-template-columns track count, or 0 (unset) */
    /* Sizes of the first CSS_GRID_TRACKS_MAX tracks (2026-07-11): 0 = auto (an
     * equal 1fr share), > 0 = fixed px, < 0 = fr weight x100 (2fr -> -200).
     * minmax(a,b) resolves to its max component; %/unknown fall to auto. Tracks
     * past the cap lay out as auto. Only meaningful when grid_cols > 0. */
    int         grid_col_w[CSS_GRID_TRACKS_MAX];
    /* grid-template-rows sizes, encoded like grid_col_w (0 auto / >0 px / <0 fr
     * x100); grid_rows is their count. */
    int         grid_row_h[CSS_GRID_TRACKS_MAX];
    /* Named grid placement (2026-08-14, CSS Grid 1 7.3 + 8.4). On a CONTAINER,
     * grid_areas is the raw `grid-template-areas` value (the quoted row strings);
     * "" = not declared. On an ITEM, grid_area_name is its `grid-area: <name>`
     * already reduced to fx_grid_area_hash (0 = not declared) -- a hash rather than
     * text because the item's placement has to cross the IPC codec, and one int
     * does that with no string channel. page_view is what pairs the two: it parses
     * the container's template once and resolves each item's hash to a rectangle,
     * so only resolved row/column numbers reach the painter. */
    char        grid_areas[CSS_GRID_AREAS_MAX];
    unsigned    grid_area_name;
    /* Author box model (px; NOT inherited — read from the element's own style).
     * margins: CSS_LEN_UNSET / CSS_LEN_AUTO / signed px. padding: CSS_LEN_UNSET /
     * px >= 0. width/max_width: CSS_LEN_UNSET / px > 0 (auto/none -> unset). */
    int         margin_top, margin_right, margin_bottom, margin_left;
    int         pad_top, pad_right, pad_bottom, pad_left;
    int         width, max_width, min_width;
    int         height, min_height, max_height;
    /* The percentage component of every <length-percentage> property, in
     * per-mille of the containing block, indexed by css_pct_slot. 0 = none.
     * Resolved against the px field of the SAME property at layout time
     * (bx_lp_px). See the css_pct_slot documentation above. */
    int         pct[CSS_PCT_N];
    /* Author text-presentation extensions (Hito 23b-6). All inherit in CSS; in this
     * flat model the caller takes the nearest ancestor that sets each. Like the other
     * author presentation, they are gated behind caps.css downstream. */
    int         font_family;     /* css_font_family, 0 (unset) */
    /* Author @font-face match (spec/webfont.md): wf_name_hash of the winning
     * font-family's FIRST name when it names a @font-face family of the sheet,
     * else 0. Inherits like font_family; gated behind caps.css downstream.
     * 0 also when there is no sheet (inline-only resolve) or no match. */
    unsigned    fontface;
    /* The winning font-family list, normalized (unquoted, lowercased, spaces
     * collapsed, comma-separated), "" = undeclared. The painter resolves it to a
     * LOCAL family (spec/text_shape.md section 7); the worker never sees a font. */
    char        font_list[CSS_FONT_LIST_MAX];
    int         text_transform;  /* css_text_transform, 0 (unset) */
    int         letter_spacing;  /* px (signed), 0 = normal, CSS_LEN_UNSET (unset) */
    int         word_spacing;    /* px (signed), 0 = normal, CSS_LEN_UNSET (unset) */
    int         shadow_dx;       /* text-shadow x offset px (signed), 0 if no shadow */
    int         shadow_dy;       /* text-shadow y offset px (signed), 0 if no shadow */
    int         shadow_color;    /* text-shadow color 0xRRGGBB, or -1 (none/unset) */
    int         opacity;         /* percent 0..100, or -1 (unset) */
    int         valign;          /* css_valign, 0 (unset) */
    /* vertical-align's OTHER production: a <length-percentage> baseline shift
     * (CSS 2.1 10.8.1), positive = raise. Signed px, CSS_LEN_UNSET when the value
     * was a keyword or absent. The two are exclusive by grammar, but they are
     * separate slots because they answer different questions and a single field
     * would have to overload the keyword enum with a signed length. The
     * percentage production resolves against the element's own line-height and
     * therefore rides the shared pct[] channel, at CSS_PCT_VALIGN. */
    int         valign_shift;
    int         text_indent;     /* px (signed), CSS_LEN_UNSET (unset) */
    int         white_space;     /* css_white_space, 0 (unset) */
    /* text-wrap-style: 1 balance, 0 an explicit other style (wrap/pretty/stable/
     * nowrap), -1 unset. Inherits (CSS Text 4 6.4.2). */
    int         text_balance;
    int         tab_size;        /* number of spaces for tab, 0 (unset) */
    int         list_style;      /* css_list_style, 0 (unset) */
    int         direction;       /* css_direction, 0 (unset) */
    /* --- Layout / box decoration (Hito 23b-7). NOT inherited (read from the
     * element's own resolved style), like the box model. All gated behind caps.css
     * downstream except `display`/flex/grid container params, which are structure. */
    int         position;        /* css_position, 0 (unset) */
    int         inset_top, inset_right, inset_bottom, inset_left; /* CSS_LEN_UNSET/AUTO/px */
    int         z_index;         /* signed, or CSS_LEN_UNSET (unset; `auto` -> unset) */
    int         box_sizing;      /* css_box_sizing, 0 (unset) */
    /* Per-side border. widths: px >= 0 or CSS_LEN_UNSET. styles: css_border_style.
     * colors: 0xRRGGBB or -1 (unset). */
    int         border_top_width, border_right_width, border_bottom_width, border_left_width;
    int         border_top_style, border_right_style, border_bottom_style, border_left_style;
    int         border_top_color, border_right_color, border_bottom_color, border_left_color;
    /* border-radius, one value per corner (px >= 0, or CSS_LEN_UNSET). The
     * `border-radius` shorthand writes all four in the CSS corner order
     * top-left, top-right, bottom-right, bottom-left; the four longhands and
     * the logical spellings write one each. border_radius IS the top-left
     * corner -- kept under that name because it is the one the shorthand-only
     * pages set, and every consumer already reads it.
     *
     * Only a CIRCULAR radius is modelled: CSS's elliptical `r1 / r2` form keeps
     * its first (horizontal) component, a stated simplification, not a parse
     * failure -- dropping the declaration would lose the rounding entirely. */
    int         border_radius;         /* top-left */
    int         border_radius_tr;
    int         border_radius_br;
    int         border_radius_bl;
    /* box-shadow (single layer). offsets/blur/spread px; color 0xRRGGBB or -1 (none/
     * unset); inset 1/0, or -1 (unset). */
    int         shadow2_dx, shadow2_dy, shadow2_blur, shadow2_spread;
    int         box_shadow_color;
    int         box_shadow_inset;
    /* box-shadow layers 2..CSS_BSHADOW_LAYERS, each {dx, dy, blur, spread, color,
     * inset} exactly like the first layer above; box_shadow_n counts ALL layers
     * (0 = none declared). A color carries its alpha as transparency in bits
     * 24..30 (cv_parse_color_t7), so an opaque color is the plain 0xRRGGBB. */
    int         box_shadow_n;
    int         box_shadow_x[CSS_BSHADOW_LAYERS - 1][6];
    int         outline_width;   /* px >= 0, or CSS_LEN_UNSET */
    int         outline_style;   /* css_border_style */
    int         outline_color;   /* 0xRRGGBB or -1 */
    int         outline_offset;  /* px (signed), CSS_LEN_UNSET (unset) */
    /* Flex item + container extras. grow/shrink stored x100 (1.0 -> 100), -1 unset.
     * basis: px >= 0 / CSS_LEN_AUTO / CSS_LEN_UNSET. order/z reuse CSS_LEN_UNSET. */
    int         flex_grow, flex_shrink, flex_basis;
    int         order;           /* signed, or CSS_LEN_UNSET */
    int         align_items, align_self, align_content, justify_items; /* css_align_kw */
    int         justify_self;    /* css_align_kw: the item's inline-axis alignment in its grid area */
    int         flex_direction;  /* css_flex_direction */
    int         flex_wrap;       /* css_flex_wrap */
    /* Grid extras. grid_rows: track count, 0 unset. row_gap: px or -1 unset. */
    int         grid_rows;
    int         row_gap;
    int         grid_auto_flow;  /* css_grid_flow */
    /* grid-{column,row}-{start,end}: one placement SIDE each, encoded as in
     * flex_layout.h (FX_GL_AUTO = 0 = unset/auto, FX_GL_SPAN_BASE + n = span n,
     * else a line number). Resolved against the container's tracks in page_view.
     * spec/grid_areas.md section 6. */
    int         grid_col_start, grid_col_end, grid_row_start, grid_row_end;
    /* Float layout (spec/float.md). NOT inherited (read from the element's own resolved
     * style, like the box model). Layout structure, applied regardless of caps.css. */
    int         float_side;      /* css_float, 0 (unset) */
    int         clear;           /* css_clear, 0 (unset) */
    /* visibility / overflow / cursor. NOT inherited (read from the element's own
     * resolved style, like position/border). All gated behind caps.css downstream
     * (presentation, like the box decoration). */
    int         visibility;      /* css_visibility, 0 (unset) */
    int         overflow_x;      /* css_overflow, 0 (unset) */
    int         overflow_y;      /* css_overflow, 0 (unset) */
    int         cursor;          /* css_cursor, 0 (unset) */
    /* text-overflow / word-break (unified, see css_word_break). These INHERIT like
     * white-space, so the caller (page_view) takes the nearest ancestor that sets
     * each. Gated behind caps.css downstream. */
    int         text_overflow;   /* css_text_overflow, 0 (unset) */
    int         word_break;      /* css_word_break, 0 (unset) */
    /* Table properties. NOT inherited (read from the element's own resolved style). */
    int         border_collapse; /* css_border_collapse, 0 (unset) */
    int         border_spacing;  /* px (signed), CSS_LEN_UNSET (unset) */
    int         empty_cells;     /* css_empty_cells, 0 (unset) */
    int         caption_side;    /* css_caption_side, 0 (unset) */
    int         table_layout;    /* css_table_layout, 0 (unset) */
    /* Text-presentation extension (font-variant). INHERITS like other text props. */
    int         font_variant;    /* css_font_variant, 0 (unset) */
    /* Text/interaction properties. INHERIT by default (user-select, hyphens,
     * caret-color, pointer-events inherit per spec; appearance does not). */
    int         hyphens;         /* css_hyphens, 0 (unset) */
    int         user_select;     /* css_user_select, 0 (unset) */
    int         caret_color;     /* 0xRRGGBB or -1 (unset) */
    int         appearance;      /* css_appearance, 0 (unset) */
    int         pointer_events;   /* css_pointer_events, 0 (unset) */
    int         bg_repeat;        /* css_bg_repeat, 0 (unset) */
    int         bg_size;          /* css_bg_size, 0 (unset) */
    /* background-size's explicit production: one or two <length-percentage>|auto
     * components (Backgrounds 3 3.9). px, CSS_LEN_AUTO for the `auto` component,
     * CSS_LEN_UNSET when the value was a keyword (cover/contain, in bg_size) or
     * absent. The percentage halves resolve against the background POSITIONING
     * AREA, which only the painter knows, so they travel per-mille like every
     * other <length-percentage>. */
    int         bg_size_w, bg_size_h;
    /* background-image: url(...) (2026-07-16). The RAW, UNRESOLVED url() text
     * (possibly relative, e.g. "hero.jpg") -- css.c never fetches or resolves it,
     * same doctrine as bg_grad_*; resolving against the page origin and deciding
     * whether to actually fetch happens downstream (render_doc.c), under the same
     * caps.images + rdp_image_decision gate as an <img>. "" (empty) means none.
     * Mutually exclusive in practice with bg_grad_n>0 (whichever the author's
     * declaration set last wins the cascade slot; the painter prefers the
     * gradient when both are somehow set, see gui/browser_ui.c). */
    char        bg_image_url[CSS_URL_MAX];
    /* R5b: second background-image layer (painted behind the first). "" = none. */
    char        bg_image_url2[CSS_URL_MAX];
    int         bg_clip;          /* css_bg_clip, 0 (unset) */
    /* -webkit-text-fill-color (2026-07-19, gradient text): packed 0xRRGGBB,
     * CC_COLOR_TRANSPARENT, or -1 (unset). With bg_clip == CSS_BGC_TEXT and a
     * gradient/bg color, `transparent` yields gradient-filled glyphs; without
     * a clip source, `transparent` is ignored downstream (fail-visible). */
    int         text_fill_color;
    int         bg_origin;        /* css_bg_origin, 0 (unset) */
    int         bg_attachment;    /* css_bg_attachment, 0 (unset) */
    int         isolation;        /* css_isolation, 0 (unset) */
    int         contain;          /* bitmask CSS_CONTAIN_*, 0 = none/unset */
    int         content_visibility; /* css_content_visibility, 0 (unset) */
    int         image_rendering;  /* css_image_rendering, 0 (unset) */
    int         color_scheme;     /* css_color_scheme, 0 (unset) */
    int         accent_color;     /* 0xRRGGBB or -1 (unset) */
    int         print_color_adjust;   /* css_print_color_adjust, 0 (unset) */
    int         forced_color_adjust;  /* css_forced_color_adjust, 0 (unset) */
    int         mix_blend_mode;   /* css_mix_blend, 0 (unset) */
    int         object_fit;       /* css_object_fit, 0 (unset) */
    int         object_pos;       /* CSS_OBJPOS_PACK, 0 (unset) = centred */
    /* counter-reset / counter-increment (spec/css_counter.md). Not inherited; the
     * values are applied by page_view in document order, never by the cascade. */
    cnt_decl    cnt_reset[CNT_MAX_DECL], cnt_inc[CNT_MAX_DECL];
    int         cnt_reset_n, cnt_inc_n;
    int         list_style_pos;   /* css_list_pos, 0 (unset) */
    int         font_kerning;     /* css_font_kerning, 0 (unset) */
    int         text_rendering;   /* css_text_rendering, 0 (unset) */
    int         font_stretch;     /* css_font_stretch, 0 (unset) */
    int         resize;           /* css_resize, 0 (unset) */
    int         scroll_behavior;  /* css_scroll_behavior, 0 (unset) */
    int         touch_action;     /* css_touch_action, 0 (unset) */
    int         overscroll_behavior; /* css_overscroll, 0 (unset) */
    int         backface_visibility; /* css_backface, 0 (unset) */
    /* aspect-ratio (Hito 2026-07-10). Stored as numerator/denominator x1000
     * (so 16/9 -> num=16000, den=9000). auto/unset -> both 0. */
    int         aspect_num, aspect_den;
    /* transform (M1.2 translate; M1.2b adds scale/rotate): signed px offsets from
     * translate()/translateX()/translateY(); transform_sx/sy are scale()/scaleX()/
     * scaleY() as a PERCENT of identity (100 = scale(1), matching font_scale's
     * convention); transform_rotate is rotate() in whole DEGREES (deg unit only,
     * same convention as the linear-gradient angle grammar -- rad/turn/grad and
     * fractional degrees are unsupported). CSS_LEN_UNSET on any field means that
     * function was not declared (identity: 0 offset / 100% scale / 0deg).
     * Percentage translate arguments are not supported (the whole declaration
     * fails closed to unset -- see expand_transform in css.c and
     * spec/compositor.md). Exactly one transform FUNCTION may be declared per
     * `transform` value (no chaining); the fields are independent cascade slots
     * so two different rules matching the same element (e.g. one setting
     * translate, a more specific one setting rotate) can still combine -- see
     * expand_transform's comment. A box with any field set (even to an identity
     * value, e.g. translate(0,0) or scale(1)) still establishes a CSS stacking
     * context per spec, independent of the actual value.
     * M1.2c (2026-07-19): transform_skx/sky are skew()/skewX()/skewY() in whole
     * DEGREES (same grammar as rotate); matrix(a,b,c,d,e,f) is QR-decomposed at
     * parse time into ALL seven slots (singular matrices fail closed);
     * transform_ox/oy are transform-origin as PERCENTS of the box
     * (CSS_LEN_UNSET = the CSS default 50% center; keywords and % only, px
     * lengths fail closed). transform-origin alone is inert: it neither forms
     * a stacking context nor registers a box. */
    int         transform_tx, transform_ty;
    int         transform_sx, transform_sy;
    int         transform_rotate;
    int         transform_skx, transform_sky;
    int         transform_ox, transform_oy;
    /* animation-duration (Phase R1): parsed time in ms, 0 = unset/no-animation.
     * Other animation-* properties and @keyframes follow in Phase R1b. */
    int         anim_duration_ms;
    /* @keyframes / animation-* (R1b) */
    char        anim_name[64];  /* @keyframes name, "" = none */
    int         anim_iterations;          /* -1 = infinite, 0 = unset (default 1) */
    int         anim_direction;           /* ip_direction: 0=normal, 1=reverse, 2=alternate, 3=alt-reverse, -1=unset */
    int         anim_fill_mode;           /* ip_fill_mode, -1=unset */
    int         anim_timing;             /* ip_easing, -1=unset */
    int         anim_delay_ms;           /* delay in ms, 0=unset */
    /* Resolved @keyframes data (Phase R1c): the keyframe stops from the matching
     * @keyframes rule, resolved after cascade. anim_nkf=0 means no keyframes
     * (use default 0→100 opacity). */
#define CSS_MAX_KF_STOPS 8
    int         anim_nkf;                /* 0..CSS_MAX_KF_STOPS, 0 = use default */
    int         anim_kf_pct[CSS_MAX_KF_STOPS];  /* percentage * 100 (0..10000) */
    int         anim_kf_val[CSS_MAX_KF_STOPS];  /* opacity value 0..100 */
    /* Phase R1e: background/foreground color keyframe values per stop.
     * -1 = unset; otherwise packed 0xRRGGBB. */
    int         anim_kf_bg[CSS_MAX_KF_STOPS];  /* background-color per stop */
    int         anim_kf_fg[CSS_MAX_KF_STOPS];  /* color per stop */
    /* Phase R1d: transform keyframe values per stop. PV_LEN_UNSET = unset for
     * tx/ty; 0 = unset for sx/sy/rotate (identity). */
    int         anim_kf_tx[CSS_MAX_KF_STOPS];
    int         anim_kf_ty[CSS_MAX_KF_STOPS];
    int         anim_kf_sx[CSS_MAX_KF_STOPS];
    int         anim_kf_sy[CSS_MAX_KF_STOPS];
    int         anim_kf_rot[CSS_MAX_KF_STOPS];
    /* transition (v1 parse-only; runtime interpolation deferred). */
    int         transition_duration_ms;
    int         transition_property;      /* -1=unset, 0=none, 1=all, 2=opacity, 3=transform */
    int         transition_timing;        /* ip_easing, -1=unset */
    int         transition_delay_ms;
    /* filter (Phase R3): blur radius in px (0=none), grayscale 0..100% (0=none),
     * brightness/contrast/sepia/invert/saturate 0..100% (100=identity for
     * brightness/contrast/saturate, 0=none for sepia/invert),
     * hue-rotate 0..359 degrees. */
    int         filter_blur;
    int         filter_grayscale;
    /* backdrop-filter: blur(Npx) (2026-07-19, glassmorphism v1). 0 = none.
     * Parsed from the same lax function-list grammar as filter (unsupported
     * functions in the list are ignored; -webkit-backdrop-filter aliases).
     * Painted by blurring the already-painted backdrop under the box rect
     * before the box's own (typically translucent) background composites on
     * top. See spec/compositor.md. */
    int         backdrop_blur;
    int         filter_brightness;
    int         filter_contrast;
    int         filter_sepia;
    int         filter_invert;
    int         filter_saturate;
    int         filter_hue_rotate;
    /* filter: drop-shadow(dx dy [blur] [color]) (2026-07-19): signed px
     * offsets, blur radius px (0 = sharp), packed color. drop_color -1 =
     * no drop-shadow declared (dx/dy/blur then meaningless). Unlike
     * box-shadow this follows the ALPHA of the composited group (the real
     * content silhouette), see spec/css.md. */
    int         filter_drop_dx;
    int         filter_drop_dy;
    int         filter_drop_blur;
    int         filter_drop_color;
    /* background-position (R5a): px offset from top-left, CSS_LEN_UNSET = unset
     * ("0% 0%" which is also the CSS initial value). */
    int         bg_pos_x;
    int         bg_pos_y;
    /* --- Multi-column (CSS Multi-column Layout 1) ---------------------------
     * NOT inherited (they describe the container element itself), like the
     * flex/grid container params, and like them they are LAYOUT STRUCTURE.
     * column_count 0 = `auto`; column_width 0 = `auto`; with both auto the
     * element is not a multi-column container at all and lays out unchanged.
     * The used gap is css_style.gap -- `column-gap` is the same property in CSS
     * Box Alignment, so it keeps writing that one slot. */
    /* line-clamp / -webkit-line-clamp (CSS Overflow 4; the -webkit- form is
     * specified in Overflow 3 "Legacy"). A block with a clamp of N and
     * `overflow` other than visible shows at most N line boxes and clips the
     * rest. NOT inherited. 0 = none.
     *
     * It is layout, not decoration: dropping it made every clamped card title
     * on jkanime render its full four lines where the author asked for two, and
     * that difference multiplies over a grid of cards. */
    int         line_clamp;
    int         column_count;    /* <integer>, 0 = auto */
    int         column_width;    /* px, 0 = auto */
    int         column_fill;     /* css_column_fill */
    int         column_span;     /* css_column_span */
    int         column_rule_width; /* px >= 0, or CSS_LEN_UNSET */
    int         column_rule_style; /* css_border_style */
    int         column_rule_color; /* 0xRRGGBB, or -1 */
    /* clip: rect(top,right,bottom,left) for positioned boxes (CSS 2.1 §11.1.2).
     * px values, CSS_LEN_UNSET on a side = auto (the border-box edge). Only
     * applies to position:absolute/fixed per spec. */
    int         clip_top;
    int         clip_right;
    int         clip_bottom;
    int         clip_left;
    /* Per-mille of the border box added to each clip edge above (clip-path: inset()); 0 for
     * `clip: rect()`. Meaningful only while the px half is not CSS_LEN_UNSET. */
    int         clip_top_pm, clip_right_pm, clip_bottom_pm, clip_left_pm;
} css_style;

typedef struct css_sheet css_sheet; /* opaque; owns the parsed rules */

/* Render-time media context for evaluating @media at parse time. width_px is a
 * fixed, normalized desktop width, so a (min/max-width) query leaks no real viewport
 * size (anti-fingerprinting). */
struct css_sel;  /* css_select.h */

typedef struct css_media {
    int prefers_dark; /* 1: the user prefers a dark color scheme */
    int print;        /* 1: rendering for print (PDF); 0: screen */
    int width_px;     /* assumed viewport width for min/max-width queries */
    /* Optional root matcher for custom-property collection (spec/css.md, "Root
     * matcher"): answers nonzero iff the parsed selector matches the document's
     * <html> or <body>. NULL keeps the class-list-only root scope. The css
     * module never sees the DOM; the caller answers with its real matcher. */
    int (*scope_match)(void *ctx, const struct css_sel *sel);
    void *scope_ctx;
} css_media;

#define CSS_MEDIA_DEFAULT_WIDTH 1920
/* Normalized viewport height paired with CSS_MEDIA_DEFAULT_WIDTH (16:9 desktop,
 * the top fp_bucket_screen bucket). vw/vh/vmin/vmax lengths resolve against this
 * fixed 1920x1080 viewport at parse time, so a hostile stylesheet never learns
 * real window geometry (anti-fingerprinting; see spec/css.md "Viewport units"). */
#define CSS_MEDIA_DEFAULT_HEIGHT 1080

/* Parses a <style> text (one or many blocks concatenated) into *out. Malformed
 * input never fails: unparseable rules are skipped. text == NULL is treated as
 * empty. out == NULL => CSS_ERR_NULL_ARG; allocation failure => CSS_ERR_OOM. On
 * CSS_OK, *out must be freed with css_free. Equivalent to css_parse_media with a
 * default screen / light / 1920px context. */
css_status css_parse(const char *text, size_t len, css_sheet **out);

/* 1 when any rule of the sheet declares counter-reset / counter-increment. page_view
 * runs its document-order counter pass only then (it costs a second tree walk). */
int css_sheet_uses_counters(const css_sheet *sheet);

/* As css_parse, but @media blocks are gated against *media (prefers-color-scheme,
 * screen/print/all, min/max-width). media == NULL uses the default screen/light/
 * 1920px context. Matched @media rules fold into the sheet as if unconditional, so
 * css_resolve is unchanged. @import/@font-face/other @-rules are still skipped. */
css_status css_parse_media(const char *text, size_t len, const css_media *media,
                           css_sheet **out);

/* As css_parse_media, plus a root scope for custom-property collection:
 * root_scope is a space-separated list of the class names actually present on the
 * document's <html>/<body> elements (NULL = none). A `--name: value` declaration
 * is collected into the page-global var() table only from rules whose enclosing
 * @media blocks match *media and whose selector group contains a root-scoped
 * selector (`:root`, `html`, `body`, the universal selector, and/or .classes from
 * root_scope; one compound, no combinators). Everything else is skipped fail-safe,
 * so an inactive theme
 * palette (a dark palette in a light render) can never clobber the active one.
 * css_parse/css_parse_media delegate here with root_scope == NULL. */
css_status css_parse_scoped(const char *text, size_t len, const css_media *media,
                            const char *root_scope, css_sheet **out);

/* Why a declaration was discarded. The distinction is the point of the log: the two
 * causes need different fixes, and BAD_VALUE is the dangerous one because the
 * property IS implemented -- it shows up in every name-based inventory while the
 * declaration is lost anyway (a leading-dot number, a percentage inside a shorthand,
 * a `pt` length have each cost a whole parity batch). See spec/css_drops.md. */
typedef enum css_drop_cause {
    CSS_DROP_UNKNOWN_PROP = 0,  /* the name is not in interpret_prop's dispatch */
    CSS_DROP_BAD_VALUE    = 1   /* the name is known; the value grammar rejected it */
} css_drop_cause;

#define CSS_DROP_PROP_MAX 64
#define CSS_DROP_VAL_MAX  96

/* One coalesced drop: every occurrence of the same (property, cause) pair folds into
 * a single entry, because a real sheet repeats one unsupported property hundreds of
 * times and an un-grouped listing is unreadable. `val` samples the FIRST occurrence,
 * bounded and sanitised (control bytes -> '.'), because it is hostile remote text
 * headed for a terminal. */
typedef struct css_drop {
    char prop[CSS_DROP_PROP_MAX];
    char val[CSS_DROP_VAL_MAX];
    int  cause;   /* css_drop_cause */
    int  count;   /* occurrences folded into this entry */
} css_drop;

/* Optional write-only sink the parser fills while parsing. `items` is CALLER-OWNED
 * (the parser allocates nothing, so the log adds no OOM path); items == NULL or
 * cap == 0 disables the listing but NOT the counting, so a report can always state
 * the true magnitude ("N dropped, first M listed") instead of silently understating
 * it. Nothing recorded here feeds back into the cascade. */
typedef struct css_drop_log {
    css_drop *items;
    size_t    cap;
    size_t    n;
    size_t    total;
} css_drop_log;

/* As css_parse_scoped, plus the drop sink. log == NULL is exactly css_parse_scoped:
 * same path, same sheet, same cost. The resulting sheet is identical with and
 * without a log -- the instrument can never change the render. */
css_status css_parse_logged(const char *text, size_t len, const css_media *media,
                            const char *root_scope, css_sheet **out,
                            css_drop_log *log);

/* Idempotent; NULL-safe. */
void css_free(css_sheet *s);

/* Computes the presentation for one element: every matching sheet rule applied in
 * cascade order (specificity, then document order), then the element's own
 * inline_style (which wins). sheet/tag/classes/inline_style may be NULL. Pure,
 * allocates nothing, reentrant. Inheritance is the caller's job (call per ancestor,
 * merge unset fields). */
css_style css_resolve(const css_sheet *sheet, const char *tag, const char *id,
                      const char *const *classes, size_t nclasses,
                      const char *inline_style, size_t inline_len);

/* One element attribute, for attribute selectors ([attr], [attr=v], [attr~=v], ...).
 * name is the lowercased local name; value is the attribute text ("" if empty). Both
 * alias caller storage (nothing is copied/owned). */
typedef struct css_attr {
    const char *name;
    const char *value;
} css_attr;

/* An element plus its ancestor chain, for combinator matching. Each field aliases
 * caller storage (nothing is copied/owned). parent walks toward the root (NULL at
 * the top). A bounded/partial chain is fine: a descendant compound that would have
 * matched a missing deeper ancestor simply does not match (fail closed). attrs/nattrs
 * may be empty (NULL/0): an attribute selector then simply does not match. */
typedef struct css_element {
    const char *tag;                  /* lowercased local name, or NULL */
    const char *id;                   /* id attribute value, or NULL */
    const char *const *classes;       /* class tokens (not NUL-joined) */
    size_t nclasses;
    const css_attr *attrs;            /* element attributes, or NULL */
    size_t nattrs;
    const struct css_element *parent; /* parent element, or NULL at the root */
    /* Sibling context (Hito 23b-9). All optional; zero/NULL = unknown, and the
     * structural pseudo-classes / sibling combinators then fail closed (never
     * mis-match). nth is the 1-based index among element siblings; nsib the
     * total element siblings including self; prev the previous element sibling. */
    int nth;
    int nsib;
    const struct css_element *prev;
    /* Sibling-of-type context (R2). Like nth/nsib but only counting siblings with
     * the SAME tag name. Zero = unknown (pseudo fails closed). */
    int nth_of_type;
    int nsib_of_type;
    /* Total child node count (elements + text + comments, from the DOM walk;
     * R2). -1 = unknown (:empty fails closed), 0 = truly empty (:empty matches). */
    int child_count;
    /* Pointer back to the source DOM node (lxb_dom_node_t *), for pseudo-classes
     * that need descendant traversal (:has()). NULL means no DOM access available
     * (the pseudo fails closed). */
    const void *dom_node;
    /* Dynamic element state: a bitmask of CSEL_STATE_* (css_select.h) driving
     * :hover/:active/:focus/:focus-within/:focus-visible. Zero -- a freshly
     * loaded page, pointer nowhere, nothing focused -- is the default and makes
     * all of them fail to match, exactly as any browser does. The engine, not
     * the matcher, owns this. */
    unsigned state;
    /*
     * The computed font-size of the PARENT element, in px -- the value this
     * element inherits. <= 0 means "unknown", and every font-relative length
     * then falls back to CL_INITIAL_FONT_SIZE, which is what the whole engine
     * did before this field existed.
     *
     * It is the parent's and not this element's because that is CSS's own
     * order of operations: `font-size` is computed FIRST, against the inherited
     * value, and only then can `em` mean anything for the other properties.
     * css_resolve_el derives this element's own size from it and folds every
     * font-relative length against THAT (spec/css_length.md section 8.5).
     *
     * An engine input like `state`, not a style: the cascade cannot discover it
     * on its own, and inventing 16px for it -- which is exactly what the engine
     * used to do -- makes `width:10em` mean 160px on a 32px element.
     */
    double font_size;
    /* The element's INHERITED custom properties (struct cvr_chain, css_vars.h):
     * its parent's computed chain, supplied top-down by the engine like
     * font_size. NULL = none, and var() then sees only the page-global table. */
    const struct cvr_chain *vars;
    /* (appended last so positional initialisers keep working) Ancestor feature filter (css_select.h csel_anc_reject): the union of csel feature bits of
     * every ANCESTOR's tag, id and classes. Valid only when anc_bloom_valid is 1; zero (the
     * default) means "unknown" and never rejects anything, so a view that does not compute it is
     * exactly as before. */
    unsigned long long anc_bloom;
    unsigned long long own_bloom;   /* the element's OWN tag/id/class bits (same hashing) */
    int anc_bloom_valid;
} css_element;

/* As css_resolve, but matches descendant (`A B`) and child (`A > B`) combinators
 * against el's ancestor chain. el == NULL resolves only inline_style. Pure,
 * allocates nothing, reentrant. css_resolve is this with a parentless element. */
css_style css_resolve_el(const css_sheet *sheet, const css_element *el,
                         const char *inline_style, size_t inline_len);

struct cvr_table;
/* As css_resolve_el, and when own_out is non-NULL (a zeroed cvr_table the caller
 * owns and frees with cvr_free) it receives the element's OWN winning custom
 * properties -- what its children inherit on top of el->vars. See
 * spec/css_vars.md, "Alcance por elemento". */
css_style css_resolve_el_ex(const css_sheet *sheet, const css_element *el,
                            const char *inline_style, size_t inline_len,
                            struct cvr_table *own_out);

/* Which generated box css_resolve_pseudo resolves (CSS 2.1 12.1). */
#define CSS_PSEUDO_BEFORE 1
#define CSS_PSEUDO_AFTER  2
#define CSS_PSEUDO_PLACEHOLDER 3   /* ::placeholder of a text control */

/* The computed style of el's ::before (CSS_PSEUDO_BEFORE) or ::after
 * (CSS_PSEUDO_AFTER) generated box: only the rules whose subject carries that
 * pseudo-element take part, and they apply EVERY declaration (content included,
 * in content_str). No inline style (an attribute cannot style a pseudo). The
 * result is not merged with the originating element: inherited properties the
 * pseudo does not set are the caller's to take from el. Pure, reentrant. */
css_style css_resolve_pseudo(const css_sheet *sheet, const css_element *el, int which);

/*
 * The computed font-size in px of an element whose resolved style is *o and
 * whose PARENT computes to inherited_px (<= 0 = unknown -> the CSS initial).
 *
 * Exported because the caller that walks the DOM is the one that can supply the
 * inherited value, and it needs the same formula css_resolve_el used internally
 * to fold font-relative lengths -- two copies of it would be two answers to
 * "how big is 1em here". Pure.
 */
double css_computed_font_size(const css_style *o, double inherited_px);

/* The user-agent sheet's font-size of <h1>..<h6> as a multiple of the parent's (HTML
 * Rendering 15.3.6: 2em, 1.5em, 1.17em, 1em, .83em, .67em). 1.0 for any other level. The
 * ONE table: the painter's theme reads it too. */
double css_ua_heading_em(int level);

/* css_computed_font_size for a named element: a heading with NO author font-size is the UA
 * sheet's multiple of its parent (an author font-size replaces it), so an `em` length on
 * an <h2> resolves against 1.5x the parent, not the parent. tag is the lowercased local
 * name (NULL = not a heading). */
double css_computed_font_size_for_tag(const css_style *o, double inherited_px, const char *tag);

/* Convenience: resolve only an inline declaration list (no sheet, no selectors). */
css_style css_parse_inline(const char *style, size_t len);

/* Web font access: returns the number of @font-face declarations parsed into
 * the sheet, and copies the i-th declaration's family and src URL into the
 * caller's buffers (both sized CSS_TOK_MAX / CSS_URL_MAX). Returns 0 on
 * success, -1 if i is out of range. */
size_t css_font_face_count(const css_sheet *sheet);
int    css_font_face_at(const css_sheet *sheet, size_t i,
                        char *family, size_t fam_cap,
                        char *src_url, size_t url_cap);

/* Resolve @keyframes: scans sheet->keyframes[] for a name matching
 * s->anim_name (first-char match). If found, copies the stops into
 * s->anim_kf_* fields and updates s->anim_name with the full name.
 * NULL-safe on both parameters. */
void css_resolve_anim_keyframes(css_style *s, const css_sheet *sheet);

#endif /* FREEDOM_CSS_H */
