"""
Icon generator for Screens — 5 distinct 1024x1024 iOS app icons.
Philosophy: Luminous Distance (see PHILOSOPHY.md).

Each icon fills the canvas edge-to-edge. iOS applies the squircle mask.
"""
from __future__ import annotations
import math, os
from PIL import Image, ImageDraw, ImageFilter, ImageChops

SIZE = 1024
OUT = os.path.dirname(os.path.abspath(__file__))


# ---------------------------------------------------------------- helpers

def blank(size=SIZE):
    return Image.new("RGBA", (size, size), (0, 0, 0, 0))


def hex_to_rgba(h, a=255):
    h = h.lstrip("#")
    return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), a)


def lerp(a, b, t):
    return a + (b - a) * t


def lerp_color(c1, c2, t):
    return tuple(int(lerp(c1[i], c2[i], t)) for i in range(len(c1)))


def linear_gradient(size, stops, angle_deg=90):
    """stops = [(t, (r,g,b,a)), ...] with t in [0,1]; angle 0=→, 90=↓"""
    w = h = size
    img = Image.new("RGBA", (w, h))
    px = img.load()
    rad = math.radians(angle_deg)
    dx, dy = math.cos(rad), math.sin(rad)
    # Project (x,y) onto the direction vector, normalize to [0,1]
    cx, cy = w / 2, h / 2
    # half-extent in direction
    ext = abs(dx) * w / 2 + abs(dy) * h / 2
    for y in range(h):
        for x in range(w):
            t = ((x - cx) * dx + (y - cy) * dy) / (2 * ext) + 0.5
            t = max(0.0, min(1.0, t))
            # find segment
            for i in range(len(stops) - 1):
                t0, c0 = stops[i]
                t1, c1 = stops[i + 1]
                if t0 <= t <= t1:
                    local = (t - t0) / (t1 - t0) if t1 > t0 else 0
                    px[x, y] = lerp_color(c0, c1, local)
                    break
            else:
                px[x, y] = stops[-1][1]
    return img


def radial_gradient(size, center, stops, radius=None):
    w = h = size
    if radius is None:
        radius = size * 0.75
    img = Image.new("RGBA", (w, h))
    px = img.load()
    cx, cy = center
    for y in range(h):
        for x in range(w):
            dx, dy = x - cx, y - cy
            d = math.sqrt(dx * dx + dy * dy) / radius
            d = max(0.0, min(1.0, d))
            for i in range(len(stops) - 1):
                t0, c0 = stops[i]
                t1, c1 = stops[i + 1]
                if t0 <= d <= t1:
                    local = (d - t0) / (t1 - t0) if t1 > t0 else 0
                    px[x, y] = lerp_color(c0, c1, local)
                    break
            else:
                px[x, y] = stops[-1][1]
    return img


def rounded_rect_mask(size, box, radius):
    mask = Image.new("L", size, 0)
    d = ImageDraw.Draw(mask)
    d.rounded_rectangle(box, radius=radius, fill=255)
    return mask


def drop_shadow(shape_mask, offset=(0, 12), blur=40, opacity=120):
    sh = Image.new("RGBA", shape_mask.size, (0, 0, 0, 0))
    sh.putalpha(shape_mask.point(lambda v: int(v * opacity / 255)))
    sh = sh.filter(ImageFilter.GaussianBlur(blur))
    offset_mask = Image.new("RGBA", shape_mask.size, (0, 0, 0, 0))
    offset_mask.paste(sh, offset, sh)
    return offset_mask


def specular_highlight(mask, intensity=110):
    """Top-inner glow hinting at glass."""
    w, h = mask.size
    grad = Image.new("L", (w, h))
    for y in range(h):
        v = int(255 * max(0, 1 - y / (h * 0.55)) ** 2 * (intensity / 255))
        for x in range(w):
            grad.putpixel((x, y), v)
    highlight = Image.new("RGBA", (w, h), (255, 255, 255, 0))
    highlight.putalpha(ImageChops.multiply(grad, mask))
    return highlight


