# Photoreal MacBook app mockup

> A pixel-honest 14" MacBook Pro hardware frame — brushed-aluminium lid, camera notch, tapered base with the finger-groove, and a soft contact shadow — with a real Claude-for-desktop chat screen dropped inside. Drop any screen in and it scales crisply from one width prop.

- **Category:** Marketing
- **Source (best-in-class):** [Claude for Mac](https://claude.ai/download)
- **Tags:** macbook, mockup, device-frame, laptop, desktop, claude, macos, notch, hero, marketing, svg
- **Dependencies:** react
- **Status:** stable
- **Live demo:** https://eito.me/ui/macbook-mock

## Problem

A desktop app screenshot looks flat in a hero or a pitch; the credibility comes from the machine around it — the aluminium lid, the notch, the traffic-light window chrome. Maintaining that chrome as a bitmap means re-exporting every time the screen changes. This mock authors the whole laptop once in a fixed logical-point canvas and scales it as a single CSS transform, so every corner radius, the notch and the base taper stay pixel-crisp at any size and the app screen is live React you can restyle, theme, or animate.

## When to use

- A landing hero or pitch deck needs a web/desktop app to look like it's really running on a Mac.
- You want the device chrome (lid, notch, base, window traffic lights) crisp at any width from a single number, not a fixed-resolution PNG.
- The screen inside should be live, themeable UI (light/dark, real text, typing indicator) rather than a flat screenshot.

## Code — snippet.tsx

```tsx
import type { CSSProperties, ReactNode } from "react";
import { useId } from "react";

/**
 * MacBookMock — a photoreal MacBook Pro hardware frame you can drop any screen
 * into, shipped with a pixel-honest Claude-desktop chat screen as the sample
 * payload.
 *
 * Same trick as its iPhone sibling: the whole laptop is authored once in a
 * fixed logical canvas (a 1118 × 704 "point" space that mirrors a 14" MacBook
 * Pro seen head-on) and then scaled as a single unit with one CSS transform.
 * The aluminium lid, the black-glass bezel, the camera notch, the tapered base
 * with its finger-groove and every pixel of the app are laid out in real
 * points, so nothing is re-tuned when you change `width` — you set one number
 * and the entire machine scales with sub-pixel fidelity.
 *
 * Realism lives in the details that read subconsciously:
 *   - a brushed-aluminium lid rim and a vertically-lit tapered base (not a flat
 *     grey box), with a soft contact shadow on the ground beneath it,
 *   - the real MacBook Pro camera notch dipping into the top of the display,
 *   - a macOS window shell: traffic-light buttons, a warm sidebar, a model
 *     selector, and the exact Claude chat idiom (right-aligned user bubbles,
 *     full-width Claude turns behind the sunburst mark, and the pill composer),
 *   - two chassis finishes (Silver / Space Black) and a light/dark app theme,
 *     both live React — no bitmaps.
 *
 * Deps: react only. Everything is inline SVG + inline styles, so the snippet is
 * self-contained and theme-swaps from a single token object.
 */

/* ---- device geometry (logical points, 14" MacBook Pro head-on) ------------ */

const DISPLAY_W = 1000;
const DISPLAY_H = 640; // ~16:10.24
const BEZEL = 13; // black-glass border around the display
const RIM = 6; // thin aluminium edge outside the glass
const INSET = BEZEL + RIM; // 19
const LID_W = DISPLAY_W + INSET * 2; // 1038
const LID_H = DISPLAY_H + INSET * 2; // 678
const LID_RADIUS = 26;
const SCREEN_RADIUS = 8;
const NOTCH_W = 190;
const NOTCH_H = 20;
const BASE_H = 26;
const BASE_OVERHANG = 40; // base flares this far past the lid on each side
const BASE_W = LID_W + BASE_OVERHANG * 2; // 1118

const DEVICE_W = BASE_W; // 1118
const DEVICE_H = LID_H + BASE_H; // 704

/* ---- app colour tokens (Claude desktop) ----------------------------------- */

export type Scheme = "light" | "dark";

interface AppTokens {
  canvas: string; // main chat background
  sidebar: string; // sidebar fill
  ink: string; // primary text
  sub: string; // secondary text / icons
  faint: string; // hairlines / borders
  active: string; // active sidebar row
  bubble: string; // user bubble fill
  bubbleInk: string;
  composer: string; // composer surface
  composerBorder: string;
  accent: string; // Claude clay / send button
  accentInk: string; // glyph on the accent
}

const TOKENS: Record<Scheme, AppTokens> = {
  light: {
    canvas: "#faf9f5",
    sidebar: "#f2f0e9",
    ink: "#262624",
    sub: "#8a897f",
    faint: "#e7e3d8",
    active: "#e7e2d5",
    bubble: "#f0e8d9",
    bubbleInk: "#33322d",
    composer: "#ffffff",
    composerBorder: "#e5e0d3",
    accent: "#d97757",
    accentInk: "#ffffff",
  },
  dark: {
    canvas: "#262624",
    sidebar: "#1f1e1d",
    ink: "#f5f4ee",
    sub: "#9d9b90",
    faint: "#ffffff14",
    active: "#35332e",
    bubble: "#37342e",
    bubbleInk: "#f0ede4",
    composer: "#30302e",
    composerBorder: "#ffffff1f",
    accent: "#d97757",
    accentInk: "#fdf6f1",
  },
};

/* ---- conversation model ---------------------------------------------------- */

export interface Turn {
  role: "user" | "assistant";
  /** Supports **bold**, *italic*, and blank-line paragraphs / single newlines. */
  text: string;
  /** Render the assistant turn as an animated typing indicator. */
  typing?: boolean;
}

/** The sample script shown in the demo — swap freely, the shell renders any. */
export const sampleConversation: Turn[] = [
  {
    role: "user",
    text: "Draft a warm but concise out-of-office reply — I'm away Aug 27 – Sep 8. ✈️",
  },
  {
    role: "assistant",
    text: "Here's one that stays friendly without over-explaining:\n\n**Subject:** Out of office until Sep 8\n\nThanks for your note! I'm away until **September 8** with limited access to email, so I may be slow to reply. I'll get back to you as soon as I'm back at my desk.\n\nFor anything urgent, please reach out to my team at *team@example.com*.\n\nBest,\nEito",
  },
  { role: "user", text: "Love it. Make the tone a touch more formal." },
  { role: "assistant", text: "", typing: true },
];

const RECENTS = [
  "Out-of-office reply",
  "Refactor auth middleware",
  "Kyoto trip itinerary",
  "Explain diffusion models",
  "Weekly newsletter draft",
];

/* ---- brand + system glyphs (inline SVG, no assets) ------------------------- */

/** Claude "sunburst" mark — radiating rounded rays, drawn from a single loop. */
function ClaudeBurst({ size = 20, color = "#d97757" }: { size?: number; color?: string }) {
  const rays = 12;
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
      <g fill={color}>
        {Array.from({ length: rays }).map((_, i) => (
          <rect
            key={i}
            x={11.05}
            y={1.8}
            width={1.9}
            height={9.2}
            rx={0.95}
            transform={`rotate(${(i * 360) / rays} 12 12)`}
          />
        ))}
      </g>
    </svg>
  );
}

function EditGlyph({ color }: { color: string }) {
  return (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path
        d="M4 20h4L18.5 9.5a2.12 2.12 0 0 0-3-3L5 17v3Z"
        stroke={color}
        strokeWidth="1.7"
        strokeLinejoin="round"
      />
      <path d="M13.5 6.5l3 3" stroke={color} strokeWidth="1.7" strokeLinecap="round" />
    </svg>
  );
}

function SidebarGlyph({ color }: { color: string }) {
  return (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <rect x="3" y="4.5" width="18" height="15" rx="3.2" stroke={color} strokeWidth="1.7" />
      <line x1="9" y1="4.5" x2="9" y2="19.5" stroke={color} strokeWidth="1.7" />
    </svg>
  );
}

function SearchGlyph({ color }: { color: string }) {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <circle cx="11" cy="11" r="6.5" stroke={color} strokeWidth="1.7" />
      <line x1="16" y1="16" x2="20.5" y2="20.5" stroke={color} strokeWidth="1.7" strokeLinecap="round" />
    </svg>
  );
}

function ChevronDown({ color, size = 14 }: { color: string; size?: number }) {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true">
      <path d="M4 6l4 4 4-4" stroke={color} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function PlusGlyph({ color }: { color: string }) {
  return (
    <svg width="20" height="20" viewBox="0 0 22 22" fill="none" aria-hidden="true">
      <line x1="11" y1="5" x2="11" y2="17" stroke={color} strokeWidth="1.9" strokeLinecap="round" />
      <line x1="5" y1="11" x2="17" y2="11" stroke={color} strokeWidth="1.9" strokeLinecap="round" />
    </svg>
  );
}

function ArrowUp({ color }: { color: string }) {
  return (
    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <line x1="12" y1="19" x2="12" y2="6" stroke={color} strokeWidth="2.2" strokeLinecap="round" />
      <path d="M6 12l6-6 6 6" stroke={color} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function ShareGlyph({ color }: { color: string }) {
  return (
    <svg width="17" height="17" viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <circle cx="18" cy="5" r="2.6" stroke={color} strokeWidth="1.6" />
      <circle cx="6" cy="12" r="2.6" stroke={color} strokeWidth="1.6" />
      <circle cx="18" cy="19" r="2.6" stroke={color} strokeWidth="1.6" />
      <path d="M8.3 10.7l7.4-4.3M8.3 13.3l7.4 4.3" stroke={color} strokeWidth="1.6" strokeLinecap="round" />
    </svg>
  );
}

/* ---- screen: Claude for desktop ------------------------------------------- */

export interface ClaudeScreenProps {
  scheme?: Scheme;
  conversation?: Turn[];
  /** Model label shown in the header + composer, e.g. "Claude Opus 4.5". */
  model?: string;
  /** Signed-in display name, shown in the sidebar footer. */
  user?: string;
}

/**
 * A faithful Claude-desktop screen: the macOS traffic lights, a warm sidebar
 * with recents and an account footer, a model-selector header, the centred
 * chat column (right-bubbled user turns, full-width Claude turns behind the
 * sunburst), and the pill composer. Sized to the laptop's inner display
 * (1000 × 640 pt) so it drops straight into MacBookMock, but renders standalone.
 */
export function ClaudeScreen({
  scheme = "light",
  conversation = sampleConversation,
  model = "Claude Opus 4.5",
  user = "Eito Miyamura",
}: ClaudeScreenProps) {
  const t = TOKENS[scheme];
  const styleId = useId().replace(/[:]/g, "");

  return (
    <div
      style={{
        width: DISPLAY_W,
        height: DISPLAY_H,
        background: t.canvas,
        color: t.ink,
        display: "flex",
        fontFamily:
          '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
        WebkitFontSmoothing: "antialiased",
        overflow: "hidden",
      }}
    >
      <style>{`
        @keyframes ${styleId}-blink { 0%,60%,100%{opacity:.25} 30%{opacity:1} }
        .${styleId}-dot { animation: ${styleId}-blink 1.3s infinite ease-in-out; }
      `}</style>

      {/* ---- Sidebar ---- */}
      <aside
        style={{
          width: 236,
          flexShrink: 0,
          background: t.sidebar,
          borderRight: `1px solid ${t.faint}`,
          display: "flex",
          flexDirection: "column",
          padding: "0 10px",
        }}
      >
        {/* traffic lights + collapse */}
        <div
          style={{
            height: 44,
            display: "flex",
            alignItems: "center",
            justifyContent: "space-between",
            paddingLeft: 4,
          }}
        >
          <div style={{ display: "flex", gap: 8 }}>
            {["#ff5f57", "#febc2e", "#28c840"].map((c) => (
              <span
                key={c}
                style={{
                  width: 12,
                  height: 12,
                  borderRadius: "50%",
                  background: c,
                  boxShadow: "inset 0 0 0 0.5px rgba(0,0,0,0.18)",
                }}
              />
            ))}
          </div>
          <span style={{ opacity: 0.55 }}>
            <SidebarGlyph color={t.sub} />
          </span>
        </div>

        {/* new chat */}
        <button
          type="button"
          style={{
            display: "flex",
            alignItems: "center",
            gap: 9,
            width: "100%",
            border: "none",
            background: "transparent",
            color: t.accent,
            fontSize: 14.5,
            fontWeight: 600,
            padding: "9px 8px",
            borderRadius: 9,
            cursor: "pointer",
          }}
        >
          <EditGlyph color={t.accent} />
          New chat
        </button>

        {/* search */}
        <div
          style={{
            display: "flex",
            alignItems: "center",
            gap: 8,
            padding: "8px 9px",
            margin: "2px 0 10px",
            borderRadius: 9,
            color: t.sub,
            fontSize: 13.5,
          }}
        >
          <SearchGlyph color={t.sub} />
          <span style={{ flex: 1 }}>Search chats</span>
          <span style={{ fontSize: 11.5, opacity: 0.7 }}>⌘K</span>
        </div>

        {/* recents */}
        <div
          style={{
            fontSize: 11,
            fontWeight: 600,
            letterSpacing: 0.4,
            textTransform: "uppercase",
            color: t.sub,
            padding: "0 9px 6px",
          }}
        >
          Recents
        </div>
        <nav style={{ display: "flex", flexDirection: "column", gap: 1 }}>
          {RECENTS.map((label, i) => (
            <div
              key={label}
              style={{
                display: "flex",
                alignItems: "center",
                gap: 8,
                padding: "8px 9px",
                borderRadius: 8,
                fontSize: 13.5,
                color: t.ink,
                background: i === 0 ? t.active : "transparent",
                fontWeight: i === 0 ? 500 : 400,
                whiteSpace: "nowrap",
                overflow: "hidden",
                textOverflow: "ellipsis",
              }}
            >
              {label}
            </div>
          ))}
        </nav>

        <div style={{ flex: 1 }} />

        {/* account footer */}
        <div
          style={{
            display: "flex",
            alignItems: "center",
            gap: 9,
            padding: "9px 8px",
            marginBottom: 10,
            borderRadius: 10,
          }}
        >
          <span
            style={{
              width: 28,
              height: 28,
              borderRadius: "50%",
              background: "linear-gradient(135deg,#d97757,#c15f3c)",
              color: "#fff",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              fontSize: 12,
              fontWeight: 600,
              flexShrink: 0,
            }}
          >
            EM
          </span>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div
              style={{
                fontSize: 13,
                fontWeight: 600,
                color: t.ink,
                whiteSpace: "nowrap",
                overflow: "hidden",
                textOverflow: "ellipsis",
              }}
            >
              {user}
            </div>
            <div style={{ fontSize: 11.5, color: t.sub }}>Max plan</div>
          </div>
          <ChevronDown color={t.sub} />
        </div>
      </aside>

      {/* ---- Main ---- */}
      <main style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
        {/* header — model selector + share */}
        <header
          style={{
            height: 52,
            flexShrink: 0,
            display: "flex",
            alignItems: "center",
            justifyContent: "space-between",
            padding: "0 20px",
          }}
        >
          <button
            type="button"
            style={{
              display: "inline-flex",
              alignItems: "center",
              gap: 6,
              border: "none",
              background: "transparent",
              color: t.ink,
              fontSize: 15.5,
              fontWeight: 600,
              cursor: "pointer",
              padding: "6px 8px",
              borderRadius: 8,
            }}
          >
            {model}
            <span style={{ opacity: 0.45 }}>
              <ChevronDown color={t.ink} />
            </span>
          </button>
          <span style={{ opacity: 0.7 }}>
            <ShareGlyph color={t.sub} />
          </span>
        </header>

        {/* thread — centred column */}
        <div style={{ flex: 1, overflow: "hidden", display: "flex", justifyContent: "center" }}>
          <div
            style={{
              width: "100%",
              maxWidth: 640,
              display: "flex",
              flexDirection: "column",
              gap: 22,
              padding: "10px 24px 4px",
            }}
          >
            {conversation.map((turn, i) =>
              turn.role === "user" ? (
                <div key={i} style={{ display: "flex", justifyContent: "flex-end" }}>
                  <div
                    style={{
                      maxWidth: "82%",
                      background: t.bubble,
                      color: t.bubbleInk,
                      padding: "11px 16px",
                      borderRadius: 18,
                      borderBottomRightRadius: 6,
                      fontSize: 15,
                      lineHeight: 1.5,
                    }}
                  >
                    {turn.text}
                  </div>
                </div>
              ) : (
                <div key={i} style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
                  <div
                    style={{
                      flexShrink: 0,
                      width: 28,
                      height: 28,
                      borderRadius: "50%",
                      border: `1px solid ${t.faint}`,
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "center",
                      background: scheme === "light" ? "#fff" : "#ffffff08",
                    }}
                  >
                    <ClaudeBurst size={17} color={t.accent} />
                  </div>
                  {turn.typing ? (
                    <div style={{ display: "flex", gap: 5, paddingTop: 11 }}>
                      {[0, 1, 2].map((d) => (
                        <span
                          key={d}
                          className={`${styleId}-dot`}
                          style={{
                            width: 7,
                            height: 7,
                            borderRadius: "50%",
                            background: t.sub,
                            animationDelay: `${d * 0.18}s`,
                          }}
                        />
                      ))}
                    </div>
                  ) : (
                    <div style={{ flex: 1, fontSize: 15, lineHeight: 1.6, color: t.ink, paddingTop: 3 }}>
                      {renderRich(turn.text)}
                    </div>
                  )}
                </div>
              ),
            )}
          </div>
        </div>

        {/* composer */}
        <div style={{ flexShrink: 0, display: "flex", justifyContent: "center", padding: "6px 24px 8px" }}>
          <div style={{ width: "100%", maxWidth: 700 }}>
            <div
              style={{
                background: t.composer,
                border: `1px solid ${t.composerBorder}`,
                borderRadius: 22,
                padding: "12px 12px 10px 16px",
                boxShadow: scheme === "light" ? "0 2px 8px rgba(60,50,30,0.06)" : "none",
              }}
            >
              <div style={{ fontSize: 15, color: t.sub, padding: "2px 0 12px" }}>Reply to Claude…</div>
              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                <span
                  style={{
                    width: 30,
                    height: 30,
                    borderRadius: "50%",
                    border: `1px solid ${t.faint}`,
                    display: "inline-flex",
                    alignItems: "center",
                    justifyContent: "center",
                  }}
                >
                  <PlusGlyph color={t.sub} />
                </span>
                <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
                  <span style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 13, color: t.sub }}>
                    {model.replace("Claude ", "")}
                    <ChevronDown color={t.sub} size={13} />
                  </span>
                  <span
                    style={{
                      width: 32,
                      height: 32,
                      borderRadius: "50%",
                      background: t.accent,
                      display: "inline-flex",
                      alignItems: "center",
                      justifyContent: "center",
                      flexShrink: 0,
                    }}
                  >
                    <ArrowUp color={t.accentInk} />
                  </span>
                </div>
              </div>
            </div>
            <div style={{ textAlign: "center", fontSize: 11.5, color: t.sub, paddingTop: 7 }}>
              Claude can make mistakes. Please double-check responses.
            </div>
          </div>
        </div>
      </main>
    </div>
  );
}

/** Minimal renderer: blank-line paragraphs, single newlines, **bold**, *italic*. */
function renderRich(text: string): ReactNode {
  return text.split("\n\n").map((block, bi) => (
    <p key={bi} style={{ margin: bi === 0 ? 0 : "12px 0 0" }}>
      {block.split("\n").map((line, li) => (
        <span key={li}>
          {li > 0 && <br />}
          {renderInline(line, `${bi}-${li}`)}
        </span>
      ))}
    </p>
  ));
}

function renderInline(text: string, keyBase: string): ReactNode[] {
  return text
    .split(/(\*\*[^*]+\*\*|\*[^*]+\*)/g)
    .filter(Boolean)
    .map((chunk, i) => {
      const key = `${keyBase}-${i}`;
      if (chunk.startsWith("**") && chunk.endsWith("**"))
        return <strong key={key}>{chunk.slice(2, -2)}</strong>;
      if (chunk.startsWith("*") && chunk.endsWith("*")) return <em key={key}>{chunk.slice(1, -1)}</em>;
      return <span key={key}>{chunk}</span>;
    });
}

/* ---- hardware: the MacBook Pro frame -------------------------------------- */

export interface MacBookMockProps {
  /** Rendered width in CSS px. The whole laptop scales from here. Default 720. */
  width?: number;
  /** Screen payload. Defaults to the bundled Claude screen. */
  children?: ReactNode;
  /** Aluminium finish the chassis reads as. */
  chassis?: "silver" | "spaceblack";
  style?: CSSProperties;
}

interface ChassisTokens {
  lid: string; // lid rim gradient
  baseTop: string; // base gradient top stop
  baseMid: string;
  baseBot: string;
  edge: string; // hairline edge
}

const CHASSIS: Record<NonNullable<MacBookMockProps["chassis"]>, ChassisTokens> = {
  silver: {
    lid: "linear-gradient(180deg,#e2e2e5,#c6c6ca 55%,#b4b4b8)",
    baseTop: "#ececed",
    baseMid: "#cfcfd3",
    baseBot: "#a8a8ad",
    edge: "rgba(255,255,255,0.6)",
  },
  spaceblack: {
    lid: "linear-gradient(180deg,#48484b,#37373a 55%,#2c2c2e)",
    baseTop: "#54545a",
    baseMid: "#3c3c40",
    baseBot: "#242427",
    edge: "rgba(255,255,255,0.14)",
  },
};

/**
 * The device frame. Everything inside is authored in the 1118 × 704 logical
 * canvas and scaled together via a single transform, so the laptop stays crisp
 * at any `width` and the caller only ever thinks in one number.
 */
export default function MacBookMock({
  width = 720,
  children,
  chassis = "silver",
  style,
}: MacBookMockProps) {
  const scale = width / DEVICE_W;
  const gid = useId().replace(/[:]/g, "");
  const c = CHASSIS[chassis];

  // Base outline: a shallow tapered wedge with rounded bottom corners and a
  // finger-groove scooped from the top-centre front edge.
  const bw = BASE_W;
  const cx = bw / 2;
  const notchR = 54;
  const notchDepth = 11;
  const topInset = 46;
  const br = 11;
  const basePath = [
    `M ${topInset} 1`,
    `L ${cx - notchR} 1`,
    `Q ${cx} ${1 + notchDepth} ${cx + notchR} 1`,
    `L ${bw - topInset} 1`,
    `L ${bw - 1} ${BASE_H - br}`,
    `Q ${bw - 1} ${BASE_H} ${bw - 1 - br} ${BASE_H}`,
    `L ${br + 1} ${BASE_H}`,
    `Q 1 ${BASE_H} 1 ${BASE_H - br}`,
    "Z",
  ].join(" ");

  return (
    <div style={{ width: DEVICE_W * scale, height: DEVICE_H * scale, ...style }}>
      <div
        style={{
          width: DEVICE_W,
          height: DEVICE_H,
          transform: `scale(${scale})`,
          transformOrigin: "top left",
          position: "relative",
        }}
      >
        {/* contact shadow on the ground */}
        <div
          aria-hidden="true"
          style={{
            position: "absolute",
            left: DEVICE_W * 0.07,
            bottom: -14,
            width: DEVICE_W * 0.86,
            height: 30,
            borderRadius: "50%",
            background: "radial-gradient(ellipse at center, rgba(0,0,0,0.42), rgba(0,0,0,0) 72%)",
            filter: "blur(6px)",
          }}
        />

        {/* base / deck front */}
        <svg
          width={BASE_W}
          height={BASE_H}
          viewBox={`0 0 ${BASE_W} ${BASE_H}`}
          style={{ position: "absolute", left: 0, top: LID_H }}
          aria-hidden="true"
        >
          <defs>
            <linearGradient id={`${gid}-base`} x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" stopColor={c.baseTop} />
              <stop offset="0.4" stopColor={c.baseMid} />
              <stop offset="1" stopColor={c.baseBot} />
            </linearGradient>
          </defs>
          <path d={basePath} fill={`url(#${gid}-base)`} />
          {/* bright top hairline + groove shading */}
          <path
            d={`M ${topInset} 1 L ${cx - notchR} 1 Q ${cx} ${1 + notchDepth} ${cx + notchR} 1 L ${bw - topInset} 1`}
            fill="none"
            stroke={c.edge}
            strokeWidth="1.2"
          />
        </svg>

        {/* lid */}
        <div
          style={{
            position: "absolute",
            left: BASE_OVERHANG,
            top: 0,
            width: LID_W,
            height: LID_H,
            borderRadius: LID_RADIUS,
            background: c.lid,
            padding: RIM,
            boxSizing: "border-box",
            boxShadow: [
              "0 22px 48px -14px rgba(0,0,0,0.5)",
              "inset 0 1px 0 rgba(255,255,255,0.35)",
              "inset 0 0 0 0.5px rgba(0,0,0,0.25)",
            ].join(","),
          }}
        >
          {/* black-glass bezel */}
          <div
            style={{
              width: "100%",
              height: "100%",
              borderRadius: LID_RADIUS - RIM,
              background: "#050505",
              padding: BEZEL,
              boxSizing: "border-box",
              boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.9)",
            }}
          >
            {/* display — clips the app */}
            <div
              style={{
                position: "relative",
                width: "100%",
                height: "100%",
                borderRadius: SCREEN_RADIUS,
                overflow: "hidden",
                background: "#000",
              }}
            >
              {children ?? <ClaudeScreen />}

              {/* camera notch */}
              <div
                style={{
                  position: "absolute",
                  top: 0,
                  left: "50%",
                  transform: "translateX(-50%)",
                  width: NOTCH_W,
                  height: NOTCH_H,
                  background: "#050505",
                  borderBottomLeftRadius: 11,
                  borderBottomRightRadius: 11,
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  pointerEvents: "none",
                }}
              >
                <span
                  style={{
                    width: 6,
                    height: 6,
                    borderRadius: "50%",
                    background: "radial-gradient(circle at 35% 30%, #223, #000 70%)",
                    boxShadow: "inset 0 0 1px rgba(120,150,200,0.5)",
                  }}
                />
              </div>

              {/* glass glare */}
              <div
                aria-hidden="true"
                style={{
                  position: "absolute",
                  inset: 0,
                  borderRadius: SCREEN_RADIUS,
                  background:
                    "linear-gradient(120deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0) 24%, rgba(255,255,255,0) 100%)",
                  pointerEvents: "none",
                }}
              />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
```

## Related

- Machine-readable index (all patterns, with code): https://eito.me/ui/manifest.json
- Plain-HTML mirror: https://eito.me/ui/all.html
- Interactive gallery: https://eito.me/ui
