Repository: Kozea/WeasyPrint

Issue:
Improve flex layout
*This meta-issue is sponsored by 💖 [the NLnet Foundation](https://nlnet.nl/project/Weasyprint/) 💖.*

Let’s rewrite the whole Flex layout, and fix (hopefully) all these old and dirty issues!

All the work is done in the [`flex` branch](https://github.com/Kozea/WeasyPrint/tree/flex) until it is merged before releasing v65.

Discussion:

Patch:
diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py
index f2a842d3..c3c39176 100644
--- a/weasyprint/formatting_structure/build.py
+++ b/weasyprint/formatting_structure/build.py
@@ -724,9 +724,7 @@ def is_whitespace(box, _has_non_whitespace=re.compile('\\S').search):
 
 
 def wrap_improper(box, children, wrapper_type, test=None):
-    """
-    Wrap consecutive children that do not pass ``test`` in a box of type
-    ``wrapper_type``.
+    """Wrap consecutive children that do not pass ``test`` in a ``wrapper_type`` box.
 
     ``test`` defaults to children being of the same type as ``wrapper_type``.
 
@@ -744,17 +742,7 @@ def wrap_improper(box, children, wrapper_type, test=None):
                 improper = []
             yield child
         else:
-            # Whitespace either fail the test or were removed earlier,
-            # so there is no need to take special care with the definition
-            # of "consecutive".
-            if isinstance(box, boxes.FlexContainerBox):
-                # The display value of a flex item must be "blockified", see
-                # https://www.w3.org/TR/css-flexbox-1/#flex-items
-                # TODO: These blocks are currently ignored, we should
-                # "blockify" them and their children.
-                pass
-            else:
-                improper.append(child)
+            improper.append(child)
     if improper:
         wrapper = wrapper_type.anonymous_from(box, children=[])
         # Apply the rules again on the new wrapper
@@ -1023,6 +1011,7 @@ def flex_children(box, children):
     if isinstance(box, boxes.FlexContainerBox):
         flex_children = []
         for child in children:
+            child.is_floated = lambda: False
             if child.is_in_normal_flow():
                 child.is_flex_item = True
             if isinstance(child, boxes.TextBox) and not child.text.strip(' '):
@@ -1030,8 +1019,14 @@ def flex_children(box, children):
                 # affected by the white-space property"
                 # https://www.w3.org/TR/css-flexbox-1/#flex-items
                 continue
-            if isinstance(child, boxes.InlineLevelBox):
-                anonymous = boxes.BlockBox.anonymous_from(box, [child])
+            if isinstance(child, boxes.InlineBlockBox):
+                anonymous = boxes.BlockBox.anonymous_from(child, child.children)
+                anonymous.style = child.style
+                anonymous.is_flex_item = True
+                flex_children.append(anonymous)
+            elif isinstance(child, boxes.InlineLevelBox):
+                anonymous = boxes.BlockBox.anonymous_from(child, [child])
+                anonymous.style = child.style
                 anonymous.is_flex_item = True
                 flex_children.append(anonymous)
             else:
@@ -1069,7 +1064,12 @@ def grid_children(box, children):
                 # affected by the white-space property"
                 # https://drafts.csswg.org/css-grid-2/#grid-item
                 continue
-            if isinstance(child, boxes.InlineLevelBox):
+            if isinstance(child, boxes.InlineBlockBox):
+                anonymous = boxes.BlockBox.anonymous_from(child, child.children)
+                anonymous.style = child.style
+                anonymous.is_grid_item = True
+                grid_children.append(anonymous)
+            elif isinstance(child, boxes.InlineLevelBox):
                 anonymous = boxes.BlockBox.anonymous_from(child, [child])
                 anonymous.style = child.style
                 child.is_grid_item = False
diff --git a/weasyprint/layout/__init__.py b/weasyprint/layout/__init__.py
index 6e0cc322..67d929b1 100644
--- a/weasyprint/layout/__init__.py
+++ b/weasyprint/layout/__init__.py
@@ -215,6 +215,12 @@ def layout_document(html, root_box, context, max_loops=8):
         yield page
 
 
+class FakeList(list):
+    """List in which you can’t append objects."""
+    def append(self, item):
+        pass
+
+
 class LayoutContext:
     def __init__(self, style_for, get_image_from_uri, font_config,
                  counter_style, target_collector):
@@ -271,6 +277,17 @@ class LayoutContext:
         else:
             self.excluded_shapes = None
 
+    def create_flex_formatting_context(self):
+        self.excluded_shapes = FakeList()
+        self._excluded_shapes_lists.append(self.excluded_shapes)
+
+    def finish_flex_formatting_context(self, root_box):
+        self._excluded_shapes_lists.pop()
+        if self._excluded_shapes_lists:
+            self.excluded_shapes = self._excluded_shapes_lists[-1]
+        else:
+            self.excluded_shapes = None
+
     def get_string_set_for(self, page, name, keyword='first'):
         """Resolve value of string function."""
         return self.get_string_or_element_for(
diff --git a/weasyprint/layout/absolute.py b/weasyprint/layout/absolute.py
index a7ba5ae1..1c05a627 100644
--- a/weasyprint/layout/absolute.py
+++ b/weasyprint/layout/absolute.py
@@ -205,7 +205,7 @@ def absolute_block(context, box, containing_block, fixed_boxes, bottom_space,
         new_box, resume_at, _, _, _ = flex_layout(
             context, box, bottom_space, skip_stack, containing_block,
             page_is_empty=True, absolute_boxes=absolute_boxes,
-            fixed_boxes=fixed_boxes)
+            fixed_boxes=fixed_boxes, discard=False)
     elif isinstance(box, (boxes.GridContainerBox)):
         new_box, resume_at, _, _, _ = grid_layout(
             context, box, bottom_space, skip_stack, containing_block,
diff --git a/weasyprint/layout/block.py b/weasyprint/layout/block.py
index ad732ab1..bcbab9c0 100644
--- a/weasyprint/layout/block.py
+++ b/weasyprint/layout/block.py
@@ -10,7 +10,7 @@ from .float import avoid_collisions, float_layout, get_clearance
 from .grid import grid_layout
 from .inline import iter_line_boxes
 from .min_max import handle_min_max_width
-from .percent import resolve_percentages, resolve_position_percentages
+from .percent import percentage, resolve_percentages, resolve_position_percentages
 from .replaced import block_replaced_box_layout
 from .table import table_layout, table_wrapper_width
 
@@ -81,7 +81,7 @@ def block_level_layout_switch(context, box, bottom_space, skip_stack,
     elif isinstance(box, boxes.FlexBox):
         result = flex_layout(
             context, box, bottom_space, skip_stack, containing_block,
-            page_is_empty, absolute_boxes, fixed_boxes)
+            page_is_empty, absolute_boxes, fixed_boxes, discard)
     elif isinstance(box, boxes.GridBox):
         result = grid_layout(
             context, box, bottom_space, skip_stack, containing_block,
@@ -496,11 +496,24 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
                 adjoining_margins = []
                 position_y = box.content_box_y()
 
-    if adjoining_margins and box.is_table_wrapper:
-        collapsed_margin = collapse_margin(adjoining_margins)
-        child.position_y += collapsed_margin
-        position_y += collapsed_margin
-        adjoining_margins = []
+    # TODO: Merge this with block_container_layout, block_level_layout, _in_flow_layout,
+    # and check code above.
+    if adjoining_margins:
+        if box.is_table_wrapper:  # should not be a special case
+            collapsed_margin = collapse_margin(adjoining_margins)
+            child.position_y += collapsed_margin
+            position_y += collapsed_margin
+            adjoining_margins = []
+        elif not isinstance(child, boxes.BlockBox):  # blocks handle that themselves
+            if child.style['margin_top'] == 'auto':
+                margin_top = 0
+            else:
+                margin_top = percentage(child.style['margin_top'], box.width)
+            adjoining_margins.append(margin_top)
+            offset_y = collapse_margin(adjoining_margins) - margin_top
+            child.position_y += offset_y
+            position_y += offset_y
+            adjoining_margins = []
 
     page_is_empty_with_no_children = page_is_empty and not any(
         child for child in new_children
@@ -515,15 +528,6 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
          fixed_boxes, adjoining_margins, discard, max_lines)
 
     if new_child is not None:
-        # We need to do this after the child layout to have the
-        # used value for margin_top (eg. it might be a percentage.)
-        if not isinstance(new_child, (boxes.BlockBox, boxes.TableBox)):
-            adjoining_margins.append(new_child.margin_top)
-            offset_y = (
-                collapse_margin(adjoining_margins) - new_child.margin_top)
-            new_child.translate(0, offset_y)
-        # else: blocks handle that themselves.
-
         if not collapsing_through:
             new_content_position_y = (
                 new_child.content_box_y() + new_child.height)
@@ -629,13 +633,12 @@ def block_container_layout(context, box, bottom_space, skip_stack,
                            page_is_empty, absolute_boxes, fixed_boxes,
                            adjoining_margins, discard, max_lines):
     """Set the ``box`` height."""
-    # TODO: boxes.FlexBox is allowed here because flex_layout calls
-    # block_container_layout, there's probably a better solution.
-    assert isinstance(box, (boxes.BlockContainerBox, boxes.FlexBox))
+    assert isinstance(box, boxes.BlockContainerBox)
 
     if box.establishes_formatting_context():
         context.create_block_formatting_context()
 
+    # TODO: merge this with _in_flow_layout, flex_layout…
     is_start = skip_stack is None
     box.remove_decoration(start=not is_start, end=False)
 
@@ -753,7 +756,7 @@ def block_container_layout(context, box, bottom_space, skip_stack,
                 max_lines)
         elif stop:
             if box.height != 'auto':
-                if context.overflows(box.position_y + box.height, position_y):
+                if context.overflows(box.position_y + box.border_height(), position_y):
                     # Box heigh is fixed and it doesn’t overflow page, forget
                     # overflowing children.
                     resume_at = None
diff --git a/weasyprint/layout/flex.py b/weasyprint/layout/flex.py
index d2f65dd9..43e2bbb5 100644
--- a/weasyprint/layout/flex.py
+++ b/weasyprint/layout/flex.py
@@ -5,126 +5,88 @@ from math import inf, log10
 
 from ..css.properties import Dimension
 from ..formatting_structure import boxes
+from . import percent
 from .absolute import AbsolutePlaceholder, absolute_layout
-from .percent import resolve_one_percentage, resolve_percentages
-from .preferred import max_content_width, min_content_width
-from .table import find_in_flow_baseline
+from .preferred import max_content_width, min_content_width, min_max
+from .table import find_in_flow_baseline, table_wrapper_width
 
 
 class FlexLine(list):
-    pass
+    """Flex container line."""
 
 
-def flex_layout(context, box, bottom_space, skip_stack, containing_block,
-                page_is_empty, absolute_boxes, fixed_boxes):
-    from . import block, preferred
+def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_is_empty,
+                absolute_boxes, fixed_boxes, discard):
+    from . import block
 
-    context.create_block_formatting_context()
+    # TODO: merge this with block_container_layout.
+    context.create_flex_formatting_context()
     resume_at = None
 
+    is_start = skip_stack is None
+    box.remove_decoration(start=not is_start, end=False)
+
+    discard |= box.style['continue'] == 'discard'
+    draw_bottom_decoration = discard or box.style['box_decoration_break'] == 'clone'
+
+    row_gap, column_gap = box.style['row_gap'], box.style['column_gap']
+
+    if draw_bottom_decoration:
+        bottom_space += box.padding_bottom + box.border_bottom_width + box.margin_bottom
+
     if box.style['position'] == 'relative':
         # New containing block, use a new absolute list
         absolute_boxes = []
 
-    # 9.x references are to: https://www.w3.org/TR/css-flexbox-1/#layout-algorithm.
+    # References are to: https://www.w3.org/TR/css-flexbox-1/#layout-algorithm.
+
+    # 1 Initial setup, done in formatting_structure.build.
 
-    # 9.1. Generate anonymous flex items, done in formatting_structure.boxes.
-    # 9.2. Line length determination.
+    # 2 Determine the available main and cross space for the flex items.
     if box.style['flex_direction'].startswith('row'):
-        axis, cross = 'width', 'height'
+        main, cross = 'width', 'height'
     else:
-        axis, cross = 'height', 'width'
+        main, cross = 'height', 'width'
 
     margin_left = 0 if box.margin_left == 'auto' else box.margin_left
     margin_right = 0 if box.margin_right == 'auto' else box.margin_right
-    margin_top = 0 if box.margin_top == 'auto' else box.margin_top
-    margin_bottom = 0 if box.margin_bottom == 'auto' else box.margin_bottom
 
-    # 9.2.2 For each dimension, if that dimension of the flex
-    # container’s content box is a definite size, use that.
-    if getattr(box, axis) != 'auto':
-        available_main_space = getattr(box, axis)
+    # Define available main space.
+    # TODO: min- and max-content not implemented.
+    if getattr(box, main) != 'auto':
+        # If that dimension of the flex container’s content box is a definite size…
+        available_main_space = getattr(box, main)
     else:
-        # 9.2.2 If that dimension of the flex container is being sized
-        # under a min or max-content constraint, the available space
-        # in that dimension is that constraint.
-        # TODO: not implemented (at least, not correctly).
-
-        # 9.2.2 Otherwise, subtract the flex container’s margin,
-        # border, and padding from the space available to the flex
-        # container in that dimension and use that value. This might
-        # result in an infinite value.
-        if axis == 'width':
+        # Otherwise, subtract the flex container’s margin, border, and padding…
+        if main == 'width':
             available_main_space = (
                 containing_block.width -
                 margin_left - margin_right -
                 box.padding_left - box.padding_right -
                 box.border_left_width - box.border_right_width)
         else:
-            main_space = context.page_bottom - bottom_space - box.position_y
-            if containing_block.height != 'auto':
-                if isinstance(containing_block.height, Dimension):
-                    assert containing_block.height.unit == 'px'
-                    main_space = min(main_space, containing_block.height.value)
-                else:
-                    main_space = min(main_space, containing_block.height)
-            available_main_space = (
-                main_space -
-                margin_top - margin_bottom -
-                box.padding_top - box.padding_bottom -
-                box.border_top_width - box.border_bottom_width)
+            available_main_space = inf
 
-    # Same as above for available cross space
+    # Same as above for available cross space.
+    # TODO: min- and max-content not implemented.
     if getattr(box, cross) != 'auto':
         available_cross_space = getattr(box, cross)
     else:
-        # TODO: As above, min- and max-content not implemented (at least, not
-        # correctly).
-        if cross == 'height':
-            main_space = (
-                context.page_bottom - bottom_space - box.content_box_y())
-            if containing_block.height != 'auto':
-                if isinstance(containing_block.height, Dimension):
-                    assert containing_block.height.unit == 'px'
-                    main_space = min(main_space, containing_block.height.value)
-                else:
-                    main_space = min(main_space, containing_block.height)
-            available_cross_space = (
-                main_space -
-                margin_top - margin_bottom -
-                box.padding_top - box.padding_bottom -
-                box.border_top_width - box.border_bottom_width)
-        else:
+        if cross == 'width':
             available_cross_space = (
                 containing_block.width -
                 margin_left - margin_right -
                 box.padding_left - box.padding_right -
                 box.border_left_width - box.border_right_width)
+        else:
+            available_cross_space = inf
 
-    # 9.2.3 Determine the flex base size and hypothetical main size of each
-    # item.
-    # TODO: Check that we really want to drop already calculated used values.
-    children = box.children
-    parent_box = box.copy_with_children(children)
-    resolve_percentages(parent_box, containing_block)
-    # We remove auto margins from parent_box (which is a throwaway) only but
-    # keep them on box itself. They will get computed later, once we have done
-    # some layout.
-    if parent_box.margin_top == 'auto':
-        parent_box.margin_top = 0
-    if parent_box.margin_bottom == 'auto':
-        parent_box.margin_bottom = 0
-    if parent_box.margin_left == 'auto':
-        parent_box.margin_left = 0
-    if parent_box.margin_right == 'auto':
-        parent_box.margin_right = 0
-    if isinstance(parent_box, boxes.FlexBox):
-        block.block_level_width(parent_box, containing_block)
-    else:
-        parent_box.width = preferred.flex_max_content_width(
-            context, parent_box)
+    # 3 Determine the flex base size and hypothetical main size of each item.
+    parent_box = box.copy()
+    percent.resolve_percentages(parent_box, containing_block)
+    block.block_level_width(parent_box, containing_block)
+    children = sorted(box.children, key=lambda item: item.style['order'])
     original_skip_stack = skip_stack
-    children = sorted(children, key=lambda item: item.style['order'])
     if skip_stack is not None:
         (skip, skip_stack), = skip_stack.items()
         if box.style['flex_direction'].endswith('-reverse'):
@@ -133,16 +95,46 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
             children = children[skip:]
         skip_stack = skip_stack
     else:
-        skip = 0
-        skip_stack = None
+        skip, skip_stack = 0, None
     child_skip_stack = skip_stack
 
+    if row_gap == 'normal':
+        row_gap = 0
+    elif row_gap.unit == '%':
+        if box.height == 'auto':
+            row_gap = 0
+        else:
+            row_gap = row_gap.value / 100 * box.height
+    else:
+        row_gap = row_gap.value
+    if column_gap == 'normal':
+        column_gap = 0
+    elif column_gap.unit == '%':
+        if box.width == 'auto':
+            column_gap = 0
+        else:
+            column_gap = column_gap.value / 100 * box.width
+    else:
+        column_gap = column_gap.value
+    if main == 'width':
+        main_gap, cross_gap = column_gap, row_gap
+    else:
+        main_gap, cross_gap = row_gap, column_gap
+
+    position_x = (
+        parent_box.position_x + parent_box.border_left_width + parent_box.padding_left)
+    if parent_box.margin_left != 'auto':
+        position_x += parent_box.margin_left
+    position_y = (
+        parent_box.position_y + parent_box.border_top_width + parent_box.padding_top)
+    if parent_box.margin_top != 'auto':
+        position_y += parent_box.margin_top
     for index, child in enumerate(children):
         if not child.is_flex_item:
             # Absolute child layout: create placeholder.
             if child.is_absolutely_positioned():
-                child.position_x = box.content_box_x()
-                child.position_y = box.content_box_y()
+                child.position_x = position_x
+                child.position_y = position_y
                 new_child = placeholder = AbsolutePlaceholder(child)
                 placeholder.index = index
                 children[index] = placeholder
@@ -156,200 +148,155 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                 context.running_elements[running_name][page].append(child)
             continue
         # See https://www.w3.org/TR/css-flexbox-1/#min-size-auto.
-        if child.style['overflow'] == 'visible':
-            main_flex_direction = axis
+        if main == 'width':
+            child_containing_block = (available_main_space, parent_box.height)
         else:
-            main_flex_direction = None
-        resolve_percentages(child, parent_box, main_flex_direction)
-        child.position_x = parent_box.content_box_x()
-        child.position_y = parent_box.content_box_y()
-        if child.min_width == 'auto':
-            specified_size = child.width if child.width != 'auto' else inf
-            if isinstance(child, boxes.ParentBox):
-                new_child = child.copy_with_children(child.children)
-            else:
-                new_child = child.copy()
+            child_containing_block = (parent_box.width, available_main_space)
+        percent.resolve_percentages(child, child_containing_block)
+        if child.is_table_wrapper:
+            table_wrapper_width(context, child, child_containing_block)
+        child.position_x = position_x
+        child.position_y = position_y
+        if child.style['min_width'] == 'auto':
+            specified_size = child.width
+            new_child = child.copy()
             new_child.style = child.style.copy()
             new_child.style['width'] = 'auto'
             new_child.style['min_width'] = Dimension(0, 'px')
             new_child.style['max_width'] = Dimension(inf, 'px')
             content_size = min_content_width(context, new_child, outer=False)
-            child.min_width = min(specified_size, content_size)
-        elif child.min_height == 'auto':
-            # TODO: Find a way to get min-content-height.
-            specified_size = child.height if child.height != 'auto' else inf
-            if isinstance(child, boxes.ParentBox):
-                new_child = child.copy_with_children(child.children)
+            transferred_size = None
+            if isinstance(child, boxes.ReplacedBox):
+                image = box.replacement
+                _, intrinsic_height, intrinsic_ratio = image.get_intrinsic_size(
+                    box.style['image_resolution'], box.style['font_size'])
+                if intrinsic_ratio and intrinsic_height:
+                    transferred_size = intrinsic_height * intrinsic_ratio
+            if specified_size != 'auto':
+                child.min_width = min(specified_size, content_size)
+            elif transferred_size is not None:
+                child.min_width = min(transferred_size, content_size)
             else:
-                new_child = child.copy()
+                child.min_width = content_size
+        if child.style['min_height'] == 'auto':
+            # TODO: avoid calling block_level_layout, write min_content_height instead.
+            specified_size = child.height
+            new_child = child.copy()
             new_child.style = child.style.copy()
+            if new_child.style['width'] == 'auto':
+                new_child_width = max_content_width(context, new_child)
+                new_child.style['width'] = Dimension(new_child_width, 'px')
             new_child.style['height'] = 'auto'
             new_child.style['min_height'] = Dimension(0, 'px')
             new_child.style['max_height'] = Dimension(inf, 'px')
             new_child = block.block_level_layout(
-                context, new_child, -inf, child_skip_stack, parent_box,
+                context, new_child, bottom_space, child_skip_stack, parent_box,
                 page_is_empty)[0]
-            content_size = new_child.height
-            child.min_height = min(specified_size, content_size)
-
-        child.style = child.style.copy()
+            content_size = new_child.height if new_child else 0
+            transferred_size = None
+            if isinstance(child, boxes.ReplacedBox):
+                image = box.replacement
+                intrinsic_width, _, intrinsic_ratio = image.get_intrinsic_size(
+                    box.style['image_resolution'], box.style['font_size'])
+                if intrinsic_ratio and intrinsic_width:
+                    transferred_size = intrinsic_width / intrinsic_ratio
+            if specified_size != 'auto':
+                child.min_height = min(specified_size, content_size)
+            elif transferred_size is not None:
+                child.min_height = min(transferred_size, content_size)
+            else:
+                child.min_height = content_size
 
         if child.style['flex_basis'] == 'content':
-            flex_basis = child.flex_basis = 'content'
+            flex_basis = 'content'
         else:
-            resolve_one_percentage(child, 'flex_basis', available_main_space)
-            flex_basis = child.flex_basis
-
-        # "If a value would resolve to auto for width, it instead resolves
-        # to content for flex-basis." Let's do this for height too.
-        # See https://www.w3.org/TR/css-flexbox-1/#propdef-flex-basis.
-        resolve_one_percentage(child, axis, available_main_space)
-        if flex_basis == 'auto':
-            if child.style[axis] == 'auto':
-                flex_basis = 'content'
-            else:
-                if axis == 'width':
-                    flex_basis = child.border_width()
-                    if child.margin_left != 'auto':
-                        flex_basis += child.margin_left
-                    if child.margin_right != 'auto':
-                        flex_basis += child.margin_right
-                else:
-                    flex_basis = child.border_height()
-                    if child.margin_top != 'auto':
-                        flex_basis += child.margin_top
-                    if child.margin_bottom != 'auto':
-                        flex_basis += child.margin_bottom
+            flex_basis = percent.percentage(
+                child.style['flex_basis'], available_main_space)
+            if flex_basis == 'auto':
+                if (flex_basis := getattr(child, main)) == 'auto':
+                    flex_basis = 'content'
 
-        # 9.2.3.A If the item has a definite used flex basis, that’s the flex
-        # base size.
+        # 3.A If the item has a definite used flex basis…
         if flex_basis != 'content':
             child.flex_base_size = flex_basis
+            if main == 'width':
+                child.main_outer_extra = (
+                    child.border_left_width + child.border_right_width)
+                if child.margin_left != 'auto':
+                    child.main_outer_extra += child.margin_left
+                if child.margin_right != 'auto':
+                    child.main_outer_extra += child.margin_right
+            else:
+                child.main_outer_extra = (
+                    child.border_top_width + child.border_bottom_width)
+                if child.margin_top != 'auto':
+                    child.main_outer_extra += child.margin_top
+                if child.margin_bottom != 'auto':
+                    child.main_outer_extra += child.margin_bottom
         elif False:
-            # TODO: 9.2.3.B If the flex item has … an intrinsic aspect ratio, a
-            # used flex basis of 'content', and a definite cross size, then the
-            # flex base size is calculated from its inner cross size and the
-            # flex item’s intrinsic aspect ratio.
-            pass
-        elif False:
-            # TODO: 9.2.3.C If the used flex basis is 'content' or depends on
-            # its available space, and the flex container is being sized under
-            # a 'min-content' or 'max-content' constraint (e.g. when performing
-            # automatic table layout [CSS21]), size the item under that
-            # constraint. The flex base size is the item’s resulting main size.
-            pass
-        elif False:
-            # TODO: 9.2.3.D Otherwise, if the used flex basis is 'content' or
-            # depends on its available space, the available main size is
-            # infinite, and the flex item’s inline axis is parallel to the main
-            # axis, lay the item out using the rules for a box in an orthogonal
-            # flow [CSS3-WRITING-MODES]. The flex base size is the item’s
-            # max-content main size. This case occurs, for example, in an
-            # English document (horizontal writing mode) containing a column
-            # flex container containing a vertical Japanese (vertical writing
-            # mode) flex item. We *can* have infinite main size (only for
-            # flex-direction: column - see
-            # https://www.w3.org/TR/css-flexbox-1/#pagination), but we don't
-            # support vertical writing mode (or writing-mode in general) yet,
-            # so this isn't relevant.
+            # TODO: 3.B If the flex item has an intrinsic aspect ratio…
+            # TODO: 3.C If the used flex basis is 'content'…
+            # TODO: 3.D Otherwise, if the used flex basis is 'content'…
             pass
         else:
-            # 9.2.3.E Otherwise, size the item into the available
-            # space using its used flex basis in place of its main size,
-            # treating a value of 'content' as 'max-content'. If a cross size
-            # is needed to determine the main size (e.g. when the flex item’s
-            # main size is in its block axis) and the flex item’s cross size is
-            # 'auto' and not definite, in this calculation use 'fit-content' as
-            # the flex item’s cross size. The flex base size is the item’s
-            # resulting main size.
-            child.style[axis] = 'max-content'
-
-            # TODO: Don't set style value, support *-content values instead.
-            if child.style[axis] == 'max-content':
-                child.style[axis] = 'auto'
-                if axis == 'width':
-                    child.flex_base_size = max_content_width(context, child)
-                else:
-                    if isinstance(child, boxes.ParentBox):
-                        new_child = child.copy_with_children(child.children)
-                    else:
-                        new_child = child.copy()
-                    new_child.width = inf
-                    new_child = block.block_level_layout(
-                        context, new_child, -inf, child_skip_stack, parent_box,
-                        page_is_empty, absolute_boxes, fixed_boxes)[0]
-                    child.flex_base_size = new_child.margin_height()
-            elif child.style[axis] == 'min-content':
-                child.style[axis] = 'auto'
-                if axis == 'width':
-                    child.flex_base_size = min_content_width(context, child)
-                else:
-                    if isinstance(child, boxes.ParentBox):
-                        new_child = child.copy_with_children(child.children)
-                    else:
-                        new_child = child.copy()
-                    new_child.width = 0
-                    new_child = block.block_level_layout(
-                        context, new_child, -inf, child_skip_stack, parent_box,
-                        page_is_empty, absolute_boxes, fixed_boxes)[0]
-                    child.flex_base_size = new_child.margin_height()
+            # 3.E Otherwise…
+            if main == 'width':
+                child.flex_base_size = max_content_width(context, child, outer=False)
+                child.main_outer_extra = (
+                    max_content_width(context, child) - child.flex_base_size)
             else:
-                assert child.style[axis].unit == 'px'
-                # TODO: To be consistent with the above we may wish to add
-                # padding, border, and margins here (but it's not done
-                # consistently above either, and may not be correct).
-                child.flex_base_size = child.style[axis].value
+                new_child = child.copy()
+                new_child.width = inf
+                new_child = block.block_level_layout(
+                    context, new_child, bottom_space, child_skip_stack, parent_box,
+                    page_is_empty, absolute_boxes, fixed_boxes)[0]
+                child.flex_base_size = new_child.height
+                child.main_outer_extra = new_child.margin_height() - new_child.height
+
+        if main == 'width':
+            position_x += child.flex_base_size + child.main_outer_extra
+        else:
+            position_y += child.flex_base_size + child.main_outer_extra
 
+        min_size = getattr(child, f'min_{main}')
+        max_size = getattr(child, f'max_{main}')
         child.hypothetical_main_size = max(
-            getattr(child, f'min_{axis}'), min(
-                child.flex_base_size, getattr(child, f'max_{axis}')))
+            min_size, min(child.flex_base_size, max_size))
 
         # Skip stack is only for the first child.
         child_skip_stack = None
 
-    # 9.2.4 Determine the main size of the flex container
-    # using the rules of the formatting context in which it
-    # participates. For this computation, auto margins on flex items
-    # are treated as 0.
-    # TODO: The whole step has to be fixed.
-    if axis == 'width':
+    # 4 Determine the main size of the flex container using the rules of the formatting
+    # context in which it participates.
+    if main == 'width':
         block.block_level_width(box, containing_block)
     else:
-        if box.style['height'] != 'auto' and box.style['height'].unit != '%':
-            box.height = box.style['height'].value
-        elif box.style['height'] != 'auto' and containing_block.height != 'auto':
-            box.height = box.style['height'].value / 100 * containing_block.height
-        else:
+        if box.height == 'auto':
             box.height = 0
-            for index, child in enumerate(children):
-                if not child.is_flex_item:
-                    continue
-                child_height = (
-                    child.hypothetical_main_size +
-                    child.border_top_width + child.border_bottom_width +
-                    child.padding_top + child.padding_bottom)
-                if getattr(box, axis) == 'auto' and (
-                        child_height + box.height > available_main_space):
-                    resume_at = {index: None}
-                    children = children[:index + 1]
-                    break
-                box.height += child_height
-
-    # 9.3.5 If the flex container is single-line, collect all the flex items
-    # into a single flex line.
+            flex_items = (child for child in children if child.is_flex_item)
+            for i, child in enumerate(flex_items):
+                box.height += child.hypothetical_main_size + child.main_outer_extra
+                if i:
+                    box.height += main_gap
+        box.height = max(box.min_height, min(box.height, box.max_height))
+
+    # 5 If the flex container is single-line, collect all the flex items into a single
+    # flex line.
     flex_lines = []
     line = []
     line_size = 0
-    axis_size = getattr(box, axis)
+    main_size = getattr(box, main)
     for i, child in enumerate(children, start=skip):
         if not child.is_flex_item:
             continue
-        line_size += child.hypothetical_main_size
-        if box.style['flex_wrap'] != 'nowrap' and line_size > axis_size:
+        line_size += child.hypothetical_main_size + child.main_outer_extra
+        if i > skip:
+            line_size += main_gap
+        if box.style['flex_wrap'] != 'nowrap' and line_size > main_size:
             if line:
                 flex_lines.append(FlexLine(line))
                 line = [(i, child)]
-                line_size = child.hypothetical_main_size
+                line_size = child.hypothetical_main_size + child.main_outer_extra
             else:
                 line.append((i, child))
                 flex_lines.append(FlexLine(line))
@@ -367,72 +314,58 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
         for line in flex_lines:
             line.reverse()
 
-    # 9.3.6 Resolve the flexible lengths of all the flex items to find their
-    # used main size.
-    # See https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths.
-    available_main_space = getattr(box, axis)
+    # 6 Resolve the flexible lengths of all the flex items to find their used main size.
+    available_main_space = getattr(box, main)
     for line in flex_lines:
-        # 9.7.1 Determine the used flex factor. Sum the outer hypothetical main
-        # sizes of all items on the line. If the sum is less than the flex
-        # container’s inner main size, use the flex grow factor for the rest of
-        # this algorithm; otherwise, use the flex shrink factor.
+        # 9.7.1 Determine the used flex factor.
         hypothetical_main_size = sum(
-            child.hypothetical_main_size for index, child in line)
+            child.hypothetical_main_size + child.main_outer_extra
+            for index, child in line)
         if hypothetical_main_size < available_main_space:
             flex_factor_type = 'grow'
         else:
             flex_factor_type = 'shrink'
 
-        # 9.7.2 Size inflexible items. Freeze, setting its target main size to
-        # its hypothetical main size…
-        #  - any item that has a flex factor of zero,
-        #  - if using the flex grow factor: any item that has a flex base size
-        #    greater than its hypothetical main size,
-        #  - if using the flex shrink factor: any item that has a flex base
-        #    size smaller than its hypothetical main size.
+        # 9.7.3 Size inflexible items.
         for index, child in line:
             if flex_factor_type == 'grow':
                 child.flex_factor = child.style['flex_grow']
+                flex_condition = child.flex_base_size > child.hypothetical_main_size
             else:
                 child.flex_factor = child.style['flex_shrink']
-            if (child.flex_factor == 0 or
-                (flex_factor_type == 'grow' and
-                 child.flex_base_size > child.hypothetical_main_size) or
-                (flex_factor_type == 'shrink' and
-                 child.flex_base_size < child.hypothetical_main_size)):
+                flex_condition = child.flex_base_size < child.hypothetical_main_size
+            if child.flex_factor == 0 or flex_condition:
                 child.target_main_size = child.hypothetical_main_size
                 child.frozen = True
             else:
                 child.frozen = False
 
-        # 9.7.3 Calculate initial free space. Sum the outer sizes of all items
-        # on the line, and subtract this from the flex container’s inner main
-        # size. For frozen items, use their outer target main size; for other
-        # items, use their outer flex base size.
+        # 9.7.4 Calculate initial free space.
         initial_free_space = available_main_space
-        for index, child in line:
+        for i, (index, child) in enumerate(line):
             if child.frozen:
-                initial_free_space -= child.target_main_size
+                initial_free_space -= child.target_main_size + child.main_outer_extra
             else:
-                initial_free_space -= child.flex_base_size
+                initial_free_space -= child.flex_base_size + child.main_outer_extra
+            if i:
+                initial_free_space -= main_gap
 
-        # 9.7.4.a Check for flexible items. If all the flex items on the line
-        # are frozen, free space has been distributed; exit this loop.
+        # 9.7.5.a Check for flexible items.
         while not all(child.frozen for index, child in line):
             unfrozen_factor_sum = 0
             remaining_free_space = available_main_space
 
-            # 9.7.4.b Calculate the remaining free space as for initial free
-            # space, above. If the sum of the unfrozen flex items’ flex factors
-            # is less than one, multiply the initial free space by this sum. If
-            # the magnitude of this value is less than the magnitude of the
-            # remaining free space, use this as the remaining free space.
-            for index, child in line:
+            # 9.7.5.b Calculate the remaining free space.
+            for i, (index, child) in enumerate(line):
                 if child.frozen:
-                    remaining_free_space -= child.target_main_size
+                    remaining_free_space -= (
+                        child.target_main_size + child.main_outer_extra)
                 else:
-                    remaining_free_space -= child.flex_base_size
+                    remaining_free_space -= (
+                        child.flex_base_size + child.main_outer_extra)
                     unfrozen_factor_sum += child.flex_factor
+                if i:
+                    remaining_free_space -= main_gap
 
             if unfrozen_factor_sum < 1:
                 initial_free_space *= unfrozen_factor_sum
@@ -443,19 +376,16 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                 remaining_free_space = sys.maxsize
 
             initial_magnitude = (
-                int(log10(initial_free_space)) if initial_free_space > 0
-                else -inf)
+                int(log10(initial_free_space)) if initial_free_space > 0 else -inf)
             remaining_magnitude = (
-                int(log10(remaining_free_space)) if remaining_free_space > 0
-                else -inf)
+                int(log10(remaining_free_space)) if remaining_free_space > 0 else -inf)
             if initial_magnitude < remaining_magnitude:
                 remaining_free_space = initial_free_space
 