def paste_masked(base, layer, mask):
    """Paste `layer` onto `base` through `mask` (L mode)."""
    combined = Image.new("RGBA", base.size, (0, 0, 0, 0))
    combined.paste(layer, (0, 0), mask)
    return Image.alpha_composite(base, combined)


def noise_layer(size, amount=6):
    """Fine film grain for tactile quality."""
    import random
    random.seed(42)
    layer = Image.new("L", (size, size))
    px = layer.load()
    for y in range(size):
        for x in range(size):
            px[x, y] = 128 + random.randint(-amount, amount)
    noise = Image.new("RGBA", (size, size), (255, 255, 255, 0))
    noise.putalpha(layer.point(lambda v: abs(v - 128) * 2))
    return noise


# ---------------------------------------------------------------- icon 1 — Portal

def icon_portal():
    canvas = linear_gradient(SIZE, [
        (0.0, hex_to_rgba("#0B0B2A")),
        (0.6, hex_to_rgba("#1E1757")),
        (1.0, hex_to_rgba("#2A0E4D")),
    ], angle_deg=115)

    # Soft aura behind the portal
    aura = radial_gradient(SIZE, (SIZE / 2, SIZE / 2 - 20), [
        (0.0, hex_to_rgba("#7B6DF5", 230)),
        (0.45, hex_to_rgba("#5B3FC0", 100)),
        (1.0, hex_to_rgba("#140B36", 0)),
    ], radius=SIZE * 0.55)
    canvas = Image.alpha_composite(canvas, aura)

    # Screen rectangle
    inset = 200
    box = (inset, inset + 40, SIZE - inset, SIZE - inset - 40)
    radius = 70

    # Back-lit glow
    glow_mask = rounded_rect_mask((SIZE, SIZE), (box[0] - 40, box[1] - 40, box[2] + 40, box[3] + 40), radius + 30)
    glow = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
    glow.putalpha(glow_mask.filter(ImageFilter.GaussianBlur(36)).point(lambda v: int(v * 0.85)))
    glow_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#8A7BFF", 200))
    glow_layer.putalpha(glow.getchannel("A"))
    canvas = Image.alpha_composite(canvas, glow_layer)

    # Screen surface gradient
    screen = linear_gradient(SIZE, [
        (0.0, hex_to_rgba("#CBB8FF")),
        (0.35, hex_to_rgba("#7B63F0")),
        (1.0, hex_to_rgba("#2B1C6B")),
    ], angle_deg=112)
    screen_mask = rounded_rect_mask((SIZE, SIZE), box, radius)
    canvas = paste_masked(canvas, screen, screen_mask)

    # Inner bezel / rim light
    rim_outer = rounded_rect_mask((SIZE, SIZE), box, radius)
    inset_box = (box[0] + 8, box[1] + 8, box[2] - 8, box[3] - 8)
    rim_inner = rounded_rect_mask((SIZE, SIZE), inset_box, radius - 8)
    rim = ImageChops.subtract(rim_outer, rim_inner)
    rim_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#E4D8FF", 150))
    rim_layer.putalpha(rim)
    canvas = Image.alpha_composite(canvas, rim_layer)

    # Specular sheen (top-inner highlight, clipped to screen)
    sheen = specular_highlight(screen_mask, intensity=90)
    canvas = Image.alpha_composite(canvas, sheen)

    # Inner scanline bloom — a soft horizontal ribbon implying content
    ribbon_box = (box[0] + 80, int((box[1] + box[3]) / 2) - 3,
                  box[2] - 80, int((box[1] + box[3]) / 2) + 3)
    ribbon_mask = rounded_rect_mask((SIZE, SIZE), ribbon_box, 3).filter(ImageFilter.GaussianBlur(4))
    ribbon_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#FFFFFF", 180))
    ribbon_layer.putalpha(ribbon_mask)
    canvas = Image.alpha_composite(canvas, ribbon_layer)

    canvas.save(os.path.join(OUT, "icon-1-portal.png"))


# ---------------------------------------------------------------- icon 2 — Cursor

