tiles--thirds.html
<section class="tiles">
    <div class="container">
        <div class="tiles__grid -featured">
            <!-- 
                Thirds grid: 3 columns at desktop
                - (no modifier) = 1×1 small square
                - -featured = 2×2 large square  
                - -wide = 2×1 horizontal
                - -tall = 1×2 vertical
            -->
            <div class="tiles__set -thirds">
                <div class="tiles__tile">
                    <div class="tiles__media">
                        <img src="https://images.unsplash.com/photo-1557683316-973673baf926?w=400&h=400&fit=crop" alt="">
                    </div>
                </div>

                <div class="tiles__tile -featured -dark -has-content">
                    <div class="tiles__media">
                        <img src="https://images.unsplash.com/photo-1478760329108-5c3ed9d495a0?w=800&h=800&fit=crop" alt="">
                    </div>
                    <div class="tiles__overlay"></div>
                    <div class="tiles__content">
                        <h3 class="tiles__heading">Featured tile</h3>
                        <p class="tiles__description">This tile uses -featured to span 2×2</p>
                        <div class="tiles__actions">
                            <a href="#" class="button -outline">Learn More</a>
                        </div>
                    </div>
                </div>

                <div class="tiles__tile">
                    <div class="tiles__media">
                        <img src="https://images.unsplash.com/photo-1579546929518-9e396f3cc809?w=400&h=400&fit=crop" alt="">
                    </div>
                </div>
            </div>

            <!-- 
                3-column staggered: DOM order below is for md+ placement.
                Mobile-first CSS order: fill|fill / short|short / fill|short; at md+ order resets so DOM matches desktop stagger.

                Grid rows (md+):    Col 1      Col 2      Col 3
                Row 1:      [fill-top] [short]  [fill-top]
                Row 2:      [fill-bot] [fill-top] [fill-bot]
                Row 3:      [short] [fill-bot] [short]
            -->

            <h2>Staggered</h2>
            <div class="tiles__set -thirds -staggered">
                <!-- Tile order (desktop DOM): fill, short, fill, fill, short, short -->
                <div class="tiles__tile -fill">
                    <div class="tiles__media">
                        <img src="https://images.unsplash.com/photo-1557683316-973673baf926?w=400&h=600&fit=crop" alt="">
                    </div>
                </div>

                <div class="tiles__tile -short">
                    <div class="tiles__media">
                        <img src="https://images.unsplash.com/photo-1478760329108-5c3ed9d495a0?w=800&h=450&fit=crop" alt="">
                    </div>
                </div>

                <div class="tiles__tile -fill">
                    <div class="tiles__media">
                        <img src="https://images.unsplash.com/photo-1579546929518-9e396f3cc809?w=400&h=600&fit=crop" alt="">
                    </div>
                </div>

                <div class="tiles__tile -fill">
                    <div class="tiles__media">
                        <img src="https://images.unsplash.com/photo-1557682250-33bd709cbe85?w=800&h=450&fit=crop" alt="">
                    </div>
                </div>

                <div class="tiles__tile -short">
                    <div class="tiles__media">
                        <img src="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=400&h=600&fit=crop" alt="">
                    </div>
                </div>

                <div class="tiles__tile -short">
                    <div class="tiles__media">
                        <img src="https://images.unsplash.com/photo-1614850523296-d8c1af93d400?w=800&h=450&fit=crop" alt="">
                    </div>
                </div>
            </div>
        </div>
    </div>
</section>
index.scss
@use "@/core" as *;