-            # 9.7.4.c Distribute free space proportional to the flex factors.
+            # 9.7.5.c Distribute free space proportional to the flex factors.
             if remaining_free_space == 0:
-                # If the remaining free space is zero: "Do nothing", but we at
-                # least set the flex_base_size as target_main_size for next
-                # step.
+                # If the remaining free space is zero: "Do nothing", but we at least set
+                # the flex_base_size as target_main_size for next step.
                 for index, child in line:
                     if not child.frozen:
                         child.target_main_size = child.flex_base_size
@@ -471,30 +401,12 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                         flex_grow_factors_sum += child.style['flex_grow']
                 for index, child in line:
                     if not child.frozen:
-                        # If using the flex grow factor: "Find the ratio of the
-                        # item’s flex grow factor to the sum of the flex grow
-                        # factors of all unfrozen items on the line. Set the
-                        # item’s target main size to its flex base size plus a
-                        # fraction of the remaining free space proportional to
-                        # the ratio."
+                        # If using the flex grow factor…
                         if flex_factor_type == 'grow':
-                            ratio = (
-                                child.style['flex_grow'] /
-                                flex_grow_factors_sum)
+                            ratio = child.style['flex_grow'] / flex_grow_factors_sum
                             child.target_main_size = (
-                                child.flex_base_size +
-                                remaining_free_space * ratio)
-                        # If using the flex shrink factor: "For every unfrozen
-                        # item on the line, multiply its flex shrink factor by
-                        # its inner flex base size, and note this as its scaled
-                        # flex shrink factor. Find the ratio of the item’s
-                        # scaled flex shrink factor to the sum of the scaled
-                        # flex shrink factors of all unfrozen items on the
-                        # line. Set the item’s target main size to its flex
-                        # base size minus a fraction of the absolute value of
-                        # the remaining free space proportional to the ratio.
-                        # Note this may result in a negative inner main size;
-                        # it will be corrected in the next step."
+                                child.flex_base_size + remaining_free_space * ratio)
+                        # If using the flex shrink factor…
                         elif flex_factor_type == 'shrink':
                             if scaled_flex_shrink_factors_sum == 0:
                                 child.target_main_size = child.flex_base_size
