Skip to main content

atmos/os_lib/layout/
tree.rs

1// 分割: layout.rs より機械的に移動(2026-07-16 リファクタ フェーズ4)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5/// 計算量: **O(N × D)** — N はスタイル済みノード数、D は
6/// テキストを含むノードでの行分割コスト(文字数に比例)。
7///
8/// ブロック整形は 1 パスだが、flex / grid コンテナは子の寸法を決めるのに
9/// **子を 2 回走査**する(伸長・収縮の分配)。入れ子の深さぶん掛かるため
10/// 深いフレックス入れ子では定数倍が増える。
11///
12/// 実測(sugi-lab.net: N=358): 48〜89 tick。
13/// `style_tree`(291 tick)の 1/4 以下で、**レイアウトの支配項ではない**。
14pub fn layout_tree<'a>(node: &'a StyledNode<'a>, containing_block: Dimensions) -> LayoutBox<'a> {
15    // 初期包含ブロック = ビューポート。absolute(positioned祖先なし)と fixed の基準。
16    let icb = &containing_block.content;
17    let icb_h = if icb.height > 0 {
18        icb.height
19    } else {
20        VIEWPORT_HINT_H.load(AtomicOrdering::Relaxed)
21    };
22    abs_cb_set(icb.x, icb.y, icb.width, icb_h);
23    VP_ORIGIN_X.store(icb.x, AtomicOrdering::Relaxed);
24    VP_ORIGIN_Y.store(icb.y, AtomicOrdering::Relaxed);
25    let mut layout_box = build_layout_tree(node);
26    layout_box.layout(containing_block);
27    layout_box
28}
29
30pub(crate) fn resolved_display<'a>(node: &'a StyledNode<'a>) -> &'a str {
31    match &node.node.node_type {
32        NodeType::Element { tag_name, .. } => node
33            .value_ref("display")
34            .unwrap_or_else(|| default_display_for(tag_name)),
35        NodeType::Text(_) => "inline",
36    }
37}
38
39pub(crate) fn build_layout_tree<'a>(node: &'a StyledNode<'a>) -> LayoutBox<'a> {
40    let box_type = match &node.node.node_type {
41        NodeType::Element { .. } => {
42            let display = resolved_display(node);
43            match display {
44                "block" => BoxType::BlockNode(node),
45                "inline-block" => BoxType::InlineBlockNode(node),
46                "flex" => BoxType::FlexNode(node),
47                "grid" | "inline-grid" => BoxType::GridNode(node),
48                "table" => BoxType::TableNode(node),
49                "table-row-group" => BoxType::TableRowGroupNode(node),
50                "table-row" => BoxType::TableRowNode(node),
51                "table-cell" => BoxType::TableCellNode(node),
52                _ => BoxType::InlineNode(node), // "none" は下で除外
53            }
54        }
55        NodeType::Text(_) => BoxType::InlineNode(node),
56    };
57
58    let mut layout_box = LayoutBox {
59        dimensions: Dimensions::default(),
60        box_type,
61        children: Vec::new(),
62    };
63
64    for child in &node.children {
65        if resolved_display(child) != "none" {
66            // visibility: hidden — include in layout but mark invisible (handled in web_engine)
67            layout_box.children.push(build_layout_tree(child));
68        }
69    }
70
71    // テーブルの行グループ (thead/tbody/tfoot) は透過し、行 (tr) を table の直接の子へ引き上げる。
72    // これにより layout_table は「table の子 = 行」という単純な前提で計算できる。
73    if matches!(layout_box.box_type, BoxType::TableNode(_)) {
74        let mut flattened: Vec<LayoutBox<'a>> = Vec::new();
75        for child in layout_box.children.drain(..) {
76            if matches!(child.box_type, BoxType::TableRowGroupNode(_)) {
77                for row in child.children {
78                    flattened.push(row);
79                }
80            } else {
81                flattened.push(child);
82            }
83        }
84        layout_box.children = flattened;
85    }
86
87    layout_box
88}
89
90impl<'a> LayoutBox<'a> {
91    /// 自身と全子孫の絶対 y 座標を dy だけ平行移動する(table セルの `vertical-align` 実装用)。
92    /// 各ボックスの content.y は既に絶対ピクセル座標として確定しているため、
93    /// この再帰1回で子孫すべての位置がまとめてずれる。
94    fn translate_y(&mut self, dy: i32) {
95        if dy == 0 {
96            return;
97        }
98        self.dimensions.content.y += dy;
99        for child in &mut self.children {
100            child.translate_y(dy);
101        }
102    }
103
104    /// 自身と全子孫の絶対座標を (dx, dy) だけ平行移動する。
105    ///
106    /// 多段組で「通常フローで積み終えた子を列の位置へ移す」ために使う。
107    /// `translate_y` の 2 軸版で、考え方は同じ(content 座標が絶対値なので
108    /// 再帰 1 回で子孫がまとめてずれる)。
109    fn translate(&mut self, dx: i32, dy: i32) {
110        if dx == 0 && dy == 0 {
111            return;
112        }
113        self.dimensions.content.x += dx;
114        self.dimensions.content.y += dy;
115        for child in &mut self.children {
116            child.translate(dx, dy);
117        }
118    }
119
120    /// `indices` で指定した caption 群を y から順に縦積みでレイアウトし、消費した合計高さを返す。
121    /// `caption-side:top`(既定)/`bottom` どちらの配置にも使う共通ヘルパー。
122    fn layout_captions_at(&mut self, indices: &[usize], base_x: i32, y: i32, width: i32) -> i32 {
123        let mut consumed = 0i32;
124        for &ci in indices {
125            let cap_y = y + consumed;
126            let cap = &mut self.children[ci];
127            let mut cb = Dimensions::default();
128            cb.content.x = base_x;
129            cb.content.y = cap_y;
130            cb.content.height = 0;
131            cb.content.width = width;
132            cap.layout(cb);
133            consumed += cap.dimensions.margin_box().height;
134        }
135        consumed
136    }
137
138    fn layout(&mut self, containing_block: Dimensions) {
139        match &self.box_type {
140            BoxType::BlockNode(_) => self.layout_block(containing_block),
141            BoxType::InlineNode(_) => self.layout_inline(containing_block, false),
142            BoxType::InlineBlockNode(_) => self.layout_inline(containing_block, true),
143            BoxType::FlexNode(_) => self.layout_flex(containing_block),
144            BoxType::GridNode(_) => self.layout_grid(containing_block),
145            BoxType::TableNode(_) => self.layout_table(containing_block),
146            // 行グループ・行・セルは table 経由でレイアウトされる。
147            // 単独で呼ばれた場合 (table の外にある等) は block としてフォールバック。
148            BoxType::TableRowGroupNode(_)
149            | BoxType::TableRowNode(_)
150            | BoxType::TableCellNode(_) => self.layout_block(containing_block),
151            BoxType::AnonymousBlock => {}
152        }
153    }
154
155    /// この要素が positioned(relative/absolute/fixed)なら、子孫 absolute の包含ブロック
156    /// (ABS_CB)を self の content box に切替え、切替前の値を返す。戻り値は
157    /// `restore_abs_cb` に渡して復元する。非 positioned なら None。
158    /// content.width / content.height が確定済みであることを前提とする(bottom/right 解決のため)。
159    fn establish_abs_cb(&self) -> Option<(i32, i32, i32, i32)> {
160        if matches!(
161            box_position(self).as_str(),
162            "relative" | "absolute" | "fixed"
163        ) {
164            let prev = abs_cb_get();
165            abs_cb_set(
166                self.dimensions.content.x,
167                self.dimensions.content.y,
168                self.dimensions.content.width,
169                self.dimensions.content.height,
170            );
171            Some(prev)
172        } else {
173            None
174        }
175    }
176
177    /// `establish_abs_cb` の戻り値を使って ABS_CB を元に戻す。
178    fn restore_abs_cb(saved: Option<(i32, i32, i32, i32)>) {
179        if let Some(prev) = saved {
180            abs_cb_set(prev.0, prev.1, prev.2, prev.3);
181        }
182    }
183
184    /// 子のうち out-of-flow(absolute / fixed)なものを判定するヘルパ。
185    fn child_is_out_of_flow(child: &LayoutBox) -> bool {
186        matches!(box_position(child).as_str(), "absolute" | "fixed")
187    }
188
189    /// position 後処理。全フォーマッティングコンテキスト(block/flex/grid/table)で共有。
190    /// - relative: フロー確定位置から top/left(または bottom/right の符号反転)でオフセット。
191    /// - absolute: 最近傍 positioned 祖先(ABS_CB、昇り解決済み)基準で top/left/right/bottom 配置。
192    /// - fixed: ビューポート原点基準で配置。
193    /// - transform: translate を最後に視覚シフトとして適用(兄弟は再フローしない=仕様通り)。
194    ///
195    /// self.dimensions.content.{width,height} が確定済みであることを前提とする。
196    fn apply_positioned_offsets(&mut self) {
197        let cw = self.dimensions.content.width;
198        let ch = self.dimensions.content.height;
199        for child in &mut self.children {
200            match box_position(child).as_str() {
201                "relative" => {
202                    let dx = box_inset_len(child, "left", cw)
203                        .or_else(|| box_inset_len(child, "right", cw).map(|r| -r))
204                        .unwrap_or(0);
205                    let dy = box_inset_len(child, "top", ch)
206                        .or_else(|| box_inset_len(child, "bottom", ch).map(|b| -b))
207                        .unwrap_or(0);
208                    if dx != 0 || dy != 0 {
209                        shift_layout_box(child, dx, dy);
210                    }
211                }
212                pos @ ("absolute" | "fixed") => {
213                    // 基準矩形: absolute=ABS_CB(昇り解決済み)、fixed=ビューポート原点。
214                    let (bx, by, bw, bh) = if pos == "fixed" {
215                        let (vx, vy) = vp_origin_get();
216                        (
217                            vx,
218                            vy,
219                            VIEWPORT_HINT_W.load(AtomicOrdering::Relaxed),
220                            VIEWPORT_HINT_H.load(AtomicOrdering::Relaxed),
221                        )
222                    } else {
223                        abs_cb_get()
224                    };
225                    let mb = child.dimensions.margin_box();
226                    let tx = if let Some(l) = box_inset_len(child, "left", bw) {
227                        bx + l
228                    } else if let Some(r) = box_inset_len(child, "right", bw) {
229                        bx + bw - r - mb.width
230                    } else {
231                        bx
232                    };
233                    let ty = if let Some(t) = box_inset_len(child, "top", bh) {
234                        by + t
235                    } else if let Some(b) = box_inset_len(child, "bottom", bh) {
236                        by + bh - b - mb.height
237                    } else {
238                        by
239                    };
240                    shift_layout_box(child, tx - mb.x, ty - mb.y);
241                }
242                _ => {}
243            }
244
245            // transform: translate(レイアウト確定後の視覚シフト。position の後に適用、
246            // 兄弟は再フローしない=仕様通り)。% は要素自身の content 寸法基準。
247            let (txx, tyy) = box_translate(child);
248            if txx != 0 || tyy != 0 {
249                shift_layout_box(child, txx, tyy);
250            }
251        }
252    }
253
254    /// テーブルレイアウト (RFC 風の簡易固定/自動テーブル)。
255    /// - 列数 = 最大セル数の行
256    /// - 列幅 = 各列セルの intrinsic 幅の最大値。総和が利用幅を超えたら比例縮小、
257    ///   下回ったら余りを均等配分して幅いっぱいに広げる
258    /// - 各行は縦に積み、行内のセルは列幅で横並び、行高さ = セル最大高さに揃える
259    // ===== CSS Grid レイアウト =====
260    fn layout_grid(&mut self, containing_block: Dimensions) {
261        self.apply_box_model_styles_with_base(containing_block.content.width);
262
263        let node = match &self.box_type {
264            BoxType::GridNode(n) => *n,
265            _ => return,
266        };
267        let avail_w = containing_block.content.width.max(1);
268
269        // box-sizing / margin:auto 適用(block と同じロジック)。
270        let inner_non_content = self.dimensions.border.left
271            + self.dimensions.border.right
272            + self.dimensions.padding.left
273            + self.dimensions.padding.right;
274        let h_non_content =
275            self.dimensions.margin.left + self.dimensions.margin.right + inner_non_content;
276        let width_val = node.value("width");
277        let box_sizing = node.value("box-sizing").unwrap_or_default();
278        let h_auto = margin_is_h_auto(node);
279        let mut content_w = match parse_length_value(width_val) {
280            Some(LengthValue::Auto) | None => (avail_w - h_non_content).max(1),
281            Some(LengthValue::Px(px)) => px.max(1),
282            Some(LengthValue::Percent(pct)) => round_f32_to_i32(avail_w as f32 * pct).max(1),
283            Some(LengthValue::MinContent) => {
284                let (minc, _maxc) = self.block_intrinsic_widths();
285                minc.max(1)
286            }
287            Some(LengthValue::MaxContent) => {
288                let (_minc, maxc) = self.block_intrinsic_widths();
289                maxc.max(1)
290            }
291            Some(LengthValue::FitContent) => {
292                let (minc, maxc) = self.block_intrinsic_widths();
293                let avail = (avail_w - h_non_content).max(0);
294                avail.min(maxc).max(minc).max(1)
295            }
296            Some(LengthValue::MathExpr(expr)) => {
297                crate::os_lib::css::eval_css_math(&expr, avail_w)
298                    .unwrap_or((avail_w - h_non_content).max(1))
299                    .max(1)
300            }
301        };
302        if box_sizing.trim() == "border-box" {
303            content_w = (content_w - inner_non_content).max(1);
304        }
305        self.dimensions.content.width = content_w;
306        if h_auto {
307            let free = (avail_w - content_w - inner_non_content).max(0);
308            self.dimensions.margin.left = free / 2;
309            self.dimensions.margin.right = free / 2;
310        }
311
312        let flow_y = containing_block.content.y
313            + containing_block.content.height
314            + self.dimensions.margin.top;
315        self.dimensions.content.x = containing_block.content.x
316            + self.dimensions.margin.left
317            + self.dimensions.border.left
318            + self.dimensions.padding.left;
319        self.dimensions.content.y =
320            flow_y + self.dimensions.border.top + self.dimensions.padding.top;
321
322        // grid-template-columns を解析して列幅の Vec を作る。
323        let mut col_widths = parse_grid_template_columns(
324            node.value("grid-template-columns").as_deref().unwrap_or(""),
325            content_w,
326        );
327        let n_cols = col_widths.len().max(1);
328
329        // grid-auto-columns: 暗黙列(`grid-auto-flow: column` が明示列数を使い切って
330        // 新規に作る列)のサイズ。複数値は循環適用。px/minmax の下限のみを解決し、
331        // fr/auto は「最後の明示列と同じ幅」にフォールバックする簡略実装
332        // (暗黙列の内容ベースの自動採寸は行わない。行側の grid-auto-rows のような
333        // 内容実測パスをそのまま列へ持ち込むのは改修規模が大きいための割り切り)。
334        let auto_col_tracks = parse_grid_tracks(
335            node.value("grid-auto-columns").as_deref().unwrap_or(""),
336            content_w,
337        );
338        let auto_col_fallback_px = col_widths.last().copied().unwrap_or(100).max(1);
339        let resolve_auto_col_px = |idx: usize| -> i32 {
340            if auto_col_tracks.is_empty() {
341                return auto_col_fallback_px;
342            }
343            match auto_col_tracks[idx % auto_col_tracks.len()] {
344                GridTrack::Px(px) => px.max(1),
345                GridTrack::MinMax { min_px, .. } => min_px.max(1),
346                _ => auto_col_fallback_px,
347            }
348        };
349
350        // grid-template-areas: 名前付き領域を grid-area で参照するための行×列テーブル。
351        let template_areas =
352            parse_grid_template_areas(node.value("grid-template-areas").as_deref().unwrap_or(""));
353
354        let (row_gap, col_gap) = {
355            // px / % 両対応の gap 解決(% は content 幅基準)。
356            let gpx = |s: &str| -> i32 {
357                match parse_length_value(Some(String::from(s.trim()))) {
358                    Some(LengthValue::Px(p)) => p,
359                    Some(LengthValue::Percent(pct)) => {
360                        round_f32_to_i32(content_w as f32 * pct).max(0)
361                    }
362                    _ => 0,
363                }
364            };
365            let gap_all = node.value("gap").unwrap_or_default();
366            if !gap_all.is_empty() {
367                let toks: Vec<&str> = gap_all.split_whitespace().collect();
368                let rg = gpx(toks.first().copied().unwrap_or("0"));
369                let cg = gpx(toks
370                    .get(1)
371                    .copied()
372                    .unwrap_or(toks.first().copied().unwrap_or("0")));
373                (rg, cg)
374            } else {
375                let rg = node
376                    .value("row-gap")
377                    .or_else(|| node.value("grid-row-gap"))
378                    .map(|v| gpx(&v))
379                    .unwrap_or(0);
380                let cg = node
381                    .value("column-gap")
382                    .or_else(|| node.value("grid-column-gap"))
383                    .map(|v| gpx(&v))
384                    .unwrap_or(0);
385                (rg, cg)
386            }
387        };
388
389        // grid-template-rows/grid-auto-rows の `%` トラック解決基準。明示 height
390        // (px/%)があればそれを使い、無指定/auto は 0(`%` トラックは 0 扱い、
391        // 高さがコンテンツから決まる場合の一般的な簡略化)とする。以前はここが常に
392        // 固定値 0 で、明示 height を指定していても `grid-template-rows:50%` 等が
393        // 常に 0px に丸め込まれるバグがあった(列側は content_w を正しく渡していたのに
394        // 行側だけ取り残されていた)。
395        let row_base = match parse_length_value(node.value("height")) {
396            Some(LengthValue::Px(px)) => px.max(0),
397            Some(LengthValue::Percent(pct)) => {
398                round_f32_to_i32(containing_block.content.height as f32 * pct).max(0)
399            }
400            _ => 0,
401        };
402
403        // 明示 grid-template-rows トラック定義(px/fr/auto)。空なら全行 auto 扱い。
404        let row_tracks = parse_grid_tracks(
405            node.value("grid-template-rows").as_deref().unwrap_or(""),
406            row_base,
407        );
408
409        // grid-auto-rows: 暗黙行(明示トラック数を超えた行)のサイズ。複数値は循環適用。
410        let auto_row_tracks = parse_grid_tracks(
411            node.value("grid-auto-rows").as_deref().unwrap_or(""),
412            row_base,
413        );
414
415        // 行 r の実効トラックを返す: 明示トラックがあればそれ、無ければ grid-auto-rows を循環。
416        let eff_row_track = |r: usize| -> Option<GridTrack> {
417            if let Some(t) = row_tracks.get(r) {
418                return Some(*t);
419            }
420            if auto_row_tracks.is_empty() {
421                return None;
422            }
423            let implicit_idx = r - row_tracks.len();
424            auto_row_tracks
425                .get(implicit_idx % auto_row_tracks.len())
426                .copied()
427        };
428
429        // コンテナの justify-items(インライン=列軸)/ align-items(ブロック=行軸)。
430        // 既定 stretch。各アイテムの justify-self / align-self が優先する。
431        let justify_items = node
432            .value("justify-items")
433            .unwrap_or_else(|| String::from("stretch"))
434            .trim()
435            .to_lowercase();
436        let align_items = node
437            .value("align-items")
438            .unwrap_or_else(|| String::from("stretch"))
439            .trim()
440            .to_lowercase();
441
442        // ---- パス1: 配置の解決(占有マップ + auto-flow row/column) ----
443        // 各フロー子の (row, col, row_span, col_span) を確定。out-of-flow は別管理。
444        // grid-auto-flow: column なら列優先(grid-template-rows の行数を縦に埋めてから次列へ)。
445        let auto_flow_col = node
446            .value("grid-auto-flow")
447            .map(|v| v.to_lowercase().contains("column"))
448            .unwrap_or(false);
449        // 列優先時の行数上限(明示 grid-template-rows の本数。無ければ 1)。
450        let flow_rows = row_tracks.len().max(1);
451        let mut occ: Vec<Vec<bool>> = Vec::new();
452        // `grid-auto-flow: column` が明示列数を使い切って新規に列を作った実際の列数。
453        // 以前はこれが常に固定の n_cols のままで、`grid_cells_free` が
454        // `c + col_span > n_cols` で常に false を返し続けるため、列優先フローで
455        // 明示列数を超える個数のアイテムを配置しようとすると無限ループしてハングする
456        // バグがあった。
457        let mut n_cols_dyn = n_cols;
458        // 子のインデックス → 配置(フロー子のみ)。
459        let n_children = self.children.len();
460        let mut placement: Vec<Option<(usize, usize, usize, usize)>> =
461            alloc::vec![None; n_children];
462        let mut cursor_r = 0usize;
463        let mut cursor_c = 0usize;
464        for (idx, child) in self.children.iter().enumerate() {
465            if Self::child_is_out_of_flow(child) {
466                continue;
467            }
468            let (mut col_start, mut col_span_raw) =
469                grid_axis_placement(child, "grid-column", "grid-column-start", "grid-column-end");
470            let (mut row_start, mut row_span_raw) =
471                grid_axis_placement(child, "grid-row", "grid-row-start", "grid-row-end");
472            // grid-column/grid-row が未指定(auto)の場合のみ、grid-area 名を
473            // grid-template-areas から解決してフォールバックする。
474            if col_start.is_none() && row_start.is_none() {
475                if let Some(area_name) = box_style_value(child, "grid-area") {
476                    let area_name = area_name.trim();
477                    if !area_name.is_empty() {
478                        if let Some((ar, ac, arspan, acspan)) =
479                            find_grid_area(&template_areas, area_name)
480                        {
481                            row_start = Some(ar);
482                            col_start = Some(ac);
483                            row_span_raw = arspan;
484                            col_span_raw = acspan;
485                        }
486                    }
487                }
488            }
489            let col_span = col_span_raw.min(n_cols).max(1);
490            let row_span = row_span_raw.max(1);
491
492            // 配置位置の決定。明示があれば尊重、無ければ auto-flow(行優先)。
493            let (r, c) = match (row_start, col_start) {
494                (Some(r), Some(c)) => (r, c.min(n_cols.saturating_sub(1))),
495                (Some(r), None) => {
496                    // 行固定・列 auto: その行で空く最初の列。
497                    grid_ensure_row(&mut occ, r + row_span - 1, n_cols);
498                    let mut cc = 0usize;
499                    while cc < n_cols && !grid_cells_free(&occ, r, cc, row_span, col_span, n_cols) {
500                        cc += 1;
501                    }
502                    (r, cc.min(n_cols.saturating_sub(1)))
503                }
504                (None, Some(c)) => {
505                    // 列固定・行 auto: カーソル行以降で空く最初の行。
506                    let c = c.min(n_cols.saturating_sub(1));
507                    let mut rr = cursor_r;
508                    loop {
509                        grid_ensure_row(&mut occ, rr + row_span - 1, n_cols);
510                        if grid_cells_free(&occ, rr, c, row_span, col_span, n_cols) {
511                            break;
512                        }
513                        rr += 1;
514                    }
515                    (rr, c)
516                }
517                (None, None) => {
518                    if auto_flow_col {
519                        // 列優先: (cursor_r, cursor_c) から行を縦に進め、flow_rows を超えたら
520                        // 次の列の先頭へ。空きセルが見つかるまで走査する。
521                        // 明示列数(n_cols)を使い切ったら grid-auto-columns で新規に
522                        // 列を作る(n_cols_dyn を伸ばす)。これをしないと
523                        // grid_cells_free が `c + col_span > n_cols` で恒久的に false を
524                        // 返し続け、無限ループでハングする。
525                        let mut rr = cursor_r;
526                        let mut cc = cursor_c;
527                        loop {
528                            if rr + row_span > flow_rows {
529                                cc += 1;
530                                rr = 0;
531                            }
532                            if cc + col_span > n_cols_dyn {
533                                n_cols_dyn = cc + col_span;
534                                grid_ensure_col(&mut occ, n_cols_dyn);
535                            }
536                            grid_ensure_row(&mut occ, rr + row_span - 1, n_cols_dyn);
537                            if grid_cells_free(&occ, rr, cc, row_span, col_span, n_cols_dyn) {
538                                break;
539                            }
540                            rr += 1;
541                        }
542                        cursor_r = rr + row_span;
543                        cursor_c = cc;
544                        (rr, cc)
545                    } else {
546                        // 行優先: カーソルから次の空きへ。
547                        let mut rr = cursor_r;
548                        let mut cc = cursor_c;
549                        loop {
550                            if cc + col_span > n_cols {
551                                rr += 1;
552                                cc = 0;
553                            }
554                            grid_ensure_row(&mut occ, rr + row_span - 1, n_cols);
555                            if grid_cells_free(&occ, rr, cc, row_span, col_span, n_cols) {
556                                break;
557                            }
558                            cc += 1;
559                        }
560                        cursor_r = rr;
561                        cursor_c = cc + col_span;
562                        (rr, cc)
563                    }
564                }
565            };
566            grid_ensure_row(&mut occ, r + row_span - 1, n_cols_dyn);
567            grid_mark(&mut occ, r, c, row_span, col_span, n_cols_dyn);
568            placement[idx] = Some((r, c, row_span, col_span));
569        }
570
571        // grid-auto-flow: column が明示列数を使い切って新しい列を作った分だけ、
572        // col_widths を grid-auto-columns(未指定なら最後の明示列と同じ幅)で伸ばす。
573        while col_widths.len() < n_cols_dyn {
574            let idx = col_widths.len() - n_cols;
575            col_widths.push(resolve_auto_col_px(idx));
576        }
577
578        // グリッドコンテナの `justify-content`: トラック総幅がコンテナ幅より
579        // 小さい場合に、トラック集合全体をどう配置するか(flexbox の主軸
580        // justify-content と同じ考え方。以前はここが完全に未実装で、
581        // トラック合計がコンテナ幅に満たなくても常に左詰めに固定されていた)。
582        let n_cols_eff = col_widths.len();
583        let cols_total: i32 =
584            col_widths.iter().sum::<i32>() + (n_cols_eff.saturating_sub(1) as i32) * col_gap;
585        let cols_free = (content_w - cols_total).max(0);
586        let justify_content_grid = node
587            .value("justify-content")
588            .unwrap_or_default()
589            .trim()
590            .to_lowercase();
591        let (col_start_offset, col_extra_gap) = if cols_free > 0 && n_cols_eff > 0 {
592            match justify_content_grid.as_str() {
593                "end" | "flex-end" => (cols_free, 0),
594                "center" => (cols_free / 2, 0),
595                "space-between" if n_cols_eff > 1 => (0, cols_free / (n_cols_eff as i32 - 1)),
596                "space-around" if n_cols_eff > 0 => {
597                    let g = cols_free / n_cols_eff as i32;
598                    (g / 2, g)
599                }
600                "space-evenly" if n_cols_eff > 0 => {
601                    let g = cols_free / (n_cols_eff as i32 + 1);
602                    (g, g)
603                }
604                _ => (0, 0),
605            }
606        } else {
607            (0, 0)
608        };
609        let col_gap_eff = col_gap + col_extra_gap;
610
611        let n_rows = occ.len().max(row_tracks.len());
612
613        // ---- パス2: 子を仮レイアウト(列幅基準)して各行の自然高さを測定 ----
614        // 行高さ(row_heights[r])を、明示トラックと span=1 アイテムの実測で決める。
615        let mut row_heights: Vec<i32> = alloc::vec![0; n_rows];
616        // まず明示 px / % トラック、および minmax の min_px 下限を反映(暗黙行は grid-auto-rows)。
617        for (r, h) in row_heights.iter_mut().enumerate() {
618            match eff_row_track(r) {
619                Some(GridTrack::Px(px)) => *h = px.max(0),
620                Some(GridTrack::MinMax { min_px, .. }) => *h = min_px.max(0),
621                _ => {}
622            }
623        }
624        // 子を一旦レイアウトして高さ実測(span=1 のみが行高さに寄与)。
625        // ここでは x 位置だけ正しく与え、y は後で確定するため 0 起点で測る。
626        let base_x = self.dimensions.content.x;
627        let base_y = self.dimensions.content.y;
628        for (idx, child) in self.children.iter_mut().enumerate() {
629            if Self::child_is_out_of_flow(child) {
630                continue;
631            }
632            let Some((_r, c, _rs, cs)) = placement[idx] else {
633                continue;
634            };
635            let cell_w = grid_span_width(&col_widths, col_gap_eff, c, cs);
636            let cell_cb = Dimensions {
637                content: Rect {
638                    x: base_x + col_start_offset + grid_col_x(&col_widths, col_gap_eff, c),
639                    y: base_y,
640                    width: cell_w,
641                    height: 0,
642                },
643                ..Dimensions::default()
644            };
645            child.layout(cell_cb);
646        }
647        // 行の自然高さ(auto トラック)を span=1 アイテムの実測から求める。
648        for (idx, child) in self.children.iter().enumerate() {
649            let Some((r, _c, rs, _cs)) = placement[idx] else {
650                continue;
651            };
652            if rs != 1 {
653                continue;
654            }
655            let explicit_px = matches!(eff_row_track(r), Some(GridTrack::Px(_)));
656            if explicit_px {
657                continue;
658            }
659            let ch = child.dimensions.margin_box().height;
660            if let Some(h) = row_heights.get_mut(r) {
661                *h = (*h).max(ch);
662            }
663            // minmax の max が px(非fr・非auto)なら上限でクランプ。
664            if let Some(GridTrack::MinMax {
665                max_px,
666                max_is_fr,
667                max_is_auto,
668                ..
669            }) = eff_row_track(r)
670            {
671                if !max_is_fr && !max_is_auto {
672                    if let Some(h) = row_heights.get_mut(r) {
673                        *h = (*h).min(max_px);
674                    }
675                }
676            }
677        }
678        // span>1 アイテムが行高さ合計を超える場合、末尾行を伸ばす。
679        for (idx, child) in self.children.iter().enumerate() {
680            let Some((r, _c, rs, _cs)) = placement[idx] else {
681                continue;
682            };
683            if rs <= 1 {
684                continue;
685            }
686            let ch = child.dimensions.margin_box().height;
687            let mut covered = 0i32;
688            for rr in r..(r + rs).min(row_heights.len()) {
689                covered += row_heights.get(rr).copied().unwrap_or(0);
690            }
691            covered += (rs.saturating_sub(1) as i32) * row_gap;
692            if ch > covered {
693                let last = (r + rs - 1).min(row_heights.len().saturating_sub(1));
694                if let Some(h) = row_heights.get_mut(last) {
695                    *h += ch - covered;
696                }
697            }
698        }
699
700        // fr 行トラックの解決(コンテナに明示高さがある場合のみ余白を fr へ分配)。
701        let mut container_h_explicit: Option<i32> = None;
702        let height_val = node.value("height");
703        if let Some(h) = parse_length_value(height_val.clone())
704            .and_then(|v| resolve_length_value(v, containing_block.content.height.max(0)))
705        {
706            let vert_non = self.dimensions.border.top
707                + self.dimensions.border.bottom
708                + self.dimensions.padding.top
709                + self.dimensions.padding.bottom;
710            container_h_explicit = Some(if box_sizing.trim() == "border-box" {
711                (h - vert_non).max(0)
712            } else {
713                h.max(0)
714            });
715        }
716        if let Some(ch) = container_h_explicit {
717            // fr 係数(純 Fr + minmax の fr max)。
718            let track_fr = |t: &GridTrack| -> f32 {
719                match t {
720                    GridTrack::Fr(f) => *f,
721                    GridTrack::MinMax {
722                        max_fr, max_is_fr, ..
723                    } if *max_is_fr => *max_fr,
724                    _ => 0.0,
725                }
726            };
727            // 暗黙行(grid-auto-rows)も含めて全行を走査。
728            let mut fr_total = 0.0f32;
729            for r in 0..n_rows {
730                if let Some(t) = eff_row_track(r) {
731                    fr_total += track_fr(&t);
732                }
733            }
734            if fr_total > 0.0 {
735                let mut used = 0i32;
736                for (r, h) in row_heights.iter().enumerate() {
737                    let is_fr = eff_row_track(r).map(|t| track_fr(&t)).unwrap_or(0.0) > 0.0;
738                    if !is_fr {
739                        used += *h;
740                    }
741                }
742                used += (n_rows.saturating_sub(1) as i32) * row_gap;
743                let free = (ch - used).max(0);
744                for r in 0..n_rows {
745                    let Some(t) = eff_row_track(r) else { continue };
746                    let f = track_fr(&t);
747                    if f > 0.0 {
748                        let share = round_f32_to_i32(free as f32 * f / fr_total);
749                        // minmax の min_px 下限を保証。
750                        let floor = match t {
751                            GridTrack::MinMax { min_px, .. } => min_px,
752                            _ => 0,
753                        };
754                        if let Some(rh) = row_heights.get_mut(r) {
755                            *rh = share.max(floor);
756                        }
757                    }
758                }
759            }
760        }
761
762        // グリッドコンテナの `align-content`: 行トラック総高さがコンテナの明示
763        // 高さより小さい場合に、トラック集合全体を交差軸(行軸)方向にどう配置
764        // するか(`justify-content` の行版。コンテナに明示高さが無い場合は
765        // 高さがトラック合計そのものになるため余剰は常に0で無関係)。
766        let rows_total_h: i32 =
767            row_heights.iter().sum::<i32>() + (n_rows.saturating_sub(1) as i32) * row_gap;
768        let rows_free = container_h_explicit
769            .map(|ch| (ch - rows_total_h).max(0))
770            .unwrap_or(0);
771        let align_content_grid = node
772            .value("align-content")
773            .unwrap_or_default()
774            .trim()
775            .to_lowercase();
776        let (row_start_offset, row_extra_gap) = if rows_free > 0 && n_rows > 0 {
777            match align_content_grid.as_str() {
778                "end" | "flex-end" => (rows_free, 0),
779                "center" => (rows_free / 2, 0),
780                "space-between" if n_rows > 1 => (0, rows_free / (n_rows as i32 - 1)),
781                "space-around" if n_rows > 0 => {
782                    let g = rows_free / n_rows as i32;
783                    (g / 2, g)
784                }
785                "space-evenly" if n_rows > 0 => {
786                    let g = rows_free / (n_rows as i32 + 1);
787                    (g, g)
788                }
789                _ => (0, 0),
790            }
791        } else {
792            (0, 0)
793        };
794        let row_gap_eff = row_gap + row_extra_gap;
795
796        // 各行 top の累積(row_gap_eff 込み)。
797        let mut row_tops: Vec<i32> = alloc::vec![0; n_rows + 1];
798        for r in 0..n_rows {
799            let h = row_heights.get(r).copied().unwrap_or(0);
800            row_tops[r + 1] = row_tops[r] + h + if r + 1 < n_rows { row_gap_eff } else { 0 };
801        }
802
803        // ---- パス3: 確定行 y で再レイアウト ----
804        for (idx, child) in self.children.iter_mut().enumerate() {
805            if Self::child_is_out_of_flow(child) {
806                // out-of-flow(absolute/fixed)はセルを消費しない。サイズ確定のため
807                // レイアウトのみ行い、最終位置は apply_positioned_offsets で確定する。
808                let oof_cb = Dimensions {
809                    content: Rect {
810                        x: base_x,
811                        y: base_y,
812                        width: content_w,
813                        height: 0,
814                    },
815                    ..Dimensions::default()
816                };
817                child.layout(oof_cb);
818                continue;
819            }
820            let Some((r, c, rs, cs)) = placement[idx] else {
821                continue;
822            };
823            let cell_w = grid_span_width(&col_widths, col_gap_eff, c, cs);
824            let cell_x = base_x + col_start_offset + grid_col_x(&col_widths, col_gap_eff, c);
825            let cell_y = base_y + row_start_offset + row_tops.get(r).copied().unwrap_or(0);
826            // セル高さ = span 行の高さ合計(行間 gap 込み)。
827            let mut cell_h = 0i32;
828            for rr in r..(r + rs).min(row_heights.len()) {
829                cell_h += row_heights.get(rr).copied().unwrap_or(0);
830            }
831            cell_h += (rs.saturating_sub(1) as i32) * row_gap;
832
833            // アイテムの整列を解決(justify=列/インライン軸、align=行/ブロック軸)。
834            let j = grid_item_align(child, "justify-self", &justify_items);
835            let a = grid_item_align(child, "align-self", &align_items);
836
837            // 列幅をコンテナ幅として測る。stretch(既定)は子がセル幅を埋める。
838            let cell_cb = Dimensions {
839                content: Rect {
840                    x: cell_x,
841                    y: cell_y,
842                    width: cell_w,
843                    height: 0,
844                },
845                ..Dimensions::default()
846            };
847            child.layout(cell_cb);
848
849            // align:stretch かつ高さ未指定なら、セル高さへ伸ばす。
850            if a == "stretch" && !box_has_explicit(child, "height") && cell_h > 0 {
851                let mb = child.dimensions.margin_box();
852                let non_content = mb.height - child.dimensions.content.height;
853                child.dimensions.content.height = (cell_h - non_content).max(0);
854            }
855
856            // セル内整列(インライン軸=justify, ブロック軸=align)。
857            // 幅方向の整列は、明示 width を持つ(=セル幅より狭い余地がある)アイテムにのみ
858            // 適用する。width:auto のブロックはセル幅を埋めるため整列の余地が無く start 相当。
859            let mb = child.dimensions.margin_box();
860            let dx = if j == "stretch" || !box_has_explicit(child, "width") {
861                0
862            } else {
863                let free = (cell_w - mb.width).max(0);
864                match j {
865                    "center" => free / 2,
866                    "end" => free,
867                    _ => 0, // start
868                }
869            };
870            // 高さ方向の整列は、明示 height を持つアイテム(=セル高より低い余地がある)に適用。
871            let dy = if a == "stretch" || !box_has_explicit(child, "height") {
872                0
873            } else {
874                let free = (cell_h - mb.height).max(0);
875                match a {
876                    "center" => free / 2,
877                    "end" => free,
878                    _ => 0, // start
879                }
880            };
881            if dx != 0 || dy != 0 {
882                shift_layout_box(child, dx, dy);
883            }
884        }
885
886        // コンテナ高さ。
887        let natural_h = row_tops.get(n_rows).copied().unwrap_or(0).max(0);
888        self.dimensions.content.height = container_h_explicit.unwrap_or(natural_h);
889
890        // positioned 子要素の配置(grid コンテナを包含ブロック起点にできる)。
891        let saved_abs_cb = self.establish_abs_cb();
892        self.apply_positioned_offsets();
893        Self::restore_abs_cb(saved_abs_cb);
894    }
895
896    fn layout_table(&mut self, containing_block: Dimensions) {
897        self.apply_box_model_styles_with_base(containing_block.content.width);
898
899        let h_non_content = self.dimensions.margin.left
900            + self.dimensions.margin.right
901            + self.dimensions.border.left
902            + self.dimensions.border.right
903            + self.dimensions.padding.left
904            + self.dimensions.padding.right;
905        let avail = (containing_block.content.width - h_non_content).max(1);
906
907        // テーブル幅: width 指定があれば尊重、なければ利用幅
908        let table_width = match &self.box_type {
909            BoxType::TableNode(node) => match parse_length_value(node.value("width")) {
910                Some(LengthValue::Px(px)) => px.max(1).min(avail),
911                Some(LengthValue::Percent(pct)) => round_f32_to_i32(avail as f32 * pct).max(1),
912                _ => avail,
913            },
914            _ => avail,
915        };
916
917        // 位置 (block と同じ規約)
918        let flow_y = containing_block.content.y
919            + containing_block.content.height
920            + self.dimensions.margin.top;
921        self.dimensions.content.x = containing_block.content.x
922            + self.dimensions.margin.left
923            + self.dimensions.border.left
924            + self.dimensions.padding.left;
925        self.dimensions.content.y =
926            flow_y + self.dimensions.border.top + self.dimensions.padding.top;
927        self.dimensions.content.width = table_width;
928
929        let base_x = self.dimensions.content.x;
930
931        // border-collapse: collapse 時は隣接セルの境界を 1px 重ねて二重線を解消する。
932        let collapse = match &self.box_type {
933            BoxType::TableNode(node) => node
934                .value("border-collapse")
935                .map(|v| v.trim().eq_ignore_ascii_case("collapse"))
936                .unwrap_or(false),
937            _ => false,
938        };
939        let collapse_px: i32 = if collapse { 1 } else { 0 };
940
941        // table-layout: fixed 時は最初の行の各セルの明示 width のみで列幅を決定し、
942        // 内容の計測(intrinsic_inline_size)を行わない(仕様通り、パフォーマンス目的の機能)。
943        // 未指定の列は auto 列として残りの利用可能幅を均等に分け合う。
944        let table_layout_fixed = match &self.box_type {
945            BoxType::TableNode(node) => node
946                .value("table-layout")
947                .map(|v| v.trim().eq_ignore_ascii_case("fixed"))
948                .unwrap_or(false),
949            _ => false,
950        };
951
952        // border-spacing: セル間の隙間(px)。border-collapse:collapse では無効。
953        // `10px`(水平・垂直共通)/ `10px 5px`(水平 垂直)の2形式に対応。
954        let (h_spacing, v_spacing): (i32, i32) = if collapse {
955            (0, 0)
956        } else {
957            match &self.box_type {
958                BoxType::TableNode(node) => match node.value("border-spacing") {
959                    Some(v) => {
960                        let toks: Vec<&str> = v.split_whitespace().collect();
961                        let px = |s: &str| -> i32 {
962                            match parse_length_value(Some(String::from(s))) {
963                                Some(LengthValue::Px(px)) => px.max(0),
964                                _ => 0,
965                            }
966                        };
967                        match toks.as_slice() {
968                            [h] => (px(h), px(h)),
969                            [h, w] => (px(h), px(w)),
970                            _ => (0, 0),
971                        }
972                    }
973                    None => (0, 0),
974                },
975                _ => (0, 0),
976            }
977        };
978
979        // caption (<caption>) は既定でテーブル本体の上に積むが、`caption-side:bottom` なら
980        // 本体の下へ配置する。後者は行の高さが確定してから配置する必要があるため、
981        // 先頭 caption 自身の caption-side を見て分岐する。
982        let caption_indices: Vec<usize> = (0..self.children.len())
983            .filter(|&i| box_tag_name(&self.children[i]) == Some("caption"))
984            .collect();
985        let caption_side_bottom = caption_indices
986            .first()
987            .and_then(|&ci| box_style_value(&self.children[ci], "caption-side"))
988            .map(|v| v.trim().eq_ignore_ascii_case("bottom"))
989            .unwrap_or(false);
990        let mut top_offset = 0i32;
991        if !caption_side_bottom {
992            let y = self.dimensions.content.y;
993            top_offset = self.layout_captions_at(&caption_indices, base_x, y, table_width);
994        }
995        let rows_top = self.dimensions.content.y + top_offset;
996
997        // 行 (tr) を抽出。セルは行の children。
998        // `visibility:collapse` の行は display:none 相当に扱い、他の行を詰めて配置する
999        // (実ブラウザは列幅の再計算まではしない簡略実装だが、行自体を除去する点は同じ)。
1000        let row_indices: Vec<usize> = (0..self.children.len())
1001            .filter(|&i| {
1002                matches!(self.children[i].box_type, BoxType::TableRowNode(_))
1003                    && box_style_value(&self.children[i], "visibility")
1004                        .map(|v| !v.trim().eq_ignore_ascii_case("collapse"))
1005                        .unwrap_or(true)
1006            })
1007            .collect();
1008        let nrows = row_indices.len();
1009
1010        // --- グリッド構築: colspan/rowspan を解決して各セルの (列, 行, span) を確定 ---
1011        struct CellPlace {
1012            row_ord: usize,
1013            cell_ord: usize,
1014            col: usize,
1015            colspan: usize,
1016            rowspan: usize,
1017        }
1018        let mut occ: Vec<Vec<bool>> = Vec::new();
1019        let mut places: Vec<CellPlace> = Vec::new();
1020        let mut ncols = 0usize;
1021        for (row_ord, &ri) in row_indices.iter().enumerate() {
1022            let ncells = self.children[ri].children.len();
1023            let mut c = 0usize;
1024            for cell_ord in 0..ncells {
1025                // rowspan で先行行から占有されている列を読み飛ばす
1026                while occ_is(&occ, row_ord, c) {
1027                    c += 1;
1028                }
1029                let (cs, rs) = cell_spans(&self.children[ri].children[cell_ord]);
1030                let rs = rs.min(nrows - row_ord); // テーブル末尾を超える rowspan はクランプ
1031                for rr in row_ord..(row_ord + rs) {
1032                    for cc in c..(c + cs) {
1033                        occ_mark(&mut occ, rr, cc);
1034                    }
1035                }
1036                places.push(CellPlace {
1037                    row_ord,
1038                    cell_ord,
1039                    col: c,
1040                    colspan: cs,
1041                    rowspan: rs.max(1),
1042                });
1043                c += cs;
1044                ncols = ncols.max(c);
1045            }
1046        }
1047
1048        if ncols == 0 {
1049            if caption_side_bottom {
1050                let y = self.dimensions.content.y + top_offset;
1051                top_offset += self.layout_captions_at(&caption_indices, base_x, y, table_width);
1052            }
1053            self.dimensions.content.height = top_offset;
1054            return;
1055        }
1056
1057        // --- 列幅: まず colspan==1 のセルで基礎幅、次に colspan>1 の不足分を分配 ---
1058        let mut col_w = alloc::vec![0i32; ncols];
1059        let mut is_fixed_col = alloc::vec![false; ncols];
1060        let pad = 24; // 既定 padding 4px*2 + 余裕
1061        let usable_width = (table_width - h_spacing * (ncols as i32 - 1).max(0)).max(1);
1062
1063        if table_layout_fixed {
1064            // table-layout:fixed: 最初の行(row_ord==0)の colspan==1 セルの明示 width のみで
1065            // 列幅を決定し、内容の計測は一切行わない(仕様通り、パフォーマンス目的の機能)。
1066            for p in &places {
1067                if p.row_ord != 0 || p.colspan != 1 {
1068                    continue;
1069                }
1070                let cell = &self.children[row_indices[p.row_ord]].children[p.cell_ord];
1071                if let Some(w) = box_style_value(cell, "width") {
1072                    match parse_length_value(Some(w)) {
1073                        Some(LengthValue::Px(px)) => {
1074                            col_w[p.col] = px.max(1);
1075                            is_fixed_col[p.col] = true;
1076                        }
1077                        Some(LengthValue::Percent(pct)) => {
1078                            col_w[p.col] = round_f32_to_i32(usable_width as f32 * pct).max(1);
1079                            is_fixed_col[p.col] = true;
1080                        }
1081                        _ => {}
1082                    }
1083                }
1084            }
1085            // auto 列(明示 width の無い列)は、固定列を除いた残り幅を均等に分け合う。
1086            let fixed_sum: i32 = (0..ncols).filter(|&c| is_fixed_col[c]).map(|c| col_w[c]).sum();
1087            let auto_cols: alloc::vec::Vec<usize> =
1088                (0..ncols).filter(|&c| !is_fixed_col[c]).collect();
1089            if !auto_cols.is_empty() {
1090                let remaining = (usable_width - fixed_sum).max(0);
1091                let auto_w = (remaining / auto_cols.len() as i32).max(1);
1092                for &c in &auto_cols {
1093                    col_w[c] = auto_w;
1094                }
1095            }
1096        } else {
1097            for p in &places {
1098                if p.colspan != 1 {
1099                    continue;
1100                }
1101                let (cw, _) = self.children[row_indices[p.row_ord]].children[p.cell_ord]
1102                    .intrinsic_inline_size(table_width);
1103                col_w[p.col] = col_w[p.col].max(cw + pad);
1104            }
1105            for p in &places {
1106                if p.colspan <= 1 {
1107                    continue;
1108                }
1109                let (cw, _) = self.children[row_indices[p.row_ord]].children[p.cell_ord]
1110                    .intrinsic_inline_size(table_width);
1111                let need = cw + pad;
1112                let cur: i32 = col_w[p.col..p.col + p.colspan].iter().sum();
1113                if need > cur {
1114                    let deficit = need - cur;
1115                    let add = deficit / p.colspan as i32;
1116                    for cc in p.col..p.col + p.colspan {
1117                        col_w[cc] += add;
1118                    }
1119                    col_w[p.col] += deficit - add * p.colspan as i32; // 端数は先頭列へ
1120                }
1121            }
1122
1123            // 総和を利用可能幅に合わせる (超過→比例縮小 / 不足→均等加算)。
1124            // table-layout:fixed では列幅は明示値で確定しているため、この正規化は auto モードのみ。
1125            let total: i32 = col_w.iter().sum::<i32>().max(1);
1126            if total > usable_width {
1127                for w in col_w.iter_mut() {
1128                    *w = ((*w as i64 * usable_width as i64) / total as i64).max(1) as i32;
1129                }
1130            } else {
1131                let extra = (usable_width - total) / ncols as i32;
1132                for w in col_w.iter_mut() {
1133                    *w += extra;
1134                }
1135            }
1136        }
1137        // 列開始 x オフセット(列間に border-spacing を挿入)
1138        let mut col_x = alloc::vec![0i32; ncols];
1139        let mut acc = 0i32;
1140        for c in 0..ncols {
1141            col_x[c] = acc;
1142            acc += col_w[c];
1143            if c + 1 < ncols {
1144                acc += h_spacing;
1145            }
1146        }
1147        let span_w = |col: usize, colspan: usize| -> i32 {
1148            col_w[col..(col + colspan).min(ncols)].iter().sum::<i32>()
1149                + h_spacing * colspan.saturating_sub(1) as i32
1150        };
1151
1152        // --- 高さ計測パス: 各セルを確定列幅でレイアウトして外形高さを得る ---
1153        let mut place_h = alloc::vec![0i32; places.len()];
1154        for (pi, p) in places.iter().enumerate() {
1155            let cw_total = span_w(p.col, p.colspan);
1156            let cell = &mut self.children[row_indices[p.row_ord]].children[p.cell_ord];
1157            let mut cb = Dimensions::default();
1158            cb.content.x = base_x + col_x[p.col];
1159            cb.content.y = rows_top;
1160            cb.content.height = 0;
1161            cb.content.width = cw_total;
1162            cell.layout(cb);
1163            place_h[pi] = cell.dimensions.margin_box().height;
1164        }
1165        // 行高さ: rowspan==1 で確定、rowspan>1 は不足分を最終行へ加算
1166        let mut row_h = alloc::vec![0i32; nrows];
1167        for (pi, p) in places.iter().enumerate() {
1168            if p.rowspan == 1 {
1169                row_h[p.row_ord] = row_h[p.row_ord].max(place_h[pi]);
1170            }
1171        }
1172        for (pi, p) in places.iter().enumerate() {
1173            if p.rowspan <= 1 {
1174                continue;
1175            }
1176            let end = (p.row_ord + p.rowspan).min(nrows);
1177            let cur: i32 = row_h[p.row_ord..end].iter().sum();
1178            if place_h[pi] > cur {
1179                row_h[end - 1] += place_h[pi] - cur;
1180            }
1181        }
1182        // 行 y 先頭オフセット (テーブル本体内の相対、行間に border-spacing を挿入)
1183        let mut row_y = alloc::vec![0i32; nrows];
1184        let mut accy = 0i32;
1185        for r in 0..nrows {
1186            row_y[r] = accy;
1187            accy += row_h[r];
1188            if r + 1 < nrows {
1189                accy += v_spacing;
1190            }
1191        }
1192        let rows_total = accy;
1193
1194        // --- 確定パス: 各セルを最終位置で再レイアウトし、列幅・スパン行高さに合わせる ---
1195        for p in places.iter() {
1196            let cw_total = span_w(p.col, p.colspan);
1197            let end_r = (p.row_ord + p.rowspan).min(nrows);
1198            let span_h: i32 = row_h[p.row_ord..end_r].iter().sum();
1199            let x0 = base_x + col_x[p.col] - collapse_px * p.col as i32;
1200            let y0 = rows_top + row_y[p.row_ord] - collapse_px * p.row_ord as i32;
1201            let cell = &mut self.children[row_indices[p.row_ord]].children[p.cell_ord];
1202            let mut cb = Dimensions::default();
1203            cb.content.x = x0;
1204            cb.content.y = y0;
1205            cb.content.height = 0;
1206            cb.content.width = cw_total;
1207            cell.layout(cb); // 子要素を最終位置で配置し直す
1208            let w_extra = cell.dimensions.padding.left
1209                + cell.dimensions.padding.right
1210                + cell.dimensions.border.left
1211                + cell.dimensions.border.right;
1212            let h_extra = cell.dimensions.padding.top
1213                + cell.dimensions.padding.bottom
1214                + cell.dimensions.border.top
1215                + cell.dimensions.border.bottom;
1216            // vertical-align: middle/bottom — 行がセルの自然な高さより高い場合、
1217            // 余った縦方向の空間分だけ子孫全体を下へ平行移動する(既定/top/baseline は無移動)。
1218            let natural_h = cell.dimensions.content.height;
1219            let target_h = (span_h - h_extra).max(natural_h);
1220            let extra_space = (target_h - natural_h).max(0);
1221            if extra_space > 0 {
1222                let va = match &cell.box_type {
1223                    BoxType::TableCellNode(node) => node.value("vertical-align").unwrap_or_default(),
1224                    _ => String::new(),
1225                };
1226                let dy = match va.trim() {
1227                    "middle" | "center" => extra_space / 2,
1228                    "bottom" => extra_space,
1229                    _ => 0, // top / baseline(簡略化) / 未指定
1230                };
1231                // セル自身の box(背景・境界)は行の先頭に留め、中身(子要素)だけを下げる。
1232                for child in &mut cell.children {
1233                    child.translate_y(dy);
1234                }
1235            }
1236            cell.dimensions.content.width = (cw_total - w_extra).max(1);
1237            cell.dimensions.content.height = target_h;
1238        }
1239
1240        // 行ボックスの寸法を設定 (背景・境界の整列用)
1241        for (row_ord, &ri) in row_indices.iter().enumerate() {
1242            let row = &mut self.children[ri];
1243            row.dimensions.content.x = base_x;
1244            row.dimensions.content.y = rows_top + row_y[row_ord] - collapse_px * row_ord as i32;
1245            row.dimensions.content.width = table_width;
1246            row.dimensions.content.height = row_h[row_ord];
1247            // セル(行の直下子)の relative/absolute/fixed オフセット + transform を適用。
1248            // 行を包含ブロックとして共有後処理を流用する。
1249            let saved_row_cb = row.establish_abs_cb();
1250            row.apply_positioned_offsets();
1251            Self::restore_abs_cb(saved_row_cb);
1252        }
1253
1254        // collapse 時は重ね合わせぶん全体が縮む
1255        let collapse_shrink = collapse_px * (nrows.saturating_sub(1)) as i32;
1256        let rows_height = (rows_total - collapse_shrink).max(0);
1257        if caption_side_bottom {
1258            let y = self.dimensions.content.y + top_offset + rows_height;
1259            top_offset += self.layout_captions_at(&caption_indices, base_x, y, table_width);
1260        }
1261        self.dimensions.content.height = top_offset + rows_height;
1262
1263        // positioned 直下子要素の配置(table を包含ブロック起点にできる)。
1264        // 通常フローのセル/行はすでに配置済み。relative オフセットや、table が
1265        // positioned のときの子孫 absolute 解決のため共有後処理を適用する。
1266        let saved_abs_cb = self.establish_abs_cb();
1267        self.apply_positioned_offsets();
1268        Self::restore_abs_cb(saved_abs_cb);
1269    }
1270
1271    fn layout_flex(&mut self, containing_block: Dimensions) {
1272        self.apply_box_model_styles_with_base(containing_block.content.width);
1273
1274        let (width_val, min_width_val, max_width_val, height_val) = match &self.box_type {
1275            BoxType::FlexNode(node) => (
1276                node.value("width"),
1277                node.value("min-width"),
1278                node.value("max-width"),
1279                node.value("height"),
1280            ),
1281            _ => return,
1282        };
1283
1284        let horizontal_non_content = self.dimensions.margin.left
1285            + self.dimensions.margin.right
1286            + self.dimensions.border.left
1287            + self.dimensions.border.right
1288            + self.dimensions.padding.left
1289            + self.dimensions.padding.right;
1290        let available_width = containing_block.content.width.max(1);
1291
1292        let mut content_width = match parse_length_value(width_val) {
1293            Some(LengthValue::Auto) | None => (available_width - horizontal_non_content).max(1),
1294            Some(LengthValue::Px(px)) => px.max(1),
1295            Some(LengthValue::Percent(pct)) => {
1296                round_f32_to_i32(available_width as f32 * pct).max(1)
1297            }
1298            Some(LengthValue::MinContent) => {
1299                let (minc, _maxc) = self.block_intrinsic_widths();
1300                minc.max(1)
1301            }
1302            Some(LengthValue::MaxContent) => {
1303                let (_minc, maxc) = self.block_intrinsic_widths();
1304                maxc.max(1)
1305            }
1306            Some(LengthValue::FitContent) => {
1307                let (minc, maxc) = self.block_intrinsic_widths();
1308                let avail = (available_width - horizontal_non_content).max(0);
1309                avail.min(maxc).max(minc).max(1)
1310            }
1311            Some(LengthValue::MathExpr(expr)) => {
1312                crate::os_lib::css::eval_css_math(&expr, available_width)
1313                    .unwrap_or((available_width - horizontal_non_content).max(1))
1314                    .max(1)
1315            }
1316        };
1317
1318        if let Some(min_width) =
1319            parse_length_value(min_width_val).and_then(|v| resolve_length_value(v, available_width))
1320        {
1321            content_width = content_width.max(min_width);
1322        }
1323        if let Some(max_width) =
1324            parse_length_value(max_width_val).and_then(|v| resolve_length_value(v, available_width))
1325        {
1326            content_width = content_width.min(max_width);
1327        }
1328        self.dimensions.content.width = content_width.max(1);
1329
1330        let flow_y = containing_block.content.y
1331            + containing_block.content.height
1332            + self.dimensions.margin.top;
1333        self.dimensions.content.x = containing_block.content.x
1334            + self.dimensions.margin.left
1335            + self.dimensions.border.left
1336            + self.dimensions.padding.left;
1337        self.dimensions.content.y =
1338            flow_y + self.dimensions.border.top + self.dimensions.padding.top;
1339
1340        let (flex_dir, justify_content, align_items, align_content, row_gap, col_gap, flex_wrap) =
1341            match &self.box_type {
1342                BoxType::FlexNode(node) => {
1343                    let (rg, cg) = parse_flex_gap(node, self.dimensions.content.width);
1344                    (
1345                        node.value("flex-direction")
1346                            .unwrap_or_else(|| String::from("row"))
1347                            .trim()
1348                            .to_lowercase(),
1349                        node.value("justify-content")
1350                            .unwrap_or_else(|| String::from("flex-start"))
1351                            .trim()
1352                            .to_lowercase(),
1353                        node.value("align-items")
1354                            .unwrap_or_else(|| String::from("stretch"))
1355                            .trim()
1356                            .to_lowercase(),
1357                        node.value("align-content")
1358                            .unwrap_or_else(|| String::from("stretch"))
1359                            .trim()
1360                            .to_lowercase(),
1361                        rg,
1362                        cg,
1363                        node.value("flex-wrap")
1364                            .unwrap_or_else(|| String::from("nowrap"))
1365                            .trim()
1366                            .to_lowercase(),
1367                    )
1368                }
1369                _ => return,
1370            };
1371
1372        // row / row-reverse は主軸が水平。column / column-reverse は垂直。
1373        let is_row = flex_dir == "row" || flex_dir == "row-reverse";
1374        let is_reverse = flex_dir == "row-reverse" || flex_dir == "column-reverse";
1375        // flex-wrap: wrap / wrap-reverse のとき複数行へ折り返す。
1376        let do_wrap = flex_wrap == "wrap" || flex_wrap == "wrap-reverse";
1377        // 主軸方向の gap(row なら列間=column-gap、column なら行間=row-gap)。
1378        let gap_main = if is_row { col_gap } else { row_gap };
1379        // 交差軸方向の gap(行間)。
1380        let gap_cross = if is_row { row_gap } else { col_gap };
1381
1382        if do_wrap {
1383            self.layout_flex_wrapped(
1384                is_row,
1385                is_reverse,
1386                flex_wrap == "wrap-reverse",
1387                &justify_content,
1388                &align_items,
1389                &align_content,
1390                gap_main,
1391                gap_cross,
1392                height_val.clone(),
1393            );
1394            return;
1395        }
1396
1397        // Layout all children as if they were inline-blocks (or blocks) with 0 initial x/y to measure them
1398        let mut total_main = 0;
1399        let mut max_cross = 0;
1400        // out-of-flow(absolute/fixed)はフレックスラインに参加しない。サイズ確定のため
1401        // レイアウトはするが、主軸合計・交差軸最大・justify の item 数には数えない。
1402        let oof: Vec<bool> = self
1403            .children
1404            .iter()
1405            .map(|c| Self::child_is_out_of_flow(c))
1406            .collect();
1407        let n = oof.iter().filter(|&&b| !b).count();
1408
1409        // flex-basis の % 解決基準(コンテナ主軸長)。row は content 幅、column は明示高さ。
1410        let basis_main_ref = if is_row {
1411            self.dimensions.content.width
1412        } else {
1413            match parse_length_value(height_val.clone()) {
1414                Some(LengthValue::Px(px)) => px,
1415                _ => 0,
1416            }
1417        };
1418
1419        for (ci, child) in self.children.iter_mut().enumerate() {
1420            let mut cb = self.dimensions.clone();
1421            cb.content.x = 0;
1422            cb.content.y = 0;
1423            cb.content.height = 0; // unlimited
1424            child.layout(cb);
1425            if oof.get(ci).copied().unwrap_or(false) {
1426                continue;
1427            }
1428            // flex アイテムの主軸 auto margin は flex 側で処理する。layout_block が
1429            // margin:auto を「ブロック水平センタリング」として margin.left/right の両方へ
1430            // free/2 を入れてしまうため、主軸 auto margin がある辺方向の両端 margin を
1431            // 0 へ戻す(margin_box を実サイズに戻し、flex の auto 分配へ委ねる)。
1432            {
1433                let (lead, trail) = box_main_auto_margins(child, is_row);
1434                if lead || trail {
1435                    if is_row {
1436                        child.dimensions.margin.left = 0;
1437                        child.dimensions.margin.right = 0;
1438                    } else {
1439                        child.dimensions.margin.top = 0;
1440                        child.dimensions.margin.bottom = 0;
1441                    }
1442                }
1443            }
1444            // flex-basis 指定があれば主軸 content サイズを basis へ上書き(grow/shrink の基準)。
1445            if let Some(basis) = box_flex_basis(child, basis_main_ref) {
1446                if is_row {
1447                    child.dimensions.content.width = basis;
1448                } else {
1449                    child.dimensions.content.height = basis;
1450                }
1451            }
1452            // 【2026-09-04】幅指定が無い flex アイテムの基準幅は、
1453            // **自身の max-content 幅**(CSS Flexbox L1 §9.2「flex base size」)。
1454            //
1455            // 従来は「幅 auto のブロックとしてレイアウトした結果」=
1456            // 親いっぱいの幅をそのまま基準にしていた。その結果どの項目も
1457            // 基準が同じ値になり、縮小が**均等割り**になっていた。
1458            // 実サイト(www.sugi-lab.net)のナビ 8 項目が全部 88px に
1459            // 揃えられ、幅の合計が描画幅を超えてはみ出していた
1460            // (実測: container=924 なのに item0..7 すべて base=924)。
1461            // 正しくは項目ごとに基準が違うので、比例配分で
1462            // 短い項目は短いまま、長い項目が多く縮む。
1463            if is_row
1464                && box_flex_basis(child, basis_main_ref).is_none()
1465                && !box_has_explicit(child, "width")
1466            {
1467                let max_content = child.block_intrinsic_widths().1;
1468                if max_content > 0 && max_content != child.dimensions.content.width {
1469                    // 幅を変えたら、その幅でレイアウトをやり直す。
1470                    // 子は親いっぱいの幅で組まれているので、幅だけ
1471                    // 書き換えると中身が古い位置に取り残される
1472                    // (ナビが本来より右へずれて出た)。縮小側と同じ手当て。
1473                    let mut cb2 = self.dimensions.clone();
1474                    cb2.content.x = 0;
1475                    cb2.content.y = 0;
1476                    cb2.content.width = max_content;
1477                    cb2.content.height = 0; // unlimited
1478                    child.layout(cb2);
1479                    child.dimensions.content.width = max_content;
1480                }
1481            }
1482            let child_w = child.dimensions.margin_box().width;
1483            let child_h = child.dimensions.margin_box().height;
1484            if is_row {
1485                total_main += child_w;
1486                max_cross = max_cross.max(child_h);
1487            } else {
1488                total_main += child_h;
1489                max_cross = max_cross.max(child_w);
1490            }
1491        }
1492        // アイテム間の gap を主軸合計に加える(n-1 箇所)。
1493        if n > 1 {
1494            total_main += gap_main * (n as i32 - 1);
1495        }
1496
1497        let container_main = if is_row {
1498            self.dimensions.content.width
1499        } else {
1500            match parse_length_value(height_val.clone()) {
1501                Some(LengthValue::Px(px)) => px,
1502                _ => total_main,
1503            }
1504        };
1505
1506        let mut container_cross = if is_row {
1507            match parse_length_value(height_val.clone()) {
1508                Some(LengthValue::Px(px)) => px,
1509                _ => max_cross,
1510            }
1511        } else {
1512            self.dimensions.content.width
1513        };
1514
1515        if !is_row {
1516            self.dimensions.content.height = container_main;
1517        } else {
1518            self.dimensions.content.height = container_cross;
1519        }
1520
1521        // flex-grow / flex:N: grower を flex-basis 0 扱いにし、主軸の残余空間を grow 比で分配。
1522        // grower が無ければ何もしない(従来挙動を完全維持)。
1523        let grows: Vec<i32> = self
1524            .children
1525            .iter()
1526            .enumerate()
1527            .map(|(i, c)| {
1528                if oof.get(i).copied().unwrap_or(false) {
1529                    0
1530                } else {
1531                    box_flex_grow(c)
1532                }
1533            })
1534            .collect();
1535        let total_grow: i32 = grows.iter().sum();
1536        if total_grow > 0 {
1537            // grower の主軸 content を 0 とみなした基準長(非content分=padding/border/margin は残す)。
1538            let mut base_main = total_main;
1539            for (i, child) in self.children.iter().enumerate() {
1540                if grows.get(i).copied().unwrap_or(0) <= 0 {
1541                    continue;
1542                }
1543                base_main -= if is_row {
1544                    child.dimensions.content.width
1545                } else {
1546                    child.dimensions.content.height
1547                };
1548            }
1549            let free = (container_main - base_main).max(0);
1550            if free > 0 {
1551                let last_grow_idx = grows.iter().rposition(|g| *g > 0);
1552                let mut remaining = free;
1553                for (i, child) in self.children.iter_mut().enumerate() {
1554                    let g = grows.get(i).copied().unwrap_or(0);
1555                    if g <= 0 {
1556                        continue;
1557                    }
1558                    // 端数は最後の grower に寄せて合計を厳密一致させる。
1559                    let extra = if Some(i) == last_grow_idx {
1560                        remaining
1561                    } else {
1562                        free * g / total_grow
1563                    };
1564                    remaining -= extra;
1565                    if is_row {
1566                        child.dimensions.content.width = extra.max(0);
1567                    } else {
1568                        child.dimensions.content.height = extra.max(0);
1569                    }
1570                }
1571                total_main = base_main + free; // = container_main(justify 余白 0)
1572            }
1573        }
1574
1575        // flex-shrink: 主軸がコンテナを超過する場合、shrink 係数に比例して各アイテムを縮める。
1576        // grower がいた(既に container にフィット)場合はスキップ。CSS では基準サイズで
1577        // 重み付けするが、ここでは簡易に shrink 係数のみで余剰超過分を按分する。
1578        let overflow = total_main - container_main;
1579        if total_grow == 0 && overflow > 0 {
1580            let shrinks: Vec<i32> = self
1581                .children
1582                .iter()
1583                .enumerate()
1584                .map(|(i, c)| {
1585                    if oof.get(i).copied().unwrap_or(false) {
1586                        0
1587                    } else {
1588                        box_flex_shrink(c)
1589                    }
1590                })
1591                .collect();
1592            let total_shrink: i32 = shrinks.iter().sum();
1593            if total_shrink > 0 {
1594                // 【2026-07-28 規格対応】従来は `.max(0)` で 0 までしか止めておらず、
1595                // **自動最小サイズ(min-width:auto)を一切見ていなかった**。その結果
1596                // `.hero-content` が 240px まで縮み、14 文字の見出しが 3 行に
1597                // 過剰折り返しされてサブテキストと重なっていた。
1598                // 仕様・不変条件は `spec/flex_min_size.md`(CSS Flexbox L1 §4.5/§9.7)。
1599                let items: Vec<crate::os_lib::layout::flex_min::ShrinkItem> = self
1600                    .children
1601                    .iter()
1602                    .enumerate()
1603                    .map(|(i, c)| {
1604                        let base = if is_row {
1605                            c.dimensions.content.width
1606                        } else {
1607                            c.dimensions.content.height
1608                        };
1609                        // 自動最小サイズは min-content(分割できない最長語)で代用する近似。
1610                        let min = if is_row {
1611                            c.block_intrinsic_widths().0
1612                        } else {
1613                            0
1614                        };
1615                        crate::os_lib::layout::flex_min::ShrinkItem {
1616                            base: base.max(0),
1617                            min: min.clamp(0, base.max(0)),
1618                            shrink: shrinks.get(i).copied().unwrap_or(0).max(0),
1619                        }
1620                    })
1621                    .collect();
1622
1623                match crate::os_lib::layout::flex_min::distribute_shrink(&items, overflow.max(0)) {
1624                    Ok(sizes) => {
1625                        for (i, child) in self.children.iter_mut().enumerate() {
1626                            if let Some(&s) = sizes.get(i) {
1627                                if is_row {
1628                                    child.dimensions.content.width = s;
1629                                } else {
1630                                    child.dimensions.content.height = s;
1631                                }
1632                            }
1633                        }
1634                        // 【2026-09-04】縮めた子は、その幅でレイアウトをやり直す。
1635                        //
1636                        // 幅だけ書き換えても、テキストは**元の広い幅で組まれたまま**
1637                        // なので折り返さず、高さも更新されない。実サイト
1638                        // (www.sugi-lab.net)のヘッダで、本来 2 行に折り返して
1639                        // 収まるロゴとナビ項目が 1 行のままになり、ナビ 8 項目の
1640                        // うち 4 項目が画面外へ押し出されていた。
1641                        //
1642                        // 主軸が横のときだけでよい。縦のときに縮むのは高さで、
1643                        // 高さを変えても行の折り返しは変わらない。
1644                        if is_row {
1645                            for (i, child) in self.children.iter_mut().enumerate() {
1646                                if oof.get(i).copied().unwrap_or(false) {
1647                                    continue;
1648                                }
1649                                let Some(&s) = sizes.get(i) else {
1650                                    continue;
1651                                };
1652                                let base = items.get(i).map(|it| it.base).unwrap_or(s);
1653                                // 縮んでいない子はやり直さない(無駄な再計算を避ける)。
1654                                if s >= base {
1655                                    continue;
1656                                }
1657                                let mut cb = self.dimensions.clone();
1658                                cb.content.x = 0;
1659                                cb.content.y = 0;
1660                                cb.content.width = s;
1661                                cb.content.height = 0; // unlimited
1662                                child.layout(cb);
1663                                // やり直しで幅が戻ることがあるので、縮小後の値を残す。
1664                                child.dimensions.content.width = s;
1665                            }
1666                        }
1667                        // 下限で吸収しきれない場合ははみ出すのが正しい(F-4)ので、
1668                        // container_main に丸めず実際の合計を使う。
1669                        total_main = sizes.iter().sum::<i32>().max(container_main.min(total_main));
1670                    }
1671                    Err(_) => {
1672                        // 引数不正。エラーは flex_min 側でログ済み。
1673                        // 安全側=縮小を行わない(はみ出す方がレイアウト崩壊より軽い)。
1674                    }
1675                }
1676            }
1677        }
1678
1679        // 【2026-09-05】縮小で子を組み直したら、**親の高さも計算し直す**。
1680        //
1681        // 交差軸のサイズ(横並びなら高さ)は縮小より前に確定していた。
1682        // 縮小した子はその幅で組み直すので行数が増えることがあり、
1683        // 高さが 1 行ぶんのまま取り残されて**次の要素と重なる**。
1684        // 実サイト `forms.htm` の目次で、2 行に折り返した項目が
1685        // 次の項目に重なって出ていた。
1686        //
1687        // 高さが明示されている場合は指定を尊重する(伸ばさない)。
1688        if is_row && !matches!(parse_length_value(height_val.clone()), Some(LengthValue::Px(_))) {
1689            let mut recomputed = 0;
1690            for (ci, child) in self.children.iter().enumerate() {
1691                if oof.get(ci).copied().unwrap_or(false) {
1692                    continue;
1693                }
1694                recomputed = recomputed.max(child.dimensions.margin_box().height);
1695            }
1696            if recomputed > container_cross {
1697                container_cross = recomputed;
1698                self.dimensions.content.height = container_cross;
1699            }
1700        }
1701
1702        // align-items: stretch(既定値): 交差軸サイズ未指定の子をコンテナ交差幅へ伸ばす。
1703        // ただし align-self が明示された子は除外(個別指定を尊重)。
1704        if align_items == "stretch" {
1705            let cross_prop = if is_row { "height" } else { "width" };
1706            for (ci, child) in self.children.iter_mut().enumerate() {
1707                if oof.get(ci).copied().unwrap_or(false) {
1708                    continue;
1709                }
1710                if box_has_explicit(child, cross_prop) {
1711                    continue;
1712                }
1713                // align-self が stretch 以外で指定されていれば stretch しない。
1714                if let Some(s) = box_align_self(child) {
1715                    if s != "stretch" {
1716                        continue;
1717                    }
1718                }
1719                let mb = child.dimensions.margin_box();
1720                let (cur_cross, cur_content) = if is_row {
1721                    (mb.height, child.dimensions.content.height)
1722                } else {
1723                    (mb.width, child.dimensions.content.width)
1724                };
1725                let non_content = cur_cross - cur_content; // padding/border/margin 分
1726                let new_content = (container_cross - non_content).max(0);
1727                if is_row {
1728                    child.dimensions.content.height = new_content;
1729                } else {
1730                    child.dimensions.content.width = new_content;
1731                }
1732            }
1733        }
1734
1735        // Justify Content (Main Axis)
1736        let main_free_space = container_main - total_main;
1737
1738        // flex 主軸 auto margin: 主軸方向に auto margin を持つアイテムがあれば、
1739        // 余剰空間を全 auto margin 辺で均等分配する(justify-content より優先)。
1740        let mut total_auto_margins = 0i32;
1741        for (ci, child) in self.children.iter().enumerate() {
1742            if oof.get(ci).copied().unwrap_or(false) {
1743                continue;
1744            }
1745            let (a, b) = box_main_auto_margins(child, is_row);
1746            total_auto_margins += a as i32 + b as i32;
1747        }
1748        let auto_margin_unit = if total_auto_margins > 0 && main_free_space > 0 {
1749            main_free_space / total_auto_margins
1750        } else {
1751            0
1752        };
1753        let use_auto_margins = total_auto_margins > 0 && main_free_space > 0;
1754
1755        let main_offset = if use_auto_margins {
1756            0
1757        } else {
1758            match justify_content.as_str() {
1759                // CSS Box Alignment Level 3 の汎用キーワード `end` は `flex-end` の別名
1760                // (`align-content` は既に対応済みだったが、主軸側のここは漏れており
1761                // `justify-content: end` が `flex-start` と同じ挙動に落ちる静かなバグ
1762                // だった)。
1763                "flex-end" | "end" => main_free_space,
1764                "center" => main_free_space / 2,
1765                "space-around" if n > 0 => main_free_space / (n as i32 * 2),
1766                // `space-evenly` はアイテム間だけでなく先頭/末尾の余白も均等にする
1767                // (align-content の space-evenly と同じ `free / (n+1)` の式)。
1768                // 以前はこの分岐が無く flex-start と同じ挙動に落ちる静かなバグだった。
1769                "space-evenly" if n > 0 => main_free_space / (n as i32 + 1),
1770                // flex-start / start / space-between(ループで処理)/ space-around・space-evenly(n==0)
1771                _ => 0,
1772            }
1773        };
1774
1775        let spacing = if use_auto_margins {
1776            0
1777        } else {
1778            match justify_content.as_str() {
1779                "space-between" if n > 1 => main_free_space / (n as i32 - 1),
1780                "space-around" if n > 0 => main_free_space / (n as i32),
1781                "space-evenly" if n > 0 => main_free_space / (n as i32 + 1),
1782                _ => 0,
1783            }
1784        };
1785
1786        // 主軸の配置。reverse なら主軸終端(row なら右端)から DOM 順に詰める。
1787        // cursor は「正方向なら先頭からの距離 / reverse なら終端からの距離」。
1788        // order: アイテムを order 昇順(同値は DOM 順)に並べ替えて配置する。
1789        let mut order_idx: Vec<usize> = (0..self.children.len()).collect();
1790        order_idx.sort_by_key(|&i| (box_order(&self.children[i]), i));
1791        let mut cursor = main_offset;
1792        for &ci in &order_idx {
1793            // out-of-flow はフロー配置をスキップ(cursor を進めない)。後段で配置する。
1794            if oof.get(ci).copied().unwrap_or(false) {
1795                continue;
1796            }
1797            // 主軸 auto margin: 先頭辺ぶんを cursor へ加算(アイテムを後方へ押す)。
1798            let (lead_auto, trail_auto) = if use_auto_margins {
1799                box_main_auto_margins(&self.children[ci], is_row)
1800            } else {
1801                (false, false)
1802            };
1803            if lead_auto {
1804                cursor += auto_margin_unit;
1805            }
1806            let child_w = self.children[ci].dimensions.margin_box().width;
1807            let child_h = self.children[ci].dimensions.margin_box().height;
1808            let child_main = if is_row { child_w } else { child_h };
1809
1810            let cross_free_space = container_cross - if is_row { child_h } else { child_w };
1811            // align-self が指定されていればそれを優先、無ければ align-items。
1812            let effective_align =
1813                box_align_self(&self.children[ci]).unwrap_or_else(|| align_items.clone());
1814            let cross_offset = match effective_align.as_str() {
1815                // `end`/`self-end` は `flex-end` の別名(CSS Box Alignment Level 3)。
1816                // 以前はここで漏れており `align-items: end` が `flex-start` と
1817                // 同じ挙動に落ちる静かなバグだった。
1818                "flex-end" | "end" | "self-end" => cross_free_space,
1819                "center" => cross_free_space / 2,
1820                _ => 0, // flex-start / start / stretch
1821            };
1822
1823            // 主軸開始位置(コンテナ content 原点からの相対)。reverse は終端から逆算。
1824            let main_start = if is_reverse {
1825                container_main - cursor - child_main
1826            } else {
1827                cursor
1828            };
1829
1830            // Recursively update positions relative to container
1831            let start_x = self.dimensions.content.x;
1832            let start_y = self.dimensions.content.y;
1833
1834            let target_x = if is_row {
1835                start_x + main_start
1836            } else {
1837                start_x + cross_offset
1838            };
1839            let target_y = if is_row {
1840                start_y + cross_offset
1841            } else {
1842                start_y + main_start
1843            };
1844
1845            let shift_x = target_x - self.children[ci].dimensions.margin_box().x;
1846            let shift_y = target_y - self.children[ci].dimensions.margin_box().y;
1847
1848            shift_layout_box(&mut self.children[ci], shift_x, shift_y);
1849
1850            cursor += child_main + spacing + gap_main;
1851            if trail_auto {
1852                cursor += auto_margin_unit;
1853            }
1854        }
1855
1856        // positioned 子要素の配置(flex コンテナを包含ブロック起点にできる)。
1857        // フロー内アイテムは上で配置済み。out-of-flow とフロー内 relative の両方を処理する。
1858        let saved_abs_cb = self.establish_abs_cb();
1859        self.apply_positioned_offsets();
1860        Self::restore_abs_cb(saved_abs_cb);
1861    }
1862
1863    /// flex-wrap: wrap / wrap-reverse のレイアウト。
1864    /// 子を主軸に沿って並べ、コンテナ主軸長を超えたら次の行へ折り返す。
1865    /// 各行ごとに justify-content(主軸)と align-items / align-self(交差軸)を適用する。
1866    #[allow(clippy::too_many_arguments)]
1867    fn layout_flex_wrapped(
1868        &mut self,
1869        is_row: bool,
1870        is_reverse: bool,
1871        wrap_reverse: bool,
1872        justify_content: &str,
1873        align_items: &str,
1874        align_content: &str,
1875        gap_main: i32,
1876        gap_cross: i32,
1877        height_val: Option<String>,
1878    ) {
1879        let n = self.children.len();
1880        if n == 0 {
1881            return;
1882        }
1883
1884        // 各子を測定(main/cross サイズ)。out-of-flow(absolute/fixed)は
1885        // フレックスラインに参加しないため flow_idx から除外する(サイズ確定のため
1886        // layout は行う)。最終配置は末尾の apply_positioned_offsets が行う。
1887        let oof: Vec<bool> = self
1888            .children
1889            .iter()
1890            .map(|c| Self::child_is_out_of_flow(c))
1891            .collect();
1892        let mut sizes: Vec<(i32, i32)> = Vec::with_capacity(n); // (main, cross)
1893        for child in &mut self.children {
1894            let mut cb = self.dimensions.clone();
1895            cb.content.x = 0;
1896            cb.content.y = 0;
1897            cb.content.height = 0;
1898            child.layout(cb);
1899            let w = child.dimensions.margin_box().width;
1900            let h = child.dimensions.margin_box().height;
1901            if is_row {
1902                sizes.push((w, h));
1903            } else {
1904                sizes.push((h, w));
1905            }
1906        }
1907        // フロー参加インデックス(DOM 順)。以降のライン分割・配置はこの並びで行う。
1908        let flow_idx: Vec<usize> = (0..n).filter(|&i| !oof[i]).collect();
1909
1910        let container_main = if is_row {
1911            self.dimensions.content.width
1912        } else {
1913            match parse_length_value(height_val.clone()) {
1914                Some(LengthValue::Px(px)) => px,
1915                _ => i32::MAX / 4, // column 方向で高さ未指定なら折り返さない(実質1行)
1916            }
1917        };
1918
1919        // 行分割: 主軸長がコンテナを超えたら改行。インデックスは flow_idx の位置(pos)。
1920        let fln = flow_idx.len();
1921        let mut lines: Vec<(usize, usize, i32, i32)> = Vec::new(); // (start_pos, end_pos_excl, main_used, cross_max)
1922        let mut line_start = 0usize;
1923        let mut line_main = 0i32;
1924        let mut line_cross = 0i32;
1925        for pos in 0..fln {
1926            let (m, c) = sizes[flow_idx[pos]];
1927            let add = if pos == line_start { m } else { m + gap_main };
1928            if pos > line_start && line_main + add > container_main {
1929                lines.push((line_start, pos, line_main, line_cross));
1930                line_start = pos;
1931                line_main = m;
1932                line_cross = c;
1933            } else {
1934                line_main += add;
1935                line_cross = line_cross.max(c);
1936            }
1937        }
1938        lines.push((line_start, fln, line_main, line_cross));
1939
1940        // 行を wrap-reverse なら逆順に積む。
1941        if wrap_reverse {
1942            lines.reverse();
1943        }
1944
1945        // コンテナ交差軸長を確定。
1946        let total_cross: i32 =
1947            lines.iter().map(|l| l.3).sum::<i32>() + gap_cross * (lines.len() as i32 - 1).max(0);
1948        let container_cross = if is_row {
1949            match parse_length_value(height_val.clone()) {
1950                Some(LengthValue::Px(px)) => px,
1951                _ => total_cross,
1952            }
1953        } else {
1954            self.dimensions.content.width
1955        };
1956
1957        if is_row {
1958            self.dimensions.content.height = container_cross.max(total_cross);
1959        } else {
1960            self.dimensions.content.height = container_main.min(total_cross).max(total_cross);
1961        }
1962
1963        let base_x = self.dimensions.content.x;
1964        let base_y = self.dimensions.content.y;
1965
1966        // align-content: 複数行ブロックを交差軸方向にどう配置するか。
1967        // 余剰交差空間 = container_cross - total_cross(行の自然交差合計)。
1968        // line_cross_start[i] = 各行の交差軸開始位置、line_cross_size[i] = 各行の交差軸高さ
1969        // (stretch では余剰を均等に各行へ加算)。
1970        let nlines = lines.len() as i32;
1971        let cross_free = (container_cross - total_cross).max(0);
1972        let mut line_cross_size: Vec<i32> = lines.iter().map(|l| l.3).collect();
1973        // stretch: 余剰を各行へ均等分配(端数は先頭行へ)。単一行でもコンテナ全体へ伸ばす。
1974        if (align_content == "stretch" || align_content.is_empty()) && cross_free > 0 && nlines > 0
1975        {
1976            let per = cross_free / nlines;
1977            let mut rem = cross_free - per * nlines;
1978            for sz in line_cross_size.iter_mut() {
1979                *sz += per;
1980                if rem > 0 {
1981                    *sz += 1;
1982                    rem -= 1;
1983                }
1984            }
1985        }
1986        // 各行の開始オフセットと行間スペースを align-content から決める。
1987        let (ac_offset, ac_spacing) = match align_content {
1988            "flex-end" | "end" => (cross_free, 0),
1989            "center" => (cross_free / 2, 0),
1990            "space-between" if nlines > 1 => (0, cross_free / (nlines - 1)),
1991            "space-around" if nlines > 0 => (cross_free / (nlines * 2), cross_free / nlines),
1992            "space-evenly" if nlines > 0 => (cross_free / (nlines + 1), cross_free / (nlines + 1)),
1993            // flex-start / start / stretch(余剰は行サイズ側で消費済み)
1994            _ => (0, 0),
1995        };
1996        let mut line_cross_start: Vec<i32> = Vec::with_capacity(lines.len());
1997        let mut cc = ac_offset;
1998        for sz in &line_cross_size {
1999            line_cross_start.push(cc);
2000            cc += sz + gap_cross + ac_spacing;
2001        }
2002
2003        for (li, &(start, end_excl, line_main_used, _line_cross_nat)) in lines.iter().enumerate() {
2004            let count = end_excl - start;
2005            let main_free = container_main - line_main_used;
2006            let line_cross = line_cross_size.get(li).copied().unwrap_or(0);
2007            let cross_cursor = line_cross_start.get(li).copied().unwrap_or(0);
2008
2009            // justify-content(主軸)。`end` は `flex-end` の別名(単一行側と同じ修正)。
2010            let mut main_offset = match justify_content {
2011                "flex-end" | "end" => main_free,
2012                "center" => main_free / 2,
2013                "space-around" if count > 0 => main_free / (count as i32 * 2),
2014                "space-evenly" if count > 0 => main_free / (count as i32 + 1),
2015                _ => 0,
2016            };
2017            let spacing = match justify_content {
2018                "space-between" if count > 1 => main_free / (count as i32 - 1),
2019                "space-around" if count > 0 => main_free / (count as i32),
2020                "space-evenly" if count > 0 => main_free / (count as i32 + 1),
2021                _ => 0,
2022            };
2023
2024            // 行内の配置順(reverse 対応)。pos は flow_idx の位置。
2025            let pos_list: Vec<usize> = if is_reverse {
2026                (start..end_excl).rev().collect()
2027            } else {
2028                (start..end_excl).collect()
2029            };
2030
2031            for &pos in &pos_list {
2032                let ci = flow_idx[pos];
2033                let (m, c) = sizes[ci];
2034                let cross_free_item = line_cross - c;
2035                let effective_align =
2036                    box_align_self(&self.children[ci]).unwrap_or_else(|| String::from(align_items));
2037                let cross_in_line = match effective_align.as_str() {
2038                    "flex-end" | "end" | "self-end" => cross_free_item,
2039                    "center" => cross_free_item / 2,
2040                    _ => 0, // flex-start / start / stretch
2041                };
2042                let cross_pos = cross_cursor + cross_in_line;
2043
2044                let (target_x, target_y) = if is_row {
2045                    (base_x + main_offset, base_y + cross_pos)
2046                } else {
2047                    (base_x + cross_pos, base_y + main_offset)
2048                };
2049
2050                let shift_x = target_x - self.children[ci].dimensions.margin_box().x;
2051                let shift_y = target_y - self.children[ci].dimensions.margin_box().y;
2052                shift_layout_box(&mut self.children[ci], shift_x, shift_y);
2053
2054                main_offset += m + spacing + gap_main;
2055            }
2056        }
2057
2058        // positioned 子要素の配置(out-of-flow と relative オフセット)。
2059        let saved_abs_cb = self.establish_abs_cb();
2060        self.apply_positioned_offsets();
2061        Self::restore_abs_cb(saved_abs_cb);
2062    }
2063
2064    fn layout_block(&mut self, containing_block: Dimensions) {
2065        self.apply_box_model_styles_with_base(containing_block.content.width);
2066
2067        // テーブル系 (row-group/row/cell) も table 経由で block としてレイアウトされるため
2068        // ここで styled node を取り出せるようにする。取り出せない型のみ早期 return。
2069        let (
2070            width_val,
2071            min_width_val,
2072            max_width_val,
2073            height_val,
2074            min_height_val,
2075            box_sizing,
2076            h_auto,
2077            aspect_ratio_val,
2078        ) = match &self.box_type {
2079            BoxType::BlockNode(node)
2080            | BoxType::TableRowGroupNode(node)
2081            | BoxType::TableRowNode(node)
2082            | BoxType::TableCellNode(node) => (
2083                node.value("width"),
2084                node.value("min-width"),
2085                node.value("max-width"),
2086                node.value("height"),
2087                node.value("min-height"),
2088                node.value("box-sizing").unwrap_or_default(),
2089                margin_is_h_auto(node),
2090                node.value("aspect-ratio"),
2091            ),
2092            _ => return,
2093        };
2094
2095        let horizontal_non_content = self.dimensions.margin.left
2096            + self.dimensions.margin.right
2097            + self.dimensions.border.left
2098            + self.dimensions.border.right
2099            + self.dimensions.padding.left
2100            + self.dimensions.padding.right;
2101        let available_width = containing_block.content.width.max(1);
2102        // padding + border のみ(box-sizing:border-box / margin:auto センタリング用)。
2103        let inner_non_content = self.dimensions.border.left
2104            + self.dimensions.border.right
2105            + self.dimensions.padding.left
2106            + self.dimensions.padding.right;
2107
2108        // Calculate width
2109        let parsed_width = parse_length_value(width_val.clone());
2110        let mut content_width = match parsed_width {
2111            Some(LengthValue::Auto) | None => (available_width - horizontal_non_content).max(1),
2112            Some(LengthValue::Px(px)) => px.max(1),
2113            Some(LengthValue::Percent(pct)) => {
2114                round_f32_to_i32(available_width as f32 * pct).max(1)
2115            }
2116            // 内容ベースのキーワード: min/max/fit-content。子コンテンツを計測して解決。
2117            Some(LengthValue::MinContent) => {
2118                let (minc, _maxc) = self.block_intrinsic_widths();
2119                minc.max(1)
2120            }
2121            Some(LengthValue::MaxContent) => {
2122                let (_minc, maxc) = self.block_intrinsic_widths();
2123                maxc.max(1)
2124            }
2125            Some(LengthValue::FitContent) => {
2126                // fit-content = clamp(min-content, available, max-content)
2127                let avail = (available_width - horizontal_non_content).max(1);
2128                let (minc, maxc) = self.block_intrinsic_widths();
2129                maxc.min(avail).max(minc).max(1)
2130            }
2131            Some(LengthValue::MathExpr(expr)) => {
2132                crate::os_lib::css::eval_css_math(&expr, available_width)
2133                    .unwrap_or((available_width - horizontal_non_content).max(1))
2134                    .max(1)
2135            }
2136        };
2137        // box-sizing: border-box → 指定 width は border-box 全体なので content は padding+border を引く。
2138        if box_sizing.trim() == "border-box" {
2139            content_width = (content_width - inner_non_content).max(1);
2140        }
2141
2142        // min-width / max-width。px/% は available 基準、min/max/fit-content は内容計測で解決。
2143        if let Some(min_width) = self.resolve_sizing_value(min_width_val.clone(), available_width) {
2144            content_width = content_width.max(min_width);
2145        }
2146        if let Some(max_width) = self.resolve_sizing_value(max_width_val.clone(), available_width) {
2147            content_width = content_width.min(max_width);
2148        }
2149        self.dimensions.content.width = content_width.max(1);
2150
2151        // margin: auto(左右)→ ブロックを水平センタリング。
2152        if h_auto {
2153            let free = (available_width - content_width - inner_non_content).max(0);
2154            self.dimensions.margin.left = free / 2;
2155            self.dimensions.margin.right = free / 2;
2156        }
2157
2158        // Position (content box origin)
2159        let flow_y = containing_block.content.y
2160            + containing_block.content.height
2161            + self.dimensions.margin.top;
2162        self.dimensions.content.x = containing_block.content.x
2163            + self.dimensions.margin.left
2164            + self.dimensions.border.left
2165            + self.dimensions.padding.left;
2166        self.dimensions.content.y =
2167            flow_y + self.dimensions.border.top + self.dimensions.padding.top;
2168
2169        // この要素が positioned なら、子孫 absolute の包含ブロックを self に切替(昇り解決)。
2170        // 高さは明示値が分かればそれ、無ければ 0(top/left 配置が主。bottom/right は明示高さ前提)。
2171        let self_pos = box_position(self);
2172        let saved_abs_cb = if matches!(self_pos.as_str(), "relative" | "absolute" | "fixed") {
2173            let prev = abs_cb_get();
2174            let exp_h = parse_length_value(height_val.clone())
2175                .and_then(|v| resolve_length_value(v, containing_block.content.height.max(0)))
2176                .unwrap_or(0);
2177            abs_cb_set(
2178                self.dimensions.content.x,
2179                self.dimensions.content.y,
2180                self.dimensions.content.width,
2181                exp_h,
2182            );
2183            Some(prev)
2184        } else {
2185            None
2186        };
2187
2188        // 子要素のレイアウト計算(block/inline混在の簡易フロー)
2189        let line_height_default = 20i32;
2190        let mut y_cursor = 0i32;
2191        let mut line_x = 0i32;
2192        let mut line_h = 0i32;
2193        let max_w = self.dimensions.content.width.max(1);
2194
2195        // float: left/right の占有幅と底辺 y を追跡する。(占有幅, 底辺y)。
2196        // 複数floatの左右並び積み重ねはサポートせず、各サイド最新の1個のみを追跡する
2197        // (画像+段落、サイドバー+本文といった典型パターンをカバーする簡易実装)。
2198        let mut left_float: Option<(i32, i32)> = None;
2199        let mut right_float: Option<(i32, i32)> = None;
2200
2201        // 指定 y での float 占有を考慮した (x_offset, 利用可能幅) を返す。
2202        fn float_avail(
2203            max_w: i32,
2204            left_float: Option<(i32, i32)>,
2205            right_float: Option<(i32, i32)>,
2206            y: i32,
2207        ) -> (i32, i32) {
2208            let l = left_float
2209                .filter(|&(_, yb)| yb > y)
2210                .map(|(w, _)| w)
2211                .unwrap_or(0);
2212            let r = right_float
2213                .filter(|&(_, yb)| yb > y)
2214                .map(|(w, _)| w)
2215                .unwrap_or(0);
2216            (l, (max_w - l - r).max(1))
2217        }
2218
2219        for child in &mut self.children {
2220            let float_side = box_float(child);
2221            if float_side != "none" {
2222                // clear: float 自身は clear の対象にはしない(float 同士は積み重ね非対応のため)。
2223                let float_y = y_cursor;
2224                let is_inline_kind =
2225                    matches!(child.box_type, BoxType::InlineNode(_) | BoxType::InlineBlockNode(_));
2226                let (fw, fh) = if is_inline_kind {
2227                    child.intrinsic_inline_size(max_w)
2228                } else {
2229                    let mut cb = self.dimensions.clone();
2230                    cb.content.height = float_y;
2231                    child.layout(cb);
2232                    (
2233                        child.dimensions.margin_box().width,
2234                        child.dimensions.margin_box().height,
2235                    )
2236                };
2237
2238                let x = if float_side == "left" {
2239                    let x_off = left_float
2240                        .filter(|&(_, yb)| yb > float_y)
2241                        .map(|(w, _)| w)
2242                        .unwrap_or(0);
2243                    left_float = Some((x_off + fw, float_y + fh));
2244                    self.dimensions.content.x + x_off
2245                } else {
2246                    let x_off = right_float
2247                        .filter(|&(_, yb)| yb > float_y)
2248                        .map(|(w, _)| w)
2249                        .unwrap_or(0);
2250                    right_float = Some((x_off + fw, float_y + fh));
2251                    self.dimensions.content.x + max_w - x_off - fw
2252                };
2253
2254                if is_inline_kind {
2255                    child.dimensions.content.x = x;
2256                    child.dimensions.content.y = self.dimensions.content.y + float_y;
2257                    child.dimensions.content.width = fw;
2258                    child.dimensions.content.height = fh;
2259                    let mut cb = child.dimensions.clone();
2260                    cb.content.height = 0;
2261                    child.layout(cb);
2262                    child.dimensions.content.x = x;
2263                    child.dimensions.content.y = self.dimensions.content.y + float_y;
2264                    child.dimensions.content.width = fw;
2265                    child.dimensions.content.height = fh;
2266                } else {
2267                    // 通常 block レイアウト済みだが、x はフロートの追加分ずれているため上書きする。
2268                    let dx = x - child.dimensions.content.x;
2269                    child.dimensions.content.x += dx;
2270                }
2271                // float はフローの縦カーソルを進めない。
2272                continue;
2273            }
2274
2275            match child.box_type {
2276                BoxType::BlockNode(_)
2277                | BoxType::FlexNode(_)
2278                | BoxType::GridNode(_)
2279                | BoxType::AnonymousBlock
2280                | BoxType::TableNode(_)
2281                | BoxType::TableRowGroupNode(_)
2282                | BoxType::TableRowNode(_)
2283                | BoxType::TableCellNode(_) => {
2284                    let out_of_flow = matches!(box_position(child).as_str(), "absolute" | "fixed");
2285                    if out_of_flow {
2286                        // フロー外: サイズ確定のためレイアウトのみ。位置は後処理、cursor は進めない。
2287                        let mut cb = self.dimensions.clone();
2288                        cb.content.height = y_cursor;
2289                        child.layout(cb);
2290                    } else {
2291                        if line_x > 0 {
2292                            y_cursor += if line_h > 0 { line_h } else { line_height_default };
2293                            line_x = 0;
2294                            line_h = 0;
2295                        }
2296                        // clear: 該当サイドの float 底辺まで縦カーソルを進める。
2297                        let clear = box_clear(child);
2298                        if clear == "left" || clear == "both" {
2299                            if let Some((_, yb)) = left_float {
2300                                y_cursor = y_cursor.max(yb);
2301                            }
2302                        }
2303                        if clear == "right" || clear == "both" {
2304                            if let Some((_, yb)) = right_float {
2305                                y_cursor = y_cursor.max(yb);
2306                            }
2307                        }
2308                        let (x_off, avail) = float_avail(max_w, left_float, right_float, y_cursor);
2309                        let mut cb = self.dimensions.clone();
2310                        cb.content.height = y_cursor;
2311                        cb.content.x = self.dimensions.content.x + x_off;
2312                        cb.content.width = avail;
2313                        child.layout(cb);
2314                        y_cursor += child.dimensions.margin_box().height;
2315                    }
2316                }
2317                BoxType::InlineNode(_) | BoxType::InlineBlockNode(_) => {
2318                    if child.is_inline_line_break() {
2319                        y_cursor += if line_h > 0 { line_h } else { line_height_default };
2320                        line_x = 0;
2321                        line_h = 0;
2322                        child.dimensions.content.x = self.dimensions.content.x;
2323                        child.dimensions.content.y = self.dimensions.content.y + y_cursor;
2324                        child.dimensions.content.width = 0;
2325                        child.dimensions.content.height = 0;
2326                        continue;
2327                    }
2328                    let (mut x_off, mut eff_w) =
2329                        float_avail(max_w, left_float, right_float, y_cursor);
2330                    let (inline_w, inline_h) = child.intrinsic_inline_size(eff_w);
2331                    if line_x > 0 && line_x + inline_w > eff_w {
2332                        y_cursor += if line_h > 0 { line_h } else { line_height_default };
2333                        line_x = 0;
2334                        line_h = 0;
2335                        let recomputed = float_avail(max_w, left_float, right_float, y_cursor);
2336                        x_off = recomputed.0;
2337                        eff_w = recomputed.1;
2338                    }
2339                    let _ = eff_w;
2340                    child.dimensions.content.x = self.dimensions.content.x + x_off + line_x;
2341                    child.dimensions.content.y = self.dimensions.content.y + y_cursor;
2342                    child.dimensions.content.width = inline_w;
2343                    child.dimensions.content.height = inline_h;
2344
2345                    // 子要素(InlineNode など)に対して再帰的にレイアウトを呼び出して、さらにその中の座標を決定する。
2346                    // 注意: layout_inline は containing.content.height を「Y フローカーソル」として
2347                    // 自身の y に加算する規約のため、ここで子自身の高さ (inline_h) を渡すと
2348                    // 子が自分の高さぶん下へずれてしまう(textarea が画面外に飛ぶバグの原因)。
2349                    // 確定済みスロット位置だけを伝えるため height は 0 にして渡す。
2350                    let mut cb = child.dimensions.clone();
2351                    cb.content.height = 0;
2352                    child.layout(cb);
2353                    // layout_inline 内の再計算は % 高さを包含高さ (=0) 基準で解決して
2354                    // 潰してしまうため、親が intrinsic 計測で確定したスロット寸法を最終値とする。
2355                    child.dimensions.content.x = self.dimensions.content.x + x_off + line_x;
2356                    child.dimensions.content.y = self.dimensions.content.y + y_cursor;
2357                    child.dimensions.content.width = inline_w;
2358                    child.dimensions.content.height = inline_h;
2359
2360                    line_x += inline_w;
2361                    if inline_h > line_h {
2362                        line_h = inline_h;
2363                    }
2364                }
2365            }
2366        }
2367
2368        if line_x > 0 {
2369            y_cursor += if line_h > 0 { line_h } else { line_height_default };
2370        }
2371        // float は通常のフロー高さに寄与しないため、コンテナが「潰れて」floatだけが
2372        // はみ出す見た目を避けるべく、底辺を高さに含める(簡易実装の割り切り)。
2373        if let Some((_, yb)) = left_float {
2374            y_cursor = y_cursor.max(yb);
2375        }
2376        if let Some((_, yb)) = right_float {
2377            y_cursor = y_cursor.max(yb);
2378        }
2379
2380        // 【2026-08-31】CSS 多段組(`column-count`/`column-width`/`columns`)。
2381        //
2382        // 縦フローで子を積み終えたこの時点で、**各子を列へ振り分け直す**
2383        // 後処理として実装する。先に通常フローを完成させてから x/y を
2384        // 移し替えるので、幅計算・マージン相殺・float 等の既存経路を
2385        // 一切変更せずに済む(新しいレイアウトモードを足すより安全)。
2386        //
2387        // 割り切り: 段抜き(`column-span`)・段区切り(`break-inside`)・
2388        // 段罫線(`column-rule`)は未対応。子は「積んだ順に、各列の高さが
2389        // 均等になるよう」振り分ける(厳密なバランシングではなく、
2390        // 各列の現在高さが最小の列へ順に入れる貪欲法)。
2391        if let BoxType::BlockNode(node) = &self.box_type {
2392            let cc = node.value("column-count");
2393            let cw = node.value("column-width");
2394            let shorthand = node.value("columns");
2395            let (sh_w, sh_c) = shorthand
2396                .as_deref()
2397                .map(crate::os_lib::layout::multicol::parse_columns_shorthand)
2398                .unwrap_or((None, None));
2399            let parse_px = |v: &Option<String>| -> Option<i32> {
2400                v.as_deref()
2401                    .and_then(|s| s.trim().strip_suffix("px"))
2402                    .and_then(|n| n.trim().parse::<f32>().ok())
2403                    .filter(|f| f.is_finite() && *f > 0.0)
2404                    .map(|f| f as i32)
2405            };
2406            let count = cc
2407                .as_deref()
2408                .and_then(|s| s.trim().parse::<i32>().ok())
2409                .filter(|v| *v > 0)
2410                .or(sh_c);
2411            let width = parse_px(&cw).or(sh_w);
2412            if count.is_some() || width.is_some() {
2413                // `column-gap` の既定 `normal` は 1em 相当。フォントサイズが
2414                // 取れないときは 16px を既定にする(CSS の初期値と一致)。
2415                let gap = node
2416                    .value("column-gap")
2417                    .as_deref()
2418                    .and_then(|s| s.trim().strip_suffix("px"))
2419                    .and_then(|n| n.trim().parse::<f32>().ok())
2420                    .filter(|f| f.is_finite() && *f >= 0.0)
2421                    .map(|f| f as i32)
2422                    .unwrap_or(16);
2423                let spec = crate::os_lib::layout::multicol::MultiColSpec { count, width, gap };
2424                let cols = crate::os_lib::layout::multicol::layout_columns(
2425                    &spec,
2426                    self.dimensions.content.width.max(0),
2427                );
2428                if cols.len() > 1 {
2429                    let base_x = self.dimensions.content.x;
2430                    let base_y = self.dimensions.content.y;
2431                    // 振り分けは純粋関数へ委ねる(`multicol::assign_to_columns`、
2432                    // 単体試験あり)。ここで手書きループを持つと、
2433                    // 「同値なら左の列を優先」等の細かい規則が試験から外れる。
2434                    // float や絶対配置は通常フローから外れているので対象外。
2435                    let flow_idx: alloc::vec::Vec<usize> = self
2436                        .children
2437                        .iter()
2438                        .enumerate()
2439                        .filter(|(_, c)| box_float(c) == "none")
2440                        .map(|(i, _)| i)
2441                        .collect();
2442                    let heights: alloc::vec::Vec<i32> = flow_idx
2443                        .iter()
2444                        .map(|&i| self.children[i].dimensions.margin_box().height)
2445                        .collect();
2446                    let places =
2447                        crate::os_lib::layout::multicol::assign_to_columns(&heights, cols.len());
2448                    for (slot, &ci) in flow_idx.iter().enumerate() {
2449                        let place = places[slot];
2450                        let col = cols[place.col];
2451                        let child = &mut self.children[ci];
2452                        // 子の左上を列の座標へ移す(子孫も相対的にずれるよう
2453                        // 差分で平行移動する)。
2454                        let dx = base_x + col.x - child.dimensions.content.x
2455                            + child.dimensions.margin.left
2456                            + child.dimensions.border.left
2457                            + child.dimensions.padding.left;
2458                        let dy = base_y + place.y - child.dimensions.content.y
2459                            + child.dimensions.margin.top
2460                            + child.dimensions.border.top
2461                            + child.dimensions.padding.top;
2462                        child.translate(dx, dy);
2463                        child.dimensions.content.width = (col.width
2464                            - child.dimensions.margin.left
2465                            - child.dimensions.margin.right
2466                            - child.dimensions.border.left
2467                            - child.dimensions.border.right
2468                            - child.dimensions.padding.left
2469                            - child.dimensions.padding.right)
2470                            .max(0);
2471                    }
2472                    // 多段組の高さは最も高い列に合わせる(同じ貪欲法で集計する)。
2473                    let totals =
2474                        crate::os_lib::layout::multicol::column_heights(&heights, cols.len());
2475                    y_cursor = totals.iter().copied().max().unwrap_or(y_cursor);
2476                }
2477            }
2478        }
2479
2480        // Block要素の高さは内容フローの高さ。ただし明示 height / min-height があれば優先。
2481        self.dimensions.content.height = y_cursor;
2482        let cb_height = containing_block.content.height.max(0);
2483        if let Some(h) =
2484            parse_length_value(height_val.clone()).and_then(|v| resolve_length_value(v, cb_height))
2485        {
2486            // box-sizing:border-box は指定 height から縦 padding+border を引いて content 高に。
2487            let vertical_non_content = self.dimensions.border.top
2488                + self.dimensions.border.bottom
2489                + self.dimensions.padding.top
2490                + self.dimensions.padding.bottom;
2491            let content_h = if box_sizing.trim() == "border-box" {
2492                (h - vertical_non_content).max(0)
2493            } else {
2494                h.max(0)
2495            };
2496            self.dimensions.content.height = content_h;
2497        }
2498        if let Some(mh) =
2499            parse_length_value(min_height_val).and_then(|v| resolve_length_value(v, cb_height))
2500        {
2501            self.dimensions.content.height = self.dimensions.content.height.max(mh);
2502        }
2503
2504        // aspect-ratio: W/H。height が auto なら width から height を補完、
2505        // 逆に width が auto かつ height 明示なら height から width を補完する。
2506        if let Some((num, den)) = aspect_ratio_val.as_deref().and_then(parse_aspect_ratio) {
2507            let width_is_auto = matches!(
2508                parse_length_value(width_val.clone()),
2509                Some(LengthValue::Auto) | None
2510            );
2511            let height_is_auto = parse_length_value(height_val.clone()).is_none();
2512            if height_is_auto {
2513                // width 確定 → height = width * den / num
2514                self.dimensions.content.height =
2515                    round_f32_to_i32(self.dimensions.content.width as f32 * den / num).max(1);
2516            } else if width_is_auto {
2517                // height 明示・width auto → width = height * num / den(min/max-width で後段クランプ済みだが
2518                // ここで上書きするため、再度 min/max-width 制約を適用する)。
2519                let h = self.dimensions.content.height as f32;
2520                let mut w = round_f32_to_i32(h * num / den).max(1);
2521                if let Some(min_w) = parse_length_value(min_width_val.clone())
2522                    .and_then(|v| resolve_length_value(v, available_width))
2523                {
2524                    w = w.max(min_w);
2525                }
2526                if let Some(max_w) = parse_length_value(max_width_val.clone())
2527                    .and_then(|v| resolve_length_value(v, available_width))
2528                {
2529                    w = w.min(max_w);
2530                }
2531                self.dimensions.content.width = w.max(1);
2532            }
2533        }
2534
2535        // text-align の後処理: center / right でインライン子要素のx座標を調整
2536        // text-align 未指定時は `direction` に応じた既定値(初期値 `start` の簡易解決)を使う:
2537        // rtl なら right 相当、それ以外(既定 ltr)は従来どおり left(無指定=無調整)。
2538        let (text_align, direction, text_align_last) = match &self.box_type {
2539            BoxType::BlockNode(node) | BoxType::TableCellNode(node) => (
2540                node.value("text-align").unwrap_or_default(),
2541                node.value("direction").unwrap_or_default(),
2542                node.value("text-align-last").unwrap_or_default(),
2543            ),
2544            _ => (String::new(), String::new(), String::new()),
2545        };
2546        let is_rtl = direction.trim().eq_ignore_ascii_case("rtl");
2547        let mut ta = text_align.trim().to_lowercase();
2548        if ta.is_empty() && is_rtl {
2549            ta = String::from("right");
2550        }
2551        // `start`/`end`(CSS Logical Properties)は `direction` に応じて left/right へ
2552        // 解決する(`float`/`clear` の `inline-start`/`inline-end` と同じ考え方)。
2553        ta = match ta.as_str() {
2554            "start" => String::from(if is_rtl { "right" } else { "left" }),
2555            "end" => String::from(if is_rtl { "left" } else { "right" }),
2556            _ => ta,
2557        };
2558        // text-align-last: 最終行のみ text-align とは別の値で揃える(justify と組み合わせて
2559        // 最終行だけ left/center/right にする用途向け)。`auto`/未指定/`justify`(非対応)は
2560        // 最終行も通常の `ta` に従う。`start`/`end` も同様に direction で解決する。
2561        let ta_last_resolved = match text_align_last.trim().to_lowercase().as_str() {
2562            "start" => String::from(if is_rtl { "right" } else { "left" }),
2563            "end" => String::from(if is_rtl { "left" } else { "right" }),
2564            other => String::from(other),
2565        };
2566        let ta_last_effective = if matches!(ta_last_resolved.as_str(), "left" | "center" | "right")
2567        {
2568            Some(ta_last_resolved)
2569        } else {
2570            None
2571        };
2572        if ta == "center" || ta == "right" || ta == "justify" || ta_last_effective.is_some() {
2573            let content_x = self.dimensions.content.x;
2574            let content_w = self.dimensions.content.width.max(1);
2575            let n = self.children.len();
2576
2577            // インライン子要素のy座標を集めてラインを識別
2578            let mut line_ys: Vec<i32> = Vec::new();
2579            for ci in 0..n {
2580                match self.children[ci].box_type {
2581                    BoxType::InlineNode(_) | BoxType::InlineBlockNode(_) => {
2582                        let cy = self.children[ci].dimensions.content.y;
2583                        if !line_ys.contains(&cy) {
2584                            line_ys.push(cy);
2585                        }
2586                    }
2587                    _ => {}
2588                }
2589            }
2590            let last_line_y = line_ys.iter().copied().max();
2591
2592            for line_y in line_ys {
2593                // そのラインの子要素インデックスと合計幅を計算
2594                let mut line_indices = Vec::new();
2595                let mut total_w = 0i32;
2596                for ci in 0..n {
2597                    match self.children[ci].box_type {
2598                        BoxType::InlineNode(_) | BoxType::InlineBlockNode(_)
2599                            if self.children[ci].dimensions.content.y == line_y =>
2600                        {
2601                            line_indices.push(ci);
2602                            total_w += self.children[ci].dimensions.content.width;
2603                        }
2604                        _ => {}
2605                    }
2606                }
2607
2608                let is_last_line = last_line_y == Some(line_y);
2609                let effective_ta = if is_last_line {
2610                    ta_last_effective.as_deref().unwrap_or(if ta == "justify" {
2611                        if is_rtl { "right" } else { "left" }
2612                    } else {
2613                        ta.as_str()
2614                    })
2615                } else {
2616                    ta.as_str()
2617                };
2618
2619                if effective_ta == "justify" {
2620                    let k = line_indices.len();
2621                    if k > 1 {
2622                        let remaining = (content_w - total_w).max(0);
2623                        for (i, &ci) in line_indices.iter().enumerate() {
2624                            let shift = (remaining * i as i32) / (k as i32 - 1);
2625                            self.children[ci].dimensions.content.x = content_x
2626                                + shift
2627                                + (self.children[ci].dimensions.content.x - content_x);
2628                        }
2629                    }
2630                } else {
2631                    let offset = match effective_ta {
2632                        "center" => ((content_w - total_w) / 2).max(0),
2633                        "right" => (content_w - total_w).max(0),
2634                        _ => 0,
2635                    };
2636                    if offset > 0 {
2637                        for &ci in &line_indices {
2638                            self.children[ci].dimensions.content.x = content_x
2639                                + offset
2640                                + (self.children[ci].dimensions.content.x - content_x);
2641                        }
2642                    }
2643                }
2644            }
2645        }
2646
2647        // position の後処理(relative/absolute/fixed の配置 + transform)を共有メソッドで適用。
2648        self.apply_positioned_offsets();
2649
2650        // 子孫のために切替えた ABS_CB を復元(positioned 要素のみ切替えていた)。
2651        if let Some(prev) = saved_abs_cb {
2652            abs_cb_set(prev.0, prev.1, prev.2, prev.3);
2653        }
2654    }
2655
2656    /// avail_hint: 包含ブロックのコンテンツ幅。テキスト折り返し計測に使う。
2657    /// (以前は固定 800px フォールバックだったため、実コンテナ幅と食い違い
2658    ///   layout の行数 < flatten の行数 となって後続要素と重なるバグがあった)
2659    /// 内容ベースの寸法を返す。`avail_hint` は折り返しに使える幅。
2660    ///
2661    /// `pure` が真なら**現在の箱の幅を見ない**。内在幅
2662    /// (min-content / max-content)の算出専用の入口で、
2663    /// `block_intrinsic_widths` だけが真を渡す。
2664    ///
2665    /// 内在幅は「今どう組まれているか」に依存してはいけない値だが、
2666    /// この関数は内在幅以外の用途(表の列幅・インライン配置など)でも
2667    /// 呼ばれており、そちらは現在の幅を前提にしている。
2668    /// 【2026-09-05】一度この区別をせずに順序だけ入れ替えたところ、
2669    /// ヒーローの見出しが 2 行に折り返す退行を招いて戻した。
2670    /// 入口を分けることで、内在幅の算出だけを正す。
2671    fn intrinsic_inline_size_ext(&self, avail_hint: i32, pure: bool) -> (i32, i32) {
2672        match &self.box_type {
2673            BoxType::InlineNode(node) | BoxType::InlineBlockNode(node) => {
2674                if let Some(size) = get_replaced_element_size(node) {
2675                    return size;
2676                }
2677                let font_size = node
2678                    .value("font-size")
2679                    .and_then(|v| parse_font_size_u32(&v))
2680                    .unwrap_or(16);
2681                let line_height_val = node.value("line-height");
2682                let line_height = parse_line_height(line_height_val, font_size);
2683
2684                // Check for explicit CSS width/height (e.g., inline-block with width: 200px)
2685                // `width: 50%`等の百分率が以前は完全に無視されており(`LengthValue::Px`
2686                // のみ対応)、`<iframe width="100%">`のようなレガシー属性経由の百分率
2687                // 指定(cascade.rsでCSS `width`へマッピング済み)が正しくサイズに
2688                // 反映されないバグだった。`avail_hint`(包含ブロックのコンテンツ幅)
2689                // 基準で解決する(2026-07-21発見・修正。実サイト www.sugi-lab.net の
2690                // Access ページの地図埋め込みで発覚)。
2691                let css_w = node.value("width").and_then(|v| match parse_length_value(Some(v)) {
2692                    Some(LengthValue::Px(px)) => Some(px),
2693                    Some(LengthValue::Percent(pct)) => {
2694                        Some(round_f32_to_i32(avail_hint as f32 * pct))
2695                    }
2696                    _ => None,
2697                });
2698                let css_h = node
2699                    .value("height")
2700                    .and_then(|v| match parse_length_value(Some(v)) {
2701                        Some(LengthValue::Px(px)) => Some(px),
2702                        _ => None,
2703                    });
2704
2705                match &node.node.node_type {
2706                    NodeType::Text(text) => {
2707                        // white-space: nowrap/pre は折り返さない(1行に収める)。
2708                        // white-space は継承プロパティなのでテキストノード自身の
2709                        // styled node にも祖先から伝播済みの値が入っている。
2710                        let nowrap = matches!(
2711                            node.value("white-space").as_deref(),
2712                            Some("nowrap") | Some("pre")
2713                        );
2714                        let pre_wrap = matches!(
2715                            node.value("white-space").as_deref(),
2716                            Some("pre-wrap") | Some("pre-line")
2717                        );
2718                        let break_all = matches!(
2719                            node.value("word-break").as_deref(),
2720                            Some("break-all")
2721                        ) || matches!(
2722                            node.value("overflow-wrap").as_deref(),
2723                            Some("break-word")
2724                        );
2725                        // 【2026-09-05】`avail_hint` を優先する形へ変えたが、
2726                        // ヒーローの見出しが 2 行に折り返す退行を招いたため戻した。
2727                        //
2728                        // 内在幅は本来「今どう組まれているか」に依存しない値で、
2729                        // 現在の箱の幅を先に見るのは正しくない(min を求めても
2730                        // max を求めても同じ値が返る)。ただしこの関数は
2731                        // 内在幅の算出以外からも呼ばれており、そちらは
2732                        // 現在の幅を前提にしている。直すなら**呼び出し側を
2733                        // 全部見てから**にすること。詳細は `spec/flex_min_size.md`。
2734                        let available = if nowrap {
2735                            i32::MAX / 2
2736                        } else {
2737                            css_w.unwrap_or({
2738                                if pure {
2739                                    // 内在幅の算出。今の箱の幅は見ない。
2740                                    if avail_hint > 0 {
2741                                        avail_hint
2742                                    } else {
2743                                        800
2744                                    }
2745                                } else if self.dimensions.content.width > 0 {
2746                                    self.dimensions.content.width
2747                                } else if avail_hint > 0 {
2748                                    avail_hint
2749                                } else {
2750                                    800
2751                                }
2752                            })
2753                        };
2754                        let ls = parse_letter_spacing_ctx(node.value("letter-spacing").as_deref(), available);
2755                        let ws = parse_word_spacing_ctx(node.value("word-spacing").as_deref(), available);
2756                        let (w, h) = crate::kernel::vector_font::get_vector_string_wrapped_size_ext(
2757                            text,
2758                            font_size,
2759                            available as u32,
2760                            ls,
2761                            ws,
2762                            break_all,
2763                            pre_wrap,
2764                        );
2765                        let lines = h as i32 / (font_size as i32 + 4).max(1);
2766                        (
2767                            css_w.unwrap_or(w as i32),
2768                            css_h.unwrap_or(lines * line_height),
2769                        )
2770                    }
2771                    NodeType::Element { tag_name, .. } => {
2772                        if tag_name == "br" {
2773                            (0, line_height)
2774                        } else {
2775                            let (content_w, content_h) = if self.children.is_empty() {
2776                                (8, line_height)
2777                            } else {
2778                                let mut total_w = 0i32;
2779                                let mut max_h = line_height;
2780                                for child in &self.children {
2781                                    // 【2026-09-05】ここは `pure` を**引き継がない**。
2782                                    //
2783                                    // 引き継ぐと入れ子の要素の内在幅が変わり、
2784                                    // ナビ項目の間隔が潰れる退行が出た
2785                                    // (実測。ロゴの重なりは解消しないまま
2786                                    // 悪化だけした)。理屈の上では引き継ぐのが
2787                                    // 正しいはずで、引き継いで壊れるということは
2788                                    // **どこかが非 pure の値に依存している**。
2789                                    // 直すならその依存を先に突き止めること。
2790                                    let (cw, ch) = child.intrinsic_inline_size(avail_hint);
2791                                    total_w += cw;
2792                                    if ch > max_h {
2793                                        max_h = ch;
2794                                    }
2795                                }
2796                                (total_w, max_h)
2797                            };
2798                            (css_w.unwrap_or(content_w), css_h.unwrap_or(content_h))
2799                        }
2800                    }
2801                }
2802            }
2803            _ => (0, 0),
2804        }
2805    }
2806
2807    /// 従来どおりの入口(現在の箱の幅を見る)。
2808    fn intrinsic_inline_size(&self, avail_hint: i32) -> (i32, i32) {
2809        self.intrinsic_inline_size_ext(avail_hint, false)
2810    }
2811
2812    fn layout_inline(&mut self, containing_block: Dimensions, _block_like: bool) {
2813        self.apply_box_model_styles_with_base(containing_block.content.width);
2814
2815        let (width_val, min_width_val, max_width_val, height_val, min_height_val, max_height_val) =
2816            match &self.box_type {
2817                BoxType::InlineNode(node) | BoxType::InlineBlockNode(node) => (
2818                    node.value("width"),
2819                    node.value("min-width"),
2820                    node.value("max-width"),
2821                    node.value("height"),
2822                    node.value("min-height"),
2823                    node.value("max-height"),
2824                ),
2825                _ => return,
2826            };
2827
2828        let available_width = containing_block.content.width.max(1);
2829        let available_height = containing_block.content.height.max(1);
2830        let (intrinsic_w, intrinsic_h) = self.inline_content_intrinsic_size(available_width);
2831
2832        let mut content_width = match parse_length_value(width_val) {
2833            Some(LengthValue::Auto) | None => intrinsic_w.max(1),
2834            Some(LengthValue::Px(px)) => px.max(1),
2835            Some(LengthValue::Percent(pct)) => {
2836                round_f32_to_i32(available_width as f32 * pct).max(1)
2837            }
2838            Some(LengthValue::MinContent) => {
2839                let (minc, _maxc) = self.block_intrinsic_widths();
2840                minc.max(1)
2841            }
2842            Some(LengthValue::MaxContent) => {
2843                let (_minc, maxc) = self.block_intrinsic_widths();
2844                maxc.max(1)
2845            }
2846            Some(LengthValue::FitContent) => {
2847                let (minc, maxc) = self.block_intrinsic_widths();
2848                available_width.min(maxc).max(minc).max(1)
2849            }
2850            Some(LengthValue::MathExpr(expr)) => {
2851                crate::os_lib::css::eval_css_math(&expr, available_width)
2852                    .unwrap_or(intrinsic_w.max(1))
2853                    .max(1)
2854            }
2855        };
2856        if let Some(min_width) =
2857            parse_length_value(min_width_val).and_then(|v| resolve_length_value(v, available_width))
2858        {
2859            content_width = content_width.max(min_width);
2860        }
2861        if let Some(max_width) =
2862            parse_length_value(max_width_val).and_then(|v| resolve_length_value(v, available_width))
2863        {
2864            content_width = content_width.min(max_width);
2865        }
2866
2867        let mut content_height = match parse_length_value(height_val) {
2868            Some(LengthValue::Auto)
2869            | Some(LengthValue::MinContent)
2870            | Some(LengthValue::MaxContent)
2871            | Some(LengthValue::FitContent)
2872            | None => intrinsic_h.max(1),
2873            Some(LengthValue::Px(px)) => px.max(1),
2874            Some(LengthValue::Percent(pct)) => {
2875                round_f32_to_i32(available_height as f32 * pct).max(1)
2876            }
2877            Some(LengthValue::MathExpr(expr)) => {
2878                crate::os_lib::css::eval_css_math(&expr, available_height)
2879                    .unwrap_or(intrinsic_h.max(1))
2880                    .max(1)
2881            }
2882        };
2883        if let Some(min_height) = parse_length_value(min_height_val)
2884            .and_then(|v| resolve_length_value(v, available_height))
2885        {
2886            content_height = content_height.max(min_height);
2887        }
2888        if let Some(max_height) = parse_length_value(max_height_val)
2889            .and_then(|v| resolve_length_value(v, available_height))
2890        {
2891            content_height = content_height.min(max_height);
2892        }
2893
2894        self.dimensions.content.width = content_width.max(1);
2895        self.dimensions.content.height = content_height.max(1);
2896        self.dimensions.content.x = containing_block.content.x
2897            + self.dimensions.margin.left
2898            + self.dimensions.border.left
2899            + self.dimensions.padding.left;
2900        self.dimensions.content.y = containing_block.content.y
2901            + containing_block.content.height
2902            + self.dimensions.margin.top
2903            + self.dimensions.border.top
2904            + self.dimensions.padding.top;
2905
2906        // 子要素のレイアウト計算を行う。インライン要素(!block_like)であっても、
2907        // 内包するテキストや子インライン要素の座標を設定する必要があるため、常に実行します。
2908        let mut child_flow_x = 0i32;
2909        let mut child_flow_y = 0i32;
2910        let mut line_h = 0i32;
2911        let max_w = self.dimensions.content.width.max(1);
2912        // インライン/インラインブロックの vertical-align(既定 top/baseline は無調整): 1行の
2913        // 全子要素が揃って初めて行の高さ(line_h)が確定するため、行の開始インデックスを覚えておき、
2914        // 改行が確定した時点でその行の子要素だけを事後的に `translate_y` でシフトする。
2915        let mut line_start_idx = 0usize;
2916
2917        let mut i = 0usize;
2918        while i < self.children.len() {
2919            match self.children[i].box_type {
2920                BoxType::BlockNode(_)
2921                | BoxType::FlexNode(_)
2922                | BoxType::GridNode(_)
2923                | BoxType::TableNode(_)
2924                | BoxType::TableRowGroupNode(_)
2925                | BoxType::TableRowNode(_)
2926                | BoxType::TableCellNode(_) => {
2927                    if child_flow_x > 0 {
2928                        self.finalize_inline_line_vertical_align(line_start_idx, i, line_h, max_w);
2929                        child_flow_y += line_h;
2930                        child_flow_x = 0;
2931                        line_h = 0;
2932                    }
2933                    line_start_idx = i + 1;
2934                    let child = &mut self.children[i];
2935                    let mut cb = self.dimensions.clone();
2936                    cb.content.height = child_flow_y;
2937                    child.layout(cb);
2938                    child_flow_y += child.dimensions.margin_box().height;
2939                }
2940                BoxType::InlineNode(_) | BoxType::InlineBlockNode(_) => {
2941                    if self.children[i].is_inline_line_break() {
2942                        self.finalize_inline_line_vertical_align(line_start_idx, i, line_h, max_w);
2943                        child_flow_y += line_h.max(20);
2944                        child_flow_x = 0;
2945                        line_h = 0;
2946                        line_start_idx = i + 1;
2947                        i += 1;
2948                        continue;
2949                    }
2950                    let (child_w, child_h) = self.children[i].intrinsic_inline_size(max_w);
2951                    if child_flow_x > 0 && child_flow_x + child_w > max_w {
2952                        self.finalize_inline_line_vertical_align(line_start_idx, i, line_h, max_w);
2953                        child_flow_y += line_h.max(20);
2954                        child_flow_x = 0;
2955                        line_h = 0;
2956                        line_start_idx = i;
2957                    }
2958                    let child = &mut self.children[i];
2959                    child.dimensions.content.x = self.dimensions.content.x + child_flow_x;
2960                    child.dimensions.content.y = self.dimensions.content.y + child_flow_y;
2961                    child.dimensions.content.width = child_w;
2962                    child.dimensions.content.height = child_h;
2963
2964                    // 子要素(InlineNode など)に対して再帰的にレイアウトを呼び出して、さらにその中の座標を決定する。
2965                    // 注意: 子自身が InlineNode/InlineBlockNode の場合、渡した cb.content.height
2966                    // (ここでは子自身の高さ)を子の layout_inline が「Y フローカーソル」として
2967                    // 自身の content.y に加算してしまい、確定済みのスロット位置を上書きしてしまう
2968                    // (layout_block の inline 子ループに既存の同種ワークアラウンドと同じ理由)。
2969                    // そのため子のレイアウト後に確定済みのスロット位置へ再設定する。
2970                    let cb = child.dimensions.clone();
2971                    child.layout(cb);
2972                    child.dimensions.content.x = self.dimensions.content.x + child_flow_x;
2973                    child.dimensions.content.y = self.dimensions.content.y + child_flow_y;
2974                    child.dimensions.content.width = child_w;
2975                    child.dimensions.content.height = child_h;
2976
2977                    child_flow_x += child_w;
2978                    if child_h > line_h {
2979                        line_h = child_h;
2980                    }
2981                    i += 1;
2982                }
2983                BoxType::AnonymousBlock => {
2984                    i += 1;
2985                }
2986            }
2987        }
2988        // 最終行(末尾に改行/ブロック要素が無いまま終わる行)の vertical-align を適用する。
2989        self.finalize_inline_line_vertical_align(line_start_idx, self.children.len(), line_h, max_w);
2990    }
2991
2992    /// インラインの1行ぶん(`[start, end)` の子要素)に `vertical-align` を適用する。
2993    /// **簡略化**: `top`/`baseline`/`sub`/`super`/`text-top` はいずれも無調整(top相当)、
2994    /// `middle` は行の中央、`bottom`/`text-bottom` は行の下端に揃える。table セルの
2995    /// vertical-align(内容のみシフト)とは異なり、ここでは子要素自身(背景/境界線含む)を
2996    /// まるごとシフトする(インラインボックス自体の行内配置を変えるのが目的のため)。
2997    fn finalize_inline_line_vertical_align(&mut self, start: usize, end: usize, line_h: i32, max_w: i32) {
2998        if line_h <= 0 || end <= start {
2999            return;
3000        }
3001        for idx in start..end {
3002            let Some(child) = self.children.get_mut(idx) else {
3003                continue;
3004            };
3005            if !matches!(
3006                child.box_type,
3007                BoxType::InlineNode(_) | BoxType::InlineBlockNode(_)
3008            ) {
3009                continue;
3010            }
3011            let va = box_style_value(child, "vertical-align").unwrap_or_default();
3012            let offset = match va.trim() {
3013                "middle" => {
3014                    let (_, child_h) = child.intrinsic_inline_size(max_w);
3015                    (line_h - child_h) / 2
3016                }
3017                "bottom" | "text-bottom" => {
3018                    let (_, child_h) = child.intrinsic_inline_size(max_w);
3019                    line_h - child_h
3020                }
3021                _ => 0,
3022            };
3023            if offset != 0 {
3024                child.translate_y(offset);
3025            }
3026        }
3027    }
3028
3029    /// min-width/max-width 等のサイジング値を解決する。px/% は base 基準、
3030    /// `min-content`/`max-content`/`fit-content` は内容計測で解決。auto/未指定は None。
3031    fn resolve_sizing_value(&self, value: Option<String>, base: i32) -> Option<i32> {
3032        match parse_length_value(value)? {
3033            LengthValue::Auto => None,
3034            LengthValue::Px(px) => Some(px),
3035            LengthValue::Percent(pct) => Some(round_f32_to_i32(base as f32 * pct)),
3036            LengthValue::MinContent => Some(self.block_intrinsic_widths().0),
3037            LengthValue::MaxContent => Some(self.block_intrinsic_widths().1),
3038            LengthValue::FitContent => {
3039                let (minc, maxc) = self.block_intrinsic_widths();
3040                Some(maxc.min(base.max(0)).max(minc))
3041            }
3042            LengthValue::MathExpr(expr) => crate::os_lib::css::eval_css_math(&expr, base),
3043        }
3044    }
3045
3046    /// ブロック要素の内容ベース幅を返す: (min_content, max_content)。
3047    /// max_content = 折返しなしの最大行幅、min_content = 最大限折返したときの最小幅。
3048    /// 直下の子(inline/text/block)を計測して集約する。
3049    fn block_intrinsic_widths(&self) -> (i32, i32) {
3050        // 自身が inline/inline-block を直接表すなら、その計測を使う。
3051        if matches!(
3052            self.box_type,
3053            BoxType::InlineNode(_) | BoxType::InlineBlockNode(_)
3054        ) {
3055            let max_c = self.intrinsic_inline_size_ext(1_000_000, true).0;
3056            let min_c = self.intrinsic_inline_size_ext(1, true).0;
3057            return (min_c.max(0), max_c.max(min_c).max(0));
3058        }
3059        // 【2026-09-04】横並びの flex コンテナは、子を**足し合わせる**。
3060        //
3061        // 従来はブロックとして「子は縦に積む」前提で最大値を取っていた。
3062        // その結果、ナビの `ul`(`display:flex` の 8 項目)の max-content が
3063        // 最も広い 1 項目ぶん(80px)になり、本来の 656px と大きく食い違って
3064        // いた。親の flex は基準幅にこの値を使うので、ナビが極端に小さく
3065        // 見積もられ、`justify-content:space-between` で右へ飛んでいた。
3066        //
3067        // 折り返さない(`flex-wrap: nowrap`、既定)横並びなら、
3068        // min-content も max-content も**子の総和**になる。
3069        if let BoxType::FlexNode(node) = &self.box_type {
3070            let dir = node
3071                .value("flex-direction")
3072                .unwrap_or_else(|| alloc::string::String::from("row"))
3073                .trim()
3074                .to_lowercase();
3075            let is_row = !dir.starts_with("column");
3076            let wraps = node
3077                .value("flex-wrap")
3078                .map(|w| w.trim().to_lowercase().starts_with("wrap"))
3079                .unwrap_or(false);
3080            if is_row {
3081                let mut sum_min = 0i32;
3082                let mut sum_max = 0i32;
3083                let mut max_min = 0i32;
3084                for child in &self.children {
3085                    let (cmin, cmax) = child.block_intrinsic_widths();
3086                    sum_min += cmin;
3087                    sum_max += cmax;
3088                    max_min = max_min.max(cmin);
3089                }
3090                let (rg, cg) = parse_flex_gap(node, self.dimensions.content.width);
3091                let _ = rg;
3092                let n = self.children.len() as i32;
3093                let gaps = if n > 1 { cg * (n - 1) } else { 0 };
3094                // 折り返す指定なら、最小は 1 項目ぶんまで詰められる。
3095                let min_c = if wraps { max_min } else { sum_min + gaps };
3096                return (min_c.max(0), (sum_max + gaps).max(min_c).max(0));
3097            }
3098        }
3099
3100        // ブロックは子を集約。インライン子は同一行に並ぶため max は加算・min は最大。
3101        // ブロック子は縦に積むため両者とも最大。
3102        let mut inline_max = 0i32; // 現在のインライン連なりの max-content 合計
3103        let mut overall_max = 0i32;
3104        let mut overall_min = 0i32;
3105        for child in &self.children {
3106            match child.box_type {
3107                BoxType::InlineNode(_) | BoxType::InlineBlockNode(_) => {
3108                    // intrinsic_inline_size は inline-block の明示 width を尊重する。
3109                    let cmax = child.intrinsic_inline_size_ext(1_000_000, true).0;
3110                    let cmin = child.intrinsic_inline_size_ext(1, true).0;
3111                    inline_max += cmax;
3112                    overall_min = overall_min.max(cmin);
3113                }
3114                _ => {
3115                    overall_max = overall_max.max(inline_max);
3116                    inline_max = 0;
3117                    let (cmin, cmax) = child.block_intrinsic_widths();
3118                    overall_min = overall_min.max(cmin);
3119                    overall_max = overall_max.max(cmax);
3120                }
3121            }
3122        }
3123        overall_max = overall_max.max(inline_max);
3124        (overall_min.max(0), overall_max.max(overall_min).max(0))
3125    }
3126
3127    fn inline_content_intrinsic_size(&self, available_width: i32) -> (i32, i32) {
3128        match &self.box_type {
3129            BoxType::InlineNode(node) | BoxType::InlineBlockNode(node) => {
3130                if let Some(size) = get_replaced_element_size(node) {
3131                    return size;
3132                }
3133                match &node.node.node_type {
3134                    NodeType::Text(text) => {
3135                        let font_size = node
3136                            .value("font-size")
3137                            .and_then(|v| parse_font_size_u32(&v))
3138                            .unwrap_or(16);
3139                        let line_height_val = node.value("line-height");
3140                        let line_height = parse_line_height(line_height_val, font_size);
3141                        let pre_wrap = matches!(
3142                            node.value("white-space").as_deref(),
3143                            Some("pre-wrap") | Some("pre-line")
3144                        );
3145                        let break_all = matches!(
3146                            node.value("word-break").as_deref(),
3147                            Some("break-all")
3148                        ) || matches!(
3149                            node.value("overflow-wrap").as_deref(),
3150                            Some("break-word")
3151                        );
3152                        let (w, h) = crate::kernel::vector_font::get_vector_string_wrapped_size_ext(
3153                            text,
3154                            font_size,
3155                            available_width.max(1) as u32,
3156                            0, // ls
3157                            0, // ws
3158                            break_all,
3159                            pre_wrap,
3160                        );
3161                        let lines = h as i32 / (font_size as i32 + 4).max(1);
3162                        (w as i32, lines * line_height)
3163                    }
3164                    NodeType::Element { .. } => {
3165                        let mut max_line_w = 0i32;
3166                        let mut total_h = 0i32;
3167                        let mut current_line_w = 0i32;
3168                        let mut current_line_h = 0i32;
3169                        for child in &self.children {
3170                            if child.is_inline_line_break() {
3171                                if current_line_w > max_line_w {
3172                                    max_line_w = current_line_w;
3173                                }
3174                                total_h += current_line_h.max(20);
3175                                current_line_w = 0;
3176                                current_line_h = 0;
3177                                continue;
3178                            }
3179                            let (cw, ch) = child.intrinsic_inline_size(available_width.max(1));
3180                            if current_line_w > 0 && current_line_w + cw > available_width.max(1) {
3181                                if current_line_w > max_line_w {
3182                                    max_line_w = current_line_w;
3183                                }
3184                                total_h += current_line_h.max(20);
3185                                current_line_w = 0;
3186                                current_line_h = 0;
3187                            }
3188                            current_line_w += cw;
3189                            if ch > current_line_h {
3190                                current_line_h = ch;
3191                            }
3192                        }
3193                        if current_line_w > max_line_w {
3194                            max_line_w = current_line_w;
3195                        }
3196                        total_h += current_line_h.max(20);
3197                        if total_h == 0 {
3198                            let font_size = node
3199                                .value("font-size")
3200                                .and_then(|v| parse_font_size_u32(&v))
3201                                .unwrap_or(16);
3202                            return (8, (font_size as i32 + 4).max(12));
3203                        }
3204                        (max_line_w.max(1), total_h.max(1))
3205                    }
3206                }
3207            }
3208            _ => (0, 0),
3209        }
3210    }
3211
3212    fn is_inline_line_break(&self) -> bool {
3213        match &self.box_type {
3214            BoxType::InlineNode(node) | BoxType::InlineBlockNode(node) => {
3215                matches!(&node.node.node_type, NodeType::Element { tag_name, .. } if tag_name == "br")
3216            }
3217            _ => false,
3218        }
3219    }
3220
3221    fn apply_box_model_styles_with_base(&mut self, containing_width: i32) {
3222        let node = match &self.box_type {
3223            BoxType::BlockNode(n) => n,
3224            BoxType::InlineBlockNode(n) => n,
3225            _ => return,
3226        };
3227
3228        apply_box_value(
3229            node.value("margin"),
3230            &mut self.dimensions.margin,
3231            containing_width,
3232        );
3233        apply_box_value(
3234            node.value("padding"),
3235            &mut self.dimensions.padding,
3236            containing_width,
3237        );
3238
3239        apply_edge_value(
3240            node.value("margin-top"),
3241            &mut self.dimensions.margin.top,
3242            containing_width,
3243        );
3244        apply_edge_value(
3245            node.value("margin-right"),
3246            &mut self.dimensions.margin.right,
3247            containing_width,
3248        );
3249        apply_edge_value(
3250            node.value("margin-bottom"),
3251            &mut self.dimensions.margin.bottom,
3252            containing_width,
3253        );
3254        apply_edge_value(
3255            node.value("margin-left"),
3256            &mut self.dimensions.margin.left,
3257            containing_width,
3258        );
3259
3260        apply_edge_value(
3261            node.value("padding-top"),
3262            &mut self.dimensions.padding.top,
3263            containing_width,
3264        );
3265        apply_edge_value(
3266            node.value("padding-right"),
3267            &mut self.dimensions.padding.right,
3268            containing_width,
3269        );
3270        apply_edge_value(
3271            node.value("padding-bottom"),
3272            &mut self.dimensions.padding.bottom,
3273            containing_width,
3274        );
3275        apply_edge_value(
3276            node.value("padding-left"),
3277            &mut self.dimensions.padding.left,
3278            containing_width,
3279        );
3280
3281        // border-width / border は単純に幅だけ扱う
3282        apply_box_value(
3283            node.value("border-width"),
3284            &mut self.dimensions.border,
3285            containing_width,
3286        );
3287        if let Some(border) = node.value("border") {
3288            if let Some(px) = parse_px_like(&border) {
3289                self.dimensions.border.top = px;
3290                self.dimensions.border.right = px;
3291                self.dimensions.border.bottom = px;
3292                self.dimensions.border.left = px;
3293            }
3294        }
3295        apply_edge_value(
3296            node.value("border-top-width"),
3297            &mut self.dimensions.border.top,
3298            containing_width,
3299        );
3300        apply_edge_value(
3301            node.value("border-right-width"),
3302            &mut self.dimensions.border.right,
3303            containing_width,
3304        );
3305        apply_edge_value(
3306            node.value("border-bottom-width"),
3307            &mut self.dimensions.border.bottom,
3308            containing_width,
3309        );
3310        apply_edge_value(
3311            node.value("border-left-width"),
3312            &mut self.dimensions.border.left,
3313            containing_width,
3314        );
3315    }
3316
3317    fn apply_box_model_styles(&mut self) {
3318        self.apply_box_model_styles_with_base(0);
3319    }
3320}
3321