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        trim_leading_space: false,
67    };
68
69    // flex/grid/table 系の直下の空白だけの文字は箱を作らない(CSS Flexbox §4・Grid §6・
70    // Tables §3.4。パーサが要素間の空白を残すようになった 2026-10-09 から必要)。
71    let drops_space = match &layout_box.box_type {
72        BoxType::FlexNode(_)
73        | BoxType::GridNode(_)
74        | BoxType::TableNode(_)
75        | BoxType::TableRowGroupNode(_)
76        | BoxType::TableRowNode(_) => true,
77        BoxType::InlineBlockNode(n) => {
78            matches!(resolved_display(n), "inline-flex" | "inline-grid")
79        }
80        _ => false,
81    };
82    for child in &node.children {
83        if drops_space && matches!(&child.node.node_type, NodeType::Text(t) if t.trim().is_empty()) {
84            continue;
85        }
86        if resolved_display(child) != "none" {
87            // visibility: hidden — include in layout but mark invisible (handled in web_engine)
88            let mut b = build_layout_tree(child);
89            // flex/grid 直下の文字は匿名アイテム(ブロック)の先頭なので先頭空白を落とす。
90            if drops_space && matches!(&child.node.node_type, NodeType::Text(_)) {
91                b.trim_leading_space = true;
92            }
93            layout_box.children.push(b);
94        }
95    }
96
97    // テーブルの行グループ (thead/tbody/tfoot) は透過し、行 (tr) を table の直接の子へ引き上げる。
98    // これにより layout_table は「table の子 = 行」という単純な前提で計算できる。
99    if matches!(layout_box.box_type, BoxType::TableNode(_)) {
100        let mut flattened: Vec<LayoutBox<'a>> = Vec::new();
101        for child in layout_box.children.drain(..) {
102            if matches!(child.box_type, BoxType::TableRowGroupNode(_)) {
103                for row in child.children {
104                    flattened.push(row);
105                }
106            } else {
107                flattened.push(child);
108            }
109        }
110        layout_box.children = flattened;
111    }
112
113    layout_box
114}
115
116/// スタイルから (margin, padding, border) を解決する(`%` は `containing_width` 基準)。
117/// `init` の現在値を初期値にして、指定のある辺だけ上書きする(従来の
118/// `apply_box_model_styles_with_base` と同じ意味。内在幅の計測でも使うため切り出した)。
119pub(crate) fn style_box_edges(
120    node: &StyledNode,
121    containing_width: i32,
122    init: &Dimensions,
123) -> (EdgeSizes, EdgeSizes, EdgeSizes) {
124    // 覚え書き(`StyledNode::edges_cache`)。同じ包含幅・初期値なら前回の結果を返す。
125    let pack = |m: &EdgeSizes, p: &EdgeSizes, b: &EdgeSizes| -> [i32; 12] {
126        [m.left, m.right, m.top, m.bottom, p.left, p.right, p.top, p.bottom, b.left, b.right, b.top, b.bottom]
127    };
128    let key = pack(&init.margin, &init.padding, &init.border);
129    if let Ok(cache) = node.edges_cache.try_borrow() {
130        if let Some((_, _, v)) = cache.iter().find(|(cw, k, _)| *cw == containing_width && *k == key) {
131            let e = |i: usize| EdgeSizes { left: v[i], right: v[i + 1], top: v[i + 2], bottom: v[i + 3] };
132            return (e(0), e(4), e(8));
133        }
134    }
135    let r = style_box_edges_uncached(node, containing_width, init);
136    if let Ok(mut cache) = node.edges_cache.try_borrow_mut() {
137        if cache.len() >= 8 {
138            cache.remove(0);
139        }
140        cache.push((containing_width, key, pack(&r.0, &r.1, &r.2)));
141    }
142    r
143}
144
145fn style_box_edges_uncached(
146    node: &StyledNode,
147    containing_width: i32,
148    init: &Dimensions,
149) -> (EdgeSizes, EdgeSizes, EdgeSizes) {
150    let mut margin = init.margin.clone();
151    let mut padding = init.padding.clone();
152    let mut border = init.border.clone();
153    // 【2026-10-04】`em` は要素自身の文字サイズ基準(CSS Values 4 §6.1.1)。
154    // 下の `apply_*` → `parse_px_like` は `em` を常に 16px で換算するので、
155    // その前に要素の文字サイズで px へ置き換えておく
156    // (smarthome.htm の `h1{font-size:2.5rem; margin-top:1em}` が 16px になっていた)。
157    let font_px = node
158        .value("font-size")
159        .and_then(|v| parse_font_size_u32(&v))
160        .unwrap_or(16) as f32;
161    // `em` を含むときだけ換算する(含まない値は写すだけ。2026-10-18 レイアウトの最頻出経路の削減)。
162    let val = |name: &str| -> Option<String> {
163        node.value_ref(name).map(|s| {
164            if s.as_bytes().windows(2).any(|w| w.eq_ignore_ascii_case(b"em")) {
165                crate::os_lib::web_engine::css_units::resolve_em_lengths(s, font_px)
166            } else {
167                String::from(s)
168            }
169        })
170    };
171    apply_box_value(
172        val("margin"),
173        &mut margin,
174        containing_width,
175    );
176    apply_box_value(
177        val("padding"),
178        &mut padding,
179        containing_width,
180    );
181
182    apply_edge_value(
183        val("margin-top"),
184        &mut margin.top,
185        containing_width,
186    );
187    apply_edge_value(
188        val("margin-right"),
189        &mut margin.right,
190        containing_width,
191    );
192    apply_edge_value(
193        val("margin-bottom"),
194        &mut margin.bottom,
195        containing_width,
196    );
197    apply_edge_value(
198        val("margin-left"),
199        &mut margin.left,
200        containing_width,
201    );
202
203    apply_edge_value(
204        val("padding-top"),
205        &mut padding.top,
206        containing_width,
207    );
208    apply_edge_value(
209        val("padding-right"),
210        &mut padding.right,
211        containing_width,
212    );
213    apply_edge_value(
214        val("padding-bottom"),
215        &mut padding.bottom,
216        containing_width,
217    );
218    apply_edge_value(
219        val("padding-left"),
220        &mut padding.left,
221        containing_width,
222    );
223
224    // border-width / border は単純に幅だけ扱う
225    apply_box_value(
226        val("border-width"),
227        &mut border,
228        containing_width,
229    );
230    if let Some(border_val) = val("border") {
231        if let Some(px) = parse_px_like(&border_val) {
232            border.top = px;
233            border.right = px;
234            border.bottom = px;
235            border.left = px;
236        }
237    }
238    apply_edge_value(
239        val("border-top-width"),
240        &mut border.top,
241        containing_width,
242    );
243    apply_edge_value(
244        val("border-right-width"),
245        &mut border.right,
246        containing_width,
247    );
248    apply_edge_value(
249        val("border-bottom-width"),
250        &mut border.bottom,
251        containing_width,
252    );
253    apply_edge_value(
254        val("border-left-width"),
255        &mut border.left,
256        containing_width,
257    );
258    (margin, padding, border)
259}
260
261/// 子の内在幅(min/max-content)の寄与を、子自身の `min-width`/`max-width`(px)で挟む
262/// (CSS Sizing 3: 寄与は min/max-width で制限される)。`%` は基準が無いので無視。
263/// 【2026-09-26】英語版ヒーローで、`max-width:800px` の hero-content の寄与が見出しの全長
264/// (数千 px)のままになり、flex 項目が中央寄せで左へ大きくはみ出していた。
265/// `content_based`: `cmin`/`cmax` が content 幅か(明示の border-box 幅なら false)。
266pub(crate) fn clamp_contribution(b: &LayoutBox, cmin: i32, cmax: i32, content_based: bool) -> (i32, i32) {
267    let inner = if content_based { border_box_inner_w(b) } else { 0 };
268    let px = |prop: &str| match parse_length_value(box_style_value(b, prop)) {
269        Some(LengthValue::Px(v)) => Some((v - inner).max(0)),
270        _ => None,
271    };
272    let (mut lo, mut hi) = (cmin, cmax);
273    if let Some(mx) = px("max-width") {
274        lo = lo.min(mx);
275        hi = hi.min(mx);
276    }
277    if let Some(mn) = px("min-width") {
278        lo = lo.max(mn);
279        hi = hi.max(mn);
280    }
281    (lo, hi.max(lo))
282}
283
284/// `box-sizing:border-box` の箱なら左右の padding+border、それ以外は 0。
285///
286/// `min-width`/`max-width` の長さは box-sizing に従って border-box を測る(CSS Sizing 3 §3.1)。
287/// content 幅と比べる前にこの分を引く。【2026-10-17】`*{box-sizing:border-box}` のサイトで
288/// `max-width:800px; padding:0 20px` の箱の content が 760 でなく 800 になり、英語版ヒーロー
289/// 見出しの折り返し位置が Chrome と違っていた(sugi-lab.net/index_e.html)。
290pub(crate) fn border_box_inner_w(b: &LayoutBox) -> i32 {
291    let node = match &b.box_type {
292        BoxType::BlockNode(n)
293        | BoxType::InlineBlockNode(n)
294        | BoxType::FlexNode(n)
295        | BoxType::GridNode(n)
296        | BoxType::TableNode(n) => *n,
297        BoxType::InlineNode(n) if b.is_replaced_inline() => *n,
298        _ => return 0,
299    };
300    if !node.value("box-sizing").is_some_and(|v| v.trim() == "border-box") {
301        return 0;
302    }
303    let (_, p, bd) = style_box_edges(node, 0, &Dimensions::default());
304    (p.left + p.right + bd.left + bd.right).max(0)
305}
306
307/// `min-width`/`max-width` の値が長さ(px・%・calc)か。`min-content` 等の内容基準の
308/// キーワードは content 幅そのものなので border-box の換算をしない。
309fn is_length_sizing(v: &Option<String>) -> bool {
310    matches!(
311        parse_length_value(v.clone()),
312        Some(LengthValue::Px(_) | LengthValue::Percent(_) | LengthValue::MathExpr(_))
313    )
314}
315
316/// 内在幅の合計へ入れる、子の横方向の非 content 分(margin + border + padding)。
317/// 【2026-09-26】従来は子の content 幅だけを足しており、入れ子の flex や
318/// shrink-to-fit の箱が子の padding 分だけ狭く見積もられていた。
319/// 明示幅が `box-sizing:border-box` のときは padding/border が幅に含まれるので margin だけ。
320/// 箱の左右の margin(内在幅の計測用。`%` は 0、負は 0 として扱う)。
321fn inline_h_margins(b: &LayoutBox) -> i32 {
322    let node = match &b.box_type {
323        BoxType::InlineBlockNode(n) | BoxType::InlineNode(n) => *n,
324        _ => return 0,
325    };
326    let (m, _, _) = style_box_edges(node, 0, &Dimensions::default());
327    m.left.max(0) + m.right.max(0)
328}
329
330pub(crate) fn intrinsic_h_edges(b: &LayoutBox, explicit_width: bool) -> i32 {
331    let node = match &b.box_type {
332        BoxType::BlockNode(n)
333        | BoxType::InlineBlockNode(n)
334        | BoxType::FlexNode(n)
335        | BoxType::GridNode(n)
336        | BoxType::TableNode(n) => *n,
337        BoxType::InlineNode(n) if b.is_replaced_inline() => *n,
338        _ => return 0,
339    };
340    // 内在幅の計測中は包含幅が未確定なので `%` は 0 扱い。
341    let (m, p, bd) = style_box_edges(node, 0, &Dimensions::default());
342    let margins = m.left.max(0) + m.right.max(0);
343    let border_box = explicit_width
344        && node.value("box-sizing").map(|v| v.trim() == "border-box").unwrap_or(false);
345    if border_box {
346        margins
347    } else {
348        margins + p.left + p.right + bd.left + bd.right
349    }
350}
351
352/// 文字ノードの書体指定(font-family のリスト, ウェイト)。描画側(`web_engine` の
353/// `final_weight`)と同じく `font-weight` を数値にする(2026-10-03。従来は太字か否かの 2 値)。
354fn text_font_of(node: &StyledNode) -> (String, u16) {
355    let fam = node.value("font-family").map(|v| String::from(v.trim())).unwrap_or_default();
356    let weight = node
357        .value("font-weight")
358        .map(|v| crate::kernel::font_select::parse_font_weight(&v))
359        .unwrap_or(400);
360    (fam, weight)
361}
362
363/// `::before`/`::after` の内容の大きさ(幅, 行の高さ)。本文が空白だけで子要素も無い要素に限る
364/// (描画側 `web_engine` が疑似要素を要素の箱に描くのと同じ対象。本文がある要素では足さない)。
365///
366/// 【2026-10-03】レイアウトが疑似要素の内容を見ておらず、Font Awesome の `<i class="fas fa-home">`
367/// が幅 0・高さ 0 の箱になっていた。flex の中央寄せがその点を中心に置き、描画側は箱の左端から
368/// アイコンを描くため、円の中心から右下へずれていた(sugi-lab.net/profile.htm)。
369fn pseudo_content_size(node: &StyledNode) -> Option<(i32, i32)> {
370    let only_ws = node.node.children.iter().all(|c| match &c.node_type {
371        NodeType::Text(t) => t.trim().is_empty(),
372        _ => false,
373    });
374    if !only_ws {
375        return None;
376    }
377    let part = |which: &str| -> String {
378        if node.value(&alloc::format!("x-{which}-display")).as_deref() == Some("none") {
379            return String::new();
380        }
381        node.value(&alloc::format!("x-{which}-content")).unwrap_or_default()
382    };
383    let mut text = part("before");
384    text.push_str(&part("after"));
385    if text.is_empty() {
386        return None;
387    }
388    let font_size = node
389        .value("font-size")
390        .and_then(|v| parse_font_size_u32(&v))
391        .unwrap_or(16);
392    let fam = node
393        .value("x-before-font-family")
394        .or_else(|| node.value("font-family"))
395        .map(|v| String::from(v.trim()))
396        .unwrap_or_default();
397    let (_, weight) = text_font_of(node);
398    let (w, _) = crate::kernel::vector_font::get_vector_string_wrapped_size_family(
399        &text, font_size, 1_000_000, 0, 0, false, false, false, &fam, weight,
400    );
401    let line_h = parse_line_height(node.value("line-height"), font_size);
402    Some((w as i32, line_h.max(1)))
403}
404
405impl<'a> LayoutBox<'a> {
406    /// 明示の px 高さを content 高に換算して返す(`box-sizing:border-box` なら
407    /// 縦の padding/border を引く)。明示 px でなければ None。
408    /// 【2026-09-26】flex コンテナが border-box の明示高さをそのまま content 高に使い、
409    /// padding 分だけ背が高くなっていた(`*{box-sizing:border-box}` のサイト全般)。
410    fn explicit_content_height_px(&self, height_val: Option<String>) -> Option<i32> {
411        let Some(LengthValue::Px(px)) = parse_length_value(height_val) else {
412            return None;
413        };
414        let border_box = box_style_value(self, "box-sizing")
415            .map(|v| v.trim() == "border-box")
416            .unwrap_or(false);
417        if border_box {
418            let d = &self.dimensions;
419            Some((px - d.padding.top - d.padding.bottom - d.border.top - d.border.bottom).max(0))
420        } else {
421            Some(px.max(0))
422        }
423    }
424
425    /// 横方向の content 以外(margin + border + padding の左右合計)。
426    fn h_non_content(&self) -> i32 {
427        let d = &self.dimensions;
428        d.margin.left + d.margin.right + d.border.left + d.border.right + d.padding.left + d.padding.right
429    }
430
431    /// 行内で content の外側に占める (左, 右, 上, 下) の幅(margin+border+padding)。
432    /// inline-block(inline-flex 含む)のみ。素の inline 要素は box model 自体が
433    /// 未適用(常に 0)なので従来どおり 0。レイアウト後に呼ぶこと。
434    ///
435    /// 【2026-10-09/2026-10-13】置換要素(`img` 等)は縦・横の margin+border+padding も
436    /// 行内で占める(CSS 2.1 §10.3.2 / §10.6.2)。
437    fn inline_block_edges(&self) -> (i32, i32, i32, i32) {
438        if self.is_replaced_inline() || matches!(self.box_type, BoxType::InlineBlockNode(_)) {
439            let d = &self.dimensions;
440            return (
441                d.margin.left + d.border.left + d.padding.left,
442                d.margin.right + d.border.right + d.padding.right,
443                d.margin.top + d.border.top + d.padding.top,
444                d.margin.bottom + d.border.bottom + d.padding.bottom,
445            );
446        }
447        (0, 0, 0, 0)
448    }
449
450    /// 行内に置かれた置換要素(`display:inline` の `img`/`video`/`canvas`/`iframe` 等)か。
451    fn is_replaced_inline(&self) -> bool {
452        match &self.box_type {
453            BoxType::InlineNode(n) => matches!(
454                &n.node.node_type,
455                NodeType::Element { tag_name, .. }
456                    if matches!(tag_name.as_str(), "img" | "video" | "canvas" | "iframe" | "embed" | "object")
457            ),
458            _ => false,
459        }
460    }
461
462    /// 行内の箱のベースライン(margin-box 上端からの距離)。
463    /// CSS 2.1 §10.8.1:
464    /// - 置換要素(img, input 等): 下マージン端
465    /// - inline-block: overflow が visible 以外、または行ボックスを持たない場合は下マージン端。
466    ///   それ以外は最後の行ボックス(最後のテキスト/インライン子孫)のベースライン。
467    /// - 素の inline(InlineNode): フォントの ascent(+ 行送りの中央揃え leading)。
468    fn box_baseline(&self) -> i32 {
469        let (_, _, et, eb) = self.inline_block_edges();
470        let outer_h = et + self.dimensions.content.height + eb;
471        if self.is_replaced_inline() {
472            return outer_h;
473        }
474        match &self.box_type {
475            BoxType::InlineNode(node) => {
476                let font_px = node
477                    .value("font-size")
478                    .and_then(|v| parse_font_size_u32(&v))
479                    .unwrap_or(16) as i32;
480                let ch = self.dimensions.content.height.max(font_px);
481                let leading = (ch - font_px).max(0) / 2;
482                let ascent = (font_px * 85) / 100;
483                et + leading + ascent
484            }
485            BoxType::InlineBlockNode(_) => {
486                let clips = box_style_value(self, "overflow")
487                    .is_some_and(|v| v.split_whitespace().any(|t| t != "visible"));
488                if clips {
489                    return outer_h;
490                }
491                if let Some(inner_bl) = self.last_inflow_baseline() {
492                    et + inner_bl
493                } else {
494                    outer_h
495                }
496            }
497            _ => outer_h,
498        }
499    }
500
501    /// インラインブロック内の最後のインライン要素のベースラインを、
502    /// 自身の content-box 上端からの相対距離として取得する。
503    fn last_inflow_baseline(&self) -> Option<i32> {
504        for c in self.children.iter().rev() {
505            if c.is_whitespace_text()
506                || box_float(c) != "none"
507                || matches!(box_position(c), "absolute" | "fixed")
508            {
509                continue;
510            }
511            match &c.box_type {
512                BoxType::InlineNode(_) => {
513                    let (_, _, cet, _) = c.inline_block_edges();
514                    let cy = (c.dimensions.content.y - cet) - self.dimensions.content.y;
515                    return Some(cy + c.box_baseline());
516                }
517                BoxType::InlineBlockNode(_) => {
518                    let (_, _, cet, _) = c.inline_block_edges();
519                    let cy = (c.dimensions.content.y - cet) - self.dimensions.content.y;
520                    return Some(cy + c.box_baseline());
521                }
522                BoxType::BlockNode(_) | BoxType::AnonymousBlock => {
523                    if let Some(b) = c.last_inflow_baseline() {
524                        let (_, _, cet, _) = c.inline_block_edges();
525                        let cy = (c.dimensions.content.y - cet) - self.dimensions.content.y;
526                        return Some(cy + b);
527                    }
528                }
529                _ => {}
530            }
531        }
532        None
533    }
534
535    /// 子孫に空白以外の文字ノードがあるか(inline-block が行ボックスを持つかの近似)。
536    fn has_inflow_text(&self) -> bool {
537        if let BoxType::InlineNode(n) = &self.box_type {
538            if let NodeType::Text(t) = &n.node.node_type {
539                return !t.trim().is_empty();
540            }
541        }
542        self.children.iter().any(|c| c.has_inflow_text())
543    }
544
545    /// 空白だけの文字ノードで、空白が畳まれる(`white-space` が normal/nowrap/pre-line)もの。
546    fn is_collapsible_space(&self) -> bool {
547        if !self.is_whitespace_text() {
548            return false;
549        }
550        !matches!(
551            box_style_value(self, "white-space").as_deref().map(str::trim),
552            Some("pre" | "pre-wrap" | "break-spaces")
553        )
554    }
555
556    /// 畳まれた空白 1 つの幅(描画と同じ書体・letter/word-spacing 込み)。
557    fn collapsed_space_width(&self) -> i32 {
558        let BoxType::InlineNode(node) = &self.box_type else {
559            return 0;
560        };
561        let font_px = node
562            .value("font-size")
563            .and_then(|v| parse_font_size_u32(&v))
564            .unwrap_or(16);
565        let ls = parse_letter_spacing_with_font(node.value("letter-spacing").as_deref(), 0, font_px as f32);
566        let ws = parse_word_spacing_with_font(node.value("word-spacing").as_deref(), 0, font_px as f32);
567        let (fam, weight) = text_font_of(node);
568        crate::kernel::vector_font::get_vector_string_width_family(" ", font_px, &fam, weight, ls, ws) as i32
569    }
570
571    /// 中身が空白だけの素の inline 要素か(`<span> </span>`。前後の空白と畳まれる)。
572    fn is_blank_inline(&self) -> bool {
573        matches!(self.box_type, BoxType::InlineNode(_))
574            && !self.is_replaced_inline()
575            && !self.has_inflow_text()
576            && self.children.iter().all(|c| c.is_whitespace_text() || c.is_blank_inline())
577    }
578
579    /// 空白だけの文字ノードか(行内に並んでも見えず、揃えの対象にしない)。
580    fn is_whitespace_text(&self) -> bool {
581        matches!(&self.box_type, BoxType::InlineNode(n)
582            if matches!(&n.node.node_type, NodeType::Text(t) if t.trim().is_empty()))
583    }
584
585    /// テキストノードで末尾に空白を持つか(改行・タブ含む)。
586    fn text_ends_with_space(&self) -> bool {
587        if let BoxType::InlineNode(n) = &self.box_type {
588            if let NodeType::Text(t) = &n.node.node_type {
589                return t.ends_with(' ') || t.ends_with('\t') || t.ends_with('\n');
590            }
591        }
592        false
593    }
594
595    /// テキストノードで先頭に空白を持つか(改行・タブ含む)。
596    fn text_starts_with_space(&self) -> bool {
597        if let BoxType::InlineNode(n) = &self.box_type {
598            if let NodeType::Text(t) = &n.node.node_type {
599                return t.starts_with(' ') || t.starts_with('\t') || t.starts_with('\n');
600            }
601        }
602        false
603    }
604
605    /// `[start, end)` の子を 1 行とみなし、`vertical-align`(既定 baseline)に従って
606    /// 各箱を垂直方向に整列させる(CSS 2.1 §10.8)。
607    /// 整列後の行全体の高さを返す。
608    fn align_line_baselines(children: &mut [LayoutBox], start: usize, end: usize) -> i32 {
609        let Some(line) = children.get_mut(start..end) else {
610            return 0;
611        };
612
613        // 1. vertical-align: baseline(既定)の箱の揃え
614        let mut baseline_aligned: Vec<(usize, i32, i32)> = Vec::new();
615        for (i, c) in line.iter().enumerate() {
616            if c.is_whitespace_text()
617                || !matches!(c.box_type, BoxType::InlineNode(_) | BoxType::InlineBlockNode(_))
618                || box_float(c) != "none"
619                || matches!(box_position(c), "absolute" | "fixed")
620            {
621                continue;
622            }
623            let va = box_style_value(c, "vertical-align").unwrap_or_default();
624            let va = va.trim();
625            if va.is_empty() || va == "baseline" {
626                let b = c.box_baseline();
627                let (_, _, et, eb) = c.inline_block_edges();
628                let outer_h = et + c.dimensions.content.height + eb;
629                baseline_aligned.push((i, b, outer_h));
630            }
631        }
632
633        let mut max_baseline = 0i32;
634        let mut max_descent = 0i32;
635        if !baseline_aligned.is_empty() {
636            max_baseline = baseline_aligned.iter().map(|&(_, b, _)| b).max().unwrap_or(0);
637            for &(i, b, outer_h) in &baseline_aligned {
638                let descent = outer_h - b;
639                if descent > max_descent {
640                    max_descent = descent;
641                }
642                let shift = max_baseline - b;
643                if shift > 0 {
644                    if let Some(c) = line.get_mut(i) {
645                        c.translate_y(shift);
646                    }
647                }
648            }
649        }
650
651        // 2. vertical-align: middle / bottom / text-bottom の箱の揃え
652        let mut line_h = (max_baseline + max_descent).max(0);
653        for c in line.iter() {
654            if c.is_whitespace_text()
655                || !matches!(c.box_type, BoxType::InlineNode(_) | BoxType::InlineBlockNode(_))
656                || box_float(c) != "none"
657                || matches!(box_position(c), "absolute" | "fixed")
658            {
659                continue;
660            }
661            let (_, _, et, eb) = c.inline_block_edges();
662            let ch = et + c.dimensions.content.height + eb;
663            if ch > line_h {
664                line_h = ch;
665            }
666        }
667
668        if line_h > 0 {
669            for c in line.iter_mut() {
670                if c.is_whitespace_text()
671                    || !matches!(c.box_type, BoxType::InlineNode(_) | BoxType::InlineBlockNode(_))
672                    || box_float(c) != "none"
673                    || matches!(box_position(c), "absolute" | "fixed")
674                {
675                    continue;
676                }
677                let va = box_style_value(c, "vertical-align").unwrap_or_default();
678                let va = va.trim();
679                let (_, _, et, eb) = c.inline_block_edges();
680                let ch = et + c.dimensions.content.height + eb;
681                let offset = match va {
682                    "middle" => (line_h - ch) / 2,
683                    "bottom" | "text-bottom" => line_h - ch,
684                    _ => 0,
685                };
686                if offset > 0 {
687                    c.translate_y(offset);
688                }
689            }
690        }
691
692        line_h
693    }
694
695    /// `display: inline-flex` の箱か(外側 InlineBlockNode・内側 flex)。
696    fn is_inline_flex(&self) -> bool {
697        match &self.box_type {
698            BoxType::InlineBlockNode(n) => resolved_display(n).trim() == "inline-flex",
699            _ => false,
700        }
701    }
702
703    /// `display: inline-grid` の箱か(外側 InlineBlockNode・内側 grid)。
704    fn is_inline_grid(&self) -> bool {
705        match &self.box_type {
706            BoxType::InlineBlockNode(n) => resolved_display(n).trim() == "inline-grid",
707            _ => false,
708        }
709    }
710
711    /// inline-flex の content 幅: 明示幅があればそれ、無ければ shrink-to-fit
712    /// (flex 行の max-content を利用幅で頭打ち。下限は min-content)。
713    fn inline_flex_content_width(&self, avail: i32) -> i32 {
714        if let BoxType::InlineBlockNode(node) = &self.box_type {
715            if let Some(w) = parse_length_value(len_value(node, "width"))
716                .and_then(|v| resolve_length_value(v, avail.max(0)))
717            {
718                return w.max(0);
719            }
720        }
721        let (minc, maxc) = self.block_intrinsic_widths();
722        let d = &self.dimensions;
723        let non_content = d.margin.left + d.margin.right + d.border.left + d.border.right
724            + d.padding.left + d.padding.right;
725        maxc.min((avail - non_content).max(0)).max(minc).max(0)
726    }
727
728    /// 自身を flex コンテナとして content 幅 `content_w`・content 原点 (x, y) に組む。
729    /// `layout_flex` は FlexNode しか受け付けないので、その間だけ型を差し替える。
730    fn layout_as_flex_at(&mut self, x: i32, y: i32, content_w: i32) {
731        let BoxType::InlineBlockNode(node) = self.box_type else {
732            return;
733        };
734        let as_grid = self.is_inline_grid();
735        self.box_type = if as_grid { BoxType::GridNode(node) } else { BoxType::FlexNode(node) };
736        let d = &self.dimensions;
737        let left = d.margin.left + d.border.left + d.padding.left;
738        let top = d.margin.top + d.border.top + d.padding.top;
739        let h_non = left + d.padding.right + d.border.right + d.margin.right;
740        let cb = Dimensions {
741            content: Rect { x: x - left, y: y - top, width: content_w + h_non, height: 0 },
742            ..Default::default()
743        };
744        if as_grid {
745            self.layout_grid(cb);
746        } else {
747            self.layout_flex(cb);
748        }
749        self.box_type = BoxType::InlineBlockNode(node);
750    }
751
752    /// `display: inline-flex` のレイアウト。位置の決め方は `layout_inline` と同じ
753    /// (cb の content 原点 + 縦カーソル + margin/border/padding)で、親が最後に
754    /// スロット位置へ子孫ごと平行移動する。
755    fn layout_inline_flex(&mut self, containing_block: Dimensions) {
756        self.apply_box_model_styles_with_base(containing_block.content.width);
757        let w = self.inline_flex_content_width(containing_block.content.width.max(1));
758        let d = &self.dimensions;
759        let x = containing_block.content.x + d.margin.left + d.border.left + d.padding.left;
760        let y = containing_block.content.y
761            + containing_block.content.height
762            + d.margin.top
763            + d.border.top
764            + d.padding.top;
765        self.layout_as_flex_at(x, y, w);
766    }
767
768    /// 自身と全子孫の絶対 y 座標を dy だけ平行移動する(table セルの `vertical-align` 実装用)。
769    /// 各ボックスの content.y は既に絶対ピクセル座標として確定しているため、
770    /// この再帰1回で子孫すべての位置がまとめてずれる。
771    fn translate_y(&mut self, dy: i32) {
772        if dy == 0 {
773            return;
774        }
775        self.dimensions.content.y += dy;
776        for child in &mut self.children {
777            child.translate_y(dy);
778        }
779    }
780
781    /// 自身と全子孫の絶対座標を (dx, dy) だけ平行移動する。
782    ///
783    /// 多段組で「通常フローで積み終えた子を列の位置へ移す」ために使う。
784    /// `translate_y` の 2 軸版で、考え方は同じ(content 座標が絶対値なので
785    /// 再帰 1 回で子孫がまとめてずれる)。
786    fn translate(&mut self, dx: i32, dy: i32) {
787        if dx == 0 && dy == 0 {
788            return;
789        }
790        self.dimensions.content.x += dx;
791        self.dimensions.content.y += dy;
792        for child in &mut self.children {
793            child.translate(dx, dy);
794        }
795    }
796
797    /// `indices` で指定した caption 群を y から順に縦積みでレイアウトし、消費した合計高さを返す。
798    /// `caption-side:top`(既定)/`bottom` どちらの配置にも使う共通ヘルパー。
799    fn layout_captions_at(&mut self, indices: &[usize], base_x: i32, y: i32, width: i32) -> i32 {
800        let mut consumed = 0i32;
801        for &ci in indices {
802            let cap_y = y + consumed;
803            let cap = &mut self.children[ci];
804            let mut cb = Dimensions::default();
805            cb.content.x = base_x;
806            cb.content.y = cap_y;
807            cb.content.height = 0;
808            cb.content.width = width;
809            cap.layout(cb);
810            consumed += cap.dimensions.margin_box().height;
811        }
812        consumed
813    }
814
815    fn layout(&mut self, containing_block: Dimensions) {
816        match &self.box_type {
817            BoxType::BlockNode(_) => self.layout_block(containing_block),
818            BoxType::InlineNode(_) => self.layout_inline(containing_block, false),
819            BoxType::InlineBlockNode(_) if self.is_inline_flex() || self.is_inline_grid() => {
820                self.layout_inline_flex(containing_block)
821            }
822            BoxType::InlineBlockNode(_) => self.layout_inline(containing_block, true),
823            BoxType::FlexNode(_) => self.layout_flex(containing_block),
824            BoxType::GridNode(_) => self.layout_grid(containing_block),
825            BoxType::TableNode(_) => self.layout_table(containing_block),
826            // 行グループ・行・セルは table 経由でレイアウトされる。
827            // 単独で呼ばれた場合 (table の外にある等) は block としてフォールバック。
828            BoxType::TableRowGroupNode(_)
829            | BoxType::TableRowNode(_)
830            | BoxType::TableCellNode(_) => self.layout_block(containing_block),
831            BoxType::AnonymousBlock => {}
832        }
833    }
834
835    /// この要素が positioned(relative/absolute/fixed)なら、子孫 absolute の包含ブロック
836    /// (ABS_CB)を self の content box に切替え、切替前の値を返す。戻り値は
837    /// `restore_abs_cb` に渡して復元する。非 positioned なら None。
838    /// content.width / content.height が確定済みであることを前提とする(bottom/right 解決のため)。
839    fn establish_abs_cb(&self) -> Option<(i32, i32, i32, i32)> {
840        if matches!(
841            box_position(self),
842            "relative" | "absolute" | "fixed"
843        ) {
844            // 【2026-10-03】包含ブロックは positioned 祖先の**パディングボックス**(CSS 2.1 10.1)。
845            // 従来は content box で、`padding-bottom:56.25%; height:0` の縦横比の箱に入れた
846            // `height:100%` の絶対配置(動画・地図の埋め込みの定番)が高さ 0 になっていた。
847            let prev = abs_cb_get();
848            let pb = self.dimensions.padding_box();
849            abs_cb_set(pb.x, pb.y, pb.width, pb.height);
850            Some(prev)
851        } else {
852            None
853        }
854    }
855
856    /// `establish_abs_cb` の戻り値を使って ABS_CB を元に戻す。
857    fn restore_abs_cb(saved: Option<(i32, i32, i32, i32)>) {
858        if let Some(prev) = saved {
859            abs_cb_set(prev.0, prev.1, prev.2, prev.3);
860        }
861    }
862
863    /// 子のうち out-of-flow(absolute / fixed)なものを判定するヘルパ。
864    fn child_is_out_of_flow(child: &LayoutBox) -> bool {
865        matches!(box_position(child), "absolute" | "fixed")
866    }
867
868    /// position 後処理。全フォーマッティングコンテキスト(block/flex/grid/table)で共有。
869    /// - relative: フロー確定位置から top/left(または bottom/right の符号反転)でオフセット。
870    /// - absolute: 最近傍 positioned 祖先(ABS_CB、昇り解決済み)基準で top/left/right/bottom 配置。
871    /// - fixed: ビューポート原点基準で配置。
872    /// - transform: translate を最後に視覚シフトとして適用(兄弟は再フローしない=仕様通り)。
873    ///
874    /// self.dimensions.content.{width,height} が確定済みであることを前提とする。
875    fn apply_positioned_offsets(&mut self) {
876        let cw = self.dimensions.content.width;
877        let ch = self.dimensions.content.height;
878        for child in &mut self.children {
879            match box_position(child) {
880                "relative" => {
881                    let dx = box_inset_len(child, "left", cw)
882                        .or_else(|| box_inset_len(child, "right", cw).map(|r| -r))
883                        .unwrap_or(0);
884                    let dy = box_inset_len(child, "top", ch)
885                        .or_else(|| box_inset_len(child, "bottom", ch).map(|b| -b))
886                        .unwrap_or(0);
887                    if dx != 0 || dy != 0 {
888                        shift_layout_box(child, dx, dy);
889                    }
890                }
891                pos @ ("absolute" | "fixed") => {
892                    // 基準矩形: absolute=ABS_CB(昇り解決済み)、fixed=ビューポート原点。
893                    let (bx, by, bw, bh) = if pos == "fixed" {
894                        let (vx, vy) = vp_origin_get();
895                        (
896                            vx,
897                            vy,
898                            VIEWPORT_HINT_W.load(AtomicOrdering::Relaxed),
899                            VIEWPORT_HINT_H.load(AtomicOrdering::Relaxed),
900                        )
901                    } else {
902                        abs_cb_get()
903                    };
904                    // 【2026-09-26 バグ修正】幅 auto の絶対配置は、left と right が両方あれば
905                    // 包含ブロックから幅を決め、そうでなければ内容幅に縮む(shrink-to-fit。
906                    // CSS 2.1 §10.3.7)。従来は通常フローと同じく親の幅いっぱいに組まれ、
907                    // 実サイトのヒーロー下部の矢印(left:50% + translateX(-50%))が左端に出ていた。
908                    // 【2026-10-03】% の幅・高さは包含ブロック(bw × bh)基準で組み直す。通常フローとして
909                    // 組まれた時点では親の content 幅・未確定の高さが基準になっている。
910                    let is_pct = |name: &str| {
911                        matches!(
912                            parse_length_value(box_style_value(child, name)),
913                            Some(LengthValue::Percent(_))
914                        )
915                    };
916                    // 【2026-10-18】`%` の基準・縮み幅・top+bottom の高さを先に全部決め、組み直しは
917                    // 1 回にまとめる。従来は条件ごとに最大 3 回組み直し、入れ子の absolute ごとに
918                    // 掛け算で増えていた(YouTube の操作部でレイアウト時間の約半分)。
919                    let need_pct = is_pct("width") || is_pct("height");
920                    let block_like = matches!(
921                        child.box_type,
922                        BoxType::BlockNode(_) | BoxType::FlexNode(_) | BoxType::GridNode(_)
923                    );
924                    let h_non = child.h_non_content();
925                    let target_w: Option<i32> = if block_like && !box_has_explicit(child, "width") {
926                        let l = box_inset_len(child, "left", bw);
927                        let r = box_inset_len(child, "right", bw);
928                        Some(match (l, r) {
929                            (Some(l), Some(r)) => (bw - l - r - h_non).max(0),
930                            _ => {
931                                let (minc, maxc) = child.block_intrinsic_widths();
932                                let avail = (bw - l.or(r).unwrap_or(0) - h_non).max(0);
933                                maxc.min(avail).max(minc).max(0)
934                            }
935                        })
936                    } else {
937                        None
938                    };
939                    // 高さ auto で top と bottom が両方あれば、包含ブロックから高さを決める
940                    // (CSS 2.1 §10.6.4)。【2026-10-17】`position:absolute; inset:0` の覆い
941                    // (YouTube 埋め込みのサムネイル・操作部)が内容の高さに縮んでいた。
942                    let target_h: Option<i32> = if block_like && !box_has_explicit(child, "height") {
943                        match (box_inset_len(child, "top", bh), box_inset_len(child, "bottom", bh)) {
944                            (Some(t), Some(b)) => {
945                                let mb0 = child.dimensions.margin_box();
946                                let v_non = mb0.height - child.dimensions.content.height;
947                                Some((bh - t - b - v_non).max(0))
948                            }
949                            _ => None,
950                        }
951                    } else {
952                        None
953                    };
954                    let is_block = matches!(child.box_type, BoxType::BlockNode(_));
955                    let need_w = target_w.is_some_and(|w| w != child.dimensions.content.width);
956                    // 子の `height:100%` もこの高さで解決させるため、ブロックは高さを確定させて組み直す。
957                    let need_h = is_block && target_h.is_some_and(|h| h != child.dimensions.content.height);
958                    if need_pct || need_w || need_h {
959                        let mb0 = child.dimensions.margin_box();
960                        let _pct_h = if need_pct { Some(PctBaseHGuard::set(bh)) } else { None };
961                        let width = match target_w {
962                            Some(w) => w + h_non,
963                            None if need_pct => bw,
964                            None => mb0.width,
965                        };
966                        if need_h {
967                            if let Some(h) = target_h {
968                                FORCED_BLOCK_H.store(h, AtomicOrdering::Relaxed);
969                            }
970                        }
971                        let cb = Dimensions {
972                            content: Rect { x: mb0.x, y: mb0.y, width, height: 0 },
973                            ..Default::default()
974                        };
975                        child.layout(cb);
976                        FORCED_BLOCK_H.store(-1, AtomicOrdering::Relaxed);
977                    }
978                    if let Some(h) = target_h {
979                        if h != child.dimensions.content.height {
980                            child.dimensions.content.height = h;
981                        }
982                    }
983                    let mb = child.dimensions.margin_box();
984                    let tx = if let Some(l) = box_inset_len(child, "left", bw) {
985                        bx + l
986                    } else if let Some(r) = box_inset_len(child, "right", bw) {
987                        bx + bw - r - mb.width
988                    } else {
989                        bx
990                    };
991                    let ty = if let Some(t) = box_inset_len(child, "top", bh) {
992                        by + t
993                    } else if let Some(b) = box_inset_len(child, "bottom", bh) {
994                        by + bh - b - mb.height
995                    } else {
996                        by
997                    };
998                    shift_layout_box(child, tx - mb.x, ty - mb.y);
999                }
1000                _ => {}
1001            }
1002
1003            // transform: translate(レイアウト確定後の視覚シフト。position の後に適用、
1004            // 兄弟は再フローしない=仕様通り)。% は要素自身の content 寸法基準。
1005            let (txx, tyy) = box_translate(child);
1006            if txx != 0 || tyy != 0 {
1007                shift_layout_box(child, txx, tyy);
1008            }
1009        }
1010    }
1011
1012    // ===== CSS Grid レイアウト =====
1013    fn prepare_child_subgrid_ctx(
1014        child: &LayoutBox,
1015        col_widths: &[i32],
1016        col_gap_eff: i32,
1017        c: usize,
1018        cs: usize,
1019        cell_w: i32,
1020    ) -> Option<SubgridContext> {
1021        let child_node = match &child.box_type {
1022            BoxType::GridNode(n) => Some(*n),
1023            BoxType::InlineBlockNode(n) if child.is_inline_grid() => Some(*n),
1024            _ => None,
1025        }?;
1026        let cols_val = child_node.value("grid-template-columns").unwrap_or_default();
1027        if !is_subgrid(&cols_val) {
1028            return None;
1029        }
1030        let end_c = (c + cs).min(col_widths.len());
1031        let sub_cols = if c < end_c {
1032            col_widths[c..end_c].to_vec()
1033        } else {
1034            alloc::vec![cell_w]
1035        };
1036        Some(SubgridContext {
1037            col_widths: sub_cols,
1038            col_gap: col_gap_eff,
1039        })
1040    }
1041
1042    fn layout_grid(&mut self, containing_block: Dimensions) {
1043        self.apply_box_model_styles_with_base(containing_block.content.width);
1044
1045        let node = match &self.box_type {
1046            BoxType::GridNode(n) => *n,
1047            _ => return,
1048        };
1049        let avail_w = containing_block.content.width.max(1);
1050
1051        // box-sizing / margin:auto 適用(block と同じロジック)。
1052        let inner_non_content = self.dimensions.border.left
1053            + self.dimensions.border.right
1054            + self.dimensions.padding.left
1055            + self.dimensions.padding.right;
1056        let h_non_content =
1057            self.dimensions.margin.left + self.dimensions.margin.right + inner_non_content;
1058        let width_val = len_value(node, "width");
1059        let box_sizing = node.value("box-sizing").unwrap_or_default();
1060        let h_auto = margin_is_h_auto(node);
1061        let explicit_w =
1062            !matches!(parse_length_value(width_val.clone()), Some(LengthValue::Auto) | None);
1063        let mut content_w = match parse_length_value(width_val) {
1064            Some(LengthValue::Auto) | None => (avail_w - h_non_content).max(1),
1065            Some(LengthValue::Px(px)) => px.max(1),
1066            Some(LengthValue::Percent(pct)) => round_f32_to_i32(avail_w as f32 * pct).max(1),
1067            Some(LengthValue::MinContent) => {
1068                let (minc, _maxc) = self.block_intrinsic_widths();
1069                minc.max(1)
1070            }
1071            Some(LengthValue::MaxContent) => {
1072                let (_minc, maxc) = self.block_intrinsic_widths();
1073                maxc.max(1)
1074            }
1075            Some(LengthValue::FitContent) => {
1076                let (minc, maxc) = self.block_intrinsic_widths();
1077                let avail = (avail_w - h_non_content).max(0);
1078                avail.min(maxc).max(minc).max(1)
1079            }
1080            Some(LengthValue::FitContentWith(lim_box)) => {
1081                let avail = (avail_w - h_non_content).max(0);
1082                let limit = resolve_length_value_with_font(*lim_box, avail, self.font_size())
1083                    .unwrap_or(avail);
1084                let (minc, maxc) = self.block_intrinsic_widths();
1085                maxc.min(limit.max(minc)).max(1)
1086            }
1087            Some(LengthValue::MathExpr(expr)) => {
1088                crate::os_lib::css::eval_css_math_with_font(&expr, avail_w, self.font_size())
1089                    .unwrap_or((avail_w - h_non_content).max(1))
1090                    .max(1)
1091            }
1092        };
1093        // 【2026-09-26】border-box は明示幅のときだけ(auto 幅は既に padding を
1094        // 除いた残りなので、引くと二重に引かれる)。min/max-width も適用する
1095        // (従来は無視されており `max-width + margin:auto` の中央寄せが効かなかった)。
1096        if explicit_w && box_sizing.trim() == "border-box" {
1097            content_w = (content_w - inner_non_content).max(1);
1098        }
1099        // min/max-width の長さは border-box なら padding+border を含む(`border_box_inner_w`)。
1100        let bb_inner = if box_sizing.trim() == "border-box" { inner_non_content } else { 0 };
1101        let min_val = len_value(node, "min-width");
1102        if let Some(min_w) = self.resolve_sizing_value(min_val.clone(), avail_w) {
1103            let min_w = if is_length_sizing(&min_val) { (min_w - bb_inner).max(0) } else { min_w };
1104            content_w = content_w.max(min_w);
1105        }
1106        let max_val = len_value(node, "max-width");
1107        if let Some(max_w) = self.resolve_sizing_value(max_val.clone(), avail_w) {
1108            let max_w = if is_length_sizing(&max_val) { (max_w - bb_inner).max(0) } else { max_w };
1109            content_w = content_w.min(max_w);
1110        }
1111        let content_w = content_w.max(1);
1112        self.dimensions.content.width = content_w;
1113        if h_auto {
1114            let free = (avail_w - content_w - inner_non_content).max(0);
1115            self.dimensions.margin.left = free / 2;
1116            self.dimensions.margin.right = free / 2;
1117        }
1118
1119        let flow_y = containing_block.content.y
1120            + containing_block.content.height
1121            + self.dimensions.margin.top;
1122        self.dimensions.content.x = containing_block.content.x
1123            + self.dimensions.margin.left
1124            + self.dimensions.border.left
1125            + self.dimensions.padding.left;
1126        self.dimensions.content.y =
1127            flow_y + self.dimensions.border.top + self.dimensions.padding.top;
1128
1129        let (row_gap, col_gap) = {
1130            // px / % 両対応の gap 解決(% は content 幅基準)。
1131            let gpx = |s: &str| -> i32 {
1132                match parse_length_value(Some(String::from(s.trim()))) {
1133                    Some(LengthValue::Px(p)) => p,
1134                    Some(LengthValue::Percent(pct)) => {
1135                        round_f32_to_i32(content_w as f32 * pct).max(0)
1136                    }
1137                    _ => 0,
1138                }
1139            };
1140
1141            let gap_all = len_value(node, "gap").unwrap_or_default();
1142            if !gap_all.is_empty() {
1143                let toks: Vec<&str> = gap_all.split_whitespace().collect();
1144                let rg = gpx(toks.first().copied().unwrap_or("0"));
1145                let cg = gpx(toks
1146                    .get(1)
1147                    .copied()
1148                    .unwrap_or(toks.first().copied().unwrap_or("0")));
1149                (rg, cg)
1150            } else {
1151                let rg = node
1152                    .value("row-gap")
1153                    .or_else(|| node.value("grid-row-gap"))
1154                    .map(|v| gpx(&v))
1155                    .unwrap_or(0);
1156                let cg = node
1157                    .value("column-gap")
1158                    .or_else(|| node.value("grid-column-gap"))
1159                    .map(|v| gpx(&v))
1160                    .unwrap_or(0);
1161                (rg, cg)
1162            }
1163        };
1164
1165        // grid-template-columns を解析して列幅の Vec を作る。
1166        // subgrid の場合は親グリッドから渡されたトラック幅(SUBGRID_CTX)を使用。
1167        let t_cols = node.value("grid-template-columns").unwrap_or_default();
1168        let (mut col_widths, col_gap) = if is_subgrid(&t_cols) {
1169            if let Some(sub_ctx) = get_subgrid_ctx() {
1170                let has_own_gap = node.value("gap").is_some()
1171                    || node.value("column-gap").is_some()
1172                    || node.value("grid-column-gap").is_some();
1173                let eff_gap = if has_own_gap { col_gap } else { sub_ctx.col_gap };
1174                (sub_ctx.col_widths, eff_gap)
1175            } else {
1176                (alloc::vec![content_w], col_gap)
1177            }
1178        } else {
1179            (
1180                parse_grid_template_columns_gap(&t_cols, content_w, col_gap),
1181                col_gap,
1182            )
1183        };
1184        let n_cols = col_widths.len().max(1);
1185
1186        // grid-auto-columns: 暗黙列(`grid-auto-flow: column` が明示列数を使い切って
1187        // 新規に作る列)のサイズ。複数値は循環適用。px/minmax の下限のみを解決し、
1188        // fr/auto は「最後の明示列と同じ幅」にフォールバックする簡略実装
1189        // (暗黙列の内容ベースの自動採寸は行わない。行側の grid-auto-rows のような
1190        // 内容実測パスをそのまま列へ持ち込むのは改修規模が大きいための割り切り)。
1191        let auto_col_tracks = parse_grid_tracks(
1192            node.value("grid-auto-columns").as_deref().unwrap_or(""),
1193            content_w,
1194        );
1195        let auto_col_fallback_px = col_widths.last().copied().unwrap_or(100).max(1);
1196        let resolve_auto_col_px = |idx: usize| -> i32 {
1197            if auto_col_tracks.is_empty() {
1198                return auto_col_fallback_px;
1199            }
1200            match auto_col_tracks[idx % auto_col_tracks.len()] {
1201                GridTrack::Px(px) => px.max(1),
1202                GridTrack::MinMax { min_px, .. } => min_px.max(1),
1203                _ => auto_col_fallback_px,
1204            }
1205        };
1206
1207        // grid-template-areas: 名前付き領域を grid-area で参照するための行×列テーブル。
1208        let template_areas =
1209            parse_grid_template_areas(node.value("grid-template-areas").as_deref().unwrap_or(""));
1210
1211        // grid-template-rows/grid-auto-rows の `%` トラック解決基準。明示 height
1212        // (px/%)があればそれを使い、無指定/auto は 0(`%` トラックは 0 扱い、
1213        // 高さがコンテンツから決まる場合の一般的な簡略化)とする。以前はここが常に
1214        // 固定値 0 で、明示 height を指定していても `grid-template-rows:50%` 等が
1215        // 常に 0px に丸め込まれるバグがあった(列側は content_w を正しく渡していたのに
1216        // 行側だけ取り残されていた)。
1217        let row_base = match parse_length_value(len_value(node, "height")) {
1218            Some(LengthValue::Px(px)) => px.max(0),
1219            Some(LengthValue::Percent(pct)) => {
1220                round_f32_to_i32(containing_block.content.height as f32 * pct).max(0)
1221            }
1222            _ => 0,
1223        };
1224
1225        // 明示 grid-template-rows トラック定義(px/fr/auto)。空なら全行 auto 扱い。
1226        let row_tracks = parse_grid_tracks(
1227            node.value("grid-template-rows").as_deref().unwrap_or(""),
1228            row_base,
1229        );
1230
1231        // grid-auto-rows: 暗黙行(明示トラック数を超えた行)のサイズ。複数値は循環適用。
1232        let auto_row_tracks = parse_grid_tracks(
1233            node.value("grid-auto-rows").as_deref().unwrap_or(""),
1234            row_base,
1235        );
1236
1237        // 行 r の実効トラックを返す: 明示トラックがあればそれ、無ければ grid-auto-rows を循環。
1238        let eff_row_track = |r: usize| -> Option<GridTrack> {
1239            if let Some(t) = row_tracks.get(r) {
1240                return Some(*t);
1241            }
1242            if auto_row_tracks.is_empty() {
1243                return None;
1244            }
1245            let implicit_idx = r - row_tracks.len();
1246            auto_row_tracks
1247                .get(implicit_idx % auto_row_tracks.len())
1248                .copied()
1249        };
1250
1251        // コンテナの justify-items(インライン=列軸)/ align-items(ブロック=行軸)。
1252        // 既定 stretch。各アイテムの justify-self / align-self が優先する。
1253        let justify_items = node
1254            .value("justify-items")
1255            .unwrap_or_else(|| String::from("stretch"))
1256            .trim()
1257            .to_lowercase();
1258        let align_items = node
1259            .value("align-items")
1260            .unwrap_or_else(|| String::from("stretch"))
1261            .trim()
1262            .to_lowercase();
1263
1264        // ---- パス1: 配置の解決(占有マップ + auto-flow row/column) ----
1265        // 各フロー子の (row, col, row_span, col_span) を確定。out-of-flow は別管理。
1266        // grid-auto-flow: column なら列優先(grid-template-rows の行数を縦に埋めてから次列へ)。
1267        let auto_flow_col = node
1268            .value("grid-auto-flow")
1269            .map(|v| v.to_lowercase().contains("column"))
1270            .unwrap_or(false);
1271        // 列優先時の行数上限(明示 grid-template-rows の本数。無ければ 1)。
1272        let flow_rows = row_tracks.len().max(1);
1273        let mut occ: Vec<Vec<bool>> = Vec::new();
1274        // `grid-auto-flow: column` が明示列数を使い切って新規に列を作った実際の列数。
1275        // 以前はこれが常に固定の n_cols のままで、`grid_cells_free` が
1276        // `c + col_span > n_cols` で常に false を返し続けるため、列優先フローで
1277        // 明示列数を超える個数のアイテムを配置しようとすると無限ループしてハングする
1278        // バグがあった。
1279        let mut n_cols_dyn = n_cols;
1280        // 子のインデックス → 配置(フロー子のみ)。
1281        let n_children = self.children.len();
1282        let mut placement: Vec<Option<(usize, usize, usize, usize)>> =
1283            alloc::vec![None; n_children];
1284        let mut cursor_r = 0usize;
1285        let mut cursor_c = 0usize;
1286        for (idx, child) in self.children.iter().enumerate() {
1287            if Self::child_is_out_of_flow(child) {
1288                continue;
1289            }
1290            let (mut col_start, mut col_span_raw) =
1291                grid_axis_placement(child, "grid-column", "grid-column-start", "grid-column-end");
1292            let (mut row_start, mut row_span_raw) =
1293                grid_axis_placement(child, "grid-row", "grid-row-start", "grid-row-end");
1294            // grid-column/grid-row が未指定(auto)の場合のみ、grid-area 名を
1295            // grid-template-areas から解決してフォールバックする。
1296            if col_start.is_none() && row_start.is_none() {
1297                if let Some(area_name) = box_style_value(child, "grid-area") {
1298                    let area_name = area_name.trim();
1299                    if !area_name.is_empty() {
1300                        if let Some((ar, ac, arspan, acspan)) =
1301                            find_grid_area(&template_areas, area_name)
1302                        {
1303                            row_start = Some(ar);
1304                            col_start = Some(ac);
1305                            row_span_raw = arspan;
1306                            col_span_raw = acspan;
1307                        }
1308                    }
1309                }
1310            }
1311            let col_span = col_span_raw.min(n_cols).max(1);
1312            let row_span = row_span_raw.max(1);
1313
1314            // 配置位置の決定。明示があれば尊重、無ければ auto-flow(行優先)。
1315            let (r, c) = match (row_start, col_start) {
1316                (Some(r), Some(c)) => (r, c.min(n_cols.saturating_sub(1))),
1317                (Some(r), None) => {
1318                    // 行固定・列 auto: その行で空く最初の列。
1319                    grid_ensure_row(&mut occ, r + row_span - 1, n_cols);
1320                    let mut cc = 0usize;
1321                    while cc < n_cols && !grid_cells_free(&occ, r, cc, row_span, col_span, n_cols) {
1322                        cc += 1;
1323                    }
1324                    (r, cc.min(n_cols.saturating_sub(1)))
1325                }
1326                (None, Some(c)) => {
1327                    // 列固定・行 auto: カーソル行以降で空く最初の行。
1328                    let c = c.min(n_cols.saturating_sub(1));
1329                    let mut rr = cursor_r;
1330                    loop {
1331                        grid_ensure_row(&mut occ, rr + row_span - 1, n_cols);
1332                        if grid_cells_free(&occ, rr, c, row_span, col_span, n_cols) {
1333                            break;
1334                        }
1335                        rr += 1;
1336                    }
1337                    (rr, c)
1338                }
1339                (None, None) => {
1340                    if auto_flow_col {
1341                        // 列優先: (cursor_r, cursor_c) から行を縦に進め、flow_rows を超えたら
1342                        // 次の列の先頭へ。空きセルが見つかるまで走査する。
1343                        // 明示列数(n_cols)を使い切ったら grid-auto-columns で新規に
1344                        // 列を作る(n_cols_dyn を伸ばす)。これをしないと
1345                        // grid_cells_free が `c + col_span > n_cols` で恒久的に false を
1346                        // 返し続け、無限ループでハングする。
1347                        let mut rr = cursor_r;
1348                        let mut cc = cursor_c;
1349                        loop {
1350                            if rr + row_span > flow_rows {
1351                                cc += 1;
1352                                rr = 0;
1353                            }
1354                            if cc + col_span > n_cols_dyn {
1355                                n_cols_dyn = cc + col_span;
1356                                grid_ensure_col(&mut occ, n_cols_dyn);
1357                            }
1358                            grid_ensure_row(&mut occ, rr + row_span - 1, n_cols_dyn);
1359                            if grid_cells_free(&occ, rr, cc, row_span, col_span, n_cols_dyn) {
1360                                break;
1361                            }
1362                            rr += 1;
1363                        }
1364                        cursor_r = rr + row_span;
1365                        cursor_c = cc;
1366                        (rr, cc)
1367                    } else {
1368                        // 行優先: カーソルから次の空きへ。
1369                        let mut rr = cursor_r;
1370                        let mut cc = cursor_c;
1371                        loop {
1372                            if cc + col_span > n_cols {
1373                                rr += 1;
1374                                cc = 0;
1375                            }
1376                            grid_ensure_row(&mut occ, rr + row_span - 1, n_cols);
1377                            if grid_cells_free(&occ, rr, cc, row_span, col_span, n_cols) {
1378                                break;
1379                            }
1380                            cc += 1;
1381                        }
1382                        cursor_r = rr;
1383                        cursor_c = cc + col_span;
1384                        (rr, cc)
1385                    }
1386                }
1387            };
1388            grid_ensure_row(&mut occ, r + row_span - 1, n_cols_dyn);
1389            grid_mark(&mut occ, r, c, row_span, col_span, n_cols_dyn);
1390            placement[idx] = Some((r, c, row_span, col_span));
1391        }
1392
1393        // grid-auto-flow: column が明示列数を使い切って新しい列を作った分だけ、
1394        // col_widths を grid-auto-columns(未指定なら最後の明示列と同じ幅)で伸ばす。
1395        while col_widths.len() < n_cols_dyn {
1396            let idx = col_widths.len() - n_cols;
1397            col_widths.push(resolve_auto_col_px(idx));
1398        }
1399
1400        // 【2026-10-04】fr 列の自動最小(min-content)。`1fr` は `minmax(auto, 1fr)` で、
1401        // 列は中身の最小幅より狭くならない(CSS Grid §12.7.1)。従来は文字列だけで幅を
1402        // 配っており、sugi-lab.net トップの `.about-content{1fr 1fr}` が Chrome
1403        // (左 443.9・右 376.1)と違い等分になっていた。1 列にだけ入る要素の
1404        // min-content(余白込み)を列ごとに集め、`expand_flexible_tracks` で配り直す。
1405        {
1406            let template = node.value("grid-template-columns").unwrap_or_default();
1407            let tracks = parse_grid_tracks(&template, content_w);
1408            let has_intrinsic = tracks.iter().any(|t| {
1409                matches!(
1410                    t,
1411                    GridTrack::MinContent | GridTrack::MaxContent | GridTrack::FitContent(_)
1412                )
1413            });
1414            let flex = flex_factors(&template, col_widths.len());
1415            let has_flex = flex.iter().any(|f| f.is_some());
1416            if has_intrinsic || has_flex {
1417                let mut mins = alloc::vec![0i32; col_widths.len()];
1418                let mut maxs = alloc::vec![0i32; col_widths.len()];
1419                for (idx, child) in self.children.iter().enumerate() {
1420                    let Some((_, c, _, cs)) = placement.get(idx).copied().flatten() else {
1421                        continue;
1422                    };
1423                    if cs != 1 || c >= mins.len() {
1424                        continue;
1425                    }
1426                    let item_node = match &child.box_type {
1427                        BoxType::BlockNode(n)
1428                        | BoxType::InlineNode(n)
1429                        | BoxType::InlineBlockNode(n)
1430                        | BoxType::FlexNode(n)
1431                        | BoxType::GridNode(n)
1432                        | BoxType::TableNode(n)
1433                        | BoxType::TableRowGroupNode(n)
1434                        | BoxType::TableRowNode(n)
1435                        | BoxType::TableCellNode(n) => Some(*n),
1436                        BoxType::AnonymousBlock => None,
1437                    };
1438                    let col_w = col_widths.get(c).copied().unwrap_or(content_w);
1439                    let mut item_min = 0;
1440                    let mut item_max = 0;
1441                    if let Some(n) = item_node {
1442                        let (m, p, b) = style_box_edges(n, col_w, &Dimensions::default());
1443                        let edges = m.left + m.right + p.left + p.right + b.left + b.right;
1444                        let is_border_box = n
1445                            .value("box-sizing")
1446                            .is_some_and(|s| s.trim().eq_ignore_ascii_case("border-box"));
1447                        let bb_sub = if is_border_box {
1448                            (p.left + p.right + b.left + b.right).max(0)
1449                        } else {
1450                            0
1451                        };
1452                        let definite = |name: &str| {
1453                            parse_length_value(len_value(n, name))
1454                                .and_then(|v| resolve_length_value(v, col_w))
1455                                .map(|px| (px - bb_sub).max(0))
1456                        };
1457                        let scroll_container = ["overflow", "overflow-x"].iter().any(|k| {
1458                            n.value(k).is_some_and(|v| {
1459                                matches!(v.trim(), "hidden" | "scroll" | "auto")
1460                            })
1461                        });
1462                        // CSS Grid §6.6 自動最小:
1463                        // - min-width の指定があればそれ(`min-width:0` で縮められる)
1464                        // - スクロールコンテナ(overflow が hidden/scroll/auto)は 0
1465                        // - width が確定していればその値、無ければ min-content
1466                        // いずれも max-width で頭打ち。
1467                        item_min = if let Some(mw) = definite("min-width") {
1468                            mw + edges
1469                        } else if scroll_container {
1470                            0
1471                        } else if let Some(w) = definite("width") {
1472                            w + edges
1473                        } else {
1474                            child.block_intrinsic_widths().0 + edges
1475                        };
1476                        item_max = if let Some(w) = definite("width") {
1477                            w + edges
1478                        } else {
1479                            child.block_intrinsic_widths().1 + edges
1480                        };
1481                        if let Some(mx) = definite("max-width") {
1482                            item_min = item_min.min(mx + edges);
1483                            item_max = item_max.min(mx + edges);
1484                        }
1485                    }
1486                    mins[c] = mins[c].max(item_min);
1487                    maxs[c] = maxs[c].max(item_max);
1488                }
1489
1490                // 内容依存トラック(min-content, max-content, fit-content)の幅を確定
1491                for (c, track) in tracks.iter().enumerate() {
1492                    if c >= col_widths.len() {
1493                        break;
1494                    }
1495                    match track {
1496                        GridTrack::MinContent => {
1497                            col_widths[c] = mins[c].max(1);
1498                        }
1499                        GridTrack::MaxContent => {
1500                            col_widths[c] = maxs[c].max(1);
1501                        }
1502                        GridTrack::FitContent(limit) => {
1503                            col_widths[c] = maxs[c].min(*limit).max(mins[c]).max(1);
1504                        }
1505                        _ => {}
1506                    }
1507                }
1508
1509                // fr トラックがある場合は、確定した列幅と gap を引いた残り幅を配り直す
1510                if has_flex {
1511                    let n_cols_total = col_widths.len();
1512                    let gaps_total = col_gap * (n_cols_total as i32 - 1).max(0);
1513                    let non_flex_sum: i32 = (0..n_cols_total)
1514                        .filter(|&i| flex.get(i).copied().flatten().is_none())
1515                        .map(|i| col_widths[i])
1516                        .sum();
1517                    let flex_avail = (content_w - non_flex_sum - gaps_total).max(0);
1518                    let flex_sum: f32 = flex.iter().filter_map(|&f| f).sum();
1519                    if flex_sum > 0.0 {
1520                        for (i, f) in flex.iter().enumerate() {
1521                            if let Some(factor) = f {
1522                                col_widths[i] = round_f32_to_i32(flex_avail as f32 * factor / flex_sum).max(1);
1523                            }
1524                        }
1525                    }
1526                    col_widths = expand_flexible_tracks(&col_widths, &flex, &mins);
1527                }
1528            }
1529        }
1530
1531        // グリッドコンテナの `justify-content`: トラック総幅がコンテナ幅より
1532        // 小さい場合に、トラック集合全体をどう配置するか(flexbox の主軸
1533        // justify-content と同じ考え方。以前はここが完全に未実装で、
1534        // トラック合計がコンテナ幅に満たなくても常に左詰めに固定されていた)。
1535        let n_cols_eff = col_widths.len();
1536        let cols_total: i32 =
1537            col_widths.iter().sum::<i32>() + (n_cols_eff.saturating_sub(1) as i32) * col_gap;
1538        let cols_free = (content_w - cols_total).max(0);
1539        let justify_content_grid = node
1540            .value("justify-content")
1541            .unwrap_or_default()
1542            .trim()
1543            .to_lowercase();
1544        let (col_start_offset, col_extra_gap) = if cols_free > 0 && n_cols_eff > 0 {
1545            match justify_content_grid.as_str() {
1546                "end" | "flex-end" => (cols_free, 0),
1547                "center" => (cols_free / 2, 0),
1548                "space-between" if n_cols_eff > 1 => (0, cols_free / (n_cols_eff as i32 - 1)),
1549                "space-around" if n_cols_eff > 0 => {
1550                    let g = cols_free / n_cols_eff as i32;
1551                    (g / 2, g)
1552                }
1553                "space-evenly" if n_cols_eff > 0 => {
1554                    let g = cols_free / (n_cols_eff as i32 + 1);
1555                    (g, g)
1556                }
1557                _ => (0, 0),
1558            }
1559        } else {
1560            (0, 0)
1561        };
1562        let col_gap_eff = col_gap + col_extra_gap;
1563
1564        let n_rows = occ.len().max(row_tracks.len());
1565
1566        // ---- パス2: 子を仮レイアウト(列幅基準)して各行の自然高さを測定 ----
1567        // 行高さ(row_heights[r])を、明示トラックと span=1 アイテムの実測で決める。
1568        let mut row_heights: Vec<i32> = alloc::vec![0; n_rows];
1569        // まず明示 px / % トラック、および minmax の min_px 下限を反映(暗黙行は grid-auto-rows)。
1570        for (r, h) in row_heights.iter_mut().enumerate() {
1571            match eff_row_track(r) {
1572                Some(GridTrack::Px(px)) => *h = px.max(0),
1573                Some(GridTrack::MinMax { min_px, .. }) => *h = min_px.max(0),
1574                _ => {}
1575            }
1576        }
1577        // 子を一旦レイアウトして高さ実測(span=1 のみが行高さに寄与)。
1578        // ここでは x 位置だけ正しく与え、y は後で確定するため 0 起点で測る。
1579        let base_x = self.dimensions.content.x;
1580        let base_y = self.dimensions.content.y;
1581        for (idx, child) in self.children.iter_mut().enumerate() {
1582            if Self::child_is_out_of_flow(child) {
1583                continue;
1584            }
1585            let Some((_r, c, _rs, cs)) = placement[idx] else {
1586                continue;
1587            };
1588            let cell_w = grid_span_width(&col_widths, col_gap_eff, c, cs);
1589            let cell_cb = Dimensions {
1590                content: Rect {
1591                    x: base_x + col_start_offset + grid_col_x(&col_widths, col_gap_eff, c),
1592                    y: base_y,
1593                    width: cell_w,
1594                    height: 0,
1595                },
1596                ..Dimensions::default()
1597            };
1598            let sub_ctx = Self::prepare_child_subgrid_ctx(child, &col_widths, col_gap_eff, c, cs, cell_w);
1599            let is_sub = sub_ctx.is_some();
1600            let prev_ctx = if is_sub {
1601                set_subgrid_ctx(sub_ctx)
1602            } else {
1603                None
1604            };
1605            child.layout(cell_cb);
1606            if is_sub {
1607                set_subgrid_ctx(prev_ctx);
1608            }
1609            if let Some((num, den)) = box_aspect_ratio(child) {
1610                if num > 0.0 && den > 0.0 && !box_has_explicit(child, "height") {
1611                    let w = child.dimensions.content.width as f32;
1612                    let h = round_f32_to_i32(w * den / num).max(1);
1613                    child.dimensions.content.height = h;
1614                }
1615            }
1616        }
1617        // 行の自然高さ(auto トラック)を span=1 アイテムの実測から求める。
1618        for (idx, child) in self.children.iter().enumerate() {
1619            let Some((r, _c, rs, _cs)) = placement[idx] else {
1620                continue;
1621            };
1622            if rs != 1 {
1623                continue;
1624            }
1625            let explicit_px = matches!(eff_row_track(r), Some(GridTrack::Px(_)));
1626            if explicit_px {
1627                continue;
1628            }
1629            let ch = child.dimensions.margin_box().height;
1630            if let Some(h) = row_heights.get_mut(r) {
1631                *h = (*h).max(ch);
1632            }
1633            // minmax の max が px(非fr・非auto)なら上限でクランプ。
1634            if let Some(GridTrack::MinMax {
1635                max_px,
1636                max_is_fr,
1637                max_is_auto,
1638                ..
1639            }) = eff_row_track(r)
1640            {
1641                if !max_is_fr && !max_is_auto {
1642                    if let Some(h) = row_heights.get_mut(r) {
1643                        *h = (*h).min(max_px);
1644                    }
1645                }
1646            }
1647        }
1648        // span>1 アイテムが行高さ合計を超える場合、末尾行を伸ばす。
1649        for (idx, child) in self.children.iter().enumerate() {
1650            let Some((r, _c, rs, _cs)) = placement[idx] else {
1651                continue;
1652            };
1653            if rs <= 1 {
1654                continue;
1655            }
1656            let ch = child.dimensions.margin_box().height;
1657            let mut covered = 0i32;
1658            for rr in r..(r + rs).min(row_heights.len()) {
1659                covered += row_heights.get(rr).copied().unwrap_or(0);
1660            }
1661            covered += (rs.saturating_sub(1) as i32) * row_gap;
1662            if ch > covered {
1663                let last = (r + rs - 1).min(row_heights.len().saturating_sub(1));
1664                if let Some(h) = row_heights.get_mut(last) {
1665                    *h += ch - covered;
1666                }
1667            }
1668        }
1669
1670        // fr 行トラックの解決(コンテナに明示高さがある場合のみ余白を fr へ分配)。
1671        let mut container_h_explicit: Option<i32> = None;
1672        let height_val = len_value(node, "height");
1673        if let Some(h) = parse_length_value(height_val.clone())
1674            .and_then(|v| resolve_length_value(v, containing_block.content.height.max(0)))
1675        {
1676            let vert_non = self.dimensions.border.top
1677                + self.dimensions.border.bottom
1678                + self.dimensions.padding.top
1679                + self.dimensions.padding.bottom;
1680            container_h_explicit = Some(if box_sizing.trim() == "border-box" {
1681                (h - vert_non).max(0)
1682            } else {
1683                h.max(0)
1684            });
1685        }
1686        if let Some(ch) = container_h_explicit {
1687            // fr 係数(純 Fr + minmax の fr max)。
1688            let track_fr = |t: &GridTrack| -> f32 {
1689                match t {
1690                    GridTrack::Fr(f) => *f,
1691                    GridTrack::MinMax {
1692                        max_fr, max_is_fr, ..
1693                    } if *max_is_fr => *max_fr,
1694                    _ => 0.0,
1695                }
1696            };
1697            // 暗黙行(grid-auto-rows)も含めて全行を走査。
1698            let mut fr_total = 0.0f32;
1699            for r in 0..n_rows {
1700                if let Some(t) = eff_row_track(r) {
1701                    fr_total += track_fr(&t);
1702                }
1703            }
1704            if fr_total > 0.0 {
1705                let mut used = 0i32;
1706                for (r, h) in row_heights.iter().enumerate() {
1707                    let is_fr = eff_row_track(r).map(|t| track_fr(&t)).unwrap_or(0.0) > 0.0;
1708                    if !is_fr {
1709                        used += *h;
1710                    }
1711                }
1712                used += (n_rows.saturating_sub(1) as i32) * row_gap;
1713                let free = (ch - used).max(0);
1714                for r in 0..n_rows {
1715                    let Some(t) = eff_row_track(r) else { continue };
1716                    let f = track_fr(&t);
1717                    if f > 0.0 {
1718                        let share = round_f32_to_i32(free as f32 * f / fr_total);
1719                        // minmax の min_px 下限を保証。
1720                        let floor = match t {
1721                            GridTrack::MinMax { min_px, .. } => min_px,
1722                            _ => 0,
1723                        };
1724                        if let Some(rh) = row_heights.get_mut(r) {
1725                            *rh = share.max(floor);
1726                        }
1727                    }
1728                }
1729            }
1730        }
1731
1732        // グリッドコンテナの `align-content`: 行トラック総高さがコンテナの明示
1733        // 高さより小さい場合に、トラック集合全体を交差軸(行軸)方向にどう配置
1734        // するか(`justify-content` の行版。コンテナに明示高さが無い場合は
1735        // 高さがトラック合計そのものになるため余剰は常に0で無関係)。
1736        let rows_total_h: i32 =
1737            row_heights.iter().sum::<i32>() + (n_rows.saturating_sub(1) as i32) * row_gap;
1738        let rows_free = container_h_explicit
1739            .map(|ch| (ch - rows_total_h).max(0))
1740            .unwrap_or(0);
1741        let align_content_grid = node
1742            .value("align-content")
1743            .unwrap_or_default()
1744            .trim()
1745            .to_lowercase();
1746        let (row_start_offset, row_extra_gap) = if rows_free > 0 && n_rows > 0 {
1747            match align_content_grid.as_str() {
1748                "end" | "flex-end" => (rows_free, 0),
1749                "center" => (rows_free / 2, 0),
1750                "space-between" if n_rows > 1 => (0, rows_free / (n_rows as i32 - 1)),
1751                "space-around" if n_rows > 0 => {
1752                    let g = rows_free / n_rows as i32;
1753                    (g / 2, g)
1754                }
1755                "space-evenly" if n_rows > 0 => {
1756                    let g = rows_free / (n_rows as i32 + 1);
1757                    (g, g)
1758                }
1759                _ => (0, 0),
1760            }
1761        } else {
1762            (0, 0)
1763        };
1764        let row_gap_eff = row_gap + row_extra_gap;
1765
1766        // 各行 top の累積(row_gap_eff 込み)。
1767        let mut row_tops: Vec<i32> = alloc::vec![0; n_rows + 1];
1768        for r in 0..n_rows {
1769            let h = row_heights.get(r).copied().unwrap_or(0);
1770            row_tops[r + 1] = row_tops[r] + h + if r + 1 < n_rows { row_gap_eff } else { 0 };
1771        }
1772
1773        // ---- パス3: 確定行 y で再レイアウト ----
1774        for (idx, child) in self.children.iter_mut().enumerate() {
1775            if Self::child_is_out_of_flow(child) {
1776                // out-of-flow(absolute/fixed)はセルを消費しない。サイズ確定のため
1777                // レイアウトのみ行い、最終位置は apply_positioned_offsets で確定する。
1778                let oof_cb = Dimensions {
1779                    content: Rect {
1780                        x: base_x,
1781                        y: base_y,
1782                        width: content_w,
1783                        height: 0,
1784                    },
1785                    ..Dimensions::default()
1786                };
1787                child.layout(oof_cb);
1788                continue;
1789            }
1790            let Some((r, c, rs, cs)) = placement[idx] else {
1791                continue;
1792            };
1793            let cell_w = grid_span_width(&col_widths, col_gap_eff, c, cs);
1794            let cell_x = base_x + col_start_offset + grid_col_x(&col_widths, col_gap_eff, c);
1795            let cell_y = base_y + row_start_offset + row_tops.get(r).copied().unwrap_or(0);
1796            // セル高さ = span 行の高さ合計(行間 gap 込み)。
1797            let mut cell_h = 0i32;
1798            for rr in r..(r + rs).min(row_heights.len()) {
1799                cell_h += row_heights.get(rr).copied().unwrap_or(0);
1800            }
1801            cell_h += (rs.saturating_sub(1) as i32) * row_gap;
1802
1803            // アイテムの整列を解決(justify=列/インライン軸、align=行/ブロック軸)。
1804            let j = grid_item_align(child, "justify-self", &justify_items);
1805            let a = grid_item_align(child, "align-self", &align_items);
1806
1807            // 列幅をコンテナ幅として測る。stretch(既定)は子がセル幅を埋める。
1808            let cell_cb = Dimensions {
1809                content: Rect {
1810                    x: cell_x,
1811                    y: cell_y,
1812                    width: cell_w,
1813                    height: 0,
1814                },
1815                ..Dimensions::default()
1816            };
1817            let sub_ctx = Self::prepare_child_subgrid_ctx(child, &col_widths, col_gap_eff, c, cs, cell_w);
1818            let is_sub = sub_ctx.is_some();
1819            let prev_ctx = if is_sub {
1820                set_subgrid_ctx(sub_ctx)
1821            } else {
1822                None
1823            };
1824            child.layout(cell_cb);
1825            if is_sub {
1826                set_subgrid_ctx(prev_ctx);
1827            }
1828            if let Some((num, den)) = box_aspect_ratio(child) {
1829                if num > 0.0 && den > 0.0 && !box_has_explicit(child, "height") {
1830                    let w = child.dimensions.content.width as f32;
1831                    let h = round_f32_to_i32(w * den / num).max(1);
1832                    child.dimensions.content.height = h;
1833                }
1834            }
1835
1836            // align:stretch かつ高さ未指定なら、セル高さへ伸ばす(aspect-ratio 指定要素は除く)。
1837            if a == "stretch" && !box_has_explicit(child, "height") && box_aspect_ratio(child).is_none() && cell_h > 0 {
1838                let mb = child.dimensions.margin_box();
1839                let non_content = mb.height - child.dimensions.content.height;
1840                child.dimensions.content.height = (cell_h - non_content).max(0);
1841            }
1842
1843            // セル内整列(インライン軸=justify, ブロック軸=align)。
1844            // 幅方向の整列は、明示 width を持つ(=セル幅より狭い余地がある)アイテムにのみ
1845            // 適用する。width:auto のブロックはセル幅を埋めるため整列の余地が無く start 相当。
1846            let mb = child.dimensions.margin_box();
1847            let dx = if j == "stretch" || !box_has_explicit(child, "width") {
1848                0
1849            } else {
1850                let free = (cell_w - mb.width).max(0);
1851                match j {
1852                    "center" => free / 2,
1853                    "end" => free,
1854                    _ => 0, // start
1855                }
1856            };
1857            // 高さ方向の整列。stretch 以外では高さ未指定の項目も中身の高さのままなので、
1858            // セルとの差を揃えに使う(stretch は上で伸ばし済み)。
1859            // 【2026-10-04】以前は明示 height を持つ項目にしか適用しておらず、
1860            // sugi-lab.net トップの about 欄(`align-items:center`)の写真の箱
1861            // (高さは画像で決まる)が文章の上端に揃っていた。
1862            let dy = if a == "stretch" {
1863                0
1864            } else {
1865                let free = (cell_h - mb.height).max(0);
1866                match a {
1867                    "center" => free / 2,
1868                    "end" => free,
1869                    _ => 0, // start
1870                }
1871            };
1872            if dx != 0 || dy != 0 {
1873                shift_layout_box(child, dx, dy);
1874            }
1875        }
1876
1877        // コンテナ高さ。
1878        let natural_h = row_tops.get(n_rows).copied().unwrap_or(0).max(0);
1879        self.dimensions.content.height = container_h_explicit.unwrap_or(natural_h);
1880
1881        // positioned 子要素の配置(grid コンテナを包含ブロック起点にできる)。
1882        let saved_abs_cb = self.establish_abs_cb();
1883        self.apply_positioned_offsets();
1884        Self::restore_abs_cb(saved_abs_cb);
1885    }
1886
1887    fn layout_table(&mut self, containing_block: Dimensions) {
1888        self.apply_box_model_styles_with_base(containing_block.content.width);
1889
1890        let h_non_content = self.dimensions.margin.left
1891            + self.dimensions.margin.right
1892            + self.dimensions.border.left
1893            + self.dimensions.border.right
1894            + self.dimensions.padding.left
1895            + self.dimensions.padding.right;
1896        let avail = (containing_block.content.width - h_non_content).max(1);
1897
1898        // テーブル幅: width 指定があれば尊重、なければ利用幅
1899        let table_width = match &self.box_type {
1900            BoxType::TableNode(node) => match parse_length_value(len_value(node, "width")) {
1901                Some(LengthValue::Px(px)) => px.max(1).min(avail),
1902                Some(LengthValue::Percent(pct)) => round_f32_to_i32(avail as f32 * pct).max(1),
1903                _ => avail,
1904            },
1905            _ => avail,
1906        };
1907
1908        // 位置 (block と同じ規約)
1909        let flow_y = containing_block.content.y
1910            + containing_block.content.height
1911            + self.dimensions.margin.top;
1912        self.dimensions.content.x = containing_block.content.x
1913            + self.dimensions.margin.left
1914            + self.dimensions.border.left
1915            + self.dimensions.padding.left;
1916        self.dimensions.content.y =
1917            flow_y + self.dimensions.border.top + self.dimensions.padding.top;
1918        self.dimensions.content.width = table_width;
1919
1920        let base_x = self.dimensions.content.x;
1921
1922        // border-collapse: collapse 時は隣接セルの境界を 1px 重ねて二重線を解消する。
1923        let collapse = match &self.box_type {
1924            BoxType::TableNode(node) => node
1925                .value("border-collapse")
1926                .map(|v| v.trim().eq_ignore_ascii_case("collapse"))
1927                .unwrap_or(false),
1928            _ => false,
1929        };
1930        let collapse_px: i32 = if collapse { 1 } else { 0 };
1931
1932        // table-layout: fixed 時は最初の行の各セルの明示 width のみで列幅を決定し、
1933        // 内容の計測(intrinsic_inline_size)を行わない(仕様通り、パフォーマンス目的の機能)。
1934        // 未指定の列は auto 列として残りの利用可能幅を均等に分け合う。
1935        let table_layout_fixed = match &self.box_type {
1936            BoxType::TableNode(node) => node
1937                .value("table-layout")
1938                .map(|v| v.trim().eq_ignore_ascii_case("fixed"))
1939                .unwrap_or(false),
1940            _ => false,
1941        };
1942
1943        // border-spacing: セル間の隙間(px)。border-collapse:collapse では無効。
1944        // `10px`(水平・垂直共通)/ `10px 5px`(水平 垂直)の2形式に対応。
1945        let (h_spacing, v_spacing): (i32, i32) = if collapse {
1946            (0, 0)
1947        } else {
1948            match &self.box_type {
1949                BoxType::TableNode(node) => match node.value("border-spacing") {
1950                    Some(v) => {
1951                        let toks: Vec<&str> = v.split_whitespace().collect();
1952                        let px = |s: &str| -> i32 {
1953                            match parse_length_value(Some(String::from(s))) {
1954                                Some(LengthValue::Px(px)) => px.max(0),
1955                                _ => 0,
1956                            }
1957                        };
1958                        match toks.as_slice() {
1959                            [h] => (px(h), px(h)),
1960                            [h, w] => (px(h), px(w)),
1961                            _ => (0, 0),
1962                        }
1963                    }
1964                    None => (0, 0),
1965                },
1966                _ => (0, 0),
1967            }
1968        };
1969
1970        // caption (<caption>) は既定でテーブル本体の上に積むが、`caption-side:bottom` なら
1971        // 本体の下へ配置する。後者は行の高さが確定してから配置する必要があるため、
1972        // 先頭 caption 自身の caption-side を見て分岐する。
1973        let caption_indices: Vec<usize> = (0..self.children.len())
1974            .filter(|&i| box_tag_name(&self.children[i]) == Some("caption"))
1975            .collect();
1976        let caption_side_bottom = caption_indices
1977            .first()
1978            .and_then(|&ci| box_style_value(&self.children[ci], "caption-side"))
1979            .map(|v| v.trim().eq_ignore_ascii_case("bottom"))
1980            .unwrap_or(false);
1981        let mut top_offset = 0i32;
1982        if !caption_side_bottom {
1983            let y = self.dimensions.content.y;
1984            top_offset = self.layout_captions_at(&caption_indices, base_x, y, table_width);
1985        }
1986        let rows_top = self.dimensions.content.y + top_offset;
1987
1988        // 行 (tr) を抽出。セルは行の children。
1989        // `visibility:collapse` の行は display:none 相当に扱い、他の行を詰めて配置する
1990        // (実ブラウザは列幅の再計算まではしない簡略実装だが、行自体を除去する点は同じ)。
1991        let row_indices: Vec<usize> = (0..self.children.len())
1992            .filter(|&i| {
1993                matches!(self.children[i].box_type, BoxType::TableRowNode(_))
1994                    && box_style_value(&self.children[i], "visibility")
1995                        .map(|v| !v.trim().eq_ignore_ascii_case("collapse"))
1996                        .unwrap_or(true)
1997            })
1998            .collect();
1999        let nrows = row_indices.len();
2000
2001        // --- グリッド構築: colspan/rowspan を解決して各セルの (列, 行, span) を確定 ---
2002        struct CellPlace {
2003            row_ord: usize,
2004            cell_ord: usize,
2005            col: usize,
2006            colspan: usize,
2007            rowspan: usize,
2008        }
2009        let mut occ: Vec<Vec<bool>> = Vec::new();
2010        let mut places: Vec<CellPlace> = Vec::new();
2011        let mut ncols = 0usize;
2012        for (row_ord, &ri) in row_indices.iter().enumerate() {
2013            let ncells = self.children[ri].children.len();
2014            let mut c = 0usize;
2015            for cell_ord in 0..ncells {
2016                // rowspan で先行行から占有されている列を読み飛ばす
2017                while occ_is(&occ, row_ord, c) {
2018                    c += 1;
2019                }
2020                let (cs, rs) = cell_spans(&self.children[ri].children[cell_ord]);
2021                let rs = rs.min(nrows - row_ord); // テーブル末尾を超える rowspan はクランプ
2022                for rr in row_ord..(row_ord + rs) {
2023                    for cc in c..(c + cs) {
2024                        occ_mark(&mut occ, rr, cc);
2025                    }
2026                }
2027                places.push(CellPlace {
2028                    row_ord,
2029                    cell_ord,
2030                    col: c,
2031                    colspan: cs,
2032                    rowspan: rs.max(1),
2033                });
2034                c += cs;
2035                ncols = ncols.max(c);
2036            }
2037        }
2038
2039        if ncols == 0 {
2040            if caption_side_bottom {
2041                let y = self.dimensions.content.y + top_offset;
2042                top_offset += self.layout_captions_at(&caption_indices, base_x, y, table_width);
2043            }
2044            self.dimensions.content.height = top_offset;
2045            return;
2046        }
2047
2048        // --- 列幅: まず colspan==1 のセルで基礎幅、次に colspan>1 の不足分を分配 ---
2049        let mut col_w = alloc::vec![0i32; ncols];
2050        let mut is_fixed_col = alloc::vec![false; ncols];
2051        let pad = 24; // 既定 padding 4px*2 + 余裕
2052        let usable_width = (table_width - h_spacing * (ncols as i32 - 1).max(0)).max(1);
2053
2054        if table_layout_fixed {
2055            // table-layout:fixed: 最初の行(row_ord==0)の colspan==1 セルの明示 width のみで
2056            // 列幅を決定し、内容の計測は一切行わない(仕様通り、パフォーマンス目的の機能)。
2057            for p in &places {
2058                if p.row_ord != 0 || p.colspan != 1 {
2059                    continue;
2060                }
2061                let cell = &self.children[row_indices[p.row_ord]].children[p.cell_ord];
2062                if let Some(w) = box_style_value(cell, "width") {
2063                    match parse_length_value(Some(w)) {
2064                        Some(LengthValue::Px(px)) => {
2065                            col_w[p.col] = px.max(1);
2066                            is_fixed_col[p.col] = true;
2067                        }
2068                        Some(LengthValue::Percent(pct)) => {
2069                            col_w[p.col] = round_f32_to_i32(usable_width as f32 * pct).max(1);
2070                            is_fixed_col[p.col] = true;
2071                        }
2072                        _ => {}
2073                    }
2074                }
2075            }
2076            // auto 列(明示 width の無い列)は、固定列を除いた残り幅を均等に分け合う。
2077            let fixed_sum: i32 = (0..ncols).filter(|&c| is_fixed_col[c]).map(|c| col_w[c]).sum();
2078            let auto_cols: alloc::vec::Vec<usize> =
2079                (0..ncols).filter(|&c| !is_fixed_col[c]).collect();
2080            if !auto_cols.is_empty() {
2081                let remaining = (usable_width - fixed_sum).max(0);
2082                let auto_w = (remaining / auto_cols.len() as i32).max(1);
2083                let mut assigned = 0i32;
2084                for (i, &c) in auto_cols.iter().enumerate() {
2085                    if i == auto_cols.len() - 1 {
2086                        col_w[c] = (remaining - assigned).max(1);
2087                    } else {
2088                        col_w[c] = auto_w;
2089                        assigned += auto_w;
2090                    }
2091                }
2092            }
2093        } else {
2094            for p in &places {
2095                if p.colspan != 1 {
2096                    continue;
2097                }
2098                let (cw, _) = self.children[row_indices[p.row_ord]].children[p.cell_ord]
2099                    .intrinsic_inline_size(table_width);
2100                col_w[p.col] = col_w[p.col].max(cw + pad);
2101            }
2102            for p in &places {
2103                if p.colspan <= 1 {
2104                    continue;
2105                }
2106                let (cw, _) = self.children[row_indices[p.row_ord]].children[p.cell_ord]
2107                    .intrinsic_inline_size(table_width);
2108                let need = cw + pad;
2109                let cur: i32 = col_w[p.col..p.col + p.colspan].iter().sum();
2110                if need > cur {
2111                    let deficit = need - cur;
2112                    let add = deficit / p.colspan as i32;
2113                    for cc in p.col..p.col + p.colspan {
2114                        col_w[cc] += add;
2115                    }
2116                    col_w[p.col] += deficit - add * p.colspan as i32; // 端数は先頭列へ
2117                }
2118            }
2119
2120            // 総和を利用可能幅に合わせる (超過→比例縮小 / 不足→均等加算、端数補正込み)。
2121            let total: i32 = col_w.iter().sum::<i32>().max(1);
2122            if total > usable_width {
2123                let mut assigned = 0i32;
2124                for (c, w) in col_w.iter_mut().enumerate() {
2125                    if c == ncols - 1 {
2126                        *w = (usable_width - assigned).max(1);
2127                    } else {
2128                        let scaled = ((*w as i64 * usable_width as i64) / total as i64).max(1) as i32;
2129                        *w = scaled;
2130                        assigned += scaled;
2131                    }
2132                }
2133            } else if total < usable_width {
2134                let diff = usable_width - total;
2135                let add = diff / ncols as i32;
2136                let mut distributed = 0i32;
2137                for c in 0..ncols {
2138                    if c == ncols - 1 {
2139                        col_w[c] += diff - distributed;
2140                    } else {
2141                        col_w[c] += add;
2142                        distributed += add;
2143                    }
2144                }
2145            }
2146        }
2147        // 列開始 x オフセット(列間に border-spacing を挿入)
2148        let mut col_x = alloc::vec![0i32; ncols];
2149        let mut acc = 0i32;
2150        for c in 0..ncols {
2151            col_x[c] = acc;
2152            acc += col_w[c];
2153            if c + 1 < ncols {
2154                acc += h_spacing;
2155            }
2156        }
2157        let span_w = |col: usize, colspan: usize| -> i32 {
2158            col_w[col..(col + colspan).min(ncols)].iter().sum::<i32>()
2159                + h_spacing * colspan.saturating_sub(1) as i32
2160        };
2161
2162        // --- 高さ計測パス: 各セルを確定列幅でレイアウトして外形高さを得る ---
2163        let mut place_h = alloc::vec![0i32; places.len()];
2164        for (pi, p) in places.iter().enumerate() {
2165            let cw_total = span_w(p.col, p.colspan);
2166            let cell = &mut self.children[row_indices[p.row_ord]].children[p.cell_ord];
2167            let mut cb = Dimensions::default();
2168            cb.content.x = base_x + col_x[p.col];
2169            cb.content.y = rows_top;
2170            cb.content.height = 0;
2171            cb.content.width = cw_total;
2172            cell.layout(cb);
2173            place_h[pi] = cell.dimensions.margin_box().height;
2174        }
2175        // 行高さ: rowspan==1 で確定、rowspan>1 は不足分を最終行へ加算
2176        let mut row_h = alloc::vec![0i32; nrows];
2177        for (pi, p) in places.iter().enumerate() {
2178            if p.rowspan == 1 {
2179                row_h[p.row_ord] = row_h[p.row_ord].max(place_h[pi]);
2180            }
2181        }
2182        for (pi, p) in places.iter().enumerate() {
2183            if p.rowspan <= 1 {
2184                continue;
2185            }
2186            let end = (p.row_ord + p.rowspan).min(nrows);
2187            let cur: i32 = row_h[p.row_ord..end].iter().sum();
2188            if place_h[pi] > cur {
2189                row_h[end - 1] += place_h[pi] - cur;
2190            }
2191        }
2192        // 行 y 先頭オフセット (テーブル本体内の相対、行間に border-spacing を挿入)
2193        let mut row_y = alloc::vec![0i32; nrows];
2194        let mut accy = 0i32;
2195        for r in 0..nrows {
2196            row_y[r] = accy;
2197            accy += row_h[r];
2198            if r + 1 < nrows {
2199                accy += v_spacing;
2200            }
2201        }
2202        let rows_total = accy;
2203
2204        // --- 確定パス: 各セルを最終位置で再レイアウトし、列幅・スパン行高さに合わせる ---
2205        for p in places.iter() {
2206            let cw_total = span_w(p.col, p.colspan);
2207            let end_r = (p.row_ord + p.rowspan).min(nrows);
2208            let span_h: i32 = row_h[p.row_ord..end_r].iter().sum();
2209            let x0 = base_x + col_x[p.col] - collapse_px * p.col as i32;
2210            let y0 = rows_top + row_y[p.row_ord] - collapse_px * p.row_ord as i32;
2211            let cell = &mut self.children[row_indices[p.row_ord]].children[p.cell_ord];
2212            let mut cb = Dimensions::default();
2213            cb.content.x = x0;
2214            cb.content.y = y0;
2215            cb.content.height = 0;
2216            cb.content.width = cw_total;
2217            cell.layout(cb); // 子要素を最終位置で配置し直す
2218            let w_extra = cell.dimensions.padding.left
2219                + cell.dimensions.padding.right
2220                + cell.dimensions.border.left
2221                + cell.dimensions.border.right;
2222            let h_extra = cell.dimensions.padding.top
2223                + cell.dimensions.padding.bottom
2224                + cell.dimensions.border.top
2225                + cell.dimensions.border.bottom;
2226            // vertical-align: middle/bottom — 行がセルの自然な高さより高い場合、
2227            // 余った縦方向の空間分だけ子孫全体を下へ平行移動する(既定/top/baseline は無移動)。
2228            let natural_h = cell.dimensions.content.height;
2229            let target_h = (span_h - h_extra).max(natural_h);
2230            let extra_space = (target_h - natural_h).max(0);
2231            if extra_space > 0 {
2232                let va = match &cell.box_type {
2233                    BoxType::TableCellNode(node) => node.value("vertical-align").unwrap_or_default(),
2234                    _ => String::new(),
2235                };
2236                let dy = match va.trim() {
2237                    "middle" | "center" => extra_space / 2,
2238                    "bottom" => extra_space,
2239                    _ => 0, // top / baseline(簡略化) / 未指定
2240                };
2241                // セル自身の box(背景・境界)は行の先頭に留め、中身(子要素)だけを下げる。
2242                for child in &mut cell.children {
2243                    child.translate_y(dy);
2244                }
2245            }
2246            cell.dimensions.content.width = (cw_total - w_extra).max(1);
2247            cell.dimensions.content.height = target_h;
2248        }
2249
2250        // 行ボックスの寸法を設定 (背景・境界の整列用)
2251        for (row_ord, &ri) in row_indices.iter().enumerate() {
2252            let row = &mut self.children[ri];
2253            row.dimensions.content.x = base_x;
2254            row.dimensions.content.y = rows_top + row_y[row_ord] - collapse_px * row_ord as i32;
2255            row.dimensions.content.width = table_width;
2256            row.dimensions.content.height = row_h[row_ord];
2257            // セル(行の直下子)の relative/absolute/fixed オフセット + transform を適用。
2258            // 行を包含ブロックとして共有後処理を流用する。
2259            let saved_row_cb = row.establish_abs_cb();
2260            row.apply_positioned_offsets();
2261            Self::restore_abs_cb(saved_row_cb);
2262        }
2263
2264        // collapse 時は重ね合わせぶん全体が縮む
2265        let collapse_shrink = collapse_px * (nrows.saturating_sub(1)) as i32;
2266        let rows_height = (rows_total - collapse_shrink).max(0);
2267        if caption_side_bottom {
2268            let y = self.dimensions.content.y + top_offset + rows_height;
2269            top_offset += self.layout_captions_at(&caption_indices, base_x, y, table_width);
2270        }
2271        self.dimensions.content.height = top_offset + rows_height;
2272
2273        // positioned 直下子要素の配置(table を包含ブロック起点にできる)。
2274        // 通常フローのセル/行はすでに配置済み。relative オフセットや、table が
2275        // positioned のときの子孫 absolute 解決のため共有後処理を適用する。
2276        let saved_abs_cb = self.establish_abs_cb();
2277        self.apply_positioned_offsets();
2278        Self::restore_abs_cb(saved_abs_cb);
2279    }
2280
2281    fn layout_flex(&mut self, containing_block: Dimensions) {
2282        self.apply_box_model_styles_with_base(containing_block.content.width);
2283
2284        let (width_val, min_width_val, max_width_val, height_val, box_sizing, h_auto) =
2285            match &self.box_type {
2286                BoxType::FlexNode(node) => (
2287                    len_value(node, "width"),
2288                    len_value(node, "min-width"),
2289                    len_value(node, "max-width"),
2290                    len_value(node, "height"),
2291                    node.value("box-sizing").unwrap_or_default(),
2292                    margin_is_h_auto(node),
2293                ),
2294                _ => return,
2295            };
2296        // padding + border のみ(box-sizing:border-box / margin:auto センタリング用)。
2297        let inner_non_content = self.dimensions.border.left
2298            + self.dimensions.border.right
2299            + self.dimensions.padding.left
2300            + self.dimensions.padding.right;
2301
2302        let horizontal_non_content = self.dimensions.margin.left
2303            + self.dimensions.margin.right
2304            + self.dimensions.border.left
2305            + self.dimensions.border.right
2306            + self.dimensions.padding.left
2307            + self.dimensions.padding.right;
2308        let available_width = containing_block.content.width.max(1);
2309        let explicit_width =
2310            !matches!(parse_length_value(width_val.clone()), Some(LengthValue::Auto) | None);
2311
2312        let mut content_width = match parse_length_value(width_val) {
2313            Some(LengthValue::Auto) | None => (available_width - horizontal_non_content).max(1),
2314            Some(LengthValue::Px(px)) => px.max(1),
2315            Some(LengthValue::Percent(pct)) => {
2316                round_f32_to_i32(available_width as f32 * pct).max(1)
2317            }
2318            Some(LengthValue::MinContent) => {
2319                let (minc, _maxc) = self.block_intrinsic_widths();
2320                minc.max(1)
2321            }
2322            Some(LengthValue::MaxContent) => {
2323                let (_minc, maxc) = self.block_intrinsic_widths();
2324                maxc.max(1)
2325            }
2326            Some(LengthValue::FitContent) => {
2327                let (minc, maxc) = self.block_intrinsic_widths();
2328                let avail = (available_width - horizontal_non_content).max(0);
2329                avail.min(maxc).max(minc).max(1)
2330            }
2331            Some(LengthValue::FitContentWith(lim_box)) => {
2332                let avail = (available_width - horizontal_non_content).max(0);
2333                let limit = resolve_length_value_with_font(*lim_box, avail, self.font_size())
2334                    .unwrap_or(avail);
2335                let (minc, maxc) = self.block_intrinsic_widths();
2336                maxc.min(limit.max(minc)).max(1)
2337            }
2338            Some(LengthValue::MathExpr(expr)) => {
2339                crate::os_lib::css::eval_css_math_with_font(&expr, available_width, self.font_size())
2340                    .unwrap_or((available_width - horizontal_non_content).max(1))
2341                    .max(1)
2342            }
2343        };
2344        // box-sizing: border-box → 指定 width は border-box 全体(layout_block と同じ)。
2345        if explicit_width && box_sizing.trim() == "border-box" {
2346            content_width = (content_width - inner_non_content).max(1);
2347        }
2348
2349        if let Some(min_width) = parse_length_value(min_width_val)
2350            .and_then(|v| resolve_length_value_with_font(v, available_width, self.font_size()))
2351        {
2352            content_width = content_width.max(min_width);
2353        }
2354        if let Some(max_width) = parse_length_value(max_width_val)
2355            .and_then(|v| resolve_length_value_with_font(v, available_width, self.font_size()))
2356        {
2357            content_width = content_width.min(max_width);
2358        }
2359        self.dimensions.content.width = content_width.max(1);
2360
2361        // margin: auto(左右)→ 水平センタリング(layout_block と同じ。
2362        // 実サイトの `.container{max-width:..;margin:0 auto}` + `display:flex` がこれ)。
2363        if h_auto {
2364            let free = (available_width - self.dimensions.content.width - inner_non_content).max(0);
2365            self.dimensions.margin.left = free / 2;
2366            self.dimensions.margin.right = free / 2;
2367        }
2368
2369        let flow_y = containing_block.content.y
2370            + containing_block.content.height
2371            + self.dimensions.margin.top;
2372        self.dimensions.content.x = containing_block.content.x
2373            + self.dimensions.margin.left
2374            + self.dimensions.border.left
2375            + self.dimensions.padding.left;
2376        self.dimensions.content.y =
2377            flow_y + self.dimensions.border.top + self.dimensions.padding.top;
2378
2379        // 子の `height:%` の基準(明示 px 高さのみ。border-box なら content 高へ換算)。
2380        let definite_h = self.explicit_content_height_px(height_val.clone()).unwrap_or(-1);
2381        let _pct_h_guard = PctBaseHGuard::set(definite_h);
2382
2383        let (flex_dir, justify_content, align_items, align_content, row_gap, col_gap, flex_wrap) =
2384            match &self.box_type {
2385                BoxType::FlexNode(node) => {
2386                    let (rg, cg) = parse_flex_gap(node, self.dimensions.content.width);
2387                    (
2388                        node.value("flex-direction")
2389                            .unwrap_or_else(|| String::from("row"))
2390                            .trim()
2391                            .to_lowercase(),
2392                        node.value("justify-content")
2393                            .unwrap_or_else(|| String::from("flex-start"))
2394                            .trim()
2395                            .to_lowercase(),
2396                        node.value("align-items")
2397                            .unwrap_or_else(|| String::from("stretch"))
2398                            .trim()
2399                            .to_lowercase(),
2400                        node.value("align-content")
2401                            .unwrap_or_else(|| String::from("stretch"))
2402                            .trim()
2403                            .to_lowercase(),
2404                        rg,
2405                        cg,
2406                        node.value("flex-wrap")
2407                            .unwrap_or_else(|| String::from("nowrap"))
2408                            .trim()
2409                            .to_lowercase(),
2410                    )
2411                }
2412                _ => return,
2413            };
2414
2415        // row / row-reverse は主軸が水平。column / column-reverse は垂直。
2416        let is_row = flex_dir == "row" || flex_dir == "row-reverse";
2417        let is_reverse = flex_dir == "row-reverse" || flex_dir == "column-reverse";
2418        // flex-wrap: wrap / wrap-reverse のとき複数行へ折り返す。
2419        let do_wrap = flex_wrap == "wrap" || flex_wrap == "wrap-reverse";
2420        // 主軸方向の gap(row なら列間=column-gap、column なら行間=row-gap)。
2421        let gap_main = if is_row { col_gap } else { row_gap };
2422        // 交差軸方向の gap(行間)。
2423        let gap_cross = if is_row { row_gap } else { col_gap };
2424
2425        if do_wrap {
2426            self.layout_flex_wrapped(
2427                is_row,
2428                is_reverse,
2429                flex_wrap == "wrap-reverse",
2430                &justify_content,
2431                &align_items,
2432                &align_content,
2433                gap_main,
2434                gap_cross,
2435                height_val.clone(),
2436            );
2437            return;
2438        }
2439
2440        // Layout all children as if they were inline-blocks (or blocks) with 0 initial x/y to measure them
2441        let mut total_main = 0;
2442        let mut max_cross = 0;
2443        // out-of-flow(absolute/fixed)はフレックスラインに参加しない。サイズ確定のため
2444        // レイアウトはするが、主軸合計・交差軸最大・justify の item 数には数えない。
2445        let oof: Vec<bool> = self
2446            .children
2447            .iter()
2448            .map(|c| Self::child_is_out_of_flow(c))
2449            .collect();
2450        let n = oof.iter().filter(|&&b| !b).count();
2451
2452        // flex-basis の % 解決基準(コンテナ主軸長)。row は content 幅、column は明示高さ。
2453        let basis_main_ref = if is_row {
2454            self.dimensions.content.width
2455        } else {
2456            match parse_length_value(height_val.clone()) {
2457                Some(LengthValue::Px(px)) => px,
2458                _ => 0,
2459            }
2460        };
2461
2462        for (ci, child) in self.children.iter_mut().enumerate() {
2463            let mut cb = self.dimensions.clone();
2464            cb.content.x = 0;
2465            cb.content.y = 0;
2466            cb.content.height = 0; // unlimited
2467            child.layout(cb);
2468            if oof.get(ci).copied().unwrap_or(false) {
2469                continue;
2470            }
2471            // flex アイテムの主軸 auto margin は flex 側で処理する。layout_block が
2472            // margin:auto を「ブロック水平センタリング」として margin.left/right の両方へ
2473            // free/2 を入れてしまうため、主軸 auto margin がある辺方向の両端 margin を
2474            // 0 へ戻す(margin_box を実サイズに戻し、flex の auto 分配へ委ねる)。
2475            {
2476                let (lead, trail) = box_main_auto_margins(child, is_row);
2477                if lead || trail {
2478                    if is_row {
2479                        child.dimensions.margin.left = 0;
2480                        child.dimensions.margin.right = 0;
2481                    } else {
2482                        child.dimensions.margin.top = 0;
2483                        child.dimensions.margin.bottom = 0;
2484                    }
2485                }
2486            }
2487            // flex-basis 指定があれば主軸 content サイズを basis へ上書き(grow/shrink の基準)。
2488            if let Some(basis) = box_flex_basis(child, basis_main_ref) {
2489                if is_row {
2490                    child.dimensions.content.width = basis;
2491                } else {
2492                    child.dimensions.content.height = basis;
2493                }
2494            }
2495            // 【2026-09-04】幅指定が無い flex アイテムの基準幅は、
2496            // **自身の max-content 幅**(CSS Flexbox L1 §9.2「flex base size」)。
2497            //
2498            // 従来は「幅 auto のブロックとしてレイアウトした結果」=
2499            // 親いっぱいの幅をそのまま基準にしていた。その結果どの項目も
2500            // 基準が同じ値になり、縮小が**均等割り**になっていた。
2501            // 実サイト(www.sugi-lab.net)のナビ 8 項目が全部 88px に
2502            // 揃えられ、幅の合計が描画幅を超えてはみ出していた
2503            // (実測: container=924 なのに item0..7 すべて base=924)。
2504            // 正しくは項目ごとに基準が違うので、比例配分で
2505            // 短い項目は短いまま、長い項目が多く縮む。
2506            if is_row
2507                && box_flex_basis(child, basis_main_ref).is_none()
2508                && !box_has_explicit(child, "width")
2509            {
2510                let mut max_content = child.block_intrinsic_widths().1;
2511                // 【2026-09-26】基準サイズは min/max-width で挟む(§9.2 の
2512                // hypothetical main size)。`img{max-width:100%}` が自然幅のまま
2513                // 親をはみ出していた(実サイトのプロジェクト紹介画像)。
2514                let container_w = self.dimensions.content.width;
2515                let inner = border_box_inner_w(child);
2516                if let Some(mw) = box_len(child, "max-width", container_w) {
2517                    max_content = max_content.min((mw - inner).max(0));
2518                }
2519                if let Some(mw) = box_len(child, "min-width", container_w) {
2520                    max_content = max_content.max((mw - inner).max(0));
2521                }
2522                // 中身の無い(流れの中の子が無い)項目は内在幅 0 が正しい(従来は > 0 のときだけ
2523                // 縮め、空の項目が容器の幅を占めて margin:auto の余白を奪っていた。2026-10-18)。
2524                let no_in_flow = child.children.iter().all(|c| Self::child_is_out_of_flow(c));
2525                if (max_content > 0 || no_in_flow) && max_content != child.dimensions.content.width {
2526                    // 幅を変えたら、その幅でレイアウトをやり直す。
2527                    // 子は親いっぱいの幅で組まれているので、幅だけ
2528                    // 書き換えると中身が古い位置に取り残される
2529                    // (ナビが本来より右へずれて出た)。縮小側と同じ手当て。
2530                    let mut cb2 = self.dimensions.clone();
2531                    cb2.content.x = 0;
2532                    cb2.content.y = 0;
2533                    // 【2026-09-26 バグ修正】包含幅は content 幅 + 横の margin/border/padding。
2534                    // content 幅だけを渡すと layout が padding 等をもう一度引き、中身が
2535                    // その分だけ狭く組まれていた(ヒーロー見出しが 40px 狭くはみ出した)。
2536                    cb2.content.width = max_content + child.h_non_content();
2537                    cb2.content.height = 0; // unlimited
2538                    child.layout(cb2);
2539                    child.dimensions.content.width = max_content;
2540                }
2541            }
2542            let child_w = child.dimensions.margin_box().width;
2543            let child_h = child.dimensions.margin_box().height;
2544            if is_row {
2545                total_main += child_w;
2546                max_cross = max_cross.max(child_h);
2547            } else {
2548                total_main += child_h;
2549                max_cross = max_cross.max(child_w);
2550            }
2551        }
2552        // アイテム間の gap を主軸合計に加える(n-1 箇所)。
2553        if n > 1 {
2554            total_main += gap_main * (n as i32 - 1);
2555        }
2556
2557        let container_main = if is_row {
2558            self.dimensions.content.width
2559        } else {
2560            self.explicit_content_height_px(height_val.clone()).unwrap_or(total_main)
2561        };
2562
2563        let mut container_cross = if is_row {
2564            self.explicit_content_height_px(height_val.clone()).unwrap_or(max_cross)
2565        } else {
2566            self.dimensions.content.width
2567        };
2568
2569        if !is_row {
2570            self.dimensions.content.height = container_main;
2571        } else {
2572            self.dimensions.content.height = container_cross;
2573        }
2574
2575        // flex-grow / flex:N: grower を flex-basis 0 扱いにし、主軸の残余空間を grow 比で分配。
2576        // grower が無ければ何もしない(従来挙動を完全維持)。
2577        let grows: Vec<i32> = self
2578            .children
2579            .iter()
2580            .enumerate()
2581            .map(|(i, c)| {
2582                if oof.get(i).copied().unwrap_or(false) {
2583                    0
2584                } else {
2585                    box_flex_grow(c)
2586                }
2587            })
2588            .collect();
2589        let total_grow: i32 = grows.iter().sum();
2590        if total_grow > 0 {
2591            // grower の主軸 content を 0 とみなした基準長(非content分=padding/border/margin は残す)。
2592            let mut base_main = total_main;
2593            for (i, child) in self.children.iter().enumerate() {
2594                if grows.get(i).copied().unwrap_or(0) <= 0 {
2595                    continue;
2596                }
2597                base_main -= if is_row {
2598                    child.dimensions.content.width
2599                } else {
2600                    child.dimensions.content.height
2601                };
2602            }
2603            let free = (container_main - base_main).max(0);
2604            if free > 0 {
2605                let last_grow_idx = grows.iter().rposition(|g| *g > 0);
2606                let mut remaining = free;
2607                for (i, child) in self.children.iter_mut().enumerate() {
2608                    let g = grows.get(i).copied().unwrap_or(0);
2609                    if g <= 0 {
2610                        continue;
2611                    }
2612                    // 端数は最後の grower に寄せて合計を厳密一致させる。
2613                    let extra = if Some(i) == last_grow_idx {
2614                        remaining
2615                    } else {
2616                        free * g / total_grow
2617                    };
2618                    remaining -= extra;
2619                    if is_row {
2620                        child.dimensions.content.width = extra.max(0);
2621                    } else {
2622                        child.dimensions.content.height = extra.max(0);
2623                    }
2624                }
2625                total_main = base_main + free; // = container_main(justify 余白 0)
2626            }
2627        }
2628
2629        // flex-shrink: 主軸がコンテナを超過する場合、shrink 係数に比例して各アイテムを縮める。
2630        // grower がいた(既に container にフィット)場合はスキップ。CSS では基準サイズで
2631        // 重み付けするが、ここでは簡易に shrink 係数のみで余剰超過分を按分する。
2632        let overflow = total_main - container_main;
2633        if total_grow == 0 && overflow > 0 {
2634            let shrinks: Vec<i32> = self
2635                .children
2636                .iter()
2637                .enumerate()
2638                .map(|(i, c)| {
2639                    if oof.get(i).copied().unwrap_or(false) {
2640                        0
2641                    } else {
2642                        box_flex_shrink(c)
2643                    }
2644                })
2645                .collect();
2646            let total_shrink: i32 = shrinks.iter().sum();
2647            if total_shrink > 0 {
2648                // 【2026-07-28 規格対応】従来は `.max(0)` で 0 までしか止めておらず、
2649                // **自動最小サイズ(min-width:auto)を一切見ていなかった**。その結果
2650                // `.hero-content` が 240px まで縮み、14 文字の見出しが 3 行に
2651                // 過剰折り返しされてサブテキストと重なっていた。
2652                // 仕様・不変条件は `spec/flex_min_size.md`(CSS Flexbox L1 §4.5/§9.7)。
2653                let items: Vec<crate::os_lib::layout::flex_min::ShrinkItem> = self
2654                    .children
2655                    .iter()
2656                    .enumerate()
2657                    .map(|(i, c)| {
2658                        let base = if is_row {
2659                            c.dimensions.content.width
2660                        } else {
2661                            c.dimensions.content.height
2662                        };
2663                        // 自動最小サイズは min-content(分割できない最長語)で代用する近似。
2664                        let min = if is_row {
2665                            c.block_intrinsic_widths().0
2666                        } else {
2667                            0
2668                        };
2669                        crate::os_lib::layout::flex_min::ShrinkItem {
2670                            base: base.max(0),
2671                            min: min.clamp(0, base.max(0)),
2672                            shrink: shrinks.get(i).copied().unwrap_or(0).max(0),
2673                        }
2674                    })
2675                    .collect();
2676
2677                match crate::os_lib::layout::flex_min::distribute_shrink(&items, overflow.max(0)) {
2678                    Ok(sizes) => {
2679                        for (i, child) in self.children.iter_mut().enumerate() {
2680                            if let Some(&s) = sizes.get(i) {
2681                                if is_row {
2682                                    child.dimensions.content.width = s;
2683                                } else {
2684                                    child.dimensions.content.height = s;
2685                                }
2686                            }
2687                        }
2688                        // 【2026-09-04】縮めた子は、その幅でレイアウトをやり直す。
2689                        //
2690                        // 幅だけ書き換えても、テキストは**元の広い幅で組まれたまま**
2691                        // なので折り返さず、高さも更新されない。実サイト
2692                        // (www.sugi-lab.net)のヘッダで、本来 2 行に折り返して
2693                        // 収まるロゴとナビ項目が 1 行のままになり、ナビ 8 項目の
2694                        // うち 4 項目が画面外へ押し出されていた。
2695                        //
2696                        // 主軸が横のときだけでよい。縦のときに縮むのは高さで、
2697                        // 高さを変えても行の折り返しは変わらない。
2698                        if is_row {
2699                            for (i, child) in self.children.iter_mut().enumerate() {
2700                                if oof.get(i).copied().unwrap_or(false) {
2701                                    continue;
2702                                }
2703                                let Some(&s) = sizes.get(i) else {
2704                                    continue;
2705                                };
2706                                let base = items.get(i).map(|it| it.base).unwrap_or(s);
2707                                // 縮んでいない子はやり直さない(無駄な再計算を避ける)。
2708                                if s >= base {
2709                                    continue;
2710                                }
2711                                let mut cb = self.dimensions.clone();
2712                                cb.content.x = 0;
2713                                cb.content.y = 0;
2714                                // 基準サイズの組み直しと同じく、横の非 content 分を足して渡す
2715                                // (2026-09-26 まで padding 等が二重に引かれていた)。
2716                                cb.content.width = s + child.h_non_content();
2717                                cb.content.height = 0; // unlimited
2718                                child.layout(cb);
2719                                // やり直しで幅が戻ることがあるので、縮小後の値を残す。
2720                                child.dimensions.content.width = s;
2721                            }
2722                        }
2723                        // 下限で吸収しきれない場合ははみ出すのが正しい(F-4)ので、
2724                        // container_main に丸めず実際の合計を使う。
2725                        total_main = sizes.iter().sum::<i32>().max(container_main.min(total_main));
2726                    }
2727                    Err(_) => {
2728                        // 引数不正。エラーは flex_min 側でログ済み。
2729                        // 安全側=縮小を行わない(はみ出す方がレイアウト崩壊より軽い)。
2730                    }
2731                }
2732            }
2733        }
2734
2735        // flex アイテムの aspect-ratio による交差軸寸法の自動計算:
2736        // is_row 時: アイテムの高さが auto で aspect-ratio を持つ場合、主軸幅から高さを算出
2737        // !is_row 時: アイテムの幅が auto で aspect-ratio を持つ場合、主軸高さから幅を算出
2738        for (ci, child) in self.children.iter_mut().enumerate() {
2739            if oof.get(ci).copied().unwrap_or(false) {
2740                continue;
2741            }
2742            if let Some((num, den)) = box_aspect_ratio(child) {
2743                if num > 0.0 && den > 0.0 {
2744                    if is_row && !box_has_explicit(child, "height") {
2745                        let w = child.dimensions.content.width as f32;
2746                        let h = round_f32_to_i32(w * den / num).max(1);
2747                        child.dimensions.content.height = h;
2748                    } else if !is_row && !box_has_explicit(child, "width") {
2749                        let h = child.dimensions.content.height as f32;
2750                        let w = round_f32_to_i32(h * num / den).max(1);
2751                        child.dimensions.content.width = w;
2752                    }
2753                }
2754            }
2755        }
2756
2757        // 【2026-09-05】縮小で子を組み直したら、**親の高さも計算し直す**。
2758        //
2759        // 交差軸のサイズ(横並びなら高さ)は縮小より前に確定していた。
2760        // 縮小した子はその幅で組み直すので行数が増えることがあり、
2761        // 高さが 1 行ぶんのまま取り残されて**次の要素と重なる**。
2762        // 実サイト `forms.htm` の目次で、2 行に折り返した項目が
2763        // 次の項目に重なって出ていた。
2764        //
2765        // 高さが明示されている場合は指定を尊重する(伸ばさない)。
2766        if is_row && !matches!(parse_length_value(height_val.clone()), Some(LengthValue::Px(_))) {
2767            let mut recomputed = 0;
2768            for (ci, child) in self.children.iter().enumerate() {
2769                if oof.get(ci).copied().unwrap_or(false) {
2770                    continue;
2771                }
2772                recomputed = recomputed.max(child.dimensions.margin_box().height);
2773            }
2774            if recomputed > container_cross {
2775                container_cross = recomputed;
2776                self.dimensions.content.height = container_cross;
2777            }
2778        }
2779
2780        // align-items: stretch(既定値): 交差軸サイズ未指定の子をコンテナ交差幅へ伸ばす。
2781        // ただし align-self が明示された子は除外(個別指定を尊重)。
2782        // aspect-ratio を持つアイテムも主軸から交差軸サイズが定まるため除外。
2783        if align_items == "stretch" {
2784            let cross_prop = if is_row { "height" } else { "width" };
2785            for (ci, child) in self.children.iter_mut().enumerate() {
2786                if oof.get(ci).copied().unwrap_or(false) {
2787                    continue;
2788                }
2789                if box_has_explicit(child, cross_prop) || box_aspect_ratio(child).is_some() {
2790                    continue;
2791                }
2792                // align-self が stretch 以外で指定されていれば stretch しない。
2793                if let Some(s) = box_align_self(child) {
2794                    if s != "stretch" {
2795                        continue;
2796                    }
2797                }
2798                let mb = child.dimensions.margin_box();
2799                let (cur_cross, cur_content) = if is_row {
2800                    (mb.height, child.dimensions.content.height)
2801                } else {
2802                    (mb.width, child.dimensions.content.width)
2803                };
2804                let non_content = cur_cross - cur_content; // padding/border/margin 分
2805                let new_content = (container_cross - non_content).max(0);
2806                if is_row {
2807                    child.dimensions.content.height = new_content;
2808                } else {
2809                    child.dimensions.content.width = new_content;
2810                }
2811            }
2812        }
2813
2814        // Justify Content (Main Axis)
2815        let main_free_space = container_main - total_main;
2816
2817        // flex 主軸 auto margin: 主軸方向に auto margin を持つアイテムがあれば、
2818        // 余剰空間を全 auto margin 辺で均等分配する(justify-content より優先)。
2819        let mut total_auto_margins = 0i32;
2820        for (ci, child) in self.children.iter().enumerate() {
2821            if oof.get(ci).copied().unwrap_or(false) {
2822                continue;
2823            }
2824            let (a, b) = box_main_auto_margins(child, is_row);
2825            total_auto_margins += a as i32 + b as i32;
2826        }
2827        let auto_margin_unit = if total_auto_margins > 0 && main_free_space > 0 {
2828            main_free_space / total_auto_margins
2829        } else {
2830            0
2831        };
2832        let use_auto_margins = total_auto_margins > 0 && main_free_space > 0;
2833
2834        let main_offset = if use_auto_margins {
2835            0
2836        } else {
2837            match justify_content.as_str() {
2838                // CSS Box Alignment Level 3 の汎用キーワード `end` は `flex-end` の別名
2839                // (`align-content` は既に対応済みだったが、主軸側のここは漏れており
2840                // `justify-content: end` が `flex-start` と同じ挙動に落ちる静かなバグ
2841                // だった)。
2842                "flex-end" | "end" => main_free_space,
2843                "center" => main_free_space / 2,
2844                "space-around" if n > 0 => main_free_space / (n as i32 * 2),
2845                // `space-evenly` はアイテム間だけでなく先頭/末尾の余白も均等にする
2846                // (align-content の space-evenly と同じ `free / (n+1)` の式)。
2847                // 以前はこの分岐が無く flex-start と同じ挙動に落ちる静かなバグだった。
2848                "space-evenly" if n > 0 => main_free_space / (n as i32 + 1),
2849                // flex-start / start / space-between(ループで処理)/ space-around・space-evenly(n==0)
2850                _ => 0,
2851            }
2852        };
2853
2854        let spacing = if use_auto_margins {
2855            0
2856        } else {
2857            match justify_content.as_str() {
2858                "space-between" if n > 1 => main_free_space / (n as i32 - 1),
2859                "space-around" if n > 0 => main_free_space / (n as i32),
2860                "space-evenly" if n > 0 => main_free_space / (n as i32 + 1),
2861                _ => 0,
2862            }
2863        };
2864
2865        // 主軸の配置。reverse なら主軸終端(row なら右端)から DOM 順に詰める。
2866        // cursor は「正方向なら先頭からの距離 / reverse なら終端からの距離」。
2867        // order: アイテムを order 昇順(同値は DOM 順)に並べ替えて配置する。
2868        let mut order_idx: Vec<usize> = (0..self.children.len()).collect();
2869        order_idx.sort_by_key(|&i| (box_order(&self.children[i]), i));
2870        let mut cursor = main_offset;
2871        for &ci in &order_idx {
2872            // out-of-flow はフロー配置をスキップ(cursor を進めない)。後段で配置する。
2873            if oof.get(ci).copied().unwrap_or(false) {
2874                continue;
2875            }
2876            // 主軸 auto margin: 先頭辺ぶんを cursor へ加算(アイテムを後方へ押す)。
2877            let (lead_auto, trail_auto) = if use_auto_margins {
2878                box_main_auto_margins(&self.children[ci], is_row)
2879            } else {
2880                (false, false)
2881            };
2882            if lead_auto {
2883                cursor += auto_margin_unit;
2884            }
2885            let child_w = self.children[ci].dimensions.margin_box().width;
2886            let child_h = self.children[ci].dimensions.margin_box().height;
2887            let child_main = if is_row { child_w } else { child_h };
2888
2889            let cross_free_space = container_cross - if is_row { child_h } else { child_w };
2890            // align-self が指定されていればそれを優先、無ければ align-items。
2891            let effective_align =
2892                box_align_self(&self.children[ci]).unwrap_or_else(|| align_items.clone());
2893            let cross_offset = match effective_align.as_str() {
2894                // `end`/`self-end` は `flex-end` の別名(CSS Box Alignment Level 3)。
2895                // 以前はここで漏れており `align-items: end` が `flex-start` と
2896                // 同じ挙動に落ちる静かなバグだった。
2897                "flex-end" | "end" | "self-end" => cross_free_space,
2898                "center" => cross_free_space / 2,
2899                _ => 0, // flex-start / start / stretch
2900            };
2901
2902            // 主軸開始位置(コンテナ content 原点からの相対)。reverse は終端から逆算。
2903            let main_start = if is_reverse {
2904                container_main - cursor - child_main
2905            } else {
2906                cursor
2907            };
2908
2909            // Recursively update positions relative to container
2910            let start_x = self.dimensions.content.x;
2911            let start_y = self.dimensions.content.y;
2912
2913            let target_x = if is_row {
2914                start_x + main_start
2915            } else {
2916                start_x + cross_offset
2917            };
2918            let target_y = if is_row {
2919                start_y + cross_offset
2920            } else {
2921                start_y + main_start
2922            };
2923
2924            let shift_x = target_x - self.children[ci].dimensions.margin_box().x;
2925            let shift_y = target_y - self.children[ci].dimensions.margin_box().y;
2926
2927            shift_layout_box(&mut self.children[ci], shift_x, shift_y);
2928
2929            cursor += child_main + spacing + gap_main;
2930            if trail_auto {
2931                cursor += auto_margin_unit;
2932            }
2933        }
2934
2935        // positioned 子要素の配置(flex コンテナを包含ブロック起点にできる)。
2936        // フロー内アイテムは上で配置済み。out-of-flow とフロー内 relative の両方を処理する。
2937        let saved_abs_cb = self.establish_abs_cb();
2938        self.apply_positioned_offsets();
2939        Self::restore_abs_cb(saved_abs_cb);
2940    }
2941
2942    /// flex-wrap: wrap / wrap-reverse のレイアウト。
2943    /// 子を主軸に沿って並べ、コンテナ主軸長を超えたら次の行へ折り返す。
2944    /// 各行ごとに justify-content(主軸)と align-items / align-self(交差軸)を適用する。
2945    #[allow(clippy::too_many_arguments)]
2946    fn layout_flex_wrapped(
2947        &mut self,
2948        is_row: bool,
2949        is_reverse: bool,
2950        wrap_reverse: bool,
2951        justify_content: &str,
2952        align_items: &str,
2953        align_content: &str,
2954        gap_main: i32,
2955        gap_cross: i32,
2956        height_val: Option<String>,
2957    ) {
2958        let n = self.children.len();
2959        if n == 0 {
2960            return;
2961        }
2962
2963        // 各子を測定(main/cross サイズ)。out-of-flow(absolute/fixed)は
2964        // フレックスラインに参加しないため flow_idx から除外する(サイズ確定のため
2965        // layout は行う)。最終配置は末尾の apply_positioned_offsets が行う。
2966        let oof: Vec<bool> = self
2967            .children
2968            .iter()
2969            .map(|c| Self::child_is_out_of_flow(c))
2970            .collect();
2971        let mut sizes: Vec<(i32, i32)> = Vec::with_capacity(n); // (main, cross)
2972        for child in &mut self.children {
2973            let mut cb = self.dimensions.clone();
2974            cb.content.x = 0;
2975            cb.content.y = 0;
2976            cb.content.height = 0;
2977            child.layout(cb);
2978            let w = child.dimensions.margin_box().width;
2979            let h = child.dimensions.margin_box().height;
2980            if is_row {
2981                sizes.push((w, h));
2982            } else {
2983                sizes.push((h, w));
2984            }
2985        }
2986        // フロー参加インデックス(DOM 順)。以降のライン分割・配置はこの並びで行う。
2987        let flow_idx: Vec<usize> = (0..n).filter(|&i| !oof[i]).collect();
2988
2989        let container_main = if is_row {
2990            self.dimensions.content.width
2991        } else {
2992            // column 方向で高さ未指定なら折り返さない(実質1行)
2993            self.explicit_content_height_px(height_val.clone()).unwrap_or(i32::MAX / 4)
2994        };
2995
2996        // 行分割: 主軸長がコンテナを超えたら改行。インデックスは flow_idx の位置(pos)。
2997        let fln = flow_idx.len();
2998        let mut lines: Vec<(usize, usize, i32, i32)> = Vec::new(); // (start_pos, end_pos_excl, main_used, cross_max)
2999        let mut line_start = 0usize;
3000        let mut line_main = 0i32;
3001        let mut line_cross = 0i32;
3002        for pos in 0..fln {
3003            let (m, c) = sizes[flow_idx[pos]];
3004            let add = if pos == line_start { m } else { m + gap_main };
3005            if pos > line_start && line_main + add > container_main {
3006                lines.push((line_start, pos, line_main, line_cross));
3007                line_start = pos;
3008                line_main = m;
3009                line_cross = c;
3010            } else {
3011                line_main += add;
3012                line_cross = line_cross.max(c);
3013            }
3014        }
3015        lines.push((line_start, fln, line_main, line_cross));
3016
3017        // 行を wrap-reverse なら逆順に積む。
3018        if wrap_reverse {
3019            lines.reverse();
3020        }
3021
3022        // コンテナ交差軸長を確定。
3023        let total_cross: i32 =
3024            lines.iter().map(|l| l.3).sum::<i32>() + gap_cross * (lines.len() as i32 - 1).max(0);
3025        let container_cross = if is_row {
3026            self.explicit_content_height_px(height_val.clone()).unwrap_or(total_cross)
3027        } else {
3028            self.dimensions.content.width
3029        };
3030
3031        if is_row {
3032            self.dimensions.content.height = container_cross.max(total_cross);
3033        } else {
3034            self.dimensions.content.height = container_main.min(total_cross).max(total_cross);
3035        }
3036
3037        let base_x = self.dimensions.content.x;
3038        let base_y = self.dimensions.content.y;
3039
3040        // align-content: 複数行ブロックを交差軸方向にどう配置するか。
3041        // 余剰交差空間 = container_cross - total_cross(行の自然交差合計)。
3042        // line_cross_start[i] = 各行の交差軸開始位置、line_cross_size[i] = 各行の交差軸高さ
3043        // (stretch では余剰を均等に各行へ加算)。
3044        let nlines = lines.len() as i32;
3045        let cross_free = (container_cross - total_cross).max(0);
3046        let mut line_cross_size: Vec<i32> = lines.iter().map(|l| l.3).collect();
3047        // stretch: 余剰を各行へ均等分配(端数は先頭行へ)。単一行でもコンテナ全体へ伸ばす。
3048        if (align_content == "stretch" || align_content.is_empty()) && cross_free > 0 && nlines > 0
3049        {
3050            let per = cross_free / nlines;
3051            let mut rem = cross_free - per * nlines;
3052            for sz in line_cross_size.iter_mut() {
3053                *sz += per;
3054                if rem > 0 {
3055                    *sz += 1;
3056                    rem -= 1;
3057                }
3058            }
3059        }
3060        // 各行の開始オフセットと行間スペースを align-content から決める。
3061        let (ac_offset, ac_spacing) = match align_content {
3062            "flex-end" | "end" => (cross_free, 0),
3063            "center" => (cross_free / 2, 0),
3064            "space-between" if nlines > 1 => (0, cross_free / (nlines - 1)),
3065            "space-around" if nlines > 0 => (cross_free / (nlines * 2), cross_free / nlines),
3066            "space-evenly" if nlines > 0 => (cross_free / (nlines + 1), cross_free / (nlines + 1)),
3067            // flex-start / start / stretch(余剰は行サイズ側で消費済み)
3068            _ => (0, 0),
3069        };
3070        let mut line_cross_start: Vec<i32> = Vec::with_capacity(lines.len());
3071        let mut cc = ac_offset;
3072        for sz in &line_cross_size {
3073            line_cross_start.push(cc);
3074            cc += sz + gap_cross + ac_spacing;
3075        }
3076
3077        for (li, &(start, end_excl, line_main_used, _line_cross_nat)) in lines.iter().enumerate() {
3078            let count = end_excl - start;
3079            let main_free = container_main - line_main_used;
3080            let line_cross = line_cross_size.get(li).copied().unwrap_or(0);
3081            let cross_cursor = line_cross_start.get(li).copied().unwrap_or(0);
3082
3083            // justify-content(主軸)。`end` は `flex-end` の別名(単一行側と同じ修正)。
3084            let mut main_offset = match justify_content {
3085                "flex-end" | "end" => main_free,
3086                "center" => main_free / 2,
3087                "space-around" if count > 0 => main_free / (count as i32 * 2),
3088                "space-evenly" if count > 0 => main_free / (count as i32 + 1),
3089                _ => 0,
3090            };
3091            let spacing = match justify_content {
3092                "space-between" if count > 1 => main_free / (count as i32 - 1),
3093                "space-around" if count > 0 => main_free / (count as i32),
3094                "space-evenly" if count > 0 => main_free / (count as i32 + 1),
3095                _ => 0,
3096            };
3097
3098            // 行内の配置順(reverse 対応)。pos は flow_idx の位置。
3099            let pos_list: Vec<usize> = if is_reverse {
3100                (start..end_excl).rev().collect()
3101            } else {
3102                (start..end_excl).collect()
3103            };
3104
3105            for &pos in &pos_list {
3106                let ci = flow_idx[pos];
3107                let (m, c) = sizes[ci];
3108                let cross_free_item = line_cross - c;
3109                let effective_align =
3110                    box_align_self(&self.children[ci]).unwrap_or_else(|| String::from(align_items));
3111                let cross_in_line = match effective_align.as_str() {
3112                    "flex-end" | "end" | "self-end" => cross_free_item,
3113                    "center" => cross_free_item / 2,
3114                    _ => 0, // flex-start / start / stretch
3115                };
3116                let cross_pos = cross_cursor + cross_in_line;
3117
3118                let (target_x, target_y) = if is_row {
3119                    (base_x + main_offset, base_y + cross_pos)
3120                } else {
3121                    (base_x + cross_pos, base_y + main_offset)
3122                };
3123
3124                let shift_x = target_x - self.children[ci].dimensions.margin_box().x;
3125                let shift_y = target_y - self.children[ci].dimensions.margin_box().y;
3126                shift_layout_box(&mut self.children[ci], shift_x, shift_y);
3127
3128                main_offset += m + spacing + gap_main;
3129            }
3130        }
3131
3132        // positioned 子要素の配置(out-of-flow と relative オフセット)。
3133        let saved_abs_cb = self.establish_abs_cb();
3134        self.apply_positioned_offsets();
3135        Self::restore_abs_cb(saved_abs_cb);
3136    }
3137
3138    fn layout_block(&mut self, containing_block: Dimensions) {
3139        // 外から確定させた高さ(絶対配置の top+bottom。`FORCED_BLOCK_H`)。子孫へ漏らさないよう先に取る。
3140        let forced_h = FORCED_BLOCK_H.swap(-1, AtomicOrdering::Relaxed);
3141        self.apply_box_model_styles_with_base(containing_block.content.width);
3142        // 自分の `height:%` の基準(包含ブロックの確定高さ。未確定なら -1)。下で子のために
3143        // 差し替える前に読む。
3144        let pct_base_h = PCT_BASE_H.load(AtomicOrdering::Relaxed);
3145
3146        // テーブル系 (row-group/row/cell) も table 経由で block としてレイアウトされるため
3147        // ここで styled node を取り出せるようにする。取り出せない型のみ早期 return。
3148        let (
3149            width_val,
3150            min_width_val,
3151            max_width_val,
3152            height_val,
3153            min_height_val,
3154            max_height_val,
3155            box_sizing,
3156            h_auto,
3157            aspect_ratio_val,
3158        ) = match &self.box_type {
3159            BoxType::BlockNode(node)
3160            | BoxType::TableRowGroupNode(node)
3161            | BoxType::TableRowNode(node)
3162            | BoxType::TableCellNode(node) => (
3163                len_value(node, "width"),
3164                len_value(node, "min-width"),
3165                len_value(node, "max-width"),
3166                len_value(node, "height"),
3167                len_value(node, "min-height"),
3168                len_value(node, "max-height"),
3169                node.value("box-sizing").unwrap_or_default(),
3170                margin_is_h_auto(node),
3171                node.value("aspect-ratio"),
3172            ),
3173            _ => return,
3174        };
3175
3176        let horizontal_non_content = self.dimensions.margin.left
3177            + self.dimensions.margin.right
3178            + self.dimensions.border.left
3179            + self.dimensions.border.right
3180            + self.dimensions.padding.left
3181            + self.dimensions.padding.right;
3182        let available_width = containing_block.content.width.max(1);
3183        // padding + border のみ(box-sizing:border-box / margin:auto センタリング用)。
3184        let inner_non_content = self.dimensions.border.left
3185            + self.dimensions.border.right
3186            + self.dimensions.padding.left
3187            + self.dimensions.padding.right;
3188
3189        // Calculate width
3190        let parsed_width = parse_length_value(width_val.clone());
3191        let explicit_width = !matches!(parsed_width, Some(LengthValue::Auto) | None);
3192        let mut content_width = match parsed_width {
3193            Some(LengthValue::Auto) | None => (available_width - horizontal_non_content).max(1),
3194            Some(LengthValue::Px(px)) => px.max(1),
3195            Some(LengthValue::Percent(pct)) => {
3196                round_f32_to_i32(available_width as f32 * pct).max(1)
3197            }
3198            // 内容ベースのキーワード: min/max/fit-content。子コンテンツを計測して解決。
3199            Some(LengthValue::MinContent) => {
3200                let (minc, _maxc) = self.block_intrinsic_widths();
3201                minc.max(1)
3202            }
3203            Some(LengthValue::MaxContent) => {
3204                let (_minc, maxc) = self.block_intrinsic_widths();
3205                maxc.max(1)
3206            }
3207            Some(LengthValue::FitContent) => {
3208                // fit-content = clamp(min-content, available, max-content)
3209                let avail = (available_width - horizontal_non_content).max(1);
3210                let (minc, maxc) = self.block_intrinsic_widths();
3211                maxc.min(avail).max(minc).max(1)
3212            }
3213            Some(LengthValue::FitContentWith(lim_box)) => {
3214                // fit-content(length) = min(max-content, max(min-content, length))
3215                let avail = (available_width - horizontal_non_content).max(1);
3216                let limit = resolve_length_value_with_font(*lim_box, avail, self.font_size())
3217                    .unwrap_or(avail);
3218                let (minc, maxc) = self.block_intrinsic_widths();
3219                maxc.min(limit.max(minc)).max(1)
3220            }
3221            Some(LengthValue::MathExpr(expr)) => {
3222                crate::os_lib::css::eval_css_math_with_font(&expr, available_width, self.font_size())
3223                    .unwrap_or((available_width - horizontal_non_content).max(1))
3224                    .max(1)
3225            }
3226        };
3227        // box-sizing: border-box → 指定 width は border-box 全体なので content は padding+border を引く。
3228        // 【2026-09-26 バグ修正】auto 幅は既に padding/border を除いた残りなので対象外
3229        // (従来は `*{box-sizing:border-box}` の全ブロックで padding が二重に引かれていた)。
3230        if explicit_width && box_sizing.trim() == "border-box" {
3231            content_width = (content_width - inner_non_content).max(1);
3232        }
3233
3234        // min-width / max-width。px/% は available 基準、min/max/fit-content は内容計測で解決。
3235        // min/max-width の長さは border-box なら padding+border を含む(`border_box_inner_w`)。
3236        let bb_inner = if box_sizing.trim() == "border-box" { inner_non_content } else { 0 };
3237        if let Some(min_width) = self.resolve_sizing_value(min_width_val.clone(), available_width) {
3238            let min_width = if is_length_sizing(&min_width_val) { (min_width - bb_inner).max(0) } else { min_width };
3239            content_width = content_width.max(min_width);
3240        }
3241        if let Some(max_width) = self.resolve_sizing_value(max_width_val.clone(), available_width) {
3242            let max_width = if is_length_sizing(&max_width_val) { (max_width - bb_inner).max(0) } else { max_width };
3243            content_width = content_width.min(max_width);
3244        }
3245        self.dimensions.content.width = content_width.max(1);
3246
3247        // margin: auto(左右)→ ブロックを水平センタリング。
3248        if h_auto {
3249            let free = (available_width - content_width - inner_non_content).max(0);
3250            self.dimensions.margin.left = free / 2;
3251            self.dimensions.margin.right = free / 2;
3252        }
3253
3254        // Position (content box origin)
3255        let flow_y = containing_block.content.y
3256            + containing_block.content.height
3257            + self.dimensions.margin.top;
3258        self.dimensions.content.x = containing_block.content.x
3259            + self.dimensions.margin.left
3260            + self.dimensions.border.left
3261            + self.dimensions.padding.left;
3262        self.dimensions.content.y =
3263            flow_y + self.dimensions.border.top + self.dimensions.padding.top;
3264
3265        // 子の `height:%`(置換要素)の基準: 明示 px 高さ、または aspect-ratio と確定幅から。
3266        // 未確定なら -1(auto 扱い)。この関数を抜けると自動で戻る。
3267        // 【2026-10-18】`%` も包含ブロックの高さが確定していれば確定値(`html,body{height:100%}` の
3268        // 連鎖。YouTube 埋め込みの `body{position:absolute;height:100%}` → `#player{height:100%}`
3269        // → `#movie_player{height:100%;overflow:hidden}` が高さ 0 になり、動画が全部切られていた)。
3270        let px_h = match parse_length_value(height_val.clone()) {
3271            Some(LengthValue::Px(px)) => Some(px),
3272            Some(LengthValue::Percent(pct)) if pct_base_h >= 0 => Some(round_f32_to_i32(pct_base_h as f32 * pct)),
3273            _ => None,
3274        };
3275        let definite_h = match (px_h, parse_length_value(height_val.clone())) {
3276            (Some(px), _) => {
3277                if box_sizing.trim() == "border-box" {
3278                    let d = &self.dimensions;
3279                    (px - d.padding.top - d.padding.bottom - d.border.top - d.border.bottom).max(0)
3280                } else {
3281                    px.max(0)
3282                }
3283            }
3284            (None, None | Some(LengthValue::Auto)) => aspect_ratio_val
3285                .as_deref()
3286                .and_then(parse_aspect_ratio)
3287                .map(|(num, den)| round_f32_to_i32(self.dimensions.content.width as f32 * den / num).max(0))
3288                .unwrap_or(-1),
3289            _ => -1,
3290        };
3291        let definite_h = if forced_h >= 0 { forced_h } else { definite_h };
3292        let _pct_h_guard = PctBaseHGuard::set(definite_h);
3293
3294        // この要素が positioned なら、子孫 absolute の包含ブロックを self に切替(昇り解決)。
3295        // 高さは明示値が分かればそれ、無ければ 0(top/left 配置が主。bottom/right は明示高さ前提)。
3296        let self_pos = box_position(self);
3297        let saved_abs_cb = if matches!(self_pos, "relative" | "absolute" | "fixed") {
3298            let prev = abs_cb_get();
3299            let exp_h = if forced_h >= 0 {
3300                forced_h
3301            } else if definite_h >= 0 {
3302                definite_h
3303            } else {
3304                parse_length_value(height_val.clone())
3305                    .and_then(|v| resolve_length_value(v, containing_block.content.height.max(0)))
3306                    .unwrap_or(0)
3307            };
3308            // 包含ブロックはパディングボックス(`establish_abs_cb` と同じ)。
3309            let d = &self.dimensions;
3310            abs_cb_set(
3311                d.content.x - d.padding.left,
3312                d.content.y - d.padding.top,
3313                d.content.width + d.padding.left + d.padding.right,
3314                exp_h + d.padding.top + d.padding.bottom,
3315            );
3316            Some(prev)
3317        } else {
3318            None
3319        };
3320
3321        // 子要素のレイアウト計算(block/inline混在の簡易フロー)
3322        let line_height_default = 20i32;
3323        let mut y_cursor = 0i32;
3324        let mut line_x = 0i32;
3325        let mut line_h = 0i32;
3326        let max_w = self.dimensions.content.width.max(1);
3327
3328        // float: left/right の占有幅と底辺 y を追跡する。(占有幅, 底辺y)。
3329        // 複数floatの左右並び積み重ねはサポートせず、各サイド最新の1個のみを追跡する
3330        // (画像+段落、サイドバー+本文といった典型パターンをカバーする簡易実装)。
3331        let mut left_float: Option<(i32, i32)> = None;
3332        let mut right_float: Option<(i32, i32)> = None;
3333
3334        // 指定 y での float 占有を考慮した (x_offset, 利用可能幅) を返す。
3335        fn float_avail(
3336            max_w: i32,
3337            left_float: Option<(i32, i32)>,
3338            right_float: Option<(i32, i32)>,
3339            y: i32,
3340        ) -> (i32, i32) {
3341            let l = left_float
3342                .filter(|&(_, yb)| yb > y)
3343                .map(|(w, _)| w)
3344                .unwrap_or(0);
3345            let r = right_float
3346                .filter(|&(_, yb)| yb > y)
3347                .map(|(w, _)| w)
3348                .unwrap_or(0);
3349            (l, (max_w - l - r).max(1))
3350        }
3351
3352        // 行ごとの子の範囲 `[start, end)`。ループ後にベースライン揃えを掛ける。
3353        let mut lines: Vec<(usize, usize)> = Vec::new();
3354        let mut line_start = 0usize;
3355        // 要素間の空白(CSS Text 3 §4.1)。`last_was_space`: 直前に置いたのが空白
3356        // (行頭も含む)。`trailing_space`: 行の最後に置いた空白(行末ならぶら下げて幅 0)。
3357        let mut last_was_space = true;
3358        let mut trailing_space: Option<(usize, i32)> = None;
3359        let mut hanging: Vec<(usize, i32)> = Vec::new();
3360
3361        for (ci, child) in self.children.iter_mut().enumerate() {
3362            let float_side = box_float(child);
3363            if float_side != "none" {
3364                // clear: float 自身は clear の対象にはしない(float 同士は積み重ね非対応のため)。
3365                let float_y = y_cursor;
3366                let is_inline_kind =
3367                    matches!(child.box_type, BoxType::InlineNode(_) | BoxType::InlineBlockNode(_));
3368                let (fw, fh) = if is_inline_kind {
3369                    child.intrinsic_inline_size(max_w)
3370                } else {
3371                    let mut cb = self.dimensions.clone();
3372                    cb.content.height = float_y;
3373                    child.layout(cb);
3374                    (
3375                        child.dimensions.margin_box().width,
3376                        child.dimensions.margin_box().height,
3377                    )
3378                };
3379
3380                let x = if float_side == "left" {
3381                    let x_off = left_float
3382                        .filter(|&(_, yb)| yb > float_y)
3383                        .map(|(w, _)| w)
3384                        .unwrap_or(0);
3385                    left_float = Some((x_off + fw, float_y + fh));
3386                    self.dimensions.content.x + x_off
3387                } else {
3388                    let x_off = right_float
3389                        .filter(|&(_, yb)| yb > float_y)
3390                        .map(|(w, _)| w)
3391                        .unwrap_or(0);
3392                    right_float = Some((x_off + fw, float_y + fh));
3393                    self.dimensions.content.x + max_w - x_off - fw
3394                };
3395
3396                if is_inline_kind {
3397                    child.dimensions.content.x = x;
3398                    child.dimensions.content.y = self.dimensions.content.y + float_y;
3399                    child.dimensions.content.width = fw;
3400                    child.dimensions.content.height = fh;
3401                    let mut cb = child.dimensions.clone();
3402                    cb.content.height = 0;
3403                    child.layout(cb);
3404                    child.dimensions.content.x = x;
3405                    child.dimensions.content.y = self.dimensions.content.y + float_y;
3406                    child.dimensions.content.width = fw;
3407                    child.dimensions.content.height = fh;
3408                } else {
3409                    // 通常 block レイアウト済みだが、x はフロートの追加分ずれているため上書きする。
3410                    let dx = x - child.dimensions.content.x;
3411                    child.dimensions.content.x += dx;
3412                }
3413                // float はフローの縦カーソルを進めない。
3414                continue;
3415            }
3416
3417            match child.box_type {
3418                BoxType::BlockNode(_)
3419                | BoxType::FlexNode(_)
3420                | BoxType::GridNode(_)
3421                | BoxType::AnonymousBlock
3422                | BoxType::TableNode(_)
3423                | BoxType::TableRowGroupNode(_)
3424                | BoxType::TableRowNode(_)
3425                | BoxType::TableCellNode(_) => {
3426                    let out_of_flow = matches!(box_position(child), "absolute" | "fixed");
3427                    if out_of_flow {
3428                        // フロー外: サイズ確定のためレイアウトのみ。位置は後処理、cursor は進めない。
3429                        let mut cb = self.dimensions.clone();
3430                        cb.content.height = y_cursor;
3431                        child.layout(cb);
3432                    } else {
3433                        if line_x > 0 {
3434                            lines.push((line_start, ci));
3435                            hanging.extend(trailing_space.take());
3436                            y_cursor += if line_h > 0 { line_h } else { line_height_default };
3437                            line_x = 0;
3438                            line_h = 0;
3439                        }
3440                        last_was_space = true;
3441                        // clear: 該当サイドの float 底辺まで縦カーソルを進める。
3442                        let clear = box_clear(child);
3443                        if clear == "left" || clear == "both" {
3444                            if let Some((_, yb)) = left_float {
3445                                y_cursor = y_cursor.max(yb);
3446                            }
3447                        }
3448                        if clear == "right" || clear == "both" {
3449                            if let Some((_, yb)) = right_float {
3450                                y_cursor = y_cursor.max(yb);
3451                            }
3452                        }
3453                        let (x_off, avail) = float_avail(max_w, left_float, right_float, y_cursor);
3454                        let mut cb = self.dimensions.clone();
3455                        cb.content.height = y_cursor;
3456                        cb.content.x = self.dimensions.content.x + x_off;
3457                        cb.content.width = avail;
3458                        child.layout(cb);
3459                        y_cursor += child.dimensions.margin_box().height;
3460                    }
3461                }
3462                BoxType::InlineNode(_) | BoxType::InlineBlockNode(_) => {
3463                    if child.is_inline_line_break() {
3464                        if line_x > 0 {
3465                            lines.push((line_start, ci));
3466                        }
3467                        hanging.extend(trailing_space.take());
3468                        last_was_space = true;
3469                        y_cursor += if line_h > 0 { line_h } else { line_height_default };
3470                        line_x = 0;
3471                        line_h = 0;
3472                        child.dimensions.content.x = self.dimensions.content.x;
3473                        child.dimensions.content.y = self.dimensions.content.y + y_cursor;
3474                        child.dimensions.content.width = 0;
3475                        child.dimensions.content.height = 0;
3476                        continue;
3477                    }
3478                    let (mut x_off, mut eff_w) =
3479                        float_avail(max_w, left_float, right_float, y_cursor);
3480                    // 先頭空白の印は置き場所で決まるので、組み直しの度に付け直す。
3481                    child.trim_leading_space = false;
3482                    let (mut inline_w, mut inline_h) = child.intrinsic_inline_size(eff_w);
3483                    // 【2026-10-09】要素間の空白は畳んで半角 1 つぶんの幅を持つ。行頭と、直前が
3484                    // 空白のときは 0。空白自身では折り返さない(行末ならぶら下げ)。従来は
3485                    // 行末の空白を除く計測のため常に幅 0 で、forms.htm の `<img> <a>` が
3486                    // 折り返すべき幅で 1 行に収まった。
3487                    let is_space = child.is_collapsible_space();
3488                    let mut text_trailing_w = 0i32;
3489                    if is_space {
3490                        inline_w = if line_x == 0 || last_was_space { 0 } else { child.collapsed_space_width() };
3491                        if inline_w == 0 {
3492                            // 取り除かれた空白は行も作らない(ブロック間の改行・字下げ)。
3493                            inline_h = 0;
3494                        }
3495                    } else {
3496                        if child.text_starts_with_space() && (line_x == 0 || last_was_space) {
3497                            inline_w = inline_w.saturating_sub(child.collapsed_space_width());
3498                            child.trim_leading_space = true;
3499                        }
3500                        if child.text_ends_with_space() {
3501                            let sp_w = child.collapsed_space_width();
3502                            inline_w += sp_w;
3503                            text_trailing_w = sp_w;
3504                        }
3505                    }
3506                    // 折り返し判定も inline-block / 置換要素の余白込みで行う(2026-09-26, 2026-10-13)。
3507                    let edges_w = if matches!(child.box_type, BoxType::InlineBlockNode(_)) || child.is_replaced_inline() {
3508                        intrinsic_h_edges(child, false)
3509                    } else {
3510                        0
3511                    };
3512                    if !is_space && line_x > 0 && line_x + inline_w + edges_w > eff_w {
3513                        lines.push((line_start, ci));
3514                        hanging.extend(trailing_space.take());
3515                        y_cursor += if line_h > 0 { line_h } else { line_height_default };
3516                        line_x = 0;
3517                        line_h = 0;
3518                        let recomputed = float_avail(max_w, left_float, right_float, y_cursor);
3519                        x_off = recomputed.0;
3520                        eff_w = recomputed.1;
3521                    }
3522                    if line_x == 0 {
3523                        line_start = ci;
3524                        // 折り返して行頭に来た文字も先頭の空白を落とす。
3525                        if !is_space && !child.trim_leading_space && child.text_starts_with_space() {
3526                            inline_w = inline_w.saturating_sub(child.collapsed_space_width());
3527                            child.trim_leading_space = true;
3528                        }
3529                    }
3530                    let _ = eff_w;
3531                    child.dimensions.content.x = self.dimensions.content.x + x_off + line_x;
3532                    child.dimensions.content.y = self.dimensions.content.y + y_cursor;
3533                    child.dimensions.content.width = inline_w;
3534                    child.dimensions.content.height = inline_h;
3535
3536                    // 子要素(InlineNode など)に対して再帰的にレイアウトを呼び出して、さらにその中の座標を決定する。
3537                    // 注意: layout_inline は containing.content.height を「Y フローカーソル」として
3538                    // 自身の y に加算する規約のため、ここで子自身の高さ (inline_h) を渡すと
3539                    // 子が自分の高さぶん下へずれてしまう(textarea が画面外に飛ぶバグの原因)。
3540                    // 確定済みスロット位置だけを伝えるため height は 0 にして渡す。
3541                    let mut cb = child.dimensions.clone();
3542                    cb.content.height = 0;
3543                    child.layout(cb);
3544                    // layout_inline 内の再計算は % 高さを包含高さ (=0) 基準で解決して
3545                    // 潰してしまうため、親が intrinsic 計測で確定したスロット寸法を最終値とする。
3546                    // 【2026-09-26 バグ修正】位置は**子孫ごと**スロットへ戻す。従来は箱の
3547                    // x/y だけを上書きしたため、layout_inline が margin/padding を足した位置に
3548                    // 組んだ中身が置き去りになり、padding 付きの inline-block
3549                    // (実サイトの `.btn{display:inline-block;padding:1rem 2rem}`)で
3550                    // 文字が箱の外へずれていた。
3551                    // inline-block の margin/border/padding は行内の幅・高さを占める
3552                    // (2026-09-26 まで content だけで並べ、padding が行頭より左へはみ出した)。
3553                    let (el, er, et, eb) = child.inline_block_edges();
3554                    let slot_x = self.dimensions.content.x + x_off + line_x + el;
3555                    let slot_y = self.dimensions.content.y + y_cursor + et;
3556                    let (dx, dy) = (slot_x - child.dimensions.content.x, slot_y - child.dimensions.content.y);
3557                    child.translate(dx, dy);
3558                    child.dimensions.content.width = inline_w;
3559                    child.dimensions.content.height = inline_h;
3560
3561                    line_x += el + inline_w + er;
3562                    let outer_h = et + inline_h + eb;
3563                    if outer_h > line_h {
3564                        line_h = outer_h;
3565                    }
3566                    if is_space {
3567                        if inline_w > 0 {
3568                            trailing_space = Some((ci, inline_w));
3569                        }
3570                        last_was_space = true;
3571                    } else if child.text_ends_with_space() {
3572                        if text_trailing_w > 0 {
3573                            trailing_space = Some((ci, text_trailing_w));
3574                        }
3575                        last_was_space = true;
3576                    } else if !child.is_blank_inline() {
3577                        trailing_space = None;
3578                        last_was_space = false;
3579                    }
3580                }
3581            }
3582        }
3583
3584        hanging.extend(trailing_space.take());
3585        // 行末の空白はぶら下げ: 幅に数えない(下の text-align の行幅からも外れる)。
3586        for (i, trim_w) in hanging {
3587            if let Some(c) = self.children.get_mut(i) {
3588                c.dimensions.content.width = c.dimensions.content.width.saturating_sub(trim_w);
3589            }
3590        }
3591        if line_x > 0 {
3592            lines.push((line_start, self.children.len()));
3593            y_cursor += if line_h > 0 { line_h } else { line_height_default };
3594        }
3595        // float は通常のフロー高さに寄与しないため、コンテナが「潰れて」floatだけが
3596        // はみ出す見た目を避けるべく、底辺を高さに含める(簡易実装の割り切り)。
3597        if let Some((_, yb)) = left_float {
3598            y_cursor = y_cursor.max(yb);
3599        }
3600        if let Some((_, yb)) = right_float {
3601            y_cursor = y_cursor.max(yb);
3602        }
3603
3604        // 【2026-08-31】CSS 多段組(`column-count`/`column-width`/`columns`)。
3605        //
3606        // 縦フローで子を積み終えたこの時点で、**各子を列へ振り分け直す**
3607        // 後処理として実装する。先に通常フローを完成させてから x/y を
3608        // 移し替えるので、幅計算・マージン相殺・float 等の既存経路を
3609        // 一切変更せずに済む(新しいレイアウトモードを足すより安全)。
3610        //
3611        // 割り切り: 段抜き(`column-span`)・段区切り(`break-inside`)・
3612        // 段罫線(`column-rule`)は未対応。子は「積んだ順に、各列の高さが
3613        // 均等になるよう」振り分ける(厳密なバランシングではなく、
3614        // 各列の現在高さが最小の列へ順に入れる貪欲法)。
3615        if let BoxType::BlockNode(node) = &self.box_type {
3616            let cc = node.value("column-count");
3617            let cw = node.value("column-width");
3618            let shorthand = node.value("columns");
3619            let (sh_w, sh_c) = shorthand
3620                .as_deref()
3621                .map(crate::os_lib::layout::multicol::parse_columns_shorthand)
3622                .unwrap_or((None, None));
3623            let parse_px = |v: &Option<String>| -> Option<i32> {
3624                v.as_deref()
3625                    .and_then(|s| s.trim().strip_suffix("px"))
3626                    .and_then(|n| n.trim().parse::<f32>().ok())
3627                    .filter(|f| f.is_finite() && *f > 0.0)
3628                    .map(|f| f as i32)
3629            };
3630            let count = cc
3631                .as_deref()
3632                .and_then(|s| s.trim().parse::<i32>().ok())
3633                .filter(|v| *v > 0)
3634                .or(sh_c);
3635            let width = parse_px(&cw).or(sh_w);
3636            if count.is_some() || width.is_some() {
3637                // `column-gap`(または `gap`)の単位解決(em, rem, pt, %, px)。
3638                let gap_str = node.value("column-gap").or_else(|| node.value("gap"));
3639                let gap = crate::os_lib::layout::multicol::resolve_column_gap(
3640                    gap_str.as_deref(),
3641                    self.dimensions.content.width.max(0),
3642                    self.font_size(),
3643                );
3644                let spec = crate::os_lib::layout::multicol::MultiColSpec { count, width, gap };
3645                let cols = crate::os_lib::layout::multicol::layout_columns(
3646                    &spec,
3647                    self.dimensions.content.width.max(0),
3648                );
3649                if cols.len() > 1 {
3650                    let base_x = self.dimensions.content.x;
3651                    let base_y = self.dimensions.content.y;
3652                    // float や絶対配置は通常フローから外れているので対象外。
3653                    let flow_idx: alloc::vec::Vec<usize> = self
3654                        .children
3655                        .iter()
3656                        .enumerate()
3657                        .filter(|(_, c)| box_float(c) == "none")
3658                        .map(|(i, _)| i)
3659                        .collect();
3660
3661                    // 段抜き(column-span: all)と通常の子を分類
3662                    let items: alloc::vec::Vec<crate::os_lib::layout::multicol::ChildKind> = flow_idx
3663                        .iter()
3664                        .map(|&i| {
3665                            let child = &self.children[i];
3666                            let h = child.dimensions.margin_box().height;
3667                            let is_spanner = match &child.box_type {
3668                                BoxType::BlockNode(n) => {
3669                                    n.value("column-span").as_deref().map(|s| s.trim()) == Some("all")
3670                                }
3671                                _ => false,
3672                            };
3673                            if is_spanner {
3674                                crate::os_lib::layout::multicol::ChildKind::Spanner { height: h }
3675                            } else {
3676                                crate::os_lib::layout::multicol::ChildKind::ColumnItem { height: h }
3677                            }
3678                        })
3679                        .collect();
3680
3681                    let (places, total_h) =
3682                        crate::os_lib::layout::multicol::assign_children_with_spans(&items, cols.len());
3683
3684                    for (slot, &ci) in flow_idx.iter().enumerate() {
3685                        let place = places[slot];
3686                        let child = &mut self.children[ci];
3687                        let target_w = if place.is_span {
3688                            self.dimensions.content.width.max(0)
3689                        } else {
3690                            cols[place.col].width
3691                        };
3692                        let target_x = if place.is_span {
3693                            0
3694                        } else {
3695                            cols[place.col].x
3696                        };
3697
3698                        let dx = base_x + target_x - child.dimensions.content.x
3699                            + child.dimensions.margin.left
3700                            + child.dimensions.border.left
3701                            + child.dimensions.padding.left;
3702                        let dy = base_y + place.y - child.dimensions.content.y
3703                            + child.dimensions.margin.top
3704                            + child.dimensions.border.top
3705                            + child.dimensions.padding.top;
3706                        child.translate(dx, dy);
3707                        child.dimensions.content.width = (target_w
3708                            - child.dimensions.margin.left
3709                            - child.dimensions.margin.right
3710                            - child.dimensions.border.left
3711                            - child.dimensions.border.right
3712                            - child.dimensions.padding.left
3713                            - child.dimensions.padding.right)
3714                            .max(0);
3715                    }
3716                    y_cursor = total_h;
3717                }
3718            }
3719        }
3720
3721
3722        // Block要素の高さは内容フローの高さ。ただし明示 height / min-height があれば優先。
3723        // `::before`/`::after` だけを内容に持つ要素は、その 1 行ぶんの高さを持つ。
3724        if let BoxType::BlockNode(n) = &self.box_type {
3725            if let Some((_, h)) = pseudo_content_size(n) {
3726                y_cursor = y_cursor.max(h);
3727            }
3728        }
3729        self.dimensions.content.height = y_cursor;
3730        let cb_height = containing_block.content.height.max(0);
3731        // 【2026-10-03】`%` 高さは包含ブロックの高さが確定しているとき(`PCT_BASE_H`)だけ有効で、
3732        // 未確定なら auto(CSS 2.1 10.5)。従来は `containing_block.content.height`(実体は
3733        // 縦のフロー位置)を基準にしていた。
3734        let height_len = match parse_length_value(height_val.clone()) {
3735            Some(LengthValue::Percent(pct)) if pct_base_h >= 0 => {
3736                Some(round_f32_to_i32(pct_base_h as f32 * pct))
3737            }
3738            Some(LengthValue::Percent(_)) => None,
3739            v => v.and_then(|v| resolve_length_value(v, cb_height)),
3740        };
3741        if let Some(h) = height_len
3742        {
3743            // box-sizing:border-box は指定 height から縦 padding+border を引いて content 高に。
3744            let vertical_non_content = self.dimensions.border.top
3745                + self.dimensions.border.bottom
3746                + self.dimensions.padding.top
3747                + self.dimensions.padding.bottom;
3748            let content_h = if box_sizing.trim() == "border-box" {
3749                (h - vertical_non_content).max(0)
3750            } else {
3751                h.max(0)
3752            };
3753            self.dimensions.content.height = content_h;
3754        }
3755        // 【2026-10-04】ブロックの `max-height` は読まれておらず、未実装だった
3756        // (`height:100px; max-height:20px` が 100px のまま)。CSS 2.1 §10.7 の順で
3757        // 上限を掛けてから `min-height` を適用する(両者が衝突したら min が勝つ)。
3758        // `box-sizing:border-box` なら `height` と同じく縦の padding+border を引く。
3759        if let Some(mx) =
3760            parse_length_value(max_height_val).and_then(|v| resolve_length_value(v, cb_height))
3761        {
3762            let vertical_non_content = self.dimensions.border.top
3763                + self.dimensions.border.bottom
3764                + self.dimensions.padding.top
3765                + self.dimensions.padding.bottom;
3766            let max_content = if box_sizing.trim() == "border-box" {
3767                (mx - vertical_non_content).max(0)
3768            } else {
3769                mx.max(0)
3770            };
3771            self.dimensions.content.height = self.dimensions.content.height.min(max_content);
3772        }
3773        if let Some(mh) =
3774            parse_length_value(min_height_val).and_then(|v| resolve_length_value(v, cb_height))
3775        {
3776            self.dimensions.content.height = self.dimensions.content.height.max(mh);
3777        }
3778
3779        // aspect-ratio: W/H。height が auto なら width から height を補完、
3780        // 逆に width が auto かつ height 明示なら height から width を補完する。
3781        if let Some((num, den)) = aspect_ratio_val.as_deref().and_then(parse_aspect_ratio) {
3782            let width_is_auto = matches!(
3783                parse_length_value(width_val.clone()),
3784                Some(LengthValue::Auto) | None
3785            );
3786            let height_is_auto = parse_length_value(height_val.clone()).is_none();
3787            if height_is_auto {
3788                // width 確定 → height = width * den / num
3789                self.dimensions.content.height =
3790                    round_f32_to_i32(self.dimensions.content.width as f32 * den / num).max(1);
3791            } else if width_is_auto {
3792                // height 明示・width auto → width = height * num / den(min/max-width で後段クランプ済みだが
3793                // ここで上書きするため、再度 min/max-width 制約を適用する)。
3794                let h = self.dimensions.content.height as f32;
3795                let mut w = round_f32_to_i32(h * num / den).max(1);
3796                if let Some(min_w) = parse_length_value(min_width_val.clone())
3797                    .and_then(|v| resolve_length_value(v, available_width))
3798                {
3799                    w = w.max(min_w);
3800                }
3801                if let Some(max_w) = parse_length_value(max_width_val.clone())
3802                    .and_then(|v| resolve_length_value(v, available_width))
3803                {
3804                    w = w.min(max_w);
3805                }
3806                self.dimensions.content.width = w.max(1);
3807            }
3808        }
3809
3810        // text-align の後処理: center / right でインライン子要素のx座標を調整
3811        // text-align 未指定時は `direction` に応じた既定値(初期値 `start` の簡易解決)を使う:
3812        // rtl なら right 相当、それ以外(既定 ltr)は従来どおり left(無指定=無調整)。
3813        let (text_align, direction, text_align_last) = match &self.box_type {
3814            BoxType::BlockNode(node) | BoxType::TableCellNode(node) => (
3815                node.value("text-align").unwrap_or_default(),
3816                node.value("direction").unwrap_or_default(),
3817                node.value("text-align-last").unwrap_or_default(),
3818            ),
3819            _ => (String::new(), String::new(), String::new()),
3820        };
3821        let is_rtl = direction.trim().eq_ignore_ascii_case("rtl");
3822        let mut ta = text_align.trim().to_lowercase();
3823        if ta.is_empty() && is_rtl {
3824            ta = String::from("right");
3825        }
3826        // `start`/`end`(CSS Logical Properties)は `direction` に応じて left/right へ
3827        // 解決する(`float`/`clear` の `inline-start`/`inline-end` と同じ考え方)。
3828        ta = match ta.as_str() {
3829            "start" => String::from(if is_rtl { "right" } else { "left" }),
3830            "end" => String::from(if is_rtl { "left" } else { "right" }),
3831            _ => ta,
3832        };
3833        // text-align-last: 最終行のみ text-align とは別の値で揃える(justify と組み合わせて
3834        // 最終行だけ left/center/right にする用途向け)。`auto`/未指定/`justify`(非対応)は
3835        // 最終行も通常の `ta` に従う。`start`/`end` も同様に direction で解決する。
3836        let ta_last_resolved = match text_align_last.trim().to_lowercase().as_str() {
3837            "start" => String::from(if is_rtl { "right" } else { "left" }),
3838            "end" => String::from(if is_rtl { "left" } else { "right" }),
3839            other => String::from(other),
3840        };
3841        let ta_last_effective = if matches!(ta_last_resolved.as_str(), "left" | "center" | "right")
3842        {
3843            Some(ta_last_resolved)
3844        } else {
3845            None
3846        };
3847        if ta == "center" || ta == "right" || ta == "justify" || ta_last_effective.is_some() {
3848            let content_w = self.dimensions.content.width.max(1);
3849            let last_line_idx = lines.len().saturating_sub(1);
3850
3851            for (line_idx, &(s, e)) in lines.iter().enumerate() {
3852                // そのラインの子要素インデックスと合計幅を計算
3853                let mut line_indices = Vec::new();
3854                let mut total_w = 0i32;
3855                for ci in s..e {
3856                    match self.children[ci].box_type {
3857                        BoxType::InlineNode(_) | BoxType::InlineBlockNode(_) => {
3858                            line_indices.push(ci);
3859                            let (el, er, _, _) = self.children[ci].inline_block_edges();
3860                            total_w += el + self.children[ci].dimensions.content.width + er;
3861                        }
3862                        _ => {}
3863                    }
3864                }
3865                if line_indices.is_empty() {
3866                    continue;
3867                }
3868
3869                let is_last_line = line_idx == last_line_idx;
3870                let effective_ta = if is_last_line {
3871                    ta_last_effective.as_deref().unwrap_or(if ta == "justify" {
3872                        if is_rtl { "right" } else { "left" }
3873                    } else {
3874                        ta.as_str()
3875                    })
3876                } else {
3877                    ta.as_str()
3878                };
3879
3880                if effective_ta == "justify" {
3881                    let k = line_indices.len();
3882                    if k > 1 {
3883                        let remaining = (content_w - total_w).max(0);
3884                        for (i, &ci) in line_indices.iter().enumerate() {
3885                            let shift = (remaining * i as i32) / (k as i32 - 1);
3886                            self.children[ci].translate(shift, 0);
3887                        }
3888                    }
3889                } else {
3890                    let offset = match effective_ta {
3891                        "center" => ((content_w - total_w) / 2).max(0),
3892                        "right" => (content_w - total_w).max(0),
3893                        _ => 0,
3894                    };
3895                    if offset > 0 {
3896                        for &ci in &line_indices {
3897                            self.children[ci].translate(offset, 0);
3898                        }
3899                    }
3900                }
3901            }
3902        }
3903
3904        // 行内のベースライン揃え。行内の箱をベースラインに合わせて垂直方向に配置し、
3905        // もし行の高さが広がった場合は後続の子要素(後続の行やブロック要素)を押し下げ、
3906        // コンテナ高さも拡張する。
3907        let mut total_delta_h = 0i32;
3908        for &(s, e) in &lines {
3909            let orig_h = self.children[s..e]
3910                .iter()
3911                .filter(|c| {
3912                    !c.is_whitespace_text()
3913                        && matches!(c.box_type, BoxType::InlineNode(_) | BoxType::InlineBlockNode(_))
3914                        && box_float(c) == "none"
3915                        && !matches!(box_position(c), "absolute" | "fixed")
3916                })
3917                .map(|c| {
3918                    let (_, _, et, eb) = c.inline_block_edges();
3919                    et + c.dimensions.content.height + eb
3920                })
3921                .max()
3922                .unwrap_or(0);
3923
3924            let new_h = Self::align_line_baselines(&mut self.children, s, e);
3925            if new_h > orig_h {
3926                let delta = new_h - orig_h;
3927                total_delta_h += delta;
3928                for ci in e..self.children.len() {
3929                    self.children[ci].translate_y(delta);
3930                }
3931            }
3932        }
3933        if total_delta_h > 0 {
3934            self.dimensions.content.height += total_delta_h;
3935        }
3936        if forced_h >= 0 {
3937            self.dimensions.content.height = forced_h;
3938        }
3939
3940        // position の後処理(relative/absolute/fixed の配置 + transform)を共有メソッドで適用。
3941        // 直接の子の包含ブロックは、高さが確定した今のパディングボックス。
3942        if saved_abs_cb.is_some() {
3943            let pb = self.dimensions.padding_box();
3944            abs_cb_set(pb.x, pb.y, pb.width, pb.height);
3945        }
3946        self.apply_positioned_offsets();
3947
3948        // 子孫のために切替えた ABS_CB を復元(positioned 要素のみ切替えていた)。
3949        if let Some(prev) = saved_abs_cb {
3950            abs_cb_set(prev.0, prev.1, prev.2, prev.3);
3951        }
3952    }
3953
3954    /// avail_hint: 包含ブロックのコンテンツ幅。テキスト折り返し計測に使う。
3955    /// (以前は固定 800px フォールバックだったため、実コンテナ幅と食い違い
3956    ///   layout の行数 < flatten の行数 となって後続要素と重なるバグがあった)
3957    /// 内容ベースの寸法を返す。`avail_hint` は折り返しに使える幅。
3958    ///
3959    /// `pure` が真なら**現在の箱の幅を見ない**。内在幅
3960    /// (min-content / max-content)の算出専用の入口で、
3961    /// `block_intrinsic_widths` だけが真を渡す。
3962    ///
3963    /// 内在幅は「今どう組まれているか」に依存してはいけない値だが、
3964    /// この関数は内在幅以外の用途(表の列幅・インライン配置など)でも
3965    /// 呼ばれており、そちらは現在の幅を前提にしている。
3966    /// 【2026-09-05】一度この区別をせずに順序だけ入れ替えたところ、
3967    /// ヒーローの見出しが 2 行に折り返す退行を招いて戻した。
3968    /// 入口を分けることで、内在幅の算出だけを正す。
3969    fn intrinsic_inline_size_ext(&self, avail_hint: i32, pure: bool) -> (i32, i32) {
3970        // inline-flex / inline-grid: 幅は内在幅(shrink-to-fit)、高さは試し組みで測る。
3971        if self.is_inline_flex() || self.is_inline_grid() {
3972            if pure {
3973                let (minc, maxc) = self.block_intrinsic_widths();
3974                return (if avail_hint <= 1 { minc } else { maxc }, 0);
3975            }
3976            let w = self.inline_flex_content_width(avail_hint);
3977            let mut probe = self.clone();
3978            probe.layout_as_flex_at(0, 0, w);
3979            return (w, probe.dimensions.content.height.max(0));
3980        }
3981        match &self.box_type {
3982            BoxType::InlineNode(node) | BoxType::InlineBlockNode(node) => {
3983                // `%` 幅は包含ブロック基準。純粋な内在幅の計測中(pure)は基準が
3984                // 無いので auto(自然サイズ)扱い(2026-09-26 まではビューポート基準)。
3985                // 【2026-10-04】ただし min-content の計測(`avail_hint <= 1`)では、
3986                // `%` の width/max-width を持つ置換要素は「縮められる」ので 0 基準で解く
3987                // (CSS Sizing 3 §5.2.2。`img{width:100%}` の min-content 寄与は 0)。
3988                // これが無いと、グリッドの fr 列の自動最小で画像の自然サイズが拾われ、
3989                // sugi-lab.net トップの写真の列が画面幅を超えて広がった。
3990                let base = if !pure {
3991                    Some(avail_hint)
3992                } else if avail_hint <= 1 {
3993                    Some(0)
3994                } else {
3995                    None
3996                };
3997                if let Some(size) = get_replaced_element_size_in(node, base) {
3998                    return size;
3999                }
4000                let font_size = node
4001                    .value("font-size")
4002                    .and_then(|v| parse_font_size_u32(&v))
4003                    .unwrap_or(16);
4004                let line_height_val = node.value("line-height");
4005                let line_height = parse_line_height(line_height_val, font_size);
4006
4007                // Check for explicit CSS width/height (e.g., inline-block with width: 200px)
4008                // `width: 50%`等の百分率が以前は完全に無視されており(`LengthValue::Px`
4009                // のみ対応)、`<iframe width="100%">`のようなレガシー属性経由の百分率
4010                // 指定(cascade.rsでCSS `width`へマッピング済み)が正しくサイズに
4011                // 反映されないバグだった。`avail_hint`(包含ブロックのコンテンツ幅)
4012                // 基準で解決する(2026-07-21発見・修正。実サイト www.sugi-lab.net の
4013                // Access ページの地図埋め込みで発覚)。
4014                // 文字ノードには width/height は当たらない(検索を省く。2026-10-19)。
4015                let is_text_node = matches!(node.node.node_type, NodeType::Text(_));
4016                let css_w = if is_text_node { None } else {
4017                    len_value(node, "width").and_then(|v| match parse_length_value(Some(v)) {
4018                        Some(LengthValue::Px(px)) => Some(px),
4019                        Some(LengthValue::Percent(pct)) => {
4020                            Some(round_f32_to_i32(avail_hint as f32 * pct))
4021                        }
4022                        _ => None,
4023                    })
4024                };
4025                let css_h = if is_text_node { None } else {
4026                    node.value("height").and_then(|v| match parse_length_value(Some(v)) {
4027                        Some(LengthValue::Px(px)) => Some(px),
4028                        _ => None,
4029                    })
4030                };
4031
4032                match &node.node.node_type {
4033                    NodeType::Text(text) => {
4034                        // white-space: nowrap/pre は折り返さない(1行に収める)。
4035                        // white-space は継承プロパティなのでテキストノード自身の
4036                        // styled node にも祖先から伝播済みの値が入っている。
4037                        let nowrap = matches!(
4038                            node.value("white-space").as_deref(),
4039                            Some("nowrap") | Some("pre")
4040                        );
4041                        let pre_wrap = matches!(
4042                            node.value("white-space").as_deref(),
4043                            Some("pre-wrap") | Some("pre-line")
4044                        );
4045                        let break_all = matches!(
4046                            node.value("word-break").as_deref(),
4047                            Some("break-all")
4048                        );
4049                        let overflow_wrap = matches!(
4050                            node.value("overflow-wrap").as_deref(),
4051                            Some("break-word") | Some("anywhere")
4052                        ) || matches!(
4053                            node.value("word-wrap").as_deref(),
4054                            Some("break-word") | Some("anywhere")
4055                        ) || matches!(
4056                            node.value("word-break").as_deref(),
4057                            Some("break-word")
4058                        );
4059                        // 【2026-09-05】`avail_hint` を優先する形へ変えたが、
4060                        // ヒーローの見出しが 2 行に折り返す退行を招いたため戻した。
4061                        //
4062                        // 内在幅は本来「今どう組まれているか」に依存しない値で、
4063                        // 現在の箱の幅を先に見るのは正しくない(min を求めても
4064                        // max を求めても同じ値が返る)。ただしこの関数は
4065                        // 内在幅の算出以外からも呼ばれており、そちらは
4066                        // 現在の幅を前提にしている。直すなら**呼び出し側を
4067                        // 全部見てから**にすること。詳細は `spec/flex_min_size.md`。
4068                        let available = if nowrap {
4069                            i32::MAX / 2
4070                        } else {
4071                            css_w.unwrap_or({
4072                                if pure {
4073                                    // 内在幅の算出。今の箱の幅は見ない。
4074                                    if avail_hint > 0 {
4075                                        avail_hint
4076                                    } else {
4077                                        800
4078                                    }
4079                                } else if self.dimensions.content.width > 0 {
4080                                    // 【2026-09-26 バグ修正】前回組んだ幅が今回の利用可能幅より
4081                                    // 広ければ今回の幅で折り返す。flex の縮小などで組み直すと、
4082                                    // 文字だけが縮小前の広い幅のまま折り返し、箱の外へはみ出して
4083                                    // いた(profile.htm の研究概要)。狭い側は従来どおり前回の幅
4084                                    // (全面的に今回の幅へ切り替えるとヒーロー見出しが退行した。
4085                                    // 2026-09-05 の記録)。
4086                                    if avail_hint > 0 {
4087                                        self.dimensions.content.width.min(avail_hint)
4088                                    } else {
4089                                        self.dimensions.content.width
4090                                    }
4091                                } else if avail_hint > 0 {
4092                                    avail_hint
4093                                } else {
4094                                    800
4095                                }
4096                            })
4097                        };
4098                        let ls = parse_letter_spacing_with_font(node.value("letter-spacing").as_deref(), available, font_size as f32);
4099                        let ws = parse_word_spacing_with_font(node.value("word-spacing").as_deref(), available, font_size as f32);
4100                        // 描画側と同じ規則で空白を畳んでから測る(2026-09-26。
4101                        // 畳まずに測ると HTML の字下げの分だけ箱が広く・高くなる)。
4102                        let text = &crate::os_lib::layout::line_break::collapse_white_space(
4103                            text,
4104                            &node.value("white-space").unwrap_or_default(),
4105                        );
4106                        let text = if self.trim_leading_space {
4107                            text.strip_prefix(' ').unwrap_or(text)
4108                        } else {
4109                            text.as_str()
4110                        };
4111                        // 描画と同じ書体(font-family・太字)で折り返し行数を測る(2026-09-26)。
4112                        let (fam, weight) = text_font_of(node);
4113                        let (w, h) = crate::kernel::vector_font::get_vector_string_wrapped_size_family(
4114                            text,
4115                            font_size,
4116                            available as u32,
4117                            ls,
4118                            ws,
4119                            break_all,
4120                            overflow_wrap,
4121                            pre_wrap,
4122                            &fam,
4123                            weight,
4124                        );
4125                        let lines = h as i32 / (font_size as i32 + 4).max(1);
4126                        (
4127                            css_w.unwrap_or(w as i32),
4128                            css_h.unwrap_or(lines * line_height),
4129                        )
4130                    }
4131                    NodeType::Element { tag_name, .. } => {
4132                        if tag_name == "br" {
4133                            (0, line_height)
4134                        } else {
4135                            // 【2026-10-04】中身が空の要素(疑似要素の中身も無い)は幅 0。
4136                            // 以前は一律 8px を与えていた。papers.htm の目次リンクで、
4137                            // FA 6 に存在しないアイコン `fa-presentation`(Chrome では幅 0)が
4138                            // 8px を取り、文字が他のリンクより右へずれていた。
4139                            // 空の錨 `<a name="..."></a>` も同じく余計な隙間を作っていた。
4140                            // 疑似要素で描くアイコンはこの分岐に来ない(`pseudo_content_size`)。
4141                            let (content_w, content_h) = if self.children.is_empty() {
4142                                (0, line_height)
4143                            } else {
4144                                let mut total_w = 0i32;
4145                                let mut max_h = line_height;
4146                                // inline-block の中身は、利用可能幅から自分の左右の余白を引いた幅で
4147                                // 組む(shrink-to-fit。2026-09-26。引かないと中の文字が折り返さず、
4148                                // forms.htm の長いボタンがカードの右へはみ出した)。
4149                                let avail_hint = if matches!(self.box_type, BoxType::InlineBlockNode(_))
4150                                    && avail_hint > 0
4151                                    && avail_hint < 1_000_000
4152                                {
4153                                    (avail_hint - intrinsic_h_edges(self, css_w.is_some())).max(1)
4154                                } else {
4155                                    avail_hint
4156                                };
4157                                for child in &self.children {
4158                                    // 【2026-09-05】ここは `pure` を**引き継がない**。
4159                                    //
4160                                    // 引き継ぐと入れ子の要素の内在幅が変わり、
4161                                    // ナビ項目の間隔が潰れる退行が出た
4162                                    // (実測。ロゴの重なりは解消しないまま
4163                                    // 悪化だけした)。理屈の上では引き継ぐのが
4164                                    // 正しいはずで、引き継いで壊れるということは
4165                                    // **どこかが非 pure の値に依存している**。
4166                                    // 直すならその依存を先に突き止めること。
4167                                    // 【2026-10-18】ただし文字ノード(葉)は pure を引き継ぐ。非 pure の文字の
4168                                    // 計測は今組まれている幅を基準にするため、一度狭く組まれたインライン要素の
4169                                    // max-content がその幅に縮んでいた(YouTube の「その他の動画」の span が
4170                                    // 1 文字幅になり、縦に 1 文字ずつ折り返した)。入れ子の要素は従来どおり。
4171                                    let child_is_text = matches!(
4172                                        &child.box_type,
4173                                        BoxType::InlineNode(n) if matches!(n.node.node_type, NodeType::Text(_))
4174                                    );
4175                                    let child_is_block_level = matches!(
4176                                        child.box_type,
4177                                        BoxType::BlockNode(_) | BoxType::FlexNode(_) | BoxType::GridNode(_)
4178                                    );
4179                                    let (cw, ch) = if child_is_text {
4180                                        child.intrinsic_inline_size_ext(avail_hint, pure)
4181                                    } else if child_is_block_level {
4182                                        // 【2026-10-18】インラインの中のブロック級の箱(flex のボタン等)は、
4183                                        // ブロックとしての内在幅で測る(従来は (0,0) で、YouTube の操作部の
4184                                        // ボタンの並びが幅 0 と見積もられ、flex の余白を奪っていた)。
4185                                        let explicit = matches!(parse_length_value(box_style_value(child, "width")), Some(LengthValue::Px(_)));
4186                                        let (mn, mx) = match parse_length_value(box_style_value(child, "width")) {
4187                                            Some(LengthValue::Px(px)) => (px.max(0), px.max(0)),
4188                                            _ => child.block_intrinsic_widths(),
4189                                        };
4190                                        let w = if avail_hint <= 1 { mn } else { mx.min(avail_hint.max(0)) };
4191                                        (w + intrinsic_h_edges(child, explicit), child.dimensions.margin_box().height.max(0))
4192                                    } else {
4193                                        child.intrinsic_inline_size(avail_hint)
4194                                    };
4195                                    total_w += cw;
4196                                    if ch > max_h {
4197                                        max_h = ch;
4198                                    }
4199                                }
4200                                (total_w, max_h)
4201                            };
4202                            (css_w.unwrap_or(content_w), css_h.unwrap_or(content_h))
4203                        }
4204                    }
4205                }
4206            }
4207            _ => (0, 0),
4208        }
4209    }
4210
4211    /// 従来どおりの入口(現在の箱の幅を見る)。
4212    fn intrinsic_inline_size(&self, avail_hint: i32) -> (i32, i32) {
4213        self.intrinsic_inline_size_ext(avail_hint, false)
4214    }
4215
4216    fn layout_inline(&mut self, containing_block: Dimensions, _block_like: bool) {
4217        self.apply_box_model_styles_with_base(containing_block.content.width);
4218
4219        let (width_val, min_width_val, max_width_val, height_val, min_height_val, max_height_val) =
4220            match &self.box_type {
4221                // 文字ノードには width/height 系は当たらず継承もしない(【2026-10-19】全文字ノードで
4222                // 6 個の値を検索していた。レイアウトの最頻出の経路)。
4223                BoxType::InlineNode(node) if matches!(node.node.node_type, NodeType::Text(_)) => {
4224                    (None, None, None, None, None, None)
4225                }
4226                BoxType::InlineNode(node) | BoxType::InlineBlockNode(node) => (
4227                    len_value(node, "width"),
4228                    len_value(node, "min-width"),
4229                    len_value(node, "max-width"),
4230                    len_value(node, "height"),
4231                    len_value(node, "min-height"),
4232                    len_value(node, "max-height"),
4233                ),
4234                _ => return,
4235            };
4236
4237        let available_width = containing_block.content.width.max(1);
4238        let available_height = containing_block.content.height.max(1);
4239        let (intrinsic_w, intrinsic_h) = self.inline_content_intrinsic_size(available_width);
4240
4241        let mut content_width = match parse_length_value(width_val) {
4242            Some(LengthValue::Auto) | None => intrinsic_w.max(1),
4243            Some(LengthValue::Px(px)) => px.max(1),
4244            Some(LengthValue::Percent(pct)) => {
4245                round_f32_to_i32(available_width as f32 * pct).max(1)
4246            }
4247            Some(LengthValue::MinContent) => {
4248                let (minc, _maxc) = self.block_intrinsic_widths();
4249                minc.max(1)
4250            }
4251            Some(LengthValue::MaxContent) => {
4252                let (_minc, maxc) = self.block_intrinsic_widths();
4253                maxc.max(1)
4254            }
4255            Some(LengthValue::FitContent) => {
4256                let (minc, maxc) = self.block_intrinsic_widths();
4257                available_width.min(maxc).max(minc).max(1)
4258            }
4259            Some(LengthValue::FitContentWith(lim_box)) => {
4260                let limit = resolve_length_value_with_font(*lim_box, available_width, self.font_size())
4261                    .unwrap_or(available_width);
4262                let (minc, maxc) = self.block_intrinsic_widths();
4263                maxc.min(limit.max(minc)).max(1)
4264            }
4265            Some(LengthValue::MathExpr(expr)) => {
4266                crate::os_lib::css::eval_css_math_with_font(&expr, available_width, self.font_size())
4267                    .unwrap_or(intrinsic_w.max(1))
4268                    .max(1)
4269            }
4270        };
4271        if let Some(min_width) = parse_length_value(min_width_val)
4272            .and_then(|v| resolve_length_value_with_font(v, available_width, self.font_size()))
4273        {
4274            content_width = content_width.max(min_width);
4275        }
4276        if let Some(max_width) = parse_length_value(max_width_val)
4277            .and_then(|v| resolve_length_value_with_font(v, available_width, self.font_size()))
4278        {
4279            content_width = content_width.min(max_width);
4280        }
4281
4282        let mut content_height = match parse_length_value(height_val) {
4283            Some(LengthValue::Auto)
4284            | Some(LengthValue::MinContent)
4285            | Some(LengthValue::MaxContent)
4286            | Some(LengthValue::FitContent)
4287            | Some(LengthValue::FitContentWith(_))
4288            | None => intrinsic_h.max(1),
4289            Some(LengthValue::Px(px)) => px.max(1),
4290            // 【2026-09-26 バグ修正】`%` 高さは包含ブロックの高さが確定しているときだけ
4291            // 有効(仕様では未確定なら auto)。ここの `containing_block.content.height` は
4292            // 呼び出し側の縦カーソル位置(flex の計測では 0)で高さではないため、
4293            // `img{max-height:100%}` が 1px に潰れていた。auto として扱う。
4294            // 【2026-10-03】包含ブロックの高さが確定していれば(`PCT_BASE_H`)それ基準。
4295            Some(LengthValue::Percent(pct)) => match PCT_BASE_H.load(AtomicOrdering::Relaxed) {
4296                b if b >= 0 => round_f32_to_i32(b as f32 * pct).max(1),
4297                _ => intrinsic_h.max(1),
4298            },
4299            Some(LengthValue::MathExpr(expr)) => {
4300                crate::os_lib::css::eval_css_math_with_font(&expr, available_height, self.font_size())
4301                    .unwrap_or(intrinsic_h.max(1))
4302                    .max(1)
4303            }
4304        };
4305        if let Some(min_height) = parse_length_value(min_height_val)
4306            .filter(|v| !matches!(v, LengthValue::Percent(_)))
4307            .and_then(|v| resolve_length_value_with_font(v, available_height, self.font_size()))
4308        {
4309            content_height = content_height.max(min_height);
4310        }
4311        if let Some(max_height) = parse_length_value(max_height_val)
4312            .filter(|v| !matches!(v, LengthValue::Percent(_)))
4313            .and_then(|v| resolve_length_value_with_font(v, available_height, self.font_size()))
4314        {
4315            content_height = content_height.min(max_height);
4316        }
4317
4318        self.dimensions.content.width = content_width.max(1);
4319        self.dimensions.content.height = content_height.max(1);
4320        self.dimensions.content.x = containing_block.content.x
4321            + self.dimensions.margin.left
4322            + self.dimensions.border.left
4323            + self.dimensions.padding.left;
4324        self.dimensions.content.y = containing_block.content.y
4325            + containing_block.content.height
4326            + self.dimensions.margin.top
4327            + self.dimensions.border.top
4328            + self.dimensions.padding.top;
4329
4330        // 子要素のレイアウト計算を行う。インライン要素(!block_like)であっても、
4331        // 内包するテキストや子インライン要素の座標を設定する必要があるため、常に実行します。
4332        let mut child_flow_x = 0i32;
4333        let mut child_flow_y = 0i32;
4334        let mut line_h = 0i32;
4335        let max_w = self.dimensions.content.width.max(1);
4336        // インライン/インラインブロックの vertical-align(既定 top/baseline は無調整): 1行の
4337        // 全子要素が揃って初めて行の高さ(line_h)が確定するため、行の開始インデックスを覚えておき、
4338        // 改行が確定した時点でその行の子要素だけを事後的に `translate_y` でシフトする。
4339        let mut line_start_idx = 0usize;
4340
4341        let mut i = 0usize;
4342        while i < self.children.len() {
4343            match self.children[i].box_type {
4344                BoxType::BlockNode(_)
4345                | BoxType::FlexNode(_)
4346                | BoxType::GridNode(_)
4347                | BoxType::TableNode(_)
4348                | BoxType::TableRowGroupNode(_)
4349                | BoxType::TableRowNode(_)
4350                | BoxType::TableCellNode(_) => {
4351                    if child_flow_x > 0 {
4352                        self.finalize_inline_line_vertical_align(line_start_idx, i, line_h, max_w);
4353                        child_flow_y += line_h;
4354                        child_flow_x = 0;
4355                        line_h = 0;
4356                    }
4357                    line_start_idx = i + 1;
4358                    let child = &mut self.children[i];
4359                    let mut cb = self.dimensions.clone();
4360                    cb.content.height = child_flow_y;
4361                    child.layout(cb);
4362                    child_flow_y += child.dimensions.margin_box().height;
4363                    // 【2026-09-26 バグ修正】この `i += 1` が抜けており、インライン要素の
4364                    // 中にブロックの子があると(`<a><div>..</div></a>` 等、実サイトで
4365                    // ごく普通の形)同じ子を永遠にレイアウトし続けて**ブラウザが固まって**いた。
4366                    i += 1;
4367                }
4368                BoxType::InlineNode(_) | BoxType::InlineBlockNode(_) => {
4369                    if self.children[i].is_inline_line_break() {
4370                        self.finalize_inline_line_vertical_align(line_start_idx, i, line_h, max_w);
4371                        child_flow_y += line_h.max(20);
4372                        child_flow_x = 0;
4373                        line_h = 0;
4374                        line_start_idx = i + 1;
4375                        i += 1;
4376                        continue;
4377                    }
4378                    let (child_w, child_h) = self.children[i].intrinsic_inline_size(max_w);
4379                    if child_flow_x > 0 && child_flow_x + child_w > max_w {
4380                        self.finalize_inline_line_vertical_align(line_start_idx, i, line_h, max_w);
4381                        child_flow_y += line_h.max(20);
4382                        child_flow_x = 0;
4383                        line_h = 0;
4384                        line_start_idx = i;
4385                    }
4386                    let child = &mut self.children[i];
4387                    child.dimensions.content.x = self.dimensions.content.x + child_flow_x;
4388                    child.dimensions.content.y = self.dimensions.content.y + child_flow_y;
4389                    child.dimensions.content.width = child_w;
4390                    child.dimensions.content.height = child_h;
4391
4392                    // 子要素(InlineNode など)に対して再帰的にレイアウトを呼び出して、さらにその中の座標を決定する。
4393                    // 注意: 子自身が InlineNode/InlineBlockNode の場合、渡した cb.content.height
4394                    // (ここでは子自身の高さ)を子の layout_inline が「Y フローカーソル」として
4395                    // 自身の content.y に加算してしまい、確定済みのスロット位置を上書きしてしまう
4396                    // (layout_block の inline 子ループに既存の同種ワークアラウンドと同じ理由)。
4397                    // そのため子のレイアウト後に確定済みのスロット位置へ再設定する。
4398                    let cb = child.dimensions.clone();
4399                    child.layout(cb);
4400                    // 子孫ごとスロットへ戻す(layout_block の inline 子と同じ理由。2026-09-26)。
4401                    let (el, er, et, eb) = child.inline_block_edges();
4402                    let slot_x = self.dimensions.content.x + child_flow_x + el;
4403                    let slot_y = self.dimensions.content.y + child_flow_y + et;
4404                    let (dx, dy) = (slot_x - child.dimensions.content.x, slot_y - child.dimensions.content.y);
4405                    child.translate(dx, dy);
4406                    child.dimensions.content.width = child_w;
4407                    child.dimensions.content.height = child_h;
4408
4409                    child_flow_x += el + child_w + er;
4410                    let outer_h = et + child_h + eb;
4411                    if outer_h > line_h {
4412                        line_h = outer_h;
4413                    }
4414                    i += 1;
4415                }
4416                BoxType::AnonymousBlock => {
4417                    i += 1;
4418                }
4419            }
4420        }
4421        // 最終行(末尾に改行/ブロック要素が無いまま終わる行)の vertical-align を適用する。
4422        self.finalize_inline_line_vertical_align(line_start_idx, self.children.len(), line_h, max_w);
4423    }
4424
4425    /// インラインの1行ぶん(`[start, end)` の子要素)に `vertical-align` を適用する。
4426    /// **簡略化**: `top`/`baseline`/`sub`/`super`/`text-top` はいずれも無調整(top相当)、
4427    /// `middle` は行の中央、`bottom`/`text-bottom` は行の下端に揃える。table セルの
4428    /// vertical-align(内容のみシフト)とは異なり、ここでは子要素自身(背景/境界線含む)を
4429    /// まるごとシフトする(インラインボックス自体の行内配置を変えるのが目的のため)。
4430    fn finalize_inline_line_vertical_align(&mut self, start: usize, end: usize, line_h: i32, max_w: i32) {
4431        if line_h <= 0 || end <= start {
4432            return;
4433        }
4434        for idx in start..end {
4435            let Some(child) = self.children.get_mut(idx) else {
4436                continue;
4437            };
4438            if !matches!(
4439                child.box_type,
4440                BoxType::InlineNode(_) | BoxType::InlineBlockNode(_)
4441            ) {
4442                continue;
4443            }
4444            let va = box_style_value(child, "vertical-align").unwrap_or_default();
4445            let offset = match va.trim() {
4446                "middle" => {
4447                    let (_, child_h) = child.intrinsic_inline_size(max_w);
4448                    (line_h - child_h) / 2
4449                }
4450                "bottom" | "text-bottom" => {
4451                    let (_, child_h) = child.intrinsic_inline_size(max_w);
4452                    line_h - child_h
4453                }
4454                _ => 0,
4455            };
4456            if offset != 0 {
4457                child.translate_y(offset);
4458            }
4459        }
4460    }
4461
4462    /// min-width/max-width 等のサイジング値を解決する。px/% は base 基準、
4463    /// `min-content`/`max-content`/`fit-content` は内容計測で解決。auto/未指定は None。
4464    fn resolve_sizing_value(&self, value: Option<String>, base: i32) -> Option<i32> {
4465        match parse_length_value(value)? {
4466            LengthValue::Auto => None,
4467            LengthValue::Px(px) => Some(px),
4468            LengthValue::Percent(pct) => Some(round_f32_to_i32(base as f32 * pct)),
4469            LengthValue::MinContent => Some(self.block_intrinsic_widths().0),
4470            LengthValue::MaxContent => Some(self.block_intrinsic_widths().1),
4471            LengthValue::FitContent => {
4472                let (minc, maxc) = self.block_intrinsic_widths();
4473                Some(maxc.min(base.max(0)).max(minc))
4474            }
4475            LengthValue::FitContentWith(lim_box) => {
4476                let limit = resolve_length_value_with_font(*lim_box, base, self.font_size())
4477                    .unwrap_or(base);
4478                let (minc, maxc) = self.block_intrinsic_widths();
4479                Some(maxc.min(limit.max(minc)))
4480            }
4481            LengthValue::MathExpr(expr) => {
4482                crate::os_lib::css::eval_css_math_with_font(&expr, base, self.font_size())
4483            }
4484        }
4485    }
4486
4487    /// ブロック要素の内容ベース幅を返す: (min_content, max_content)。
4488    /// max_content = 折返しなしの最大行幅、min_content = 最大限折返したときの最小幅。
4489    /// 直下の子(inline/text/block)を計測して集約する。
4490    pub(crate) fn block_intrinsic_widths(&self) -> (i32, i32) {
4491        if let BoxType::BlockNode(n) | BoxType::InlineBlockNode(n) | BoxType::InlineNode(n) = &self.box_type {
4492            if let Some((w, _)) = pseudo_content_size(n) {
4493                return (w, w);
4494            }
4495        }
4496        // 自身が inline/inline-block を直接表すなら、その計測を使う
4497        // (inline-flex は下の flex コンテナの計測へ回す)。
4498        if matches!(
4499            self.box_type,
4500            BoxType::InlineNode(_) | BoxType::InlineBlockNode(_)
4501        ) && !self.is_inline_flex()
4502            && !self.is_inline_grid()
4503        {
4504            let max_c = self.intrinsic_inline_size_ext(1_000_000, true).0;
4505            let min_c = self.intrinsic_inline_size_ext(1, true).0;
4506            return (min_c.max(0), max_c.max(min_c).max(0));
4507        }
4508        // 【2026-09-04】横並びの flex コンテナは、子を**足し合わせる**。
4509        //
4510        // 従来はブロックとして「子は縦に積む」前提で最大値を取っていた。
4511        // その結果、ナビの `ul`(`display:flex` の 8 項目)の max-content が
4512        // 最も広い 1 項目ぶん(80px)になり、本来の 656px と大きく食い違って
4513        // いた。親の flex は基準幅にこの値を使うので、ナビが極端に小さく
4514        // 見積もられ、`justify-content:space-between` で右へ飛んでいた。
4515        //
4516        // 折り返さない(`flex-wrap: nowrap`、既定)横並びなら、
4517        // min-content も max-content も**子の総和**になる。
4518        // inline-grid: 列がすべて px なら「列幅の合計 + 列間 gap」(2026-09-26)。
4519        // fr/auto 等を含む場合は下の汎用計測(子の最大)に任せる。
4520        if let BoxType::InlineBlockNode(node) = &self.box_type {
4521            if self.is_inline_grid() {
4522                let cols = parse_grid_tracks(
4523                    node.value("grid-template-columns").as_deref().unwrap_or(""),
4524                    0,
4525                );
4526                let all_px: Option<i32> = if cols.is_empty() {
4527                    None
4528                } else {
4529                    cols.iter().try_fold(0i32, |acc, t| match t {
4530                        GridTrack::Px(px) => Some(acc + (*px).max(0)),
4531                        _ => None,
4532                    })
4533                };
4534                if let Some(sum) = all_px {
4535                    let (_, cg) = parse_flex_gap(node, 0);
4536                    let w = sum + cg.max(0) * (cols.len() as i32 - 1).max(0);
4537                    return (w, w);
4538                }
4539            }
4540        }
4541        let flex_node = match &self.box_type {
4542            BoxType::FlexNode(n) => Some(*n),
4543            BoxType::InlineBlockNode(n) if self.is_inline_flex() => Some(*n),
4544            _ => None,
4545        };
4546        if let Some(node) = flex_node {
4547            let dir = node
4548                .value("flex-direction")
4549                .unwrap_or_else(|| alloc::string::String::from("row"))
4550                .trim()
4551                .to_lowercase();
4552            let is_row = !dir.starts_with("column");
4553            let wraps = node
4554                .value("flex-wrap")
4555                .map(|w| w.trim().to_lowercase().starts_with("wrap"))
4556                .unwrap_or(false);
4557            if is_row {
4558                let mut sum_min = 0i32;
4559                let mut sum_max = 0i32;
4560                let mut max_min = 0i32;
4561                for child in &self.children {
4562                    // 【2026-09-26】項目に明示の px 幅があれば、行の内在幅への寄与はその値
4563                    // (中身から測ると `width:30px` の空ブロックが 8px 等になり、
4564                    // inline-flex の幅が狂った)。項目自身の block_intrinsic_widths は
4565                    // flex の自動最小サイズ(中身基準であるべき)にも使われるので、
4566                    // そちらは変えずにここでだけ差し替える。
4567                    let explicit = match parse_length_value(box_style_value(child, "width")) {
4568                        Some(LengthValue::Px(px)) => Some(px.max(0)),
4569                        _ => None,
4570                    };
4571                    let (cmin, cmax) = explicit.map(|w| (w, w)).unwrap_or_else(|| child.block_intrinsic_widths());
4572                    let (cmin, cmax) = clamp_contribution(child, cmin, cmax, explicit.is_none());
4573                    let edges = intrinsic_h_edges(child, explicit.is_some());
4574                    let (cmin, cmax) = (cmin + edges, cmax + edges);
4575                    sum_min += cmin;
4576                    sum_max += cmax;
4577                    max_min = max_min.max(cmin);
4578                }
4579                let (rg, cg) = parse_flex_gap(node, self.dimensions.content.width);
4580                let _ = rg;
4581                let n = self.children.len() as i32;
4582                let gaps = if n > 1 { cg * (n - 1) } else { 0 };
4583                // 折り返す指定なら、最小は 1 項目ぶんまで詰められる。
4584                let min_c = if wraps { max_min } else { sum_min + gaps };
4585                return (min_c.max(0), (sum_max + gaps).max(min_c).max(0));
4586            }
4587        }
4588
4589        // 【2026-10-04】グリッド容器は列が横に並ぶので、列ごとに項目の最大を取り
4590        // 「列の合計 + 列間 gap」にする(`grid::grid_intrinsic_widths`)。
4591        // 従来は下の汎用集計(子の最大)に落ちていて、統計カード 3 枚のグリッドの
4592        // min-content がカード 1 枚分になり、親グリッドの fr 列の自動最小に効かなかった。
4593        // 近似: 項目は記述順に行優先で 1 列ずつ並ぶとみなす(grid-column の明示配置・
4594        // 複数列にまたがる項目は内在幅の計測では考えない)。
4595        let grid_node = match &self.box_type {
4596            BoxType::GridNode(n) => Some(*n),
4597            BoxType::InlineBlockNode(n) if self.is_inline_grid() => Some(*n),
4598            _ => None,
4599        };
4600        if let Some(node) = grid_node {
4601            let template = node.value("grid-template-columns").unwrap_or_default();
4602            let n_cols = grid_column_count_for_intrinsic(&template);
4603            let tracks = parse_grid_tracks(&template, 0);
4604            let fixed: Vec<Option<i32>> = (0..n_cols)
4605                .map(|i| match tracks.get(i) {
4606                    Some(GridTrack::Px(px)) if tracks.len() == n_cols => Some(*px),
4607                    _ => None,
4608                })
4609                .collect();
4610            let mut items: Vec<(usize, i32, i32)> = Vec::new();
4611            for child in self.children.iter().filter(|c| !Self::child_is_out_of_flow(c)) {
4612                let (cmin, cmax, explicit) = match parse_length_value(box_style_value(child, "width")) {
4613                    Some(LengthValue::Px(px)) => (px.max(0), px.max(0), true),
4614                    _ => {
4615                        let (a, b) = child.block_intrinsic_widths();
4616                        (a, b, false)
4617                    }
4618                };
4619                let (cmin, cmax) = clamp_contribution(child, cmin, cmax, !explicit);
4620                let edges = intrinsic_h_edges(child, explicit);
4621                items.push((items.len() % n_cols, cmin + edges, cmax + edges));
4622            }
4623            let (_, cg) = parse_flex_gap(node, 0);
4624            return grid_intrinsic_widths(n_cols, &fixed, &items, cg);
4625        }
4626
4627        self.children_intrinsic_widths()
4628    }
4629
4630    /// 子の内在幅を集約する(ブロックとしての中身。インラインの子は同じ行に並ぶので max は加算・
4631    /// min は最大、ブロックの子は縦に積むので両方とも最大)。
4632    pub(crate) fn children_intrinsic_widths(&self) -> (i32, i32) {
4633        // ブロック子は縦に積むため両者とも最大。
4634        let mut inline_max = 0i32; // 現在のインライン連なりの max-content 合計
4635        let mut overall_max = 0i32;
4636        let mut overall_min = 0i32;
4637        for child in &self.children {
4638            // 【2026-10-03】`<br>` は行を締める(前後の行幅を足さない)。sugi-lab.net のヒーローの
4639            // 副題 2 行が 1 行として足され、flex の子の板が Chrome より広くなっていた。
4640            if child.is_inline_line_break() {
4641                overall_max = overall_max.max(inline_max);
4642                inline_max = 0;
4643                continue;
4644            }
4645            match child.box_type {
4646                BoxType::InlineNode(_) | BoxType::InlineBlockNode(_) => {
4647                    // intrinsic_inline_size は inline-block の明示 width を尊重する。
4648                    // 【2026-10-18】inline-block(と置換要素)の左右の margin も行の幅に入る。従来は
4649                    // 含めず、YouTube の中央の再生ボタン(幅 56px・`margin:0 34px`)の包みが 56px に
4650                    // 縮み、ボタンが右へはみ出してクリックが当たらなかった。
4651                    let mut cmax = child.intrinsic_inline_size_ext(1_000_000, true).0;
4652                    let mut cmin = child.intrinsic_inline_size_ext(1, true).0;
4653                    if matches!(child.box_type, BoxType::InlineBlockNode(_)) && !child.is_replaced_inline() {
4654                        let explicit = matches!(parse_length_value(box_style_value(child, "width")), Some(LengthValue::Px(_)));
4655                        // 幅 auto で中身がブロック等なら、中身の集約の方が広いことがある(従来は文字だけを測り 0)。
4656                        if !explicit && !child.is_inline_flex() && !child.is_inline_grid() {
4657                            let (amin, amax) = child.children_intrinsic_widths();
4658                            cmin = cmin.max(amin);
4659                            cmax = cmax.max(amax);
4660                        }
4661                        let edges = intrinsic_h_edges(child, explicit);
4662                        cmin += edges;
4663                        cmax += edges;
4664                    } else if child.is_replaced_inline() {
4665                        let m = inline_h_margins(child);
4666                        cmin += m;
4667                        cmax += m;
4668                    }
4669                    inline_max += cmax;
4670                    overall_min = overall_min.max(cmin);
4671                }
4672                _ => {
4673                    overall_max = overall_max.max(inline_max);
4674                    inline_max = 0;
4675                    // 子に明示の px 幅があれば寄与はその値(2026-09-26。flex 行の合計と同じ)。
4676                    let (cmin, cmax, explicit) = match parse_length_value(box_style_value(child, "width")) {
4677                        Some(LengthValue::Px(px)) => (px.max(0), px.max(0), true),
4678                        _ => {
4679                            let (a, b) = child.block_intrinsic_widths();
4680                            (a, b, false)
4681                        }
4682                    };
4683                    let (cmin, cmax) = clamp_contribution(child, cmin, cmax, !explicit);
4684                    let edges = intrinsic_h_edges(child, explicit);
4685                    let (cmin, cmax) = (cmin + edges, cmax + edges);
4686                    overall_min = overall_min.max(cmin);
4687                    overall_max = overall_max.max(cmax);
4688                }
4689            }
4690        }
4691        overall_max = overall_max.max(inline_max);
4692        (overall_min.max(0), overall_max.max(overall_min).max(0))
4693    }
4694
4695    fn inline_content_intrinsic_size(&self, available_width: i32) -> (i32, i32) {
4696        match &self.box_type {
4697            BoxType::InlineNode(node) | BoxType::InlineBlockNode(node) => {
4698                if let Some(size) = get_replaced_element_size_in(node, Some(available_width)) {
4699                    return size;
4700                }
4701                match &node.node.node_type {
4702                    NodeType::Text(text) => {
4703                        let font_size = node
4704                            .value("font-size")
4705                            .and_then(|v| parse_font_size_u32(&v))
4706                            .unwrap_or(16);
4707                        let line_height_val = node.value("line-height");
4708                        let line_height = parse_line_height(line_height_val, font_size);
4709                        let pre_wrap = matches!(
4710                            node.value("white-space").as_deref(),
4711                            Some("pre-wrap") | Some("pre-line")
4712                        );
4713                        let break_all = matches!(
4714                            node.value("word-break").as_deref(),
4715                            Some("break-all")
4716                        );
4717                        // CSS Text 3 §5.5: min-content 計測時は、`anywhere` のみ soft wrap opportunity を考慮し、
4718                        // `break-word` は `normal` と同様に単語内での折り返しを考慮しない。
4719                        let is_anywhere = matches!(
4720                            node.value("overflow-wrap").as_deref(),
4721                            Some("anywhere")
4722                        ) || matches!(
4723                            node.value("word-wrap").as_deref(),
4724                            Some("anywhere")
4725                        );
4726                        let is_break_word = matches!(
4727                            node.value("overflow-wrap").as_deref(),
4728                            Some("break-word")
4729                        ) || matches!(
4730                            node.value("word-wrap").as_deref(),
4731                            Some("break-word")
4732                        ) || matches!(
4733                            node.value("word-break").as_deref(),
4734                            Some("break-word")
4735                        );
4736                        let overflow_wrap = if available_width <= 100 {
4737                            is_anywhere
4738                        } else {
4739                            is_anywhere || is_break_word
4740                        };
4741                        // 描画側と同じ規則で空白を畳んでから測る(2026-09-26。
4742                        // 畳まずに測ると HTML の字下げの分だけ箱が広く・高くなる)。
4743                        let text = &crate::os_lib::layout::line_break::collapse_white_space(
4744                            text,
4745                            &node.value("white-space").unwrap_or_default(),
4746                        );
4747                        let text = if self.trim_leading_space {
4748                            text.strip_prefix(' ').unwrap_or(text)
4749                        } else {
4750                            text.as_str()
4751                        };
4752                        let (fam, weight) = text_font_of(node);
4753                        let (w, h) = crate::kernel::vector_font::get_vector_string_wrapped_size_family(
4754                            text,
4755                            font_size,
4756                            available_width.max(1) as u32,
4757                            0, // ls
4758                            0, // ws
4759                            break_all,
4760                            overflow_wrap,
4761                            pre_wrap,
4762                            &fam,
4763                            weight,
4764                        );
4765                        let lines = h as i32 / (font_size as i32 + 4).max(1);
4766                        (w as i32, lines * line_height)
4767                    }
4768                    NodeType::Element { .. } if pseudo_content_size(node).is_some() => {
4769                        pseudo_content_size(node).unwrap_or((0, 0))
4770                    }
4771                    NodeType::Element { .. } => {
4772                        let mut max_line_w = 0i32;
4773                        let mut total_h = 0i32;
4774                        let mut current_line_w = 0i32;
4775                        let mut current_line_h = 0i32;
4776                        for child in &self.children {
4777                            if child.is_inline_line_break() {
4778                                if current_line_w > max_line_w {
4779                                    max_line_w = current_line_w;
4780                                }
4781                                total_h += current_line_h.max(20);
4782                                current_line_w = 0;
4783                                current_line_h = 0;
4784                                continue;
4785                            }
4786                            let (cw, ch) = child.intrinsic_inline_size(available_width.max(1));
4787                            if current_line_w > 0 && current_line_w + cw > available_width.max(1) {
4788                                if current_line_w > max_line_w {
4789                                    max_line_w = current_line_w;
4790                                }
4791                                total_h += current_line_h.max(20);
4792                                current_line_w = 0;
4793                                current_line_h = 0;
4794                            }
4795                            current_line_w += cw;
4796                            if ch > current_line_h {
4797                                current_line_h = ch;
4798                            }
4799                        }
4800                        if current_line_w > max_line_w {
4801                            max_line_w = current_line_w;
4802                        }
4803                        total_h += current_line_h.max(20);
4804                        if total_h == 0 {
4805                            let font_size = node
4806                                .value("font-size")
4807                                .and_then(|v| parse_font_size_u32(&v))
4808                                .unwrap_or(16);
4809                            return (8, (font_size as i32 + 4).max(12));
4810                        }
4811                        (max_line_w.max(1), total_h.max(1))
4812                    }
4813                }
4814            }
4815            _ => (0, 0),
4816        }
4817    }
4818
4819    fn is_inline_line_break(&self) -> bool {
4820        match &self.box_type {
4821            BoxType::InlineNode(node) | BoxType::InlineBlockNode(node) => {
4822                matches!(&node.node.node_type, NodeType::Element { tag_name, .. } if tag_name == "br")
4823            }
4824            _ => false,
4825        }
4826    }
4827
4828    fn apply_box_model_styles_with_base(&mut self, containing_width: i32) {
4829        // 【2026-09-26 バグ修正】従来は Block/InlineBlock 以外で即 return しており、
4830        // flex・grid・table コンテナの margin/padding/border が常に 0 だった
4831        // (`layout_flex`/`layout_grid`/`layout_table` は値が入る前提で計算している)。
4832        // 表の行・セルは表レイアウト側で余白を扱うため従来どおり対象外。
4833        let node = match &self.box_type {
4834            BoxType::BlockNode(n)
4835            | BoxType::InlineBlockNode(n)
4836            | BoxType::FlexNode(n)
4837            | BoxType::GridNode(n)
4838            | BoxType::TableNode(n) => n,
4839            BoxType::InlineNode(n) if self.is_replaced_inline() => n,
4840            _ => return,
4841        };
4842        let (m, p, b) = style_box_edges(node, containing_width, &self.dimensions);
4843        self.dimensions.margin = m;
4844        self.dimensions.padding = p;
4845        self.dimensions.border = b;
4846    }
4847
4848    fn apply_box_model_styles(&mut self) {
4849        self.apply_box_model_styles_with_base(0);
4850    }
4851}
4852