@@ -503,24 +415,21 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                                     child.scaled_flex_shrink_factor /
                                     scaled_flex_shrink_factors_sum)
                                 child.target_main_size = (
-                                    child.flex_base_size +
-                                    remaining_free_space * ratio)
-
-            # 9.7.4.d Fix min/max violations. Clamp each non-frozen item’s
-            # target main size by its used min and max main sizes and floor its
-            # content-box size at zero. If the item’s target main size was made
-            # smaller by this, it’s a max violation. If the item’s target main
-            # size was made larger by this, it’s a min violation.
-            # TODO: First part of this step is useless until 3.E is correct.
+                                    child.flex_base_size + remaining_free_space * ratio)
+                        child.target_main_size = min_max(child, child.target_main_size)
+
+            # 9.7.5.d Fix min/max violations.
             for index, child in line:
                 child.adjustment = 0
-                if not child.frozen and child.target_main_size < 0:
-                    child.adjustment = -child.target_main_size
-                    child.target_main_size = 0
-
-            # 9.7.4.e Freeze over-flexed items. The total violation is the sum
-            # of the adjustments from the previous step
-            # ∑(clamped size - unclamped size). If the total violation is:
+                if not child.frozen:
+                    min_size = getattr(child, f'min_{main}')
+                    max_size = getattr(child, f'max_{main}')
+                    min_size = max(min_size , min(child.target_main_size, max_size))
+                    if child.target_main_size < min_size:
+                        child.adjustment = min_size - child.target_main_size
+                        child.target_main_size = min_size
+
+            # 9.7.5.e Freeze over-flexed items.
             adjustments = sum(child.adjustment for index, child in line)
             for index, child in line:
                 # Zero: Freeze all items.
@@ -533,72 +442,44 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                 elif adjustments < 0 and child.adjustment < 0:
                     child.frozen = True
 
-        # 9.7.5 Set each item’s used main size to its target main size.
+        # 9.7.6 Set each item’s used main size to its target main size.
         for index, child in line:
-            if axis == 'width':
-                child.width = (
-                    child.target_main_size -
-                    child.padding_left - child.padding_right -
-                    child.border_left_width - child.border_right_width)
-                if child.margin_left != 'auto':
-                    child.width -= child.margin_left
-                if child.margin_right != 'auto':
-                    child.width -= child.margin_right
+            if main == 'width':
+                child.width = child.target_main_size
             else:
-                child.height = (
-                    child.target_main_size -
-                    child.padding_top - child.padding_bottom -
-                    child.border_top_width - child.border_top_width)
-                if child.margin_top != 'auto':
-                    child.height -= child.margin_top
-                if child.margin_bottom != 'auto':
-                    child.height -= child.margin_bottom
+                child.height = child.target_main_size
 
-    # 9.4. Cross Size Determination.
-    # TODO: Fix TODO in build.flex_children.
+    # 7 Determine the hypothetical cross size of each item.
     # TODO: Handle breaks.
     new_flex_lines = []
     child_skip_stack = skip_stack
-    # 9.4.7 Determine the hypothetical cross size of each item by performing
-    # layout with the used main size and the available space, treating auto as
-    # fit-content.
     for line in flex_lines:
         new_flex_line = FlexLine()
         for index, child in line:
-            # TODO: This *should* do the right thing for auto?
-            # TODO: Find another way than calling block_level_layout_switch to
-            # get baseline and child.height.
+            # TODO: Fix this value, see test_flex_item_auto_margin_cross.
             if child.margin_top == 'auto':
                 child.margin_top = 0
             if child.margin_bottom == 'auto':
                 child.margin_bottom = 0
-            if isinstance(child, boxes.ParentBox):
-                child_copy = child.copy_with_children(child.children)
-            else:
-                child_copy = child.copy()
-            block.block_level_width(child_copy, parent_box)
-            # TODO: We set the style here above, otherwise used main size will
-            # get clobbered by resolve_percentages() somewhere down the line,
-            # particularly if the child is itself a flex container. Is this the
-            # right approach?
-            child_copy.style[axis] = Dimension(getattr(child, axis), 'px')
-            new_child, _, _, adjoining_margins, _, _ = (
-                block.block_level_layout_switch(
-                    context, child_copy, -inf, child_skip_stack, parent_box,
-                    page_is_empty, absolute_boxes, fixed_boxes,
-                    [], False, None))
-
+            # TODO: Find another way than calling block_level_layout_switch.
+            new_child = child.copy()
+            new_child, _, _, adjoining_margins, _, _ = block.block_level_layout_switch(
+                context, new_child, -inf, child_skip_stack, parent_box, page_is_empty,
+                absolute_boxes, fixed_boxes, [], discard, None)
             child._baseline = find_in_flow_baseline(new_child) or 0
             if cross == 'height':
                 child.height = new_child.height
-                # As flex items margins never collapse (with other flex items
-                # or with the flex container), we can add the adjoining margins
-                # to the child bottom margin.
+                # As flex items margins never collapse (with other flex items or
+                # with the flex container), we can add the adjoining margins to the
+                # child bottom margin.
                 child.margin_bottom += block.collapse_margin(adjoining_margins)
             else:
-                # TODO: We had min_content_width here but I have no idea under
-                # what circumstance that would be correct.
-                child.width = new_child.width
+                if child.width == 'auto':
+                    min_width = min_content_width(context, child, outer=False)
+                    max_width = max_content_width(context, child, outer=False)
+                    child.width = min(max(min_width, new_child.width), max_width)
+                else:
+                    child.width = new_child.width
 
             new_flex_line.append((index, child))
 
@@ -609,33 +490,25 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
             new_flex_lines.append(new_flex_line)
     flex_lines = new_flex_lines
 
+    # 8 Calculate the cross size of each flex line.
     cross_size = getattr(box, cross)
     if len(flex_lines) == 1 and cross_size != 'auto':
-        # 9.4.8 If the flex container is single-line and has a
-        # definite cross size, the cross size of the flex line is the
-        # flex container’s inner cross size.
+        # If the flex container is single-line…
         flex_lines[0].cross_size = cross_size
-    else:  # 9.4.8 Otherwise, for each flex line:
-        # 9.4.8.1 Collect all the flex items whose inline-axis is parallel to
-        # the main-axis, whose align-self is baseline, and whose cross-axis
-        # margins are both non-auto. Find the largest of the distances between
-        # each item’s baseline and its hypothetical outer cross-start edge, and
-        # the largest of the distances between each item’s baseline and its
-        # hypothetical outer cross-end edge, and sum these two values.
+    else:
+        # Otherwise, for each flex line…
+        # 8.1 Collect all the flex items whose inline-axis is parallel to the main-axis…
         for line in flex_lines:
             collected_items = []
             not_collected_items = []
             for index, child in line:
                 align_self = child.style['align_self']
-                if (box.style['flex_direction'].startswith('row') and
-                        'baseline' in align_self and
-                        child.margin_top != 'auto' and
-                        child.margin_bottom != 'auto'):
-                    collected_items.append(child)
-                else:
-                    not_collected_items.append(child)
-            cross_start_distance = 0
-            cross_end_distance = 0
+                collect = (
+                    box.style['flex_direction'].startswith('row') and
+                    'baseline' in align_self and
+                    'auto' not in (child.margin_top, child.margin_bottom))
+                (collected_items if collect else not_collected_items).append(child)
+            cross_start_distance = cross_end_distance = 0
             for child in collected_items:
                 baseline = child._baseline - child.position_y
                 cross_start_distance = max(cross_start_distance, baseline)
@@ -643,8 +516,7 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                     cross_end_distance, child.margin_height() - baseline)
             collected_cross_size = cross_start_distance + cross_end_distance
             non_collected_cross_size = 0
-            # 9.4.8.2 Among all the items not collected by the previous step,
-            # find the largest outer hypothetical cross size.
+            # 8.2 Find the largest outer hypothetical cross size.
             if not_collected_items:
                 non_collected_cross_size = -inf
                 for child in not_collected_items:
@@ -662,15 +534,10 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                             child_cross_size += child.margin_right
                     non_collected_cross_size = max(
                         child_cross_size, non_collected_cross_size)
-            # 9.4.8.3 The used cross-size of the flex line is the largest of
-            # the numbers found in the previous two steps and zero.
-            line.cross_size = max(
-                collected_cross_size, non_collected_cross_size)
-
-    # 9.4.8.3 If the flex container is single-line, then clamp the line’s
-    # cross-size to be within the container’s computed min and max cross sizes.
-    # Note that if CSS 2.1’s definition of min/max-width/height applied more
-    # generally, this behavior would fall out automatically.
+            # 8.3 Set the used cross-size of the flex line.
+            line.cross_size = max(collected_cross_size, non_collected_cross_size)
+
+    # 8.3 If the flex container is single-line…
     if len(flex_lines) == 1:
         line, = flex_lines
         min_cross_size = getattr(box, f'min_{cross}')
@@ -679,15 +546,9 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
         max_cross_size = getattr(box, f'max_{cross}')
         if max_cross_size == 'auto':
             max_cross_size = inf
-        line.cross_size = max(
-            min_cross_size, min(line.cross_size, max_cross_size))
-
-    # 9.4.9 Handle 'align-content: stretch'. If the flex container has a
-    # definite cross size, align-content is stretch, and the sum of the flex
-    # lines' cross sizes is less than the flex container’s inner cross size,
-    # increase the cross size of each flex line by equal amounts such that the
-    # sum of their cross sizes exactly equals the flex container’s inner cross
-    # size.
+        line.cross_size = max(min_cross_size, min(line.cross_size, max_cross_size))
+
+    # 9 Handle 'align-content: stretch'.
     align_content = box.style['align_content']
     if 'normal' in align_content:
         align_content = ('stretch',)
@@ -702,38 +563,16 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                 else:
                     definite_cross_size = box.width
         if definite_cross_size is not None:
-            extra_cross_size = definite_cross_size - sum(
-                line.cross_size for line in flex_lines)
+            extra_cross_size = definite_cross_size
+            extra_cross_size -= sum(line.cross_size for line in flex_lines)
+            extra_cross_size -= (len(flex_lines) - 1) * cross_gap
             if extra_cross_size:
                 for line in flex_lines:
                     line.cross_size += extra_cross_size / len(flex_lines)
 
-    # TODO: 9.4.10 Collapse visibility:collapse items. If any flex items have
-    # visibility: collapse, note the cross size of the line they’re in as the
-    # item’s strut size, and restart layout from the beginning.
-    #
-    # In this second layout round, when collecting items into lines, treat the
-    # collapsed items as having zero main size. For the rest of the algorithm
-    # following that step, ignore the collapsed items entirely (as if they were
-    # display:none) except that after calculating the cross size of the lines,
-    # if any line’s cross size is less than the largest strut size among all
-    # the collapsed items in the line, set its cross size to that strut size.
-    #
-    # Skip this step in the second layout round.
-
-    # Step 11 - 9.4.11 Determine the used cross size of each flex item. If a
-    # flex item has align-self: stretch, its computed cross size property is
-    # auto, and neither of its cross-axis margins are auto, the used outer
-    # cross size is the used cross size of its flex line, clamped according to
-    # the item’s used min and max cross sizes. Otherwise, the used cross size
-    # is the item’s hypothetical cross size.
-    #
-    # If the flex item has align-self: stretch, redo layout for its contents,
-    # treating this used size as its definite cross size so that
-    # percentage-sized children can be resolved.
-    #
-    # Note that this step does not affect the main size of the flex item, even
-    # if it has an intrinsic aspect ratio.
+    # TODO: 10 Collapse 'visibility: collapse' items.
+
+    # 11 Determine the used cross size of each flex item.
     align_items = box.style['align_items']
     if 'normal' in align_items:
         align_items = ('stretch',)