def icon_cursor():
    canvas = linear_gradient(SIZE, [
        (0.0, hex_to_rgba("#2E36E8")),
        (0.5, hex_to_rgba("#9B38E8")),
        (1.0, hex_to_rgba("#FF5A8F")),
    ], angle_deg=135)

    # Additional top-left glow to lift the cursor tip
    glow = radial_gradient(SIZE, (SIZE * 0.32, SIZE * 0.30), [
        (0.0, hex_to_rgba("#FFFFFF", 160)),
        (0.5, hex_to_rgba("#B9B0FF", 60)),
        (1.0, hex_to_rgba("#3A2C9A", 0)),
    ], radius=SIZE * 0.6)
    canvas = Image.alpha_composite(canvas, glow)

    # Classic macOS arrow cursor — authored as polygon coordinates on a 100-unit grid
    cursor_norm = [
        (0, 0), (0, 73), (20, 56), (32, 85), (44, 80), (33, 52), (56, 52)
    ]
    # Scale + center the cursor
    scale = 7.0
    cursor_w = 56 * scale
    cursor_h = 85 * scale
    ox = (SIZE - cursor_w) / 2 - 20
    oy = (SIZE - cursor_h) / 2 - 20
    pts = [(ox + x * scale, oy + y * scale) for (x, y) in cursor_norm]

    # Soft shadow behind cursor
    shadow_mask = Image.new("L", (SIZE, SIZE), 0)
    ImageDraw.Draw(shadow_mask).polygon([(p[0] + 16, p[1] + 24) for p in pts], fill=255)
    shadow_mask = shadow_mask.filter(ImageFilter.GaussianBlur(22))
    shadow_layer = Image.new("RGBA", (SIZE, SIZE), (20, 10, 60, 0))
    shadow_layer.putalpha(shadow_mask.point(lambda v: int(v * 0.55)))
    canvas = Image.alpha_composite(canvas, shadow_layer)

    # Cursor outline (thin dark) and fill (white) — draw as two polygons
    cursor_outer = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
    od = ImageDraw.Draw(cursor_outer)
    # Slight outline outset — simulate with stroked polygon
    # Draw outline by expanding the polygon by ~stroke/2 via line draw
    stroke = 18
    od.polygon(pts, fill=(30, 14, 80, 255))

    # Now shrink by drawing the white body slightly inset — simplest: re-use polygon, paint white with drawn stroke=0
    cursor_body = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
    bd = ImageDraw.Draw(cursor_body)
    # Inset polygon: compute centroid and pull points toward it by `stroke*0.35` for subtle inset
    cx = sum(p[0] for p in pts) / len(pts)
    cy = sum(p[1] for p in pts) / len(pts)
    inset = stroke * 0.35
    inset_pts = []
    for (x, y) in pts:
        dx, dy = x - cx, y - cy
        d = math.hypot(dx, dy)
        if d == 0:
            inset_pts.append((x, y))
        else:
            inset_pts.append((x - dx / d * inset, y - dy / d * inset))
    bd.polygon(inset_pts, fill=(255, 255, 255, 255))

    # Cursor highlight gradient over the body
    body_mask = Image.new("L", (SIZE, SIZE), 0)
    ImageDraw.Draw(body_mask).polygon(inset_pts, fill=255)
    body_grad = linear_gradient(SIZE, [
        (0.0, hex_to_rgba("#FFFFFF")),
        (1.0, hex_to_rgba("#D8D4FF")),
    ], angle_deg=110)
    cursor_body_grad = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
    cursor_body_grad.paste(body_grad, (0, 0), body_mask)

    canvas = Image.alpha_composite(canvas, cursor_outer)
    canvas = Image.alpha_composite(canvas, cursor_body_grad)

    canvas.save(os.path.join(OUT, "icon-2-cursor.png"))


# ---------------------------------------------------------------- icon 3 — Concentric Waves