.tiles {
    $b: &;

    padding: 2rem 0;

    &__grid {
        --columns: 1;
        --gap: var(--root-gap, 1rem);

        display: grid;
        gap: var(--gap);
        grid-template-columns: repeat(var(--columns), 1fr);
        grid-auto-flow: dense;

        @include at(md) {
            --columns: 2;
        }

        @include at(lg) {
            --columns: 4;
        }
    }

    &__tile {
        background-color: var(--tile-bg, var(--color-gray-100, #f5f5f5));
        display: grid;
        overflow: hidden;
        position: relative;

        // Position for content tiles
        &.-has-content {
            align-items: end;
        }
    }

    &__media {
        height: 100%;
        left: 0;
        position: absolute;
        top: 0;
        width: 100%;

        img,
        video {
            height: 100%;
            object-fit: cover;
            width: 100%;
        }
    }

    &__content {
        display: flex;
        flex-direction: column;
        gap: 1.5rem;
        max-width: 32rem;
        padding: 2rem;
        position: relative;
        z-index: 1;
    }

    &__heading {
        color: inherit;
        font-family: var(--root-font-family-heading);
        font-size: clamp(2rem, 5vw, 3.5rem);
        font-weight: 700;
        line-height: 1.1;
        margin: 0;
    }

    &__description {
        line-height: 1.5;
        margin: 0;
    }

    &__actions {
        display: flex;
        flex-wrap: wrap;
        gap: 1rem;
        margin-block-start: 0.5rem;
    }

    // Dark theme for tiles with dark backgrounds
    &__tile.-dark {
        color: #fff;

        #{$b}__heading,
        #{$b}__description {
            color: inherit;
        }

        .button.-outline {
            border-color: currentColor;
            color: inherit;

            &:hover {
                background-color: #fff;
                border-color: #fff;
                color: #000;
            }
        }
    }

    // Light theme for tiles with light backgrounds
    &__tile.-light {
        color: var(--root-color, #333);
    }

    // Featured layout container
    &__grid.-featured {
        display: flex;
        flex-direction: column;
        gap: var(--gap);
    }

    // ==========================================================================
    // Tile Sets - Flexible grid layouts
    // 
    // Set modifiers define the column structure:
    //   -thirds  : 3-column grid at desktop (for 1/3 + 2/3 style layouts)
    //   -halves  : 4-column grid at desktop (for 50/50 style layouts)
    //
    // Tile modifiers control spanning (work in any set):
    //   -featured : Spans 2 columns × 2 rows (large square)
    //   -wide     : Spans 2 columns × 1 row (horizontal banner)
    //   -tall     : Spans 1 column × 2 rows (vertical banner)
    //   (default) : 1 column × 1 row (small square)
    //
    // Tiles flow in DOM order with dense packing to fill gaps.
    // Add as many tiles as needed - the grid adapts automatically.
    // ==========================================================================
    &__set {
        container-type: inline-size;
        display: grid;
        gap: var(--gap);
        grid-auto-flow: dense;

        // Thirds: 3-column grid
        &.-thirds {
            --_tile-size: 100cqi; // Mobile: full width
            grid-template-columns: 1fr;

            @include at(sm) {
                --_tile-size: calc((100cqi - var(--gap)) / 2);
                grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
            }

            @include at(md) {
                --_tile-size: calc((100cqi - var(--gap) * 2) / 3);
                grid-template-columns: repeat(3, 1fr);
            }

            // Staggered layout: uses -short (1 row) and -fill (2 rows) tiles
            // No explicit row heights - tiles define their own heights via aspect-ratio/min-height
            &.-staggered {
                // Row-major flow: dense backfills holes and can strand late tiles in implicit/zero-sized tracks
                // with rowspan + mixed spans (last items “missing” in some engines).
                grid-auto-columns: minmax(0, 1fr);
                grid-auto-flow: row;

                // Short height = tile-width × 9/16
                --_short-height: calc(var(--_tile-size) * 0.5625);
                
                #{$b}__tile.-fill {
                    // Fill = 2 shorts + 1 gap
                    min-height: calc(var(--_short-height) * 2 + var(--gap));
                    aspect-ratio: auto;
                }

                // Absolute .tiles__media gives no in-flow height — tile needs intrinsic block size.
                // aspect-ratio:auto + height:100% below md was cyclic → 0× height in some rows.
                #{$b}__tile.-short {
                    align-self: stretch;
                    aspect-ratio: 16 / 9;
                    height: auto;
                    justify-self: stretch;
                    min-height: 0;
                    width: 100%;

                    @include at(md) {
                        align-self: start;
                    }
                }

                // Sandwich last two -short tiles between fills (demo = 6 tiles). md+ uses DOM order.
                > .tiles__tile:nth-child(1) {
                    order: 1;
                }

                > .tiles__tile:nth-child(2) {
                    order: 6;
                }

                > .tiles__tile:nth-child(3) {
                    order: 2;
                }

                > .tiles__tile:nth-child(4) {
                    order: 5;
                }

                > .tiles__tile:nth-child(5) {
                    order: 3;
                }

                > .tiles__tile:nth-child(6) {
                    order: 4;
                }

                @include at(md) {
                    > .tiles__tile:nth-child(1),
                    > .tiles__tile:nth-child(2),
                    > .tiles__tile:nth-child(3),
                    > .tiles__tile:nth-child(4),
                    > .tiles__tile:nth-child(5),
                    > .tiles__tile:nth-child(6) {
                        order: 0;
                    }
                }
            }
        }

        // Halves: 4-column grid (good for 50/50 layouts with sub-divisions)
        &.-halves {
            --_tile-size: 100cqi; // Mobile: full width
            grid-template-columns: 1fr;

            @include at(sm) {
                --_tile-size: calc((100cqi - var(--gap)) / 2);
                // Allow columns to shrink more before dropping
                grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
            }

            @include at(md) {
                --_tile-size: calc((100cqi - var(--gap) * 3) / 4);
                grid-template-columns: repeat(4, 1fr);
            }
        }

        // =======================================================================
        // Tile modifiers - control spanning behavior (mobile-first)
        // Default: 1×1 square tile on all screens
        // Modifiers add spanning at md breakpoint and above
        // =======================================================================

        // Default: 1×1 square tile
        #{$b}__tile {
            aspect-ratio: 1;
        }

        // Featured: Large 2×2 tile at md+ (square on mobile)
        #{$b}__tile.-featured {
            @include at(sm) {
                // Span all available columns at sm to avoid implicit tracks.
                grid-column: 1 / -1;
                grid-row: span 2;
            }

            @include at(md) {
                grid-column: span 2;
                grid-row: span 2;
            }
        }

        // Wide: Horizontal 2×1 tile at md+ (square on mobile)
        #{$b}__tile.-wide {
            @include at(sm) {
                // Span all available columns at md to avoid implicit tracks.
                grid-column: 1 / -1;
                aspect-ratio: auto;
                align-self: stretch;
                min-height: var(--_tile-size, 200px);
            }

            @include at(md) {
                grid-column: span 2;
            }
        }

        // Tall: Vertical 1×2 tile at md+ (square on mobile)
        #{$b}__tile.-tall {
            @include at(sm) {
                grid-row: span 2;
                aspect-ratio: auto;
                align-self: stretch;
                min-height: calc(var(--_tile-size, 200px) * 2 + var(--gap));
            }
        }

        // Short: default compact landscape from sm (square below sm via &__tile); staggered overrides below md
        #{$b}__tile.-short {
            @include at(sm) {
                aspect-ratio: 16 / 9;
            }
        }

        // Fill: Spans 2 grid rows at md+, pairs with -short tiles (square on mobile)
        #{$b}__tile.-fill {
            @include at(md) {
                grid-row: span 2;
                aspect-ratio: auto;
                align-self: stretch;
            }
        }
    }

    // "View more" button - positioned within tile content
    &__more {
        align-items: center;
        align-self: flex-end;
        background: #fff;
        border-radius: var(--root-border-radius);
        border: none;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
        color: var(--link-color, var(--color-blue));
        cursor: pointer;
        display: inline-flex;
        font-size: 0.875rem;
        font-weight: 500;
        gap: 0.5rem;
        margin: auto 2rem 2rem auto; // Push to bottom-right with tile padding
        padding: 0.5rem 1rem;
        position: relative;
        transition: box-shadow var(--root-ease-out-fast);
        white-space: nowrap;
        z-index: 2;

        &:hover {
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
        }

        svg,
        .icon {
            fill: currentColor;
            height: 1em;
            width: 1em;
        }
    }

    // Gradient overlay for better text readability
    &__overlay {
        background: linear-gradient(
            to top,
            rgba(0, 0, 0, 0.7) 0%,
            rgba(0, 0, 0, 0.3) 50%,
            rgba(0, 0, 0, 0) 100%
        );
        inset: 0;
        position: absolute;
        z-index: 0;
    }
}