@@ -764,12 +603,11 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                             child.border_left_width +
                             child.border_right_width)
                     setattr(child, cross, cross_size)
-                    # TODO: redo layout?
             # else: Cross size has been set by step 7.
 
-    # 9.5 Main-Axis Alignment.
-    original_position_axis = (
-        box.content_box_x() if axis == 'width'
+    # 12 Distribute any remaining free space.
+    original_position_main = (
+        box.content_box_x() if main == 'width'
         else box.content_box_y())
     justify_content = box.style['justify_content']
     if 'normal' in justify_content:
@@ -777,13 +615,16 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
     if box.style['flex_direction'].endswith('-reverse'):
         if 'flex-start' in justify_content:
             justify_content = ('flex-end',)
-        elif justify_content == 'flex-end':
+        elif 'flex-end' in justify_content:
             justify_content = ('flex-start',)
+        elif 'start' in justify_content:
+            justify_content = ('end',)
+        elif 'end' in justify_content:
+            justify_content = ('start',)
 
-    # 9.5.12 Distribute any remaining free space. For each flex line:
     for line in flex_lines:
-        position_axis = original_position_axis
-        if axis == 'width':
+        position_main = original_position_main
+        if main == 'width':
             free_space = box.width
             for index, child in line:
                 free_space -= child.border_width()
@@ -799,13 +640,12 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                     free_space -= child.margin_top
                 if child.margin_bottom != 'auto':
                     free_space -= child.margin_bottom
+        free_space -= (len(line) - 1) * main_gap
 
-        # 9.5.12.1 If the remaining free space is positive and at least one
-        # main-axis margin on this line is auto, distribute the free space
-        # equally among these margins. Otherwise, set all auto margins to zero.
+        # 12.1 If the remaining free space is positive…
         margins = 0
         for index, child in line:
-            if axis == 'width':
+            if main == 'width':
                 if child.margin_left == 'auto':
                     margins += 1
                 if child.margin_right == 'auto':
@@ -818,7 +658,7 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
         if margins:
             free_space /= margins
             for index, child in line:
-                if axis == 'width':
+                if main == 'width':
                     if child.margin_left == 'auto':
                         child.margin_left = free_space
                     if child.margin_right == 'auto':
@@ -830,68 +670,67 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                         child.margin_bottom = free_space
             free_space = 0
 
-        if box.style['direction'] == 'rtl' and axis == 'width':
+        if box.style['direction'] == 'rtl' and main == 'width':
             free_space *= -1
 
-        # 9.5.12.2 Align the items along the main-axis per justify-content.
+        # 12.2 Align the items along the main-axis per justify-content.
         if {'end', 'flex-end', 'right'} & set(justify_content):
-            position_axis += free_space
+            position_main += free_space
         elif 'center' in justify_content:
-            position_axis += free_space / 2
+            position_main += free_space / 2
         elif 'space-around' in justify_content:
-            position_axis += free_space / len(line) / 2
+            position_main += free_space / len(line) / 2
         elif 'space-evenly' in justify_content:
-            position_axis += free_space / (len(line) + 1)
-
-        for index, child in line:
-            if axis == 'width':
-                child.position_x = position_axis
-                if 'stretch' in justify_content:
-                    child.width += free_space / len(line)
+            position_main += free_space / (len(line) + 1)
+
+        growths = sum(child.style['flex_grow'] for child in children)
+        for i, (index, child) in enumerate(line):
+            if i:
+                position_main += main_gap
+            if main == 'width':
+                child.position_x = position_main
+                if 'stretch' in justify_content and growths:
+                    child.width += free_space * child.style['flex_grow'] / growths
             else:
-                child.position_y = position_axis
-            margin_axis = (
-                child.margin_width() if axis == 'width'
-                else child.margin_height())
-            if box.style['direction'] == 'rtl' and axis == 'width':
-                margin_axis *= -1
-            position_axis += margin_axis
+                child.position_y = position_main
+            margin_main = (
+                child.margin_width() if main == 'width' else child.margin_height())
+            if box.style['direction'] == 'rtl' and main == 'width':
+                margin_main *= -1
+            position_main += margin_main
             if 'space-around' in justify_content:
-                position_axis += free_space / len(line)
+                position_main += free_space / len(line)
             elif 'space-between' in justify_content:
                 if len(line) > 1:
-                    position_axis += free_space / (len(line) - 1)
+                    position_main += free_space / (len(line) - 1)
             elif 'space-evenly' in justify_content:
-                position_axis += free_space / (len(line) + 1)
+                position_main += free_space / (len(line) + 1)
 
-    # 9.6. Cross-Axis Alignment.
-    # Make sure width/margins are no longer "auto", as we did not do it above
-    # in step 9.2.4.
+    # 13 Resolve cross-axis auto margins.
     if cross == 'width':
+        # Make sure width/margins are no longer "auto", as we did not do it above in
+        # step 4.
         block.block_level_width(box, containing_block)
-    position_cross = (
-        box.content_box_y() if cross == 'height'
-        else box.content_box_x())
+    position_cross = box.content_box_y() if cross == 'height' else box.content_box_x()
     for line in flex_lines:
         line.lower_baseline = -inf
-        # TODO: don't duplicate this loop.
+        # TODO: Don't duplicate this loop.
         for index, child in line:
             align_self = child.style['align_self']
             if 'auto' in align_self:
                 align_self = align_items
-            if 'baseline' in align_self and axis == 'width':
+            if 'baseline' in align_self and main == 'width':
                 # TODO: handle vertical text.
                 child.baseline = child._baseline - position_cross
                 line.lower_baseline = max(line.lower_baseline, child.baseline)
         if line.lower_baseline == -inf:
             line.lower_baseline = line[0][1]._baseline if line else 0
-        # 9.6.13 Resolve cross-axis auto margins.
         for index, child in line:
             cross_margins = (
                 (child.margin_top, child.margin_bottom) if cross == 'height'
                 else (child.margin_left, child.margin_right))
             auto_margins = sum([margin == 'auto' for margin in cross_margins])
-            # 9.6.13 If a flex item has auto cross-axis margins:
+            # If a flex item has auto cross-axis margins…
             if auto_margins:
                 extra_cross = line.cross_size
                 if cross == 'height':
@@ -907,10 +746,7 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                     if child.margin_right != 'auto':
                         extra_cross -= child.margin_right
                 if extra_cross > 0:
-                    # 9.6.13 If its outer cross size (treating those auto
-                    # margins as zero) is less than the cross size of its flex
-                    # line, distribute the difference in those sizes equally to
-                    # the auto margins.
+                    # If its outer cross size is less than the cross size…
                     extra_cross /= auto_margins
                     if cross == 'height':
                         if child.margin_top == 'auto':
@@ -923,10 +759,7 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                         if child.margin_right == 'auto':
                             child.margin_right = extra_cross
                 else:
-                    # 9.6.13 Otherwise, if the block-start or inline-start
-                    # margin (whichever is in the cross axis) is auto, set it
-                    # to zero. Set the opposite margin so that the outer cross
-                    # size of the item equals the cross size of its flex line.
+                    # Otherwise…
                     if cross == 'height':
                         if child.margin_top == 'auto':
                             child.margin_top = 0
@@ -936,9 +769,7 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                             child.margin_left = 0
                         child.margin_right = extra_cross
             else:
-                # 9.6.14 Align all flex items along the cross-axis per
-                # align-self, if neither of the item’s cross-axis margins are
-                # auto.
+                # 14 Align all flex items along the cross-axis.
                 align_self = child.style['align_self']
                 if 'normal' in align_self:
                     align_self = ('stretch',)
@@ -948,24 +779,20 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                 setattr(child, position, position_cross)
                 if {'end', 'self-end', 'flex-end'} & set(align_self):
                     if cross == 'height':
-                        child.position_y += (
-                            line.cross_size - child.margin_height())
+                        child.position_y += line.cross_size - child.margin_height()
                     else:
-                        child.position_x += (
-                            line.cross_size - child.margin_width())
+                        child.position_x += line.cross_size - child.margin_width()
                 elif 'center' in align_self:
                     if cross == 'height':
                         child.position_y += (
                             line.cross_size - child.margin_height()) / 2
                     else:
-                        child.position_x += (
-                            line.cross_size - child.margin_width()) / 2
+                        child.position_x += (line.cross_size - child.margin_width()) / 2
                 elif 'baseline' in align_self:
                     if cross == 'height':
-                        child.position_y += (
-                            line.lower_baseline - child.baseline)
+                        child.position_y += line.lower_baseline - child.baseline
                     else:
-                        # Handle vertical text.
+                        # TODO: Handle vertical text.
                         pass
                 elif 'stretch' in align_self:
                     if child.style[cross] == 'auto':
@@ -976,78 +803,76 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
                         if child.style['box_sizing'] == 'content-box':
                             if cross == 'height':
                                 margins += (
-                                    child.border_top_width +
-                                    child.border_bottom_width +
+                                    child.border_top_width + child.border_bottom_width +
                                     child.padding_top + child.padding_bottom)
                             else:
                                 margins += (
-                                    child.border_left_width +
-                                    child.border_right_width +
+                                    child.border_left_width + child.border_right_width +
                                     child.padding_left + child.padding_right)
-                        # TODO: Don't set style width, find a way to avoid
-                        # width re-calculation after 9.6.16.
+                        # TODO: Don't set style width, find a way to avoid width
+                        # re-calculation after 16.
                         child.style[cross] = Dimension(line.cross_size - margins, 'px')
         position_cross += line.cross_size
 
-    # 9.6.15 Determine the flex container’s used cross size:
+    # 15 Determine the flex container’s used cross size.
+    # TODO: Use the updated algorithm.
     if getattr(box, cross) == 'auto':
-        # 9.6.15 Otherwise, use the sum of the flex lines' cross sizes, clamped
-        # by the used min and max cross sizes of the flex container.
+        # Otherwise, use the sum of the flex lines' cross sizes…
         # TODO: Handle min-max.
         # TODO: What about align-content here?
-        setattr(box, cross, sum(line.cross_size for line in flex_lines))
-    elif len(flex_lines) > 1:
-        # 9.6.15 If the cross size property is a definite size, use that,
-        # clamped by the used min and max cross sizes of the flex container.
-        extra_cross_size = getattr(box, cross) - sum(
-            line.cross_size for line in flex_lines)
-        # 9.6.16 Align all flex lines per align-content.
+        cross_size = sum(line.cross_size for line in flex_lines)
+        cross_size += (len(flex_lines) - 1) * cross_gap
+        setattr(box, cross, cross_size)
+
+    if len(flex_lines) > 1:
+        # 15 If the cross size property is a definite size, use that…
+        extra_cross_size = getattr(box, cross)
+        extra_cross_size -= sum(line.cross_size for line in flex_lines)
+        extra_cross_size -= (len(flex_lines) - 1) * cross_gap
+        # 16 Align all flex lines per align-content.
+        cross_translate = 0
         direction = 'position_y' if cross == 'height' else 'position_x'
-        if extra_cross_size > 0:
-            cross_translate = 0
-            for line in flex_lines:
-                for index, child in line:
-                    if child.is_flex_item:
-                        current_value = getattr(child, direction)
-                        current_value += cross_translate
-                        setattr(child, direction, current_value)
-                        if {'flex-end', 'end'} & set(align_content):
-                            setattr(
-                                child, direction,
-                                current_value + extra_cross_size)
-                        elif 'center' in align_content:
-                            setattr(
-                                child, direction,
-                                current_value + extra_cross_size / 2)
-                        elif 'space-around' in align_content:
-                            setattr(
-                                child, direction,
-                                current_value + extra_cross_size /
-                                len(flex_lines) / 2)
-                        elif 'space-evenly' in align_content:
-                            setattr(
-                                child, direction,
-                                current_value + extra_cross_size /
-                                (len(flex_lines) + 1))
-                if 'space-between' in align_content:
-                    cross_translate += extra_cross_size / (len(flex_lines) - 1)
+        for i, line in enumerate(flex_lines):
+            flex_items = tuple(child for _, child in line if child.is_flex_item)
+            if i:
+                cross_translate += cross_gap
+            for child in flex_items:
+                current_value = getattr(child, direction) + cross_translate
+                setattr(child, direction, current_value)
+            if extra_cross_size == 0:
+                continue
+            for child in flex_items:
+                if {'flex-end', 'end'} & set(align_content):
+                    setattr(child, direction, current_value + extra_cross_size)
+                elif 'center' in align_content:
+                    setattr(child, direction, current_value + extra_cross_size / 2)
                 elif 'space-around' in align_content:
-                    cross_translate += extra_cross_size / len(flex_lines)
+                    setattr(
+                        child, direction,
+                        current_value + extra_cross_size / len(flex_lines) / 2)
                 elif 'space-evenly' in align_content:
-                    cross_translate += extra_cross_size / (len(flex_lines) + 1)
+                    setattr(
+                        child, direction,
+                        current_value + extra_cross_size / (len(flex_lines) + 1))
+            if 'space-between' in align_content:
+                cross_translate += extra_cross_size / (len(flex_lines) - 1)
+            elif 'space-around' in align_content:
+                cross_translate += extra_cross_size / len(flex_lines)
+            elif 'space-evenly' in align_content:
+                cross_translate += extra_cross_size / (len(flex_lines) + 1)
 
     # Now we are no longer in the flex algorithm.
-    # TODO: Don't use block_box_level_layout_switch.
     box = box.copy_with_children(
         [child for child in children if child.is_absolutely_positioned()])
     child_skip_stack = skip_stack
     for line in flex_lines:
         for index, child in line:
             if child.is_flex_item:
+                # TODO: Don't use block_level_layout_switch.
                 new_child, child_resume_at = block.block_level_layout_switch(
-                    context, child, bottom_space, child_skip_stack, box,
-                    page_is_empty, absolute_boxes, fixed_boxes,
-                    adjoining_margins=[], discard=False, max_lines=None)[:2]
+                    context, child, bottom_space, child_skip_stack, box, page_is_empty,
+                    absolute_boxes, fixed_boxes, adjoining_margins=[], discard=discard,
+                    max_lines=None)[:2]
                 if new_child is None:
                     if resume_at:
                         resume_index, = resume_at
@@ -1079,29 +904,23 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
         # New containing block, resolve the layout of the absolute descendants.
         for absolute_box in absolute_boxes:
             absolute_layout(
-                context, absolute_box, box, fixed_boxes, bottom_space,
-                skip_stack=None)
-
-    # Set box height.
-    # TODO: This is probably useless because of 9.6.15.
-    if axis == 'width' and box.height == 'auto':
-        if flex_lines:
-            box.height = sum(line.cross_size for line in flex_lines)
-        else:
-            box.height = 0
+                context, absolute_box, box, fixed_boxes, bottom_space, skip_stack=None)
 
-    # Set baseline.
-    # See https://www.w3.org/TR/css-flexbox-1/#flex-baselines
-    # TODO: Use the real algorithm.
+    # TODO: Use real algorithm, see https://www.w3.org/TR/css-flexbox-1/#flex-baselines.
     if isinstance(box, boxes.InlineFlexBox):
-        if axis == 'width':  # and main text direction is horizontal
+        if main == 'width':  # and main text direction is horizontal
             box.baseline = flex_lines[0].lower_baseline if flex_lines else 0
         else:
-            box.baseline = ((
-                find_in_flow_baseline(box.children[0])
-                if box.children else 0) or 0)
+            for child in box.children:
+                if child.is_in_normal_flow():
+                    box.baseline = find_in_flow_baseline(child) or 0
+                    break
+            else:
+                box.baseline = 0
+
+    box.remove_decoration(start=False, end=resume_at and not discard)
 
-    context.finish_block_formatting_context(box)
+    context.finish_flex_formatting_context(box)
 
     # TODO: Check these returned values.
     return box, resume_at, {'break': 'any', 'page': None}, [], False