def icon_waves():
    canvas = linear_gradient(SIZE, [
        (0.0, hex_to_rgba("#031028")),
        (1.0, hex_to_rgba("#072047")),
    ], angle_deg=100)

    # Deep radial dim at the edges for vignette
    edge = radial_gradient(SIZE, (SIZE / 2, SIZE / 2), [
        (0.0, hex_to_rgba("#000000", 0)),
        (0.65, hex_to_rgba("#000000", 0)),
        (1.0, hex_to_rgba("#000000", 120)),
    ], radius=SIZE * 0.75)
    canvas = Image.alpha_composite(canvas, edge)

    # Concentric rounded rectangles — 5 rings, smallest is bright, biggest is subtle
    rings = [
        {"inset": 120, "radius": 160, "stroke": 18, "color": hex_to_rgba("#1AA3C2", 110)},
        {"inset": 210, "radius": 130, "stroke": 16, "color": hex_to_rgba("#1BC6E0", 150)},
        {"inset": 300, "radius": 100, "stroke": 14, "color": hex_to_rgba("#31E1F5", 190)},
        {"inset": 380, "radius": 74,  "stroke": 12, "color": hex_to_rgba("#7EF0FF", 230)},
    ]

    for r in rings:
        box = (r["inset"], r["inset"], SIZE - r["inset"], SIZE - r["inset"])
        outer = rounded_rect_mask((SIZE, SIZE), box, r["radius"])
        inset_box = (box[0] + r["stroke"], box[1] + r["stroke"], box[2] - r["stroke"], box[3] - r["stroke"])
        inner = rounded_rect_mask((SIZE, SIZE), inset_box, max(r["radius"] - r["stroke"], 10))
        ring = ImageChops.subtract(outer, inner)

        # Subtle blur — outer rings softer than inner
        blur_amt = 0.8 + (rings.index(r)) * 0.3
        ring = ring.filter(ImageFilter.GaussianBlur(blur_amt))

        layer = Image.new("RGBA", (SIZE, SIZE), r["color"][:3] + (0,))
        layer.putalpha(ring.point(lambda v, a=r["color"][3]: int(v * a / 255)))
        canvas = Image.alpha_composite(canvas, layer)

    # Central glowing dot
    dot_r = 28
    dot_box = (SIZE / 2 - dot_r, SIZE / 2 - dot_r, SIZE / 2 + dot_r, SIZE / 2 + dot_r)
    dot_mask = Image.new("L", (SIZE, SIZE), 0)
    ImageDraw.Draw(dot_mask).ellipse(dot_box, fill=255)
    dot_mask = dot_mask.filter(ImageFilter.GaussianBlur(2))
    dot_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#E8FDFF"))
    dot_layer.putalpha(dot_mask)
    canvas = Image.alpha_composite(canvas, dot_layer)

    # Dot bloom
    bloom_mask = Image.new("L", (SIZE, SIZE), 0)
    ImageDraw.Draw(bloom_mask).ellipse((SIZE / 2 - 90, SIZE / 2 - 90, SIZE / 2 + 90, SIZE / 2 + 90), fill=255)
    bloom_mask = bloom_mask.filter(ImageFilter.GaussianBlur(36))
    bloom_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#7EF0FF"))
    bloom_layer.putalpha(bloom_mask.point(lambda v: int(v * 0.45)))
    canvas = Image.alpha_composite(canvas, bloom_layer)

    canvas.save(os.path.join(OUT, "icon-3-waves.png"))


# ---------------------------------------------------------------- icon 4 — Monogram S

