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