diff --git a/weasyprint/layout/float.py b/weasyprint/layout/float.py
index e39b7f2f..de887504 100644
--- a/weasyprint/layout/float.py
+++ b/weasyprint/layout/float.py
@@ -68,7 +68,7 @@ def float_layout(context, box, containing_block, absolute_boxes, fixed_boxes,
             context, box, bottom_space=bottom_space,
             skip_stack=skip_stack, containing_block=containing_block,
             page_is_empty=True, absolute_boxes=absolute_boxes,
-            fixed_boxes=fixed_boxes)
+            fixed_boxes=fixed_boxes, discard=False)
     elif isinstance(box, boxes.GridContainerBox):
         box, resume_at, _, _, _ = grid_layout(
             context, box, bottom_space=bottom_space,
@@ -216,9 +216,11 @@ def avoid_collisions(context, box, containing_block, outer=True):
                 # bound.
                 position_x = max_right_bound - box_width
             else:
-                # The position of the right border of the replaced box is at
-                # the right bound.
-                assert isinstance(box, boxes.BlockReplacedBox)
+                # The position of the right border of the replaced box or
+                # formatting context is at the right bound.
+                assert (
+                    isinstance(box, boxes.BlockReplacedBox) or
+                    box.establishes_formatting_context())
                 position_x = max_right_bound - box_width
 
     available_width = max_right_bound - max_left_bound
diff --git a/weasyprint/layout/inline.py b/weasyprint/layout/inline.py
index d67806d2..1fddca30 100644
--- a/weasyprint/layout/inline.py
+++ b/weasyprint/layout/inline.py
@@ -370,9 +370,12 @@ def atomic_box(context, box, position_x, skip_stack, containing_block,
         if box.is_table_wrapper:
             containing_size = (containing_block.width, containing_block.height)
             table_wrapper_width(context, box, containing_size)
+            width, min_width, max_width = box.width, box.min_width, box.max_width
         box = inline_block_box_layout(
             context, box, position_x, skip_stack, containing_block,
             absolute_boxes, fixed_boxes)
+        if box.is_table_wrapper:
+            box.width, box.min_width, box.max_width = width, min_width, max_width
     else:  # pragma: no cover
         raise TypeError(f'Layout for {type(box).__name__} not handled yet')
     return box
@@ -513,7 +516,7 @@ def split_inline_level(context, box, position_x, max_x, bottom_space,
                 setattr(box, f'margin_{side}', 0)
         new_box, resume_at, _, _, _ = flex_layout(
             context, box, -inf, skip_stack, containing_block, False,
-            absolute_boxes, fixed_boxes)
+            absolute_boxes, fixed_boxes, False)
         preserved_line_break = False
         first_letter = '\u2e80'
         last_letter = '\u2e80'
@@ -1049,8 +1052,7 @@ def inline_box_verticality(box, top_bottom_subtrees, baseline_y):
 
         # the child’s `top` is `child.baseline` above (lower y) its baseline.
         top = child_baseline_y - child.baseline
-        box_types = (
-            boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)
+        box_types = (boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)
         if isinstance(child, box_types):
             # This also includes table wrappers for inline tables.
             child.translate(dy=top - child.position_y)
diff --git a/weasyprint/layout/page.py b/weasyprint/layout/page.py
index 29172c79..09165a9a 100644
--- a/weasyprint/layout/page.py
+++ b/weasyprint/layout/page.py
@@ -605,10 +605,8 @@ def make_page(context, root_box, page_type, resume_at, page_number,
         previous_resume_at = resume_at
         root_box = root_box.copy_with_children([])
 
-    # TODO: handle cases where the root element is something else.
-    # See https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
-    assert isinstance(root_box, (
-        boxes.BlockBox, boxes.FlexContainerBox, boxes.GridContainerBox))
+    # https://www.w3.org/TR/css-display-4/#root
+    assert isinstance(root_box, boxes.BlockLevelBox)
     context.create_block_formatting_context()
     context.current_page = page_number
     context.current_page_footnotes = []
diff --git a/weasyprint/layout/percent.py b/weasyprint/layout/percent.py
index c2f405f0..e3b35858 100644
--- a/weasyprint/layout/percent.py
+++ b/weasyprint/layout/percent.py
@@ -21,8 +21,7 @@ def percentage(value, refer_to):
         return refer_to * value.value / 100
 
 
-def resolve_one_percentage(box, property_name, refer_to,
-                           main_flex_direction=None):
+def resolve_one_percentage(box, property_name, refer_to):
     """Set a used length value from a computed length value.
 
     ``refer_to`` is the length for 100%. If ``refer_to`` is not a number, it
@@ -35,9 +34,7 @@ def resolve_one_percentage(box, property_name, refer_to,
     percent = percentage(value, refer_to)
     setattr(box, property_name, percent)
     if property_name in ('min_width', 'min_height') and percent == 'auto':
-        if (main_flex_direction is None or
-                property_name != (f'min_{main_flex_direction}')):
-            setattr(box, property_name, 0)
+        setattr(box, property_name, 0)
 
 
 def resolve_position_percentages(box, containing_block):
@@ -48,7 +45,7 @@ def resolve_position_percentages(box, containing_block):
     resolve_one_percentage(box, 'bottom', cb_height)
 
 
-def resolve_percentages(box, containing_block, main_flex_direction=None):
+def resolve_percentages(box, containing_block):
     """Set used values as attributes of the box object."""
     if isinstance(containing_block, boxes.Box):
         # cb is short for containing block
@@ -69,8 +66,8 @@ def resolve_percentages(box, containing_block, main_flex_direction=None):
     resolve_one_percentage(box, 'padding_top', maybe_height)
     resolve_one_percentage(box, 'padding_bottom', maybe_height)
     resolve_one_percentage(box, 'width', cb_width)
-    resolve_one_percentage(box, 'min_width', cb_width, main_flex_direction)
-    resolve_one_percentage(box, 'max_width', cb_width, main_flex_direction)
+    resolve_one_percentage(box, 'min_width', cb_width)
+    resolve_one_percentage(box, 'max_width', cb_width)
 
     # XXX later: top, bottom, left and right on positioned elements
 
@@ -83,14 +80,12 @@ def resolve_percentages(box, containing_block, main_flex_direction=None):
         else:
             assert height.unit == 'px'
             box.height = height.value
-        resolve_one_percentage(box, 'min_height', 0, main_flex_direction)
-        resolve_one_percentage(box, 'max_height', inf, main_flex_direction)
+        resolve_one_percentage(box, 'min_height', 0)
+        resolve_one_percentage(box, 'max_height', inf)
     else:
         resolve_one_percentage(box, 'height', cb_height)
-        resolve_one_percentage(
-            box, 'min_height', cb_height, main_flex_direction)
-        resolve_one_percentage(
-            box, 'max_height', cb_height, main_flex_direction)
+        resolve_one_percentage(box, 'min_height', cb_height)
+        resolve_one_percentage(box, 'max_height', cb_height)
 
     collapse = box.style['border_collapse'] == 'collapse'
     # Used value == computed value
@@ -102,38 +97,8 @@ def resolve_percentages(box, containing_block, main_flex_direction=None):
             setattr(box, prop, box.style[prop])
 
     # Shrink *content* widths and heights according to box-sizing
-    # Thanks heavens and the spec: Our validator rejects negative values
-    # for padding and border-width
-    if box.style['box_sizing'] == 'border-box':
-        horizontal_delta = (
-            box.padding_left + box.padding_right +
-            box.border_left_width + box.border_right_width)
-        vertical_delta = (
-            box.padding_top + box.padding_bottom +
-            box.border_top_width + box.border_bottom_width)
-    elif box.style['box_sizing'] == 'padding-box':
-        horizontal_delta = box.padding_left + box.padding_right
-        vertical_delta = box.padding_top + box.padding_bottom
-    else:
-        assert box.style['box_sizing'] == 'content-box'
-        horizontal_delta = 0
-        vertical_delta = 0
-
-    # Keep at least min_* >= 0 to prevent funny output in case box.width or
-    # box.height become negative.
-    # Restricting max_* seems reasonable, too.
-    if horizontal_delta > 0:
-        if box.width != 'auto':
-            box.width = max(0, box.width - horizontal_delta)
-        box.max_width = max(0, box.max_width - horizontal_delta)
-        if box.min_width != 'auto':
-            box.min_width = max(0, box.min_width - horizontal_delta)
-    if vertical_delta > 0:
-        if box.height != 'auto':
-            box.height = max(0, box.height - vertical_delta)
-        box.max_height = max(0, box.max_height - vertical_delta)
-        if box.min_height != 'auto':
-            box.min_height = max(0, box.min_height - vertical_delta)
+    adjust_box_sizing(box, 'width')
+    adjust_box_sizing(box, 'height')
 
 
 def resolve_radii_percentages(box):
@@ -154,3 +119,33 @@ def resolve_radii_percentages(box):
             rx = percentage(rx, box.border_width())
             ry = percentage(ry, box.border_height())
             setattr(box, property_name, (rx, ry))
+
+
+def adjust_box_sizing(box, axis):
+    if box.style['box_sizing'] == 'border-box':
+        if axis == 'width':
+            delta = (
+                box.padding_left + box.padding_right +
+                box.border_left_width + box.border_right_width)
+        else:
+            delta = (
+                box.padding_top + box.padding_bottom +
+                box.border_top_width + box.border_bottom_width)
+    elif box.style['box_sizing'] == 'padding-box':
+        if axis == 'width':
+            delta = box.padding_left + box.padding_right
+        else:
+            delta = box.padding_top + box.padding_bottom
+    else:
+        assert box.style['box_sizing'] == 'content-box'
+        delta = 0
+
+    # Keep at least min_* >= 0 to prevent funny output in case box.width or
+    # box.height become negative.
+    # Restricting max_* seems reasonable, too.
+    if delta > 0:
+        if getattr(box, axis) != 'auto':
+            setattr(box, axis, max(0, getattr(box, axis) - delta))
+        setattr(box, f'max_{axis}', max(0, getattr(box, f'max_{axis}') - delta))
+        if getattr(box, f'min_{axis}') != 'auto':
+            setattr(box, f'min_{axis}', max(0, getattr(box, f'min_{axis}') - delta))
diff --git a/weasyprint/layout/preferred.py b/weasyprint/layout/preferred.py
index 04203187..005baff6 100644
--- a/weasyprint/layout/preferred.py
+++ b/weasyprint/layout/preferred.py
@@ -43,14 +43,12 @@ def min_content_width(context, box, outer=True):
         return table_and_columns_preferred_widths(context, box, outer)[0]
     elif isinstance(box, boxes.TableCellBox):
         return table_cell_min_content_width(context, box, outer)
-    elif isinstance(box, (
-            boxes.BlockContainerBox, boxes.TableColumnBox, boxes.FlexBox)):
+    elif isinstance(box, (boxes.BlockContainerBox, boxes.TableColumnBox)):
         return block_min_content_width(context, box, outer)
     elif isinstance(box, boxes.TableColumnGroupBox):
         return column_group_content_width(context, box)
     elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):
-        return inline_min_content_width(
-            context, box, outer, is_line_start=True)
+        return inline_min_content_width(context, box, outer, is_line_start=True)
     elif isinstance(box, boxes.ReplacedBox):
         return replaced_min_content_width(box, outer)
     elif isinstance(box, boxes.FlexContainerBox):
@@ -59,8 +57,7 @@ def min_content_width(context, box, outer=True):
         # TODO: Get real grid size.
         return block_min_content_width(context, box, outer)
     else:
-        raise TypeError(
-            f'min-content width for {type(box).__name__} not handled yet')
+        raise TypeError(f'min-content width for {type(box).__name__} not handled yet')
 
 
 def max_content_width(context, box, outer=True):
@@ -73,14 +70,12 @@ def max_content_width(context, box, outer=True):
         return table_and_columns_preferred_widths(context, box, outer)[1]
     elif isinstance(box, boxes.TableCellBox):
         return table_cell_min_max_content_width(context, box, outer)[1]
-    elif isinstance(box, (
-            boxes.BlockContainerBox, boxes.TableColumnBox, boxes.FlexBox)):
+    elif isinstance(box, (boxes.BlockContainerBox, boxes.TableColumnBox)):
         return block_max_content_width(context, box, outer)
     elif isinstance(box, boxes.TableColumnGroupBox):
         return column_group_content_width(context, box)
     elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):
-        return inline_max_content_width(
-            context, box, outer, is_line_start=True)
+        return inline_max_content_width(context, box, outer, is_line_start=True)
     elif isinstance(box, boxes.ReplacedBox):
         return replaced_max_content_width(box, outer)
     elif isinstance(box, boxes.FlexContainerBox):
@@ -89,8 +84,7 @@ def max_content_width(context, box, outer=True):
         # TODO: Get real grid size.
         return block_max_content_width(context, box, outer)
     else:
-        raise TypeError(
-            f'max-content width for {type(box).__name__} not handled yet')
+        raise TypeError(f'max-content width for {type(box).__name__} not handled yet')
 
 
 def _block_content_width(context, box, function, outer):
@@ -344,7 +338,7 @@ def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=N
             # "By default, there is a break opportunity
             #  both before and after any inline object."
             if minimum:
-                lines = [0, max_content_width(context, child), 0]
+                lines = [0, min_content_width(context, child), 0]
             else:
                 lines = [max_content_width(context, child)]
         # The first text line goes on the current line
@@ -673,9 +667,12 @@ def replaced_min_content_width(box, outer=True):
             intrinsic_width, intrinsic_height, intrinsic_ratio = (
                 image.get_intrinsic_size(
                     box.style['image_resolution'], box.style['font_size']))
-            width, _ = default_image_sizing(
-                intrinsic_width, intrinsic_height, intrinsic_ratio, 'auto',
-                height, default_width=300, default_height=150)
+            if intrinsic_ratio and not intrinsic_width and not intrinsic_height:
+                width = 0
+            else:
+                width, _ = default_image_sizing(
+                    intrinsic_width, intrinsic_height, intrinsic_ratio, 'auto',
+                    height, default_width=300, default_height=150)
     elif box.style['width'].unit == '%':
         # See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
         width = 0
diff --git a/weasyprint/stacking.py b/weasyprint/stacking.py
index cce66811..0437d430 100644
--- a/weasyprint/stacking.py
+++ b/weasyprint/stacking.py
@@ -82,6 +82,7 @@ def _dispatch(box, page, child_contexts, blocks, floats, blocks_and_cells):
         child_contexts.append(StackingContext.from_box(box, page))
         return
 
+    stacking_classes = (boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)
     if style['position'] != 'static':
         assert style['z_index'] == 'auto'
         # "Fake" context: sub-contexts will go in this `child_contexts` list.
@@ -91,8 +92,7 @@ def _dispatch(box, page, child_contexts, blocks, floats, blocks_and_cells):
         child_contexts.insert(index, stacking_context)
     elif box.is_floated():
         floats.append(StackingContext.from_box(box, page, child_contexts))
-    elif isinstance(box, (
-            boxes.InlineBlockBox, boxes.InlineFlexBox, boxes.InlineGridBox)):
+    elif isinstance(box, stacking_classes):
         # Have this fake stacking context be part of the "normal" box tree,
         # because we need its position in the middle of a tree of inline boxes.
         return StackingContext.from_box(box, page, child_contexts)


Test patch:
diff --git a/tests/layout/test_flex.py b/tests/layout/test_flex.py
index d43c4217..6e52b971 100644
--- a/tests/layout/test_flex.py
+++ b/tests/layout/test_flex.py
@@ -17,17 +17,39 @@ def test_flex_direction_row():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'A'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'C'
-    assert (
-        div_1.position_y ==
-        div_2.position_y ==
-        div_3.position_y ==
-        article.position_y)
-    assert div_1.position_x == article.position_x
-    assert div_1.position_x < div_2.position_x < div_3.position_x
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_y == div2.position_y == div3.position_y == article.position_y
+    assert div1.position_x == article.position_x
+    assert div1.position_x < div2.position_x < div3.position_x
+
+
+@assert_no_logs
+def test_flex_direction_row_max_width():
+    page, = render_pages('''
+      <article style="display: flex; max-width: 100px">
+        <div></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    assert article.width == 100
+
+
+@assert_no_logs
+def test_flex_direction_row_min_height():
+    page, = render_pages('''
+      <article style="display: flex; min-height: 100px">
+        <div></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    assert article.height == 100
 
 
 @assert_no_logs
@@ -42,19 +64,13 @@ def test_flex_direction_row_rtl():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'A'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'C'
-    assert (
-        div_1.position_y ==
-        div_2.position_y ==
-        div_3.position_y ==
-        article.position_y)
-    assert (
-        div_1.position_x + div_1.width ==
-        article.position_x + article.width)
-    assert div_1.position_x > div_2.position_x > div_3.position_x
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_y == div2.position_y == div3.position_y == article.position_y
+    assert div1.position_x + div1.width == article.position_x + article.width
+    assert div1.position_x > div2.position_x > div3.position_x
 
 
 @assert_no_logs
@@ -69,19 +85,13 @@ def test_flex_direction_row_reverse():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'C'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'A'
-    assert (
-        div_1.position_y ==
-        div_2.position_y ==
-        div_3.position_y ==
-        article.position_y)
-    assert (
-        div_3.position_x + div_3.width ==
-        article.position_x + article.width)
-    assert div_1.position_x < div_2.position_x < div_3.position_x
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'C'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'A'
+    assert div1.position_y == div2.position_y == div3.position_y == article.position_y
+    assert div3.position_x + div3.width == article.position_x + article.width
+    assert div1.position_x < div2.position_x < div3.position_x
 
 
 @assert_no_logs
@@ -97,17 +107,13 @@ def test_flex_direction_row_reverse_rtl():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'C'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'A'
-    assert (
-        div_1.position_y ==
-        div_2.position_y ==
-        div_3.position_y ==
-        article.position_y)
-    assert div_3.position_x == article.position_x
-    assert div_1.position_x > div_2.position_x > div_3.position_x
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'C'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'A'
+    assert div1.position_y == div2.position_y == div3.position_y == article.position_y
+    assert div3.position_x == article.position_x
+    assert div1.position_x > div2.position_x > div3.position_x
 
 
 @assert_no_logs
@@ -122,17 +128,51 @@ def test_flex_direction_column():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'A'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'C'
-    assert (
-        div_1.position_x ==
-        div_2.position_x ==
-        div_3.position_x ==
-        article.position_x)
-    assert div_1.position_y == article.position_y
-    assert div_1.position_y < div_2.position_y < div_3.position_y
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_x == div2.position_x == div3.position_x == article.position_x
+    assert div1.position_y == article.position_y
+    assert div1.position_y < div2.position_y < div3.position_y
+
+
+@assert_no_logs
+def test_flex_direction_column_min_width():
+    page, = render_pages('''
+      <article style="display: flex; flex-direction: column; min-height: 100px">
+        <div></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    assert article.height == 100
+
+
+@assert_no_logs
+def test_flex_direction_column_max_height():
+    page, = render_pages('''
+      <article style="display: flex; flex-flow: column wrap; max-height: 100px">
+        <div style="height: 40px">A</div>
+        <div style="height: 40px">B</div>
+        <div style="height: 40px">C</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    assert article.height == 100
+    div1, div2, div3 = article.children
+    assert div1.height == 40
+    assert div1.position_x == 0
+    assert div1.position_y == 0
+    assert div2.height == 40
+    assert div2.position_x == 0
+    assert div2.position_y == 40
+    assert div3.height == 40
+    assert div3.position_x == div1.width
+    assert div3.position_y == 0
 
 
 @assert_no_logs
@@ -148,17 +188,13 @@ def test_flex_direction_column_rtl():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'A'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'C'
-    assert (
-        div_1.position_x ==
-        div_2.position_x ==
-        div_3.position_x ==
-        article.position_x)
-    assert div_1.position_y == article.position_y
-    assert div_1.position_y < div_2.position_y < div_3.position_y
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_x == div2.position_x == div3.position_x == article.position_x
+    assert div1.position_y == article.position_y
+    assert div1.position_y < div2.position_y < div3.position_y
 
 
 @assert_no_logs
@@ -173,19 +209,13 @@ def test_flex_direction_column_reverse():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'C'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'A'
-    assert (
-        div_1.position_x ==
-        div_2.position_x ==
-        div_3.position_x ==
-        article.position_x)
-    assert (
-        div_3.position_y + div_3.height ==
-        article.position_y + article.height)
-    assert div_1.position_y < div_2.position_y < div_3.position_y
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'C'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'A'
+    assert div1.position_x == div2.position_x == div3.position_x == article.position_x
+    assert div3.position_y + div3.height == article.position_y + article.height
+    assert div1.position_y < div2.position_y < div3.position_y
 
 
 @assert_no_logs
@@ -201,19 +231,36 @@ def test_flex_direction_column_reverse_rtl():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'C'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'A'
-    assert (
-        div_1.position_x ==
-        div_2.position_x ==
-        div_3.position_x ==
-        article.position_x)
-    assert (
-        div_3.position_y + div_3.height ==
-        article.position_y + article.height)
-    assert div_1.position_y < div_2.position_y < div_3.position_y
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'C'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'A'
+    assert div1.position_x == div2.position_x == div3.position_x == article.position_x
+    assert div3.position_y + div3.height == article.position_y + article.height
+    assert div1.position_y < div2.position_y < div3.position_y
+
+
+@assert_no_logs
+def test_flex_direction_column_box_sizing():
+    page, = render_pages('''
+      <style>
+        article {
+          box-sizing: border-box;
+          display: flex;
+          flex-direction: column;
+          height: 10px;
+          padding-top: 5px;
+          width: 10px;
+        }
+      </style>
+      <article></article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    assert article.width == 10
+    assert article.height == 5
+    assert article.padding_top == 5
 
 
 @assert_no_logs
@@ -228,14 +275,14 @@ def test_flex_row_wrap():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'A'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'C'
-    assert div_1.position_y == div_2.position_y == article.position_y
-    assert div_3.position_y == article.position_y + div_2.height
-    assert div_1.position_x == div_3.position_x == article.position_x
-    assert div_1.position_x < div_2.position_x
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_y == div2.position_y == article.position_y
+    assert div3.position_y == article.position_y + div2.height
+    assert div1.position_x == div3.position_x == article.position_x
+    assert div1.position_x < div2.position_x
 
 
 @assert_no_logs
@@ -250,14 +297,14 @@ def test_flex_column_wrap():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'A'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'C'
-    assert div_1.position_x == div_2.position_x == article.position_x
-    assert div_3.position_x == article.position_x + div_2.width
-    assert div_1.position_y == div_3.position_y == article.position_y
-    assert div_1.position_y < div_2.position_y
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_x == div2.position_x == article.position_x
+    assert div3.position_x == article.position_x + div2.width
+    assert div1.position_y == div3.position_y == article.position_y
+    assert div1.position_y < div2.position_y
 
 
 @assert_no_logs
@@ -272,17 +319,14 @@ def test_flex_row_wrap_reverse():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'C'
-    assert div_2.children[0].children[0].text == 'A'
-    assert div_3.children[0].children[0].text == 'B'
-    assert div_1.position_y == article.position_y
-    assert (
-        div_2.position_y ==
-        div_3.position_y ==
-        article.position_y + div_1.height)
-    assert div_1.position_x == div_2.position_x == article.position_x
-    assert div_2.position_x < div_3.position_x
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'C'
+    assert div2.children[0].children[0].text == 'A'
+    assert div3.children[0].children[0].text == 'B'
+    assert div1.position_y == article.position_y
+    assert div2.position_y == div3.position_y == article.position_y + div1.height
+    assert div1.position_x == div2.position_x == article.position_x
+    assert div2.position_x < div3.position_x
 
 
 @assert_no_logs
@@ -298,17 +342,14 @@ def test_flex_column_wrap_reverse():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'C'
-    assert div_2.children[0].children[0].text == 'A'
-    assert div_3.children[0].children[0].text == 'B'
-    assert div_1.position_x == article.position_x
-    assert (
-        div_2.position_x ==
-        div_3.position_x ==
-        article.position_x + div_1.width)
-    assert div_1.position_y == div_2.position_y == article.position_y
-    assert div_2.position_y < div_3.position_y
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'C'
+    assert div2.children[0].children[0].text == 'A'
+    assert div3.children[0].children[0].text == 'B'
+    assert div1.position_x == article.position_x
+    assert div2.position_x == div3.position_x == article.position_x + div1.width
+    assert div1.position_y == div2.position_y == article.position_y
+    assert div2.position_y < div3.position_y
 
 
 @assert_no_logs
@@ -326,22 +367,17 @@ def test_flex_direction_column_fixed_height_container():
     body, = html.children
     section, = body.children
     article, = section.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'A'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'C'
-    assert (
-        div_1.position_x ==
-        div_2.position_x ==
-        div_3.position_x ==
-        article.position_x)
-    assert div_1.position_y == article.position_y
-    assert div_1.position_y < div_2.position_y < div_3.position_y
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_x == div2.position_x == div3.position_x == article.position_x
+    assert div1.position_y == article.position_y
+    assert div1.position_y < div2.position_y < div3.position_y
     assert section.height == 10
     assert article.height > 10
 
 
-@pytest.mark.xfail
 @assert_no_logs
 def test_flex_direction_column_fixed_height():
     page, = render_pages('''
@@ -354,19 +390,15 @@ def test_flex_direction_column_fixed_height():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'A'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'C'
-    assert (
-        div_1.position_x ==
-        div_2.position_x ==
-        div_3.position_x ==
-        article.position_x)
-    assert div_1.position_y == article.position_y
-    assert div_1.position_y < div_2.position_y < div_3.position_y
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_x == div2.position_x == div3.position_x == article.position_x
+    assert div1.position_y == article.position_y
+    assert div1.position_y < div2.position_y < div3.position_y
     assert article.height == 10
-    assert div_3.position_y > 10
+    assert div3.position_y > 10
 
 
 @assert_no_logs
@@ -382,23 +414,136 @@ def test_flex_direction_column_fixed_height_wrap():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'A'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'C'
-    assert (
-        div_1.position_x !=
-        div_2.position_x !=
-        div_3.position_x)
-    assert div_1.position_y == article.position_y
-    assert (
-        div_1.position_y ==
-        div_2.position_y ==
-        div_3.position_y ==
-        article.position_y)
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_x < div2.position_x < div3.position_x
+    assert div1.position_y == article.position_y
+    assert div1.position_y == div2.position_y == div3.position_y == article.position_y
     assert article.height == 10
 
 
+@assert_no_logs
+def test_flex_direction_column_break():
+    # Regression test for issue #2066.
+    page1, page2 = render_pages('''
+      <style>
+        @page { size: 4px 5px }
+      </style>
+      <article style="display: flex; flex-direction: column; font: 2px weasyprint">
+        <div>A<br>B<br>C</div>
+      </article>
+    ''')
+    html, = page1.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.children[0].children[0].text == 'A'
+    assert div.children[1].children[0].text == 'B'
+    assert div.height == 5
+    html, = page2.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.children[0].children[0].text == 'C'
+    assert div.height == 2
+
+
+@assert_no_logs
+def test_flex_direction_column_break_margin():
+    # Regression test for issue #1967.
+    page1, page2 = render_pages('''
+      <style>
+        @page { size: 4px 7px }
+      </style>
+      <body style="font: 2px weasyprint">
+        <p style="margin: 1px 0">a</p>
+        <article style="display: flex; flex-direction: column">
+          <div>A<br>B<br>C</div>
+        </article>
+      </body>
+    ''')
+    html, = page1.children
+    body, = html.children
+    p, article = body.children
+    assert p.margin_height() == 4
+    assert article.position_y == 4
+    div, = article.children
+    assert div.children[0].children[0].text == 'A'
+    assert div.height == 3
+    html, = page2.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.children[0].children[0].text == 'B'
+    assert div.children[1].children[0].text == 'C'
+    assert div.height == 4
+
+
+@assert_no_logs
+def test_flex_direction_column_break_border():
+    page1, page2 = render_pages('''
+      <style>
+        @page { size: 8px 7px }
+        article, div { border: 1px solid black }
+      </style>
+      <article style="display: flex; flex-direction: column; font: 2px weasyprint">
+        <div>A B C</div>
+      </article>
+    ''')
+    html, = page1.children
+    body, = html.children
+    article, = body.children
+    assert article.border_height() == 7
+    assert article.border_top_width == 1
+    assert article.border_bottom_width == 0
+    div, = article.children
+    assert div.children[0].children[0].text == 'A'
+    assert div.children[1].children[0].text == 'B'
+    assert div.border_height() == 6
+    assert div.border_top_width == 1
+    assert div.border_bottom_width == 0
+    html, = page2.children
+    body, = html.children
+    article, = body.children
+    assert article.border_height() == 4
+    assert article.border_top_width == 0
+    assert article.border_bottom_width == 1
+    div, = article.children
+    assert div.children[0].children[0].text == 'C'
+    assert div.border_height() == 3
+    assert div.border_top_width == 0
+    assert div.border_bottom_width == 1
+
+
+@assert_no_logs
+def test_flex_direction_column_break_multiple_children():
+    page1, page2 = render_pages('''
+      <style>
+        @page { size: 4px 5px }
+      </style>
+      <article style="display: flex; flex-direction: column; font: 2px weasyprint">
+        <div>A</div>
+        <div>B<br>C</div>
+      </article>
+    ''')
+    html, = page1.children
+    body, = html.children
+    article, = body.children
+    div1, div2 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div1.height == 2
+    assert div2.children[0].children[0].text == 'B'
+    assert div2.height == 3
+    html, = page2.children
+    body, = html.children
+    article, = body.children
+    div2, = article.children
+    assert div2.children[0].children[0].text == 'C'
+    assert div2.height == 2
+
+
 @assert_no_logs
 def test_flex_item_min_width():
     page, = render_pages('''
@@ -411,21 +556,17 @@ def test_flex_item_min_width():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'A'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'C'
-    assert div_1.position_x == 0
-    assert div_1.width == 30
-    assert div_2.position_x == 30
-    assert div_2.width == 50
-    assert div_3.position_x == 80
-    assert div_3.width > 5
-    assert (
-        div_1.position_y ==
-        div_2.position_y ==
-        div_3.position_y ==
-        article.position_y)
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_x == 0
+    assert div1.width == 30
+    assert div2.position_x == 30
+    assert div2.width == 50
+    assert div3.position_x == 80
+    assert div3.width > 5
+    assert div1.position_y == div2.position_y == div3.position_y == article.position_y
 
 
 @assert_no_logs
@@ -440,29 +581,47 @@ def test_flex_item_min_height():
     html, = page.children
     body, = html.children
     article, = body.children
-    div_1, div_2, div_3 = article.children
-    assert div_1.children[0].children[0].text == 'A'
-    assert div_2.children[0].children[0].text == 'B'
-    assert div_3.children[0].children[0].text == 'C'
-    assert (
-        div_1.height ==
-        div_2.height ==
-        div_3.height ==
-        article.height ==
-        50)
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.height == div2.height == div3.height == article.height == 50
 
 
 @assert_no_logs
 def test_flex_auto_margin():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/800
-    page, = render_pages('<div id="div1" style="display: flex; margin: auto">')
+    # Regression test for issue #800.
+    page, = render_pages('<div style="display: flex; margin: auto"><div>')
     page, = render_pages(
-        '<div id="div2" style="display: flex; flex-direction: column; margin: auto">')
+        '<div style="display: flex; flex-direction: column; margin: auto"><div>')
+
+
+@assert_no_logs
+def test_flex_item_auto_margin_sized():
+    # Regression test for issue #2054.
+    page, = render_pages('''
+      <style>
+        div {
+          margin: auto;
+          display: flex;
+          width: 160px;
+          height: 160px;
+        }
+      </style>
+      <article>
+        <div></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.margin_left != 0
 
 
 @assert_no_logs
 def test_flex_no_baseline():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/765
+    # Regression test for issue #765.
     page, = render_pages('''
       <div class="references" style="display: flex; align-items: baseline;">
         <div></div>
@@ -478,7 +637,7 @@ def test_flex_no_baseline():
     ('space-evenly', 50, 10, 30),
 ))
 def test_flex_align_content(align, height, y1, y2):
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/811
+    # Regression test for issue #811.
     page, = render_pages('''
       <style>
         article {
@@ -515,7 +674,7 @@ def test_flex_align_content(align, height, y1, y2):
 
 @assert_no_logs
 def test_flex_item_percentage():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/885
+    # Regression test for issue #885.
     page, = render_pages('''
       <div style="display: flex; font-size: 15px; line-height: 1">
         <div style="height: 100%">a</div>
@@ -529,7 +688,7 @@ def test_flex_item_percentage():
 
 @assert_no_logs
 def test_flex_undefined_percentage_height_multiple_lines():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/1204
+    # Regression test for issue #1204.
     page, = render_pages('''
       <div style="display: flex; flex-wrap: wrap; height: 100%">
         <div style="width: 100%">a</div>
@@ -539,7 +698,7 @@ def test_flex_undefined_percentage_height_multiple_lines():
 
 @assert_no_logs
 def test_flex_absolute():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/1536
+    # Regression test for issue #1536.
     page, = render_pages('''
       <div style="display: flex; position: absolute">
         <div>a</div>
@@ -566,7 +725,7 @@ def test_flex_percent_height():
 
 @assert_no_logs
 def test_flex_percent_height_auto():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/2146
+    # Regression test for issue #2146.
     page, = render_pages('''
       <style>
         .a { width: 10px; }
@@ -579,7 +738,7 @@ def test_flex_percent_height_auto():
 
 @assert_no_logs
 def test_flex_break_inside_avoid():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/2183
+    # Regression test for issue #2183.
     page1, page2= render_pages('''
       <style>
         @page { size: 6px 4px }
@@ -601,7 +760,7 @@ def test_flex_break_inside_avoid():
 
 @assert_no_logs
 def test_flex_absolute_content():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/996
+    # Regression test for issue #996.
     page, = render_pages('''
       <section style="display: flex; position: relative">
          <h1 style="position: absolute; top: 0; right: 0">TEST</h1>
@@ -619,162 +778,110 @@ def test_flex_absolute_content():
 
 @assert_no_logs
 def test_flex_column_height():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/2222
+    # Regression test for issue #2222.
     page, = render_pages("""
-  <section style="display: flex; width: 300px">
-    <article style="display: flex; flex-direction: column" id="a1">
-      <div>
-        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
-        eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
-        enim ad minim veniam, quis nostrud exercitation ullamco laboris
-        nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
-        in reprehenderit in voluptate velit esse cillum dolore eu fugiat
-        nulla pariatur. Excepteur sint occaecat cupidatat non proident,
-        sunt in culpa qui officia deserunt mollit anim id est laborum.
-      </div>
-    </article>
-    <article style="display: flex; flex-direction: column" id="a2">
-      <div>
-        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
-        eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
-        enim ad minim veniam, quis nostrud exercitation ullamco laboris
-        nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
-        in reprehenderit in voluptate velit esse cillum dolore eu fugiat
-        nulla pariatur. Excepteur sint occaecat cupidatat non proident,
-        sunt in culpa qui officia deserunt mollit anim id est laborum.
-      </div>
-    </article>
-  </section>
+      <section style="display: flex; width: 10em">
+        <article style="display: flex; flex-direction: column">
+          <div>
+            Lorem ipsum dolor sit amet
+          </div>
+        </article>
+        <article style="display: flex; flex-direction: column">
+          <div>
+            Lorem ipsum dolor sit amet
+          </div>
+        </article>
+      </section>
     """)
     html, = page.children
     body, = html.children
     section, = body.children
-    a1, a2 = section.children
-    assert a1.height == section.height
-    assert a2.height == section.height
+    article1, article2 = section.children
+    assert article1.height == section.height
+    assert article2.height == section.height
 
 
 @assert_no_logs
-def test_flex_column_height2():
-    # Another regression test for
-    # https://github.com/Kozea/WeasyPrint/issues/2222
+def test_flex_column_height_margin():
+    # Regression test for issue #2222.
     page, = render_pages("""
-  <section style="display: flex; flex-direction: column; width: 300px">
-    <article style="margin: 5px" id="a1">
-      Question 1?  With quite a lot of extra text,
-      which should not overflow in the PDF, we hope.
-    </article>
-    <article style="margin: 5px" id="a2">
-      Answer 1.  With quite a lot of extra text,
-      which should not overflow in the PDF, we hope?
-    </article>
-  </section>
+      <section style="display: flex; flex-direction: column; width: 10em">
+        <article style="margin: 5px">
+          Lorem ipsum dolor sit amet
+        </article>
+        <article style="margin: 10px">
+          Lorem ipsum dolor sit amet
+        </article>
+      </section>
     """)
     html, = page.children
     body, = html.children
     section, = body.children
-    a1, a2 = section.children
-    assert section.height == (a1.height + a2.height
-                              + a1.margin_top + a1.margin_bottom
-                              + a2.margin_top + a2.margin_bottom)
+    article1, article2 = section.children
+    assert section.height == article1.margin_height() + article2.margin_height()
 
 
 @assert_no_logs
 def test_flex_column_width():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/1171
+    # Regression test for issue #1171.
     page, = render_pages("""
-    <style>
-      #paper {
-          width: 500px;
-          height: 400px;
-          display: flex;
-          flex-direction: column;
-      }
-      #content {
-          width: 100%;
-
-          display: flex;
-          flex-direction: column;
-          flex: auto;
-          justify-content: space-between;
-      }
-      #header {
-          width: 100%;
-          height: 50px;
-      }
-    </style>
-    <div id="paper">
-      <div id="header" style="background-color:lightblue">Header part,
-          should be full width, 50px height</div>
-
-      <div id="content">
-        <div style="background-color:orange" class="toppart">
-          Middle part, should be 100% width, blank space should follow
-          thanks to justify-items: between.
-        </div>
-        <div class="bottompart" style="background-color:yellow">
-          Bottom part. Should be 100% width, blank space before.
-        </div>
-      </div>
-    </div>
+      <main style="display: flex; flex-direction: column;
+                   width: 40px; height: 50px; font: 2px weasyprint">
+        <section style="width: 100%; height: 5px">a</section>
+        <section style="display: flex; flex: auto; flex-direction: column;
+                        justify-content: space-between; width: 100%">
+          <div>b</div>
+          <div>c</div>
+        </section>
+      </main>
     """)
     html, = page.children
     body, = html.children
-    paper, = body.children
-    header, content = paper.children
-    toppart, bottompart = content.children
-    assert header.width == paper.width
-    assert content.width == paper.width
-    assert toppart.width == paper.width
-    assert bottompart.position_y > toppart.position_y + toppart.height
+    main, = body.children
+    section1, section2 = main.children
+    div1, div2 = section2.children
+    assert section1.width == section2.width == main.width
+    assert div1.width == div2.width
+    assert div1.position_y == 5
+    assert div2.position_y == 48
 
 
 @assert_no_logs
-def test_flex_auto_margin2():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/2054
-    page, = render_pages('''
-<style>
-  #outer {
-    background: red;
-  }
-  #inner {
-    margin: auto;
-    display: flex;
-    width: 160px;
-    height: 160px;
-    background: black;
-  }
-</style>
-
-<div id="outer">
-  <div id="inner"></div>
-</div>
-''')
+def test_flex_column_in_flex_row():
+    page, = render_pages("""
+      <body style="display: flex; flex-wrap: wrap; font: 2px weasyprint">
+        <article>1</article>
+        <section style="display: flex; flex-direction: column">
+          <div>2</div>
+        </section>
+      </body>
+    """)
     html, = page.children
     body, = html.children
-    outer, = body.children
-    inner, = outer.children
-    assert inner.margin_left != 0
+    article, section = body.children
+    assert article.position_y == section.position_y == 0
+    assert article.position_x == 0
+    assert section.position_x == 2
 
 
 @assert_no_logs
 def test_flex_overflow():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/2292
+    # Regression test for issue #2292.
     page, = render_pages('''
-<style>
-  article {
-    display: flex;
-  }
-  section {
-    overflow: hidden;
-    width: 5px;
-  }
-</style>
-<article>
-  <section>A</section>
-  <section>B</section>
-</article>
-''')
+      <style>
+        article {
+          display: flex;
+        }
+        section {
+          overflow: hidden;
+          width: 5px;
+        }
+      </style>
+      <article>
+        <section>A</section>
+        <section>B</section>
+      </article>
+    ''')
     html, = page.children
     body, = html.children
     article, = body.children
@@ -786,14 +893,863 @@ def test_flex_overflow():
 
 @assert_no_logs
 def test_flex_column_overflow():
-    # Regression test for https://github.com/Kozea/WeasyPrint/issues/2304
+    # Regression test for issue #2304.
     render_pages('''
-<style>
-  @page {size: 20px}
-</style>
-<div style="display: flex; flex-direction: column">
-  <div></div>
-  <div><div style="height: 40px"></div></div>
-  <div><div style="height: 5px"></div></div>
-</div>
-''')
+      <style>
+        @page {size: 20px}
+      </style>
+      <div style="display: flex; flex-direction: column">
+        <div></div>
+        <div><div style="height: 40px"></div></div>
+        <div><div style="height: 5px"></div></div>
+      </div>
+    ''')
+
+
+@assert_no_logs
+def test_inline_flex():
+    render_pages('''
+      <style>
+        @page {size: 20px}
+      </style>
+      <div style="display: inline-flex; flex-direction: column">
+        <div>test</div>
+      </div>
+    ''')
+
+
+@assert_no_logs
+def test_inline_flex_empty_child():
+    render_pages('''
+      <style>
+        @page {size: 20px}
+      </style>
+      <div style="display: inline-flex; flex-direction: column">
+        <div></div>
+      </div>
+    ''')
+
+
+@assert_no_logs
+def test_inline_flex_absolute_baseline():
+    render_pages('''
+      <style>
+        @page {size: 20px}
+      </style>
+      <div style="display: inline-flex; flex-direction: column">
+        <div style="position: absolute">abs</div>
+      </div>
+    ''')
+
+
+@assert_no_logs
+def test_flex_item_overflow():
+    # Regression test for issue #2359.
+    page, = render_pages('''
+      <div style="display: flex; font: 2px weasyprint; width: 12px">
+        <div>ab</div>
+        <div>c d e</div>
+        <div>f</div>
+      </div>''')
+    html, = page.children
+    body, = html.children
+    flex, = body.children
+    div1, div2, div3 = flex.children
+    assert div1.width == 4
+    assert div2.width == 6
+    assert div3.width == 2
+    line1, line2 = div2.children
+    text1, = line1.children
+    text2, = line2.children
+    assert text1.text == 'c d'
+    assert text2.text == 'e'
+
+
+@assert_no_logs
+def test_flex_direction_row_inline_block():
+    # Regression test for issue #1652.
+    page, = render_pages('''
+      <article style="display: flex; font: 2px weasyprint; width: 14px">
+        <div style="display: inline-block">A B C D E F</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.width == 14
+    assert div.children[0].children[0].text == 'A B C D'
+    assert div.children[1].children[0].text == 'E F'
+
+
+@assert_no_logs
+def test_flex_float():
+    page, = render_pages('''
+      <article style="display: flex">
+        <div style="float: left">A</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.children[0].children[0].text == 'A'
+
+
+@assert_no_logs
+def test_flex_float_in_flex_item():
+    # Regression test for issue #1356.
+    page, = render_pages('''
+      <article style="display: flex; font: 2px weasyprint">
+        <div style="width: 10px"><span style="float: right">abc</span></div>
+        <div style="width: 10px"><span style="float: right">abc</span></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div1, div2 = article.children
+    span1, = div1.children
+    assert span1.position_y == 0
+    assert span1.position_x + span1.width == 10
+    span2, = div2.children
+    assert span2.position_y == 0
+    assert span2.position_x + span2.width == 20
+
+
+@assert_no_logs
+def test_flex_direction_row_defined_main():
+    page, = render_pages('''
+      <article style="display: flex">
+        <div style="width: 10px; padding: 1px"></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.width == 10
+    assert div.margin_width() == 12
+
+
+@assert_no_logs
+def test_flex_direction_row_defined_main_border_box():
+    page, = render_pages('''
+      <article style="display: flex">
+        <div style="box-sizing: border-box; width: 10px; padding: 1px"></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.width == 8
+    assert div.margin_width() == 10
+
+
+@assert_no_logs
+def test_flex_direction_column_defined_main():
+    page, = render_pages('''
+      <article style="display: flex; flex-direction: column">
+        <div style="height: 10px; padding: 1px"></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.height == 10
+    assert div.margin_height() == 12
+
+
+@assert_no_logs
+def test_flex_direction_column_defined_main_border_box():
+    page, = render_pages('''
+      <article style="display: flex; flex-direction: column">
+        <div style="box-sizing: border-box; height: 10px; padding: 1px"></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.height == 8
+    assert div.margin_height() == 10
+
+
+@assert_no_logs
+def test_flex_item_negative_margin():
+    page, = render_pages('''
+      <article style="display: flex">
+        <div style="margin-left: -20px; height: 10px; width: 10px"></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.height == 10
+    assert div.width == 10
+
+
+@assert_no_logs
+def test_flex_item_auto_margin_main():
+    page, = render_pages('''
+      <article style="display: flex; width: 100px">
+        <div style="margin-left: auto; height: 10px; width: 10px"></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.height == 10
+    assert div.width == 10
+    assert div.margin_left == 90
+
+
+@assert_no_logs
+def test_flex_item_auto_margin_cross():
+    # TODO: we keep this test because it used to crash, but we have to fix the top
+    # margin.
+    page, = render_pages('''
+      <article style="display: flex; height: 100px">
+        <div style="margin-top: auto; height: 10px; width: 10px"></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.height == 10
+    assert div.width == 10
+    # assert div.margin_top == 90
+
+
+@assert_no_logs
+def test_flex_direction_column_item_auto_margin():
+    page, = render_pages('''
+      <div style="font: 2px weasyprint; width: 30px; display: flex;
+                  flex-direction: column; align-items: flex-start">
+          <article style="margin: 0 auto">XXXX</article>
+      </div>
+    ''')
+    html, = page.children
+    body, = html.children
+    div, = body.children
+    article, = div.children
+    assert article.width == 8
+    assert article.margin_left == 11
+
+
+@assert_no_logs
+def test_flex_item_auto_margin_flex_basis():
+    page, = render_pages('''
+      <article style="display: flex">
+        <div style="margin-left: auto; height: 10px; flex-basis: 10px"></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div, = article.children
+    assert div.height == 10
+    assert div.width == 10
+
+
+@assert_no_logs
+@pytest.mark.parametrize('align, x1, x2, x3', (
+    ('start', 0, 2, 4),
+    ('flex-start', 0, 2, 4),
+    ('left', 0, 2, 4),
+    ('end', 6, 8, 10),
+    ('flex-end', 6, 8, 10),
+    ('right', 6, 8, 10),
+    ('center', 3, 5, 7),
+    ('space-between', 0, 5, 10),
+    ('space-around', 1, 5, 9),
+    ('space-evenly', 1.5, 5, 8.5),
+))
+def test_flex_direction_row_justify(align, x1, x2, x3):
+    page, = render_pages(f'''
+      <article style="width: 12px; font: 2px weasyprint;
+                      display: flex; justify-content: {align}">
+        <div>A</div>
+        <div>B</div>
+        <div>C</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_y == div2.position_y == div3.position_y == article.position_y
+    assert article.position_x == 0
+    assert div1.position_x == x1
+    assert div2.position_x == x2
+    assert div3.position_x == x3
+
+
+@assert_no_logs
+@pytest.mark.parametrize('align, y1, y2, y3', (
+    ('start', 0, 2, 4),
+    ('flex-start', 0, 2, 4),
+    ('left', 0, 2, 4),
+    ('end', 6, 8, 10),
+    ('flex-end', 6, 8, 10),
+    ('right', 6, 8, 10),
+    ('center', 3, 5, 7),
+    ('space-between', 0, 5, 10),
+    ('space-around', 1, 5, 9),
+    ('space-evenly', 1.5, 5, 8.5),
+))
+def test_flex_direction_column_justify(align, y1, y2, y3):
+    page, = render_pages(f'''
+      <article style="height: 12px; font: 2px weasyprint;
+                      display: flex; flex-direction: column; justify-content: {align}">
+        <div>A</div>
+        <div>B</div>
+        <div>C</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_x == div2.position_x == div3.position_x == article.position_x
+    assert article.position_y == 0
+    assert div1.position_y == y1
+    assert div2.position_y == y2
+    assert div3.position_y == y3
+
+
+@assert_no_logs
+@pytest.mark.parametrize('align, x1, x2, x3', (
+    ('start', 0, 4, 8),
+    ('flex-start', 0, 4, 8),
+    ('left', 0, 4, 8),
+    ('end', 6, 10, 14),
+    ('flex-end', 6, 10, 14),
+    ('right', 6, 10, 14),
+    ('center', 3, 7, 11),
+    ('space-between', 0, 7, 14),
+    ('space-around', 1, 7, 13),
+    ('space-evenly', 1.5, 7, 12.5),
+))
+def test_flex_direction_row_justify_gap(align, x1, x2, x3):
+    page, = render_pages(f'''
+      <article style="width: 16px; font: 2px weasyprint; gap: 2px;
+                      display: flex; justify-content: {align}">
+        <div>A</div>
+        <div>B</div>
+        <div>C</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_y == div2.position_y == div3.position_y == article.position_y
+    assert article.position_x == 0
+    assert div1.position_x == x1
+    assert div2.position_x == x2
+    assert div3.position_x == x3
+
+
+@assert_no_logs
+@pytest.mark.parametrize('align, y1, y2, y3', (
+    ('start', 0, 4, 8),
+    ('flex-start', 0, 4, 8),
+    ('left', 0, 4, 8),
+    ('end', 6, 10, 14),
+    ('flex-end', 6, 10, 14),
+    ('right', 6, 10, 14),
+    ('center', 3, 7, 11),
+    ('space-between', 0, 7, 14),
+    ('space-around', 1, 7, 13),
+    ('space-evenly', 1.5, 7, 12.5),
+))
+def test_flex_direction_column_justify_gap(align, y1, y2, y3):
+    page, = render_pages(f'''
+      <article style="height: 16px; font: 2px weasyprint; gap: 2px;
+                      display: flex; flex-direction: column; justify-content: {align}">
+        <div>A</div>
+        <div>B</div>
+        <div>C</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_x == div2.position_x == div3.position_x == article.position_x
+    assert article.position_y == 0
+    assert div1.position_y == y1
+    assert div2.position_y == y2
+    assert div3.position_y == y3
+
+
+@assert_no_logs
+@pytest.mark.parametrize('align, x1, x2, x3', (
+    ('start', 0, 4, 0),
+    ('flex-start', 0, 4, 0),
+    ('left', 0, 4, 0),
+    ('end', 3, 7, 7),
+    ('flex-end', 3, 7, 7),
+    ('right', 3, 7, 7),
+    ('center', 1.5, 5.5, 3.5),
+    ('space-between', 0, 7, 0),
+    ('space-around', 0.75, 6.25, 3.5),
+    ('space-evenly', 1, 6, 3.5),
+))
+def test_flex_direction_row_justify_gap_wrap(align, x1, x2, x3):
+    page, = render_pages(f'''
+      <article style="width: 9px; font: 2px weasyprint; gap: 1px 2px;
+                      display: flex; flex-wrap: wrap; justify-content: {align}">
+        <div>A</div>
+        <div>B</div>
+        <div>C</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_y == div2.position_y == article.position_y == 0
+    assert div3.position_y == 3
+    assert article.position_x == 0
+    assert div1.position_x == x1
+    assert div2.position_x == x2
+    assert div3.position_x == x3
+
+
+@assert_no_logs
+@pytest.mark.parametrize('align, y1, y2, y3', (
+    ('start', 0, 4, 0),
+    ('flex-start', 0, 4, 0),
+    ('left', 0, 4, 0),
+    ('end', 3, 7, 7),
+    ('flex-end', 3, 7, 7),
+    ('right', 3, 7, 7),
+    ('center', 1.5, 5.5, 3.5),
+    ('space-between', 0, 7, 0),
+    ('space-around', 0.75, 6.25, 3.5),
+    ('space-evenly', 1, 6, 3.5),
+))
+def test_flex_direction_column_justify_gap_wrap(align, y1, y2, y3):
+    page, = render_pages(f'''
+      <article style="height: 9px; width: 9px; font: 2px weasyprint; gap: 2px 1px;
+                      display: flex; flex-wrap: wrap; flex-direction: column;
+                      justify-content: {align}">
+        <div>A</div>
+        <div>B</div>
+        <div>C</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_x == div2.position_x == article.position_x == 0
+    assert div3.position_x == 5
+    assert article.position_y == 0
+    assert div1.position_y == y1
+    assert div2.position_y == y2
+    assert div3.position_y == y3
+
+
+@assert_no_logs
+def test_flex_direction_row_stretch_no_grow():
+    page, = render_pages('''
+      <article style="font: 2px weasyprint; width: 10px;
+                      display: flex; justify-content: stretch">
+        <div>A</div>
+        <div>B</div>
+        <div>C</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_y == div2.position_y == div3.position_y == article.position_y
+    assert div1.width == div2.width == div3.width == 2
+    assert div1.position_x == article.position_x == 0
+    assert div2.position_x == 2
+    assert div3.position_x == 4
+
+
+@assert_no_logs
+def test_flex_direction_row_stretch_grow():
+    page, = render_pages('''
+      <article style="font: 2px weasyprint; width: 10px;
+                      display: flex; justify-content: stretch">
+        <div>A</div>
+        <div style="flex-grow: 3">B</div>
+        <div style="flex-grow: 1">C</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_y == div2.position_y == div3.position_y == article.position_y
+    assert div1.width == 2
+    assert div2.width == 5
+    assert div3.width == 3
+    assert div1.position_x == article.position_x == 0
+    assert div2.position_x == 2
+    assert div3.position_x == 7
+
+
+@assert_no_logs
+@pytest.mark.parametrize('align, x1, x2, x3', (
+    ('start', 0, 6, 12),
+    ('flex-start', 0, 6, 12),
+    ('left', 0, 6, 12),
+    ('end', 6, 12, 18),
+    ('flex-end', 6, 12, 18),
+    ('right', 6, 12, 18),
+    ('center', 3, 9, 15),
+    ('space-between', 0, 9, 18),
+    ('space-around', 1, 9, 17),
+    ('space-evenly', 1.5, 9, 16.5),
+))
+def test_flex_direction_row_justify_margin_padding(align, x1, x2, x3):
+    page, = render_pages(f'''
+      <article style="width: 20px; font: 2px weasyprint;
+                      display: flex; justify-content: {align}">
+        <div style="margin: 0 1em">A</div>
+        <div style="padding: 0 1em">B</div>
+        <div>C</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_y == div2.position_y == div3.position_y == article.position_y
+    assert article.position_x == 0
+    assert article.width == 20
+    assert div1.position_x == x1
+    assert div2.position_x == x2
+    assert div3.position_x == x3
+    assert div1.margin_width() == 6
+    assert div2.margin_width() == 6
+    assert div3.margin_width() == 2
+
+
+@assert_no_logs
+@pytest.mark.parametrize('align, y1, y2, y3', (
+    ('start', 0, 6, 12),
+    ('flex-start', 0, 6, 12),
+    ('left', 0, 6, 12),
+    ('end', 6, 12, 18),
+    ('flex-end', 6, 12, 18),
+    ('right', 6, 12, 18),
+    ('center', 3, 9, 15),
+    ('space-between', 0, 9, 18),
+    ('space-around', 1, 9, 17),
+    ('space-evenly', 1.5, 9, 16.5),
+))
+def test_flex_direction_column_justify_margin_padding(align, y1, y2, y3):
+    page, = render_pages(f'''
+      <article style="height: 20px; font: 2px weasyprint;
+                      display: flex; flex-direction: column; justify-content: {align}">
+        <div style="margin: 1em 0">A</div>
+        <div style="padding: 1em 0">B</div>
+        <div>C</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div1, div2, div3 = article.children
+    assert div1.children[0].children[0].text == 'A'
+    assert div2.children[0].children[0].text == 'B'
+    assert div3.children[0].children[0].text == 'C'
+    assert div1.position_x == div2.position_x == div3.position_x == article.position_x
+    assert article.position_y == 0
+    assert article.height == 20
+    assert div1.position_y == y1
+    assert div2.position_y == y2
+    assert div3.position_y == y3
+    assert div1.margin_height() == 6
+    assert div2.margin_height() == 6
+    assert div3.margin_height() == 2
+
+
+@assert_no_logs
+def test_flex_item_table():
+    # Regression test for issue #1805.
+    page, = render_pages('''
+      <article style="display: flex; font: 2px weasyprint">
+        <table><tr><td>A</tr></td></table>
+        <table><tr><td>B</tr></td></table>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    table_wrapper1, table_wrapper2 = article.children
+    assert table_wrapper1.width == table_wrapper2.width == 2
+    assert table_wrapper1.position_x == 0
+    assert table_wrapper2.position_x == 2
+
+
+@assert_no_logs
+def test_flex_item_table_width():
+    # Regression test for issue #1805.
+    page, = render_pages('''
+      <article style="display: flex; font: 2px weasyprint; width: 40px">
+        <table style="width: 25%"><tr><td>A</tr></td></table>
+        <table style="width: 25%"><tr><td>B</tr></td></table>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    table_wrapper1, table_wrapper2 = article.children
+    assert table_wrapper1.width == table_wrapper2.width == 10
+    assert table_wrapper1.position_x == 0
+    assert table_wrapper2.position_x == 10
+
+
+@assert_no_logs
+def test_flex_width_on_parent():
+    page, = render_pages('''
+      <div style="font: 2px weasyprint; width: 30px; display: flex;
+                  flex-direction: column; align-items: flex-start">
+          <article>XXXX</article>
+      </div>
+    ''')
+    html, = page.children
+    body, = html.children
+    div, = body.children
+    article, = div.children
+    assert article.width == 8
+
+
+@assert_no_logs
+def test_flex_column_item_flex_1():
+    page, = render_pages('''
+      <div style="font: 2px weasyprint; display: flex; flex-direction: column">
+          <article>XXXX</article>
+          <article style="flex: 1">XXXX</article>
+      </div>
+    ''')
+    html, = page.children
+    body, = html.children
+    div, = body.children
+    assert div.height == 4
+
+
+@assert_no_logs
+def test_flex_row_item_flex_0():
+    page, = render_pages('''
+      <div style="font: 2px weasyprint; display: flex">
+          <article style="flex: 0">XXXX</article>
+          <article style="flex: 0">XXXX</article>
+      </div>
+    ''')
+    html, = page.children
+    body, = html.children
+    div, = body.children
+    article1, article2 = div.children
+    assert article1.position_x == 0
+    assert article1.width == 8
+    assert article2.position_x == 8
+    assert article2.width == 8
+
+
+@assert_no_logs
+def test_flex_item_intrinsic_width():
+    page, = render_pages('''
+      <div style="width: 100px; height: 100px;
+                  display: flex; flex-direction: column; align-items: center">
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">
+          <rect width="200" height="100" fill="red" />
+        </svg>
+      </div>
+    ''')
+    html, = page.children
+    body, = html.children
+    div, = body.children
+    svg, = div.children
+    assert svg.width == 100
+
+
+@assert_no_logs
+def test_flex_align_content_negative():
+    page, = render_pages('''
+      <div style="height: 6px; width: 20px;
+                  display: flex; flex-wrap: wrap; align-content: center">
+        <span style="height: 2px; flex: none; margin: 1px; width: 8px"></span>
+        <span style="height: 2px; flex: none; margin: 1px; width: 8px"></span>
+        <span style="height: 2px; flex: none; margin: 1px; width: 8px"></span>
+        <span style="height: 2px; flex: none; margin: 1px; width: 8px"></span>
+      </div>
+    ''')
+    html, = page.children
+    body, = html.children
+    div, = body.children
+    span1, span2, span3, span4 = div.children
+    span1.height == span2.height == span3.height == span4.height == 2
+    span1.width == span2.width == span3.width == span4.width == 8
+    assert span1.position_x == span3.position_x == 0
+    assert span2.position_x == span4.position_x == 10
+    assert span1.position_y == span2.position_y == -1
+    assert span3.position_y == span4.position_y == 3
+
+
+@assert_no_logs
+def test_flex_shrink():
+    page, = render_pages('''
+      <article style="display: flex; width: 300px">
+        <div style="flex: 0 2 auto; width: 300px"></div>
+        <div style="width: 200px"></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    div1, div2 = article.children
+    assert div1.width == div2.width == 150
+
+
+@assert_no_logs
+def test_flex_item_intrinsic_width_shrink():
+    page, = render_pages('''
+      <div style="width: 10px; height: 100px; display: flex; flex-direction: column">
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100">
+          <rect width="100" height="100" fill="red" />
+        </svg>
+      </div>
+    ''')
+    html, = page.children
+    body, = html.children
+    div, = body.children
+    svg, = div.children
+    assert svg.width == 100
+
+
+@assert_no_logs
+def test_flex_item_intrinsic_height_shrink():
+    page, = render_pages('''
+      <div style="width: 100px; height: 10px; display: flex; line-height: 0">
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" height="100">
+          <rect width="100" height="100" fill="red" />
+        </svg>
+      </div>
+    ''')
+    html, = page.children
+    body, = html.children
+    div, = body.children
+    svg, = div.children
+    assert svg.height == 100
+
+
+@assert_no_logs
+def test_flex_wrap_in_flex():
+    page, = render_pages('''
+      <main style="display: flex; font: 2px weasyprint">
+        <div style="display: flex; flex-wrap: wrap">
+          <section style="width: 25%">A</section>
+          <section style="flex: 1 75%">B</section>
+        </div>
+      </main>
+    ''')
+    html, = page.children
+    body, = html.children
+    main, = body.children
+    div, = main.children
+    section1, section2 = div.children
+    assert section1.position_y == section2.position_y == 0
+    assert section1.position_x == 0
+    assert section2.position_x == 1  # 25% * 4
+
+
+@assert_no_logs
+def test_flex_auto_break_before():
+    page1, page2 = render_pages('''
+      <style>
+        @page { size: 4px 5px }
+        body { font: 2px weasyprint }
+      </style>
+      <p>A<br>B</p>
+      <article style="display: flex">
+        <div>A</div>
+      </article>
+    ''')
+    html, = page1.children
+    body, = html.children
+    p, = body.children
+    assert p.height == 4
+    html, = page2.children
+    body, = html.children
+    article, = body.children
+    assert article.height == 2
+
+
+@assert_no_logs
+def test_flex_grow_in_flex_column():
+    page, = render_pages('''
+      <html style="width: 14px">
+        <body style="display: flex; flex-direction: column;
+                     border: 1px solid; padding: 1px">
+          <main style="flex: 1 1 auto; min-height: 0">
+            <div style="height: 5px">
+    ''')
+    html, = page.children
+    body, = html.children
+    main, = body.children
+    _, div, _ = main.children
+    assert body.height == div.height == 5
+    assert body.width == div.width == 10
+    assert body.margin_width() == 14
+    assert body.margin_height() == 9
+
+
+@assert_no_logs
+def test_flex_collapsing_margin():
+    page, = render_pages('''
+      <p style="margin-bottom: 20px; height: 100px">ABC</p>
+      <article style="display: flex; margin-top: 10px">
+        <div>A</div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    p, article = body.children
+    div, = article.children
+    assert p.position_y == 0
+    assert p.height == 100
+    assert article.position_y == 110
+    assert div.position_y == 120
diff --git a/tests/layout/test_float.py b/tests/layout/test_float.py
index 5b380f7c..8ca9449d 100644
--- a/tests/layout/test_float.py
+++ b/tests/layout/test_float.py
@@ -798,3 +798,11 @@ def test_float_table_aborted_row():
     assert textbox.text == 'f'
     textbox, = line2.children
     assert textbox.text == 'g'
+
+
+def test_formatting_context_avoid_rtl():
+    render_pages('''
+      <div style="direction: rtl">
+        <div style="overflow: hidden"></div>
+      </div>
+    ''')
diff --git a/tests/layout/test_table.py b/tests/layout/test_table.py
index 94601dbd..b93885cf 100644
--- a/tests/layout/test_table.py
+++ b/tests/layout/test_table.py
@@ -51,6 +51,23 @@ def test_inline_table():
     assert text.position_x == 90
 
 
+@assert_no_logs
+def test_inline_table_width():
+    page, = render_pages('''
+      <div style="font: 2px weasyprint; width: 20px">
+        <table style="display: inline-table; width: 20%"><tr><td>A</tr></td></table>
+        <table style="display: inline-table; width: 20%"><tr><td>B</tr></td></table>
+      </div>''')
+    html, = page.children
+    body, = html.children
+    div, = body.children
+    line, = div.children
+    table_wrapper1, space, table_wrapper2, tail = line.children
+    assert table_wrapper1.width == table_wrapper2.width == 4
+    assert table_wrapper1.position_x == 0
+    assert table_wrapper2.position_x == 6
+
+
 @assert_no_logs
 def test_implicit_width_table_col_percent():
     # See https://github.com/Kozea/WeasyPrint/issues/169