def icon_monogram():
    canvas = linear_gradient(SIZE, [
        (0.0, hex_to_rgba("#0A1430")),
        (0.5, hex_to_rgba("#1B1450")),
        (1.0, hex_to_rgba("#04061A")),
    ], angle_deg=125)

    # Chromatic bloom — blue, magenta, amber — behind the mark
    bloom1 = radial_gradient(SIZE, (SIZE * 0.28, SIZE * 0.30), [
        (0.0, hex_to_rgba("#4AA8FF", 190)),
        (1.0, hex_to_rgba("#080822", 0)),
    ], radius=SIZE * 0.55)
    bloom2 = radial_gradient(SIZE, (SIZE * 0.78, SIZE * 0.78), [
        (0.0, hex_to_rgba("#E845A8", 170)),
        (1.0, hex_to_rgba("#080822", 0)),
    ], radius=SIZE * 0.55)
    canvas = Image.alpha_composite(canvas, bloom1)
    canvas = Image.alpha_composite(canvas, bloom2)

    # Two interlocking chevrons that read as an "S"
    # Upper chevron (like a `>` opening right) at top
    # Lower chevron (like a `<` opening left) at bottom — stacked with overlap
    stroke = 110
    inset_x = 200
    center_y = SIZE / 2
    top_y = 260
    bot_y = SIZE - 260

    def chevron_points(start_x, end_x, tip_x, top, bottom, thickness):
        """A V-shaped chevron band from (start_x, top) → (tip_x, mid) → (end_x, top)
           drawn as a parallelogram-like thick stroke."""
        mid = (top + bottom) / 2
        return [
            (start_x, top),
            (tip_x, bottom),
            (end_x, top),
            (end_x, top + thickness),
            (tip_x, bottom + thickness),  # overflowed — we'll draw differently
            (start_x, top + thickness),
        ]

    # Rather than manual polygons, paint two thick rounded "V" strokes.
    upper = Image.new("L", (SIZE, SIZE), 0)
    lower = Image.new("L", (SIZE, SIZE), 0)
    ud = ImageDraw.Draw(upper)
    ld = ImageDraw.Draw(lower)

    # Upper chevron: start top-left → tip right-middle → continues back to top-right? Actually we want ">"
    # We'll draw the S as two mirrored arcs — easier: use thick polylines along a cubic path.
    # Approximate with chained line segments + round caps.
    def thick_polyline(draw, pts, width):
        for i in range(len(pts) - 1):
            draw.line([pts[i], pts[i + 1]], fill=255, width=width)
        for p in pts:
            r = width / 2
            draw.ellipse((p[0] - r, p[1] - r, p[0] + r, p[1] + r), fill=255)

    # An abstract "S": top arc (left-top → right-center), bottom arc (right-center → left-bottom)
    top_arc = [
        (inset_x + 20, top_y),
        (SIZE - inset_x - 80, top_y + 80),
        (SIZE - inset_x, center_y - 40),
        (SIZE - inset_x - 40, center_y),
    ]
    bot_arc = [
        (SIZE - inset_x - 40, center_y),
        (inset_x + 40, center_y + 40),
        (inset_x, bot_y - 80),
        (inset_x + 20, bot_y),
    ]

    # Smooth via many interpolated bezier-like points
    def smooth(points, samples=80):
        # Quadratic bezier through 4 points: treat as cubic bezier control poly
        (p0, p1, p2, p3) = points
        out = []
        for i in range(samples + 1):
            t = i / samples
            # cubic bezier
            x = (1 - t) ** 3 * p0[0] + 3 * (1 - t) ** 2 * t * p1[0] + 3 * (1 - t) * t ** 2 * p2[0] + t ** 3 * p3[0]
            y = (1 - t) ** 3 * p0[1] + 3 * (1 - t) ** 2 * t * p1[1] + 3 * (1 - t) * t ** 2 * p2[1] + t ** 3 * p3[1]
            out.append((x, y))
        return out

    top_curve = smooth(top_arc, samples=120)
    bot_curve = smooth(bot_arc, samples=120)

    thick_polyline(ud, top_curve, stroke)
    thick_polyline(ld, bot_curve, stroke)

    combined = ImageChops.lighter(upper, lower)

    # Chromatic fill: horizontal-diagonal gradient inside the S
    s_grad = linear_gradient(SIZE, [
        (0.0, hex_to_rgba("#7FB2FF")),
        (0.5, hex_to_rgba("#C97BFF")),
        (1.0, hex_to_rgba("#FF8BB8")),
    ], angle_deg=120)
    s_layer = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
    s_layer.paste(s_grad, (0, 0), combined)

    # Highlight rim on top edge of the S for glass feel
    rim = combined.filter(ImageFilter.GaussianBlur(1))
    rim_inner = combined.filter(ImageFilter.GaussianBlur(3))
    edge = ImageChops.subtract(rim, rim_inner)
    edge_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#FFFFFF", 130))
    edge_layer.putalpha(edge)

    # Drop shadow under the S
    shadow = combined.filter(ImageFilter.GaussianBlur(26))
    sh_layer = Image.new("RGBA", (SIZE, SIZE), (0, 0, 20, 0))
    sh_layer.putalpha(shadow.point(lambda v: int(v * 0.55)))
    sh_offset = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
    sh_offset.paste(sh_layer, (0, 16), sh_layer)
    canvas = Image.alpha_composite(canvas, sh_offset)

    canvas = Image.alpha_composite(canvas, s_layer)
    canvas = Image.alpha_composite(canvas, edge_layer)

    canvas.save(os.path.join(OUT, "icon-4-monogram.png"))


