{
  "slug": "paper-craft-moves",
  "title": "Paper craft moves",
  "tagline": "Two paper-craft techniques: masking-tape-slap pins a card down with tape slaps, and popup-book-rise stands cards up like a pop-up book.",
  "description": "Two paper-craft techniques: masking-tape-slap pins a card down with tape slaps, and popup-book-rise stands cards up like a pop-up book.",
  "category": "ui-entrance",
  "tags": [
    "shotcraft",
    "remotion",
    "ui-entrance"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.72,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/paper-craft-moves/poster-cf7f1f4314a1.jpg",
    "video": "https://gimgs.net/motion/media/paper-craft-moves/video-190f13cab969.mp4"
  },
  "variants": [
    {
      "key": "masking-tape-slap",
      "label": "masking-tape-slap",
      "description": "Sway = amplitude envelope × sine (rot ±1.5°/bob ±5px); the tape swoops in over 6f: scale 1.45→1 plus rotate undershoots 16°→overshoots 7°→settles, with a single-frame scaleY 0.72 squash on landing; the torn edge uses a 14-point clipPath jag.",
      "use": "Settling entrance for a single card/badge; aligned with a copy emphasis beat",
      "video": "https://gimgs.net/motion/media/paper-craft-moves/video-190f13cab969.mp4",
      "poster": "https://gimgs.net/motion/media/paper-craft-moves/poster-cf7f1f4314a1.jpg"
    },
    {
      "key": "popup-book-rise",
      "label": "popup-book-rise",
      "description": "Two-layer 3D: the scene sits at rotateX 75° overhead (perspective 2600); each card springs from rotateX 0→-90° (damping 11, overshooting to -95°) with its transform-origin on the base edge, preserve-3d carried through; back rows go first, front rows follow, staggered by 7f.",
      "use": "Full-dashboard opening; \"the system is being built\"",
      "video": "https://gimgs.net/motion/media/paper-craft-moves/video-93a624edab3f.mp4",
      "poster": "https://gimgs.net/motion/media/paper-craft-moves/poster-popup-book-rise-35275c02b1b4.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/paper-craft-moves.html",
  "json_url": "https://gimgs.net/motion/paper-craft-moves.json",
  "template_url": "https://gimgs.net/motion/paper-craft-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/paper-craft-moves.md",
  "updated_at": "2026-09-24T07:40:12.309Z",
  "prompt": "## When to use\n\nPhysical material language for paper-and-ink hero visuals: use A for a single card's settling entrance, B for a full dashboard opening build; naturally paired with paper/ink + accent-color hero visuals (paper/ink/amber template films)\n\n## Intention\n\nA UI translation of Wes Anderson notebook art and pop-up book paper craft. A: a card floats lightly into place and hangs there swaying slightly (unsecured paper), two torn strips of translucent tape slap down one after another on opposite corners, and on the second slap the card **simultaneously** stops swaying, its shadow thins, and it settles down — the instant of being \"pinned down\" is the star, and the two slap sounds are a natural audio cue. B: the whole page lies flat like an open book seen from above, and cards like paper pieces glued to the page stand up one after another along their base edges in a staggered wave to form a wall, overshooting to 95° and springing back to 90° (paper's springiness), with the shadow at the root narrowing as it stands up.\n\n## Duration & energy\n\n- Duration: A 3–4s / B 4–5.5s\n- Energy: A Medium (a two-beat hit) / B Medium-high (standing up the wall has depth impact)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\nWes Anderson 手账美术与立体书纸艺的 UI 翻译。A：卡片轻飘入位后\n悬着微晃（未固定的纸），两条撕边半透明胶带\"啪、啪\"先后拍在对角，\n第二条拍下**同帧**卡片停晃、投影变薄、整卡下沉——\"按死\"的定妆\n一瞬是主角，两声\"啪\"是天然音效点。B：整页平躺如摊开书页（俯视），\n卡片像贴在页上的纸片沿各自底边错峰立起成墙，立到 95° 回弹 90°\n（纸的韧性），根部投影随立起收窄。\n\n## 两式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A masking-tape-slap | 晃动=幅度包络×正弦（rot ±1.5°/bob ±5px）；胶带扑入 6f：scale 1.45→1 + rotate 欠 16°→过冲 7°→回正 + 落帧 scaleY 0.72 一帧压扁；撕边 14 点 clipPath 锯齿 | 单卡/徽章的定妆入场；文案重音对齐 |\n| B popup-book-rise | 双层 3D：场景 rotateX 75° 俯视（persp 2600），每卡 rotateX 0→-90° spring（damping 11 过冲 -95°），origin 底边，preserve-3d 贯通；远排先近排后错峰 7f | 整版 dashboard 开场；\"系统被搭建起来\" |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 按死三件套 | 同帧：晃动 2f 归零 + 投影 34px→8px + 全卡下沉 2px | 三件拆帧就没有\"按死\"感；第一条胶带只把包络衰到 0.45（半死） |\n| A 胶带质感 | 半透明 + 锯齿撕边 + 落点故意歪 2–3° | 歪斜是设计的手工感；直挺挺读作 UI 元素 |\n| B 立起方向 | rotateX 0→**-90°**（向观众立起） | demo 判例：方向写反卡片往画面里倒被底板裁切 |\n| B 根部投影 | blur 矩形不随卡立起，高 104→14px、透明度 0.26→0.1 随角度插值 | 无投影纸片浮空；投影跟着立起来穿帮 |\n| B 收尾 | 全部站定后场景 75°→68° 轻回正（in-out cubic） | 直接定住少一口呼吸 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- A 的晃动是\"未固定\"的叙事铺垫不是手持抖动（Q3 边界）：\n  振幅包络必须最终归零，全片只在被按死前存在\n- B 与 tilt-reveal 分工：tilt 是**机位**抬头看静止页面，\n  本卡机位基本不动、**构件自己**站起来\n- B 立墙后卡片呈 90° 侧立态，文字不可读——立墙是构图动作，\n  信息交给立起后的正视段落（Q6 同源）\n- 声音：A 两声\"啪\"（纸拍击拟音）；B 每排立起一声轻纸响，\n  错峰对齐（S2/S4 同源）\n\n## 参考实现\ndemos/ui-entrance/paper-craft-moves/\n（MaskingTapeSlap.tsx / PopupBookRise.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/ui-entrance/paper-craft-moves/MaskingTapeSlap.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/paper-craft-moves/MaskingTapeSlap.tsx\n- demos/ui-entrance/paper-craft-moves/PopupBookRise.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/paper-craft-moves/PopupBookRise.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "npx remotion render <CompositionId> out.mp4   # component: demos/ui-entrance/paper-craft-moves/MaskingTapeSlap.tsx, demos/ui-entrance/paper-craft-moves/PopupBookRise.tsx"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/ui-entrance/paper-craft-moves/MaskingTapeSlap.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/paper-craft-moves/MaskingTapeSlap.tsx"
    },
    {
      "path": "demos/ui-entrance/paper-craft-moves/PopupBookRise.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/paper-craft-moves/PopupBookRise.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 142,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/paper-craft-moves/MaskingTapeSlap.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/paper-craft-moves/PopupBookRise.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/ui-entrance/paper-craft-moves/MaskingTapeSlap.tsx =====\n// masking-tape-slap —— 纸胶带拍定\n// 一张 Card 轻飘入位后悬着微晃（±1.5° 正弦 + 5px 上下浮），两条半透明纸胶带\n// 先后从画外拍在对角（scale 1.45→1 + rotate 过冲 + 一帧压扁）。第一条拍下晃动减半，\n// 第二条拍下同帧卡片停晃、投影瞬间变薄、整卡 2px 下沉——\"按死\"的一瞬是主角。\n// 帧确定性：全部由 frame 派生，无随机。收尾 f86 后真静止 54f。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, Card, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst CARD_W = 560;\nconst CARD_H = 350;\nconst CX = (1920 - CARD_W) / 2;\nconst CY = (1080 - CARD_H) / 2 + 40;\n\nconst FLOAT_START = 12; // 开头 12f 空场静置\nconst FLOAT_END = 38;\nconst SLAP1 = 58;\nconst SLAP2 = 82;\nconst APPROACH = 6; // 胶带从画外扑向卡面的帧数\nconst FREEZE = 2; // 拍死后晃动归零帧数\n\n// 悬浮晃动幅度包络：入位后升起 → 第一条胶带拍下后减半 → 第二条拍下冻结（由外层处理）\nconst amp = (f: number): number => {\n  const rise = interpolate(f, [FLOAT_END, FLOAT_END + 8], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const damp = interpolate(f, [SLAP1, SLAP1 + 4], [1, 0.45], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  return rise * damp;\n};\n\nconst rawRot = (f: number): number => amp(f) * 1.5 * Math.sin((f - FLOAT_END) * 0.16);\nconst rawBob = (f: number): number => amp(f) * 5 * Math.sin((f - FLOAT_END) * 0.11);\n\n// 第二条拍下（SLAP2）同帧起 2f 内把晃动按死到 0\nconst frozen = (f: number, raw: (x: number) => number): number =>\n  f <= SLAP2\n    ? raw(f)\n    : interpolate(f, [SLAP2, SLAP2 + FREEZE], [raw(SLAP2), 0], {\n        extrapolateRight: 'clamp',\n      });\n\nconst Tape: React.FC<{\n  frame: number;\n  land: number;\n  cx: number; // 胶带中心点（世界坐标）\n  cy: number;\n  rot: number; // 落定角度\n  fromX: number; // 画外来向偏移\n  fromY: number;\n}> = ({ frame, land, cx, cy, rot, fromX, fromY }) => {\n  if (frame < land - APPROACH) return null; // 条件卸载：拍上前真不存在\n\n  const t = interpolate(frame, [land - APPROACH, land], [0, 1], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const scale = interpolate(t, [0, 1], [1.45, 1]);\n  const dx = fromX * (1 - t);\n  const dy = fromY * (1 - t);\n  const opacity = interpolate(frame, [land - APPROACH, land - APPROACH + 2], [0, 0.85], {\n    extrapolateRight: 'clamp',\n  });\n  // rotate 过冲：来时欠 16° → 落帧过 7° → 4f 内回正\n  const r = interpolate(frame, [land - APPROACH, land, land + 4], [rot - 16, rot + 7, rot], {\n    easing: Easing.out(Easing.quad),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  // 一帧压扁：落帧 scaleY 0.72，次帧 0.9，随后复原\n  const sy = frame === land ? 0.72 : frame === land + 1 ? 0.9 : 1;\n\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        left: cx - 160,\n        top: cy - 34,\n        width: 320,\n        height: 68,\n        transform: `translate(${dx}px, ${dy}px) rotate(${r}deg) scale(${scale}) scaleY(${sy})`,\n        transformOrigin: '50% 50%',\n        opacity,\n        background:\n          'linear-gradient(90deg, rgba(214,212,206,0.95) 0%, rgba(226,224,218,0.95) 30%, rgba(212,210,204,0.95) 60%, rgba(222,220,214,0.95) 100%)',\n        // 撕边：两端锯齿\n        clipPath:\n          'polygon(0% 8%, 2.5% 0%, 97% 3%, 100% 12%, 98.2% 30%, 100% 52%, 98% 74%, 100% 90%, 96.5% 100%, 3% 97%, 0% 88%, 1.8% 64%, 0% 42%, 2% 22%)',\n        boxShadow: '0 1px 3px rgba(0,0,0,0.15)',\n      }}\n    />\n  );\n};\n\nexport const MaskingTapeSlap: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 卡片飘入：从上方 -120px 缓落\n  const floatY = interpolate(frame, [FLOAT_START, FLOAT_END], [-120, 0], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const floatOp = interpolate(frame, [FLOAT_START, FLOAT_START + 10], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  const rot = frozen(frame, rawRot);\n  const bob = frozen(frame, rawBob);\n\n  // 按死：2px 下沉 + 投影瞬间变薄\n  const sink = interpolate(frame, [SLAP2, SLAP2 + FREEZE], [0, 2], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const shOff = interpolate(frame, [SLAP2, SLAP2 + FREEZE], [16, 3], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const shBlur = interpolate(frame, [SLAP2, SLAP2 + FREEZE], [34, 8], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const shAlpha = interpolate(frame, [SLAP2, SLAP2 + FREEZE], [0.22, 0.1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', top: 110, width: '100%', textAlign: 'center' }}>\n        <TitleBlock text=\"MASKING TAPE SLAP\" size={72} />\n      </div>\n\n      <div\n        style={{\n          position: 'absolute',\n          left: CX,\n          top: CY,\n          transform: `translateY(${floatY + bob + sink}px) rotate(${rot}deg)`,\n          transformOrigin: '50% 50%',\n          opacity: floatOp,\n        }}\n      >\n        <Card\n          w={CARD_W}\n          h={CARD_H}\n          seed={3}\n          style={{ boxShadow: `0 ${shOff}px ${shBlur}px rgba(0,0,0,${shAlpha})` }}\n        />\n      </div>\n\n      {/* 两条胶带钉在卡片对角（世界坐标，卡片在其下滑动微晃） */}\n      <Tape frame={frame} land={SLAP1} cx={CX + 55} cy={CY + 40} rot={-45} fromX={-170} fromY={-130} />\n      <Tape frame={frame} land={SLAP2} cx={CX + CARD_W - 55} cy={CY + CARD_H - 40} rot={-45} fromX={170} fromY={130} />\n    </div>\n  );\n};\n\n\n// ===== demos/ui-entrance/paper-craft-moves/PopupBookRise.tsx =====\n// popup-book-rise —— 立体书立起\n// FakeDashboard(A) 打平躺下（场景 rotateX 75° 透视俯视），6 张卡片是贴在页上的\n// 纸片，沿各自底边从平躺错峰立起（rotateX 90°→-5° 过冲→0° 回弹，即立到 95° 再回 90°），\n// 根部投影随立起角度收窄变淡。全部立起后整个场景轻微回正（75°→68°）收尾。\n// 卡片用独立网格复刻 dashboard A 区布局（Fixtures 内嵌卡无法单独驱动）。\n// 收尾 f108 后真静止 ≥52f。帧确定性：全由 frame 派生。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, spring, Easing } from 'remotion';\nimport { G, Card } from '../../_fixtures/Fixtures';\n\nconst FPS = 30;\nconst HOLD = 14; // 开头静置\nconst STAGGER = 7;\nconst RISE_DUR = 34; // spring 视觉收敛帧数\nconst LAST_START = HOLD + 5 * STAGGER; // 49\nconst SETTLE = LAST_START + RISE_DUR; // 83：全部立起\nconst REST = SETTLE + 25; // 108：场景回正完成\n\n// dashboard A 区几何（照抄 FakeDashboard：侧栏 220 + 顶栏 72 + padding 36 + gap 28）\nconst AREA_X = 220 + 36;\nconst AREA_Y = 72 + 36;\nconst AREA_W = 1920 - 220 - 72;\nconst AREA_H = 1080 - 72 - 72;\nconst GAP = 28;\nconst CELL_W = (AREA_W - 2 * GAP) / 3;\nconst CELL_H = (AREA_H - GAP) / 2;\n\nconst PageCard: React.FC<{ i: number; frame: number }> = ({ i, frame }) => {\n  const col = i % 3;\n  const row = Math.floor(i / 3);\n  // 远排（row 0）先立，近排后立；同排从左到右\n  const order = row === 0 ? col : 3 + col;\n  const start = HOLD + order * STAGGER;\n\n  const s = spring({\n    frame: frame - start,\n    fps: FPS,\n    config: { damping: 11, stiffness: 130, mass: 0.9 },\n    durationInFrames: RISE_DUR,\n    durationRestThreshold: 0.0001,\n  });\n  // 平躺（贴页面 = local 0°）→ 立起（垂直页面 = local -90°，顶边朝观众翻起），\n  // spring 过冲自然冲过 -90° 到约 -95°（纸的韧性）再回弹。\n  const rx = interpolate(s, [0, 1], [0, -90]);\n\n  // 根部投影：躺平时长影（卡片盖在页面上），立起后收成窄条\n  const lie = 1 - Math.min(Math.abs(rx) / 90, 1); // 1 = 躺平, 0 = 立直\n  const shH = 14 + 90 * Math.max(lie, 0);\n  const shAlpha = 0.1 + 0.16 * Math.max(lie, 0);\n\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        left: AREA_X + col * (CELL_W + GAP),\n        top: AREA_Y + row * (CELL_H + GAP),\n        width: CELL_W,\n        height: CELL_H,\n        transformStyle: 'preserve-3d',\n      }}\n    >\n      {/* 根部投影贴在页面上，不随卡片立起 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 6,\n          right: 6,\n          bottom: -4,\n          height: shH,\n          background: `rgba(0,0,0,${shAlpha})`,\n          borderRadius: 12,\n          filter: 'blur(10px)',\n        }}\n      />\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          transform: `rotateX(${rx}deg)`,\n          transformOrigin: '50% 100%',\n          backfaceVisibility: 'hidden',\n        }}\n      >\n        <Card w={0} h={0} seed={i + 1} style={{ width: '100%', height: '100%' }} />\n      </div>\n    </div>\n  );\n};\n\nexport const PopupBookRise: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 场景（书页）俯视角：全程 75°，全部立起后轻微回正到 68°\n  const sceneRx = interpolate(frame, [SETTLE, REST], [75, 68], {\n    easing: Easing.inOut(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: '#dddddb', position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', inset: 0, perspective: 2600, perspectiveOrigin: '50% 30%' }}>\n        <div\n          style={{\n            position: 'absolute',\n            inset: 0,\n            transform: `translateY(-40px) rotateX(${sceneRx}deg)`,\n            transformOrigin: '50% 62%',\n            transformStyle: 'preserve-3d',\n          }}\n        >\n          {/* 书页底板：dashboard 的壳（侧栏+顶栏+空白页面） */}\n          <div style={{ position: 'absolute', inset: 0, background: G.bg, boxShadow: '0 40px 80px rgba(0,0,0,0.25)' }}>\n            <div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: 220, background: G.side, padding: '28px 22px', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 18 }}>\n              <div style={{ width: 40, height: 40, borderRadius: 10, background: '#777775' }} />\n              {Array.from({ length: 7 }).map((_, i) => (\n                <div key={i} style={{ height: 12, width: `${60 + ((i * 29) % 35)}%`, background: G.sideBar, borderRadius: 6 }} />\n              ))}\n            </div>\n            <div style={{ position: 'absolute', left: 220, right: 0, top: 0, height: 72, background: G.panel, borderBottom: `2px solid ${G.line}`, display: 'flex', alignItems: 'center', padding: '0 32px', gap: 20, boxSizing: 'border-box' }}>\n              <div style={{ height: 18, width: 180, background: G.bar, borderRadius: 9 }} />\n              <div style={{ marginLeft: 'auto', height: 36, width: 320, background: '#fff', border: `2px solid ${G.line}`, borderRadius: 18, boxSizing: 'border-box' }} />\n              <div style={{ width: 36, height: 36, borderRadius: 18, background: G.mid }} />\n            </div>\n          </div>\n          {/* 6 张纸片卡沿底边立起 */}\n          {Array.from({ length: 6 }).map((_, i) => (\n            <PageCard key={i} i={i} frame={frame} />\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n};\n\n\n"
}