# ---------------------------------------------------------------- icon 5 — Split Screen

def icon_split():
    canvas = linear_gradient(SIZE, [
        (0.0, hex_to_rgba("#1A1245")),
        (0.5, hex_to_rgba("#2F1D78")),
        (1.0, hex_to_rgba("#0D0824")),
    ], angle_deg=120)

    # Warm highlight in upper-left
    warm = radial_gradient(SIZE, (SIZE * 0.28, SIZE * 0.22), [
        (0.0, hex_to_rgba("#F7B57A", 140)),
        (1.0, hex_to_rgba("#1A1245", 0)),
    ], radius=SIZE * 0.55)
    canvas = Image.alpha_composite(canvas, warm)

    # Cool counterpoint lower-right
    cool = radial_gradient(SIZE, (SIZE * 0.82, SIZE * 0.82), [
        (0.0, hex_to_rgba("#4E6CFF", 180)),
        (1.0, hex_to_rgba("#100828", 0)),
    ], radius=SIZE * 0.6)
    canvas = Image.alpha_composite(canvas, cool)

    # Two tilted rectangles — "back" one tilted left, "front" one tilted right.
    def rotate_polygon(pts, angle_deg, cx, cy):
        a = math.radians(angle_deg)
        cos_a, sin_a = math.cos(a), math.sin(a)
        out = []
        for (x, y) in pts:
            dx, dy = x - cx, y - cy
            nx = dx * cos_a - dy * sin_a + cx
            ny = dx * sin_a + dy * cos_a + cy
            out.append((nx, ny))
        return out

    def draw_screen(rect_box, tilt_deg, body_grad_stops, rim_alpha=140):
        # rect_box: (x0,y0,x1,y1)
        x0, y0, x1, y1 = rect_box
        cx, cy = (x0 + x1) / 2, (y0 + y1) / 2
        # Approximate rounded-rect by drawing on a tilted transparent layer
        layer = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
        mask = Image.new("L", (SIZE, SIZE), 0)
        radius = 80
        # Draw axis-aligned rounded rect on a working canvas sized to the screen's bounding box, then rotate.
        local_w = int(x1 - x0) + 200
        local_h = int(y1 - y0) + 200
        local_mask = Image.new("L", (local_w, local_h), 0)
        lx0 = 100
        ly0 = 100
        lx1 = local_w - 100
        ly1 = local_h - 100
        ImageDraw.Draw(local_mask).rounded_rectangle((lx0, ly0, lx1, ly1), radius=radius, fill=255)
        local_mask = local_mask.rotate(tilt_deg, resample=Image.BICUBIC, expand=False)
        # Place local mask into full-size mask at correct position
        place_x = int(cx - local_w / 2)
        place_y = int(cy - local_h / 2)
        mask.paste(local_mask, (place_x, place_y))

        # Body gradient
        body = linear_gradient(SIZE, body_grad_stops, angle_deg=110)

        # Shadow before composite
        shadow_mask = mask.filter(ImageFilter.GaussianBlur(30))
        shadow = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
        shadow.putalpha(shadow_mask.point(lambda v: int(v * 0.55)))
        sh_offset = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
        sh_offset.paste(shadow, (14, 30), shadow)

        body_layer = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
        body_layer.paste(body, (0, 0), mask)

        # Rim / edge highlight
        rim_outer = mask
        rim_inner = mask.filter(ImageFilter.GaussianBlur(2)).point(lambda v: 255 if v > 180 else 0)
        rim_inner_eroded = rim_inner.filter(ImageFilter.MinFilter(9))
        rim = ImageChops.subtract(rim_outer, rim_inner_eroded)
        rim_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#FFFFFF", rim_alpha))
        rim_layer.putalpha(rim)

        return sh_offset, body_layer, rim_layer, mask

    # Back screen: bigger, tilted left
    back_box = (200, 250, SIZE - 240, SIZE - 200)
    back = draw_screen(
        back_box, tilt_deg=-7,
        body_grad_stops=[
            (0.0, hex_to_rgba("#5A4FD9")),
            (1.0, hex_to_rgba("#1E154F")),
        ],
        rim_alpha=90
    )

    # Front screen: smaller, tilted right, shifted down-right
    front_box = (280, 330, SIZE - 160, SIZE - 160)
    front = draw_screen(
        front_box, tilt_deg=6,
        body_grad_stops=[
            (0.0, hex_to_rgba("#F5CE9A")),
            (0.5, hex_to_rgba("#E086A3")),
            (1.0, hex_to_rgba("#6B488C")),
        ],
        rim_alpha=160
    )

    # Composite: back shadow → back body → back rim → front shadow → front body → front rim
    for bundle in (back, front):
        sh, body, rim, _ = bundle
        canvas = Image.alpha_composite(canvas, sh)
        canvas = Image.alpha_composite(canvas, body)
        canvas = Image.alpha_composite(canvas, rim)

    # Specular highlight on front screen
    _, _, _, front_mask = front
    sheen = specular_highlight(front_mask, intensity=120)
    canvas = Image.alpha_composite(canvas, sheen)

    canvas.save(os.path.join(OUT, "icon-5-split.png"))


# ---------------------------------------------------------------- contact sheet

def contact_sheet():
    paths = [
        "icon-1-portal.png",
        "icon-2-cursor.png",
        "icon-3-waves.png",
        "icon-4-monogram.png",
        "icon-5-split.png",
    ]
    cell = 360
    gap = 30
    cols = 5
    rows = 1
    label_h = 70
    W = cols * cell + (cols + 1) * gap
    H = rows * cell + (rows + 1) * gap + label_h
    sheet = Image.new("RGB", (W, H), (24, 24, 30))
    d = ImageDraw.Draw(sheet)
    for i, p in enumerate(paths):
        img = Image.open(os.path.join(OUT, p)).convert("RGBA")
        # Apply iOS squircle preview mask
        mask = Image.new("L", img.size, 0)
        ImageDraw.Draw(mask).rounded_rectangle((0, 0, img.size[0], img.size[1]), radius=int(img.size[0] * 0.22), fill=255)
        img.putalpha(mask)
        img = img.resize((cell, cell), Image.LANCZOS)
        x = gap + i * (cell + gap)
        y = gap
        sheet.paste(img, (x, y), img)
        label = ["1 Portal", "2 Cursor", "3 Waves", "4 Monogram", "5 Split"][i]
        d.text((x + cell / 2 - 30, y + cell + 20), label, fill=(240, 240, 250))
    sheet.save(os.path.join(OUT, "contact-sheet.png"))


if __name__ == "__main__":
    print("Generating icons…")
    icon_portal()
    print("  ✓ portal")
    icon_cursor()
    print("  ✓ cursor")
    icon_waves()
    print("  ✓ waves")
    icon_monogram()
    print("  ✓ monogram")
    icon_split()
    print("  ✓ split")
    contact_sheet()
    print("  ✓ contact sheet")
    print("Done.")
