@12ui/design 0.2.44 → 0.2.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +7 -0
  2. package/SKILL.md +1 -1
  3. package/dist/branch-execution-types.d.ts +2 -0
  4. package/dist/branch-execution-types.d.ts.map +1 -1
  5. package/dist/branch-execution.d.ts.map +1 -1
  6. package/dist/branch-execution.js +3 -0
  7. package/dist/branch-execution.js.map +1 -1
  8. package/dist/branch-run-record.d.ts +1 -0
  9. package/dist/branch-run-record.d.ts.map +1 -1
  10. package/dist/branch-run-record.js.map +1 -1
  11. package/dist/cli-arguments.d.ts.map +1 -1
  12. package/dist/cli-arguments.js +1 -0
  13. package/dist/cli-arguments.js.map +1 -1
  14. package/dist/cli-branch-command.d.ts.map +1 -1
  15. package/dist/cli-branch-command.js +33 -2
  16. package/dist/cli-branch-command.js.map +1 -1
  17. package/dist/cli-capabilities.d.ts +1 -0
  18. package/dist/cli-capabilities.d.ts.map +1 -1
  19. package/dist/cli-capabilities.js +1 -0
  20. package/dist/cli-capabilities.js.map +1 -1
  21. package/dist/cli-help/design.d.ts +2 -2
  22. package/dist/cli-help/design.d.ts.map +1 -1
  23. package/dist/cli-help/design.js +2 -0
  24. package/dist/cli-help/design.js.map +1 -1
  25. package/dist/cli-help/index.d.ts +2 -2
  26. package/dist/cli-prototype-command.d.ts.map +1 -1
  27. package/dist/cli-prototype-command.js +14 -1
  28. package/dist/cli-prototype-command.js.map +1 -1
  29. package/dist/cli-usage.d.ts.map +1 -1
  30. package/dist/cli-usage.js +4 -3
  31. package/dist/cli-usage.js.map +1 -1
  32. package/dist/legacy-skill-catalog.d.ts.map +1 -1
  33. package/dist/legacy-skill-catalog.js +7 -0
  34. package/dist/legacy-skill-catalog.js.map +1 -1
  35. package/dist/prototype-poststep.d.ts +1 -0
  36. package/dist/prototype-poststep.d.ts.map +1 -1
  37. package/dist/prototype-poststep.js +5 -0
  38. package/dist/prototype-poststep.js.map +1 -1
  39. package/dist/prototype-process.d.ts +1 -1
  40. package/dist/prototype-process.d.ts.map +1 -1
  41. package/dist/prototype-revision.d.ts +1 -1
  42. package/dist/prototype-revision.js +1 -1
  43. package/dist/prototype-run-record.d.ts +2 -1
  44. package/dist/prototype-run-record.d.ts.map +1 -1
  45. package/dist/prototype-run-record.js +28 -6
  46. package/dist/prototype-run-record.js.map +1 -1
  47. package/dist/prototype-runner.d.ts +26 -0
  48. package/dist/prototype-runner.d.ts.map +1 -1
  49. package/dist/prototype-runner.js +55 -8
  50. package/dist/prototype-runner.js.map +1 -1
  51. package/dist/run-journal.d.ts +15 -0
  52. package/dist/run-journal.d.ts.map +1 -1
  53. package/dist/run-journal.js +9 -0
  54. package/dist/run-journal.js.map +1 -1
  55. package/open-design.json +2 -2
  56. package/package.json +2 -2
  57. package/prototype-kit/polish/ground-probe.mjs +119 -0
  58. package/prototype-kit/polish/harm.mjs +23 -0
  59. package/prototype-kit/polish/measure.mjs +333 -0
  60. package/prototype-kit/polish/overrides.mjs +55 -0
  61. package/prototype-kit/polish/polish.mjs +252 -0
  62. package/prototype-kit/polish/report.mjs +76 -0
  63. package/prototype-kit/polish/rules/align.mjs +70 -0
  64. package/prototype-kit/polish/rules/contrast.mjs +80 -0
  65. package/prototype-kit/polish/rules/escape.mjs +192 -0
  66. package/prototype-kit/polish/rules/fit-text.mjs +158 -0
  67. package/prototype-kit/polish/rules/index.mjs +61 -0
  68. package/prototype-kit/polish/rules/left-rail.mjs +77 -0
  69. package/prototype-kit/polish/rules/overlap.mjs +122 -0
  70. package/prototype-kit/polish/rules/page-cut.mjs +203 -0
  71. package/prototype-kit/polish/rules/photo-corners.mjs +38 -0
  72. package/prototype-kit/polish/rules/rhythm.mjs +148 -0
  73. package/prototype-kit/polish/rules/section-breathing.mjs +292 -0
  74. package/prototype-kit/polish/rules/shell-seam.mjs +48 -0
  75. package/prototype-kit/polish/tests/breathing-check.mjs +77 -0
  76. package/prototype-kit/polish/tests/fixtures/align/expect.json +1 -0
  77. package/prototype-kit/polish/tests/fixtures/align/fire.html +14 -0
  78. package/prototype-kit/polish/tests/fixtures/align/silent.html +28 -0
  79. package/prototype-kit/polish/tests/fixtures/contrast/expect.json +1 -0
  80. package/prototype-kit/polish/tests/fixtures/contrast/fallback.html +7 -0
  81. package/prototype-kit/polish/tests/fixtures/contrast/fire.html +9 -0
  82. package/prototype-kit/polish/tests/fixtures/contrast/silent.html +9 -0
  83. package/prototype-kit/polish/tests/fixtures/escape/expect.json +2 -0
  84. package/prototype-kit/polish/tests/fixtures/escape/fire.html +23 -0
  85. package/prototype-kit/polish/tests/fixtures/escape/silent.html +29 -0
  86. package/prototype-kit/polish/tests/fixtures/fit-text/expect.json +3 -0
  87. package/prototype-kit/polish/tests/fixtures/fit-text/fire.html +26 -0
  88. package/prototype-kit/polish/tests/fixtures/fit-text/silent.html +33 -0
  89. package/prototype-kit/polish/tests/fixtures/harm/button-overlap.html +10 -0
  90. package/prototype-kit/polish/tests/fixtures/harm/declared-gap.html +5 -0
  91. package/prototype-kit/polish/tests/fixtures/harm/offcenter.html +21 -0
  92. package/prototype-kit/polish/tests/fixtures/harm/page.html +17 -0
  93. package/prototype-kit/polish/tests/fixtures/left-rail/expect.json +1 -0
  94. package/prototype-kit/polish/tests/fixtures/left-rail/fire.html +8 -0
  95. package/prototype-kit/polish/tests/fixtures/left-rail/silent.html +8 -0
  96. package/prototype-kit/polish/tests/fixtures/overlap/expect.json +4 -0
  97. package/prototype-kit/polish/tests/fixtures/overlap/fire.html +26 -0
  98. package/prototype-kit/polish/tests/fixtures/overlap/silent.html +39 -0
  99. package/prototype-kit/polish/tests/fixtures/page-cut/expect.json +2 -0
  100. package/prototype-kit/polish/tests/fixtures/page-cut/fire.html +31 -0
  101. package/prototype-kit/polish/tests/fixtures/page-cut/silent.html +22 -0
  102. package/prototype-kit/polish/tests/fixtures/photo-corners/expect.json +1 -0
  103. package/prototype-kit/polish/tests/fixtures/photo-corners/fire.html +5 -0
  104. package/prototype-kit/polish/tests/fixtures/photo-corners/silent.html +9 -0
  105. package/prototype-kit/polish/tests/fixtures/rhythm/expect.json +6 -0
  106. package/prototype-kit/polish/tests/fixtures/rhythm/fire.html +22 -0
  107. package/prototype-kit/polish/tests/fixtures/rhythm/silent.html +27 -0
  108. package/prototype-kit/polish/tests/fixtures/section-breathing/attachment-fire.html +5 -0
  109. package/prototype-kit/polish/tests/fixtures/section-breathing/attachment-silent.html +5 -0
  110. package/prototype-kit/polish/tests/fixtures/section-breathing/expect.json +1 -0
  111. package/prototype-kit/polish/tests/fixtures/section-breathing/fire.html +6 -0
  112. package/prototype-kit/polish/tests/fixtures/section-breathing/floor.html +7 -0
  113. package/prototype-kit/polish/tests/fixtures/section-breathing/semantic-fire.html +11 -0
  114. package/prototype-kit/polish/tests/fixtures/section-breathing/semantic-silent.html +8 -0
  115. package/prototype-kit/polish/tests/fixtures/section-breathing/shadow-fire.html +5 -0
  116. package/prototype-kit/polish/tests/fixtures/section-breathing/shadow-silent.html +5 -0
  117. package/prototype-kit/polish/tests/fixtures/section-breathing/silent.html +6 -0
  118. package/prototype-kit/polish/tests/fixtures/section-breathing/tight.html +7 -0
  119. package/prototype-kit/polish/tests/fixtures/shell-seam/expect.json +1 -0
  120. package/prototype-kit/polish/tests/fixtures/shell-seam/fire.html +20 -0
  121. package/prototype-kit/polish/tests/fixtures/shell-seam/silent.html +21 -0
  122. package/prototype-kit/polish/tests/harm-check.mjs +71 -0
  123. package/prototype-kit/polish/tests/report-check.mjs +31 -0
  124. package/prototype-kit/polish/tests/rules-check.mjs +52 -0
  125. package/skills/12ui-design/SKILL.md +4 -0
@@ -0,0 +1,33 @@
1
+ <!doctype html><meta charset="utf-8"><style>
2
+ *{margin:0;padding:0;box-sizing:border-box} body{font-family:Arial,Helvetica,sans-serif}
3
+ section{position:relative;width:1536px;height:1024px;background:#fff}
4
+ .fx-col{display:flex;flex-direction:column;align-items:flex-start}
5
+ .text,h1,h2,h3{position:relative;display:block;white-space:pre;margin:0}
6
+ .badge{position:relative;display:block;white-space:pre;background:#e8f0ff;border-radius:999px;color:#1d3f8f;text-align:center}
7
+ .image{position:absolute;background:#7a8ba0}
8
+ </style>
9
+ <section id="viewport-1-a">
10
+
11
+ <!-- near-threshold: 3 px of spill is line-metric noise, not a defect -->
12
+ <div id="viewport-1-a-row" class="fx-col" style="position:absolute;left:80px;top:80px;width:183px;height:40px">
13
+ <p id="viewport-1-a-row-text" class="text" style="width:183px;height:19px;font-size:16px;line-height:19px">Monthly recurring revenue</p>
14
+ </div>
15
+
16
+ <!-- a display line deliberately bleeding out of its column on BOTH sides, over a photo panel -->
17
+ <div id="viewport-1-a-hero" style="position:absolute;left:400px;top:240px;width:520px;height:260px">
18
+ <div id="viewport-1-a-hero-image" class="image" style="left:0;top:0;width:520px;height:260px"></div>
19
+ <h1 id="viewport-1-a-hero-heading" style="position:absolute;left:110px;top:96px;width:300px;height:60px;font-size:52px;line-height:60px;color:#fff;text-indent:-90px">STAY OUT LATE</h1>
20
+ </div>
21
+
22
+ <!-- a pill whose label rightly fills it edge to edge -->
23
+ <div id="viewport-1-a-tags" class="fx-col" style="position:absolute;left:80px;top:620px;width:400px;height:40px">
24
+ <span id="viewport-1-a-tags-badge" class="badge" style="width:88px;height:22px;font-size:12px;line-height:22px">In review</span>
25
+ </div>
26
+
27
+ <!-- a label that renders wider than its measured slot but into 450px of open canvas: nothing is clipped,
28
+ covered or collided with, so there is nothing to repair (widening the box changes zero pixels) -->
29
+ <div id="viewport-1-a-panel" class="fx-col" style="position:absolute;left:80px;top:760px;width:600px;height:120px">
30
+ <p id="viewport-1-a-panel-text" class="text" style="width:150px;height:19px;font-size:16px;line-height:19px">Deployment health overview</p>
31
+ </div>
32
+
33
+ </section>
@@ -0,0 +1,10 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}section{position:relative;width:1536px;height:1024px;background:#fff}.band{position:absolute;left:60px;top:20px;width:1400px;height:60px}.btn{position:absolute;width:90px;height:40px;border:1px solid #333;border-radius:8px;background:#fff;font:14px system-ui}.text{position:absolute;font-size:12px}</style>
2
+ <section id="viewport-1-a">
3
+ <div id="viewport-1-a-header" class="band"><button id="viewport-1-a-header-signin" class="btn" style="left:1280px;top:10px">Sign in</button><!-- fire: the header's wrapped sign-in landing on the hero eyebrow (different parents) --><button id="viewport-1-a-hero-stray" class="btn" style="left:20px;top:110px">Sign in</button></div>
4
+ <div id="viewport-1-a-hero" style="position:absolute;left:60px;top:100px;width:800px;height:300px">
5
+ <p id="viewport-1-a-hero-eyebrow" class="text" style="left:0;top:42px">VOLUNTEER WITH LANTERNFUND</p>
6
+ <!-- silent: a button beside its label (no intersection) -->
7
+ <button id="viewport-1-a-hero-cta" class="btn" style="left:0;top:200px">Give</button>
8
+ <p id="viewport-1-a-hero-note" class="text" style="left:110px;top:212px">No account needed</p>
9
+ </div>
10
+ </section>
@@ -0,0 +1,5 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}section{position:relative;width:1536px;height:1024px}.block{position:absolute;left:120px;width:1296px}.text{position:absolute;left:20px;top:20px}</style>
2
+ <section id="viewport-1-a">
3
+ <div id="viewport-1-a-content" class="block" style="top:100px;height:400px;border:1px solid #ddd"><span id="viewport-1-a-content-text" class="text">Content</span></div>
4
+ <footer id="viewport-1-a-footer" class="block" style="top:516px;height:180px;background:#eee"><span id="viewport-1-a-footer-text" class="text">Footer</span></footer>
5
+ </section>
@@ -0,0 +1,21 @@
1
+ <!doctype html><meta charset="utf-8"><style>
2
+ *{margin:0;padding:0;box-sizing:border-box} body{font-family:system-ui}
3
+ section{position:relative;width:1536px;height:1024px;background:#fff}
4
+ .rail{position:absolute;left:0;top:0;width:221px;height:1024px;background:#f6f4f0}
5
+ .btn{position:absolute;left:23px;width:177px;height:43px;background:#245;border-radius:8px;display:inline-flex;align-items:center;justify-content:flex-start}
6
+ .btn > span{font-size:15px;color:#fff;white-space:nowrap}
7
+ .row{position:absolute;left:23px;width:180px;height:40px}
8
+ .row .text{position:absolute;top:12px;font-size:15px}
9
+ .row .icon{position:absolute;top:16px;width:12px;height:8px;background:#888}
10
+ </style>
11
+ <section id="viewport-1-a">
12
+ <div id="viewport-1-a-rail" class="rail" data-proto-shell="x">
13
+ <!-- off: donor-centered label left at its donor padding (80 px) in a 177 px box → insets 80 / ~27 -->
14
+ <button id="viewport-1-a-btn-off" class="btn" style="top:100px;border:0" data-proto-fit="centered"><span style="padding-left:80px">New study</span></button>
15
+ <!-- ok: the same label re-centred (padding 61 px) → insets ≈ 61 / 46 … within 8? no — use 53: text ≈ 70 px → 53 / 54 -->
16
+ <button id="viewport-1-a-btn-ok" class="btn" style="top:200px;border:0" data-proto-fit="centered"><span style="padding-left:53px">New study</span></button>
17
+ <!-- right-anchored chevron still inside (ok) vs one left at its donor offset past the box (off) -->
18
+ <div id="viewport-1-a-row-ok" class="row" style="top:300px" data-proto-fit="right"><span id="viewport-1-a-row-ok-text" class="text" style="left:0">Acme Team</span><span id="viewport-1-a-row-ok-icon" class="icon" style="left:160px"></span></div>
19
+ <div id="viewport-1-a-row-off" class="row" style="top:360px" data-proto-fit="right"><span id="viewport-1-a-row-off-text" class="text" style="left:0">Acme Team</span><span id="viewport-1-a-row-off-icon" class="icon" style="left:130px"></span></div>
20
+ </div>
21
+ </section>
@@ -0,0 +1,17 @@
1
+ <!doctype html><meta charset="utf-8"><style>
2
+ *{margin:0;padding:0;box-sizing:border-box} body{font-family:system-ui}
3
+ section{position:relative;width:1536px;height:1024px;background:#fff}
4
+ .rail{position:absolute;left:0;top:0;width:220px;height:1024px;background:#f6f4f0}
5
+ .item{position:absolute;left:16px;width:180px;height:20px}
6
+ .icon{position:absolute;left:0;top:2px;width:16px;height:16px;background:#888;border-radius:3px}
7
+ .label{position:absolute;left:28px;top:0;font-size:15px}
8
+ .content{position:absolute;left:271px;top:40px;width:1200px;height:900px;background:#fff;border:1px solid #ddd}
9
+ </style>
10
+ <section id="viewport-1-a">
11
+ <div id="viewport-1-a-rail" class="rail" data-proto-shell="viewport-1-c-nav">
12
+ <div id="viewport-1-a-nav-item-1" class="item" style="top:100px"><span id="viewport-1-a-nav-item-1-icon" class="icon"></span><span id="viewport-1-a-nav-item-1-text" class="text label">Overview</span></div>
13
+ <div id="viewport-1-a-nav-item-2" class="item" style="top:140px"><span id="viewport-1-a-nav-item-2-icon" class="icon"></span><span id="viewport-1-a-nav-item-2-text" class="text label">Proposals</span></div>
14
+ <div id="viewport-1-a-nav-item-3" class="item" style="top:180px"><span id="viewport-1-a-nav-item-3-icon" class="icon"></span><span id="viewport-1-a-nav-item-3-text" class="text label">Streams</span></div>
15
+ </div>
16
+ <div id="viewport-1-a-content" class="content"><span id="viewport-1-a-content-title" class="text" style="position:absolute;left:24px;top:24px;font-size:22px">Review</span></div>
17
+ </section>
@@ -0,0 +1 @@
1
+ [{"id":"viewport-1-a-nav-brand","dx":19.4},{"id":"viewport-1-a-catalog-results","dx":2},{"id":"viewport-1-a-catalog-filters","dx":8.2},{"id":"viewport-1-a-footer-brand","dx":-31.8},{"id":"viewport-1-a-footer-legal","dx":-42}]
@@ -0,0 +1,8 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}section{position:relative;width:1536px;height:1024px;background:#fafaf8}.blk{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}.panel{position:absolute;width:216px;height:300px;background:#fff;border:1px solid #ddd;border-radius:12px}.band{position:absolute;left:0;width:1536px;height:200px;background:#f0efe9}</style>
2
+ <section id="viewport-1-a">
3
+ <!-- ragged rail: nav 67.6, hero 87, results 85, filter panel 78.8, footer 118.8 / legal 129 -->
4
+ <div id="viewport-1-a-nav" class="blk" style="top:0;height:40px"><div id="viewport-1-a-nav-brand" style="position:absolute;left:67.6px;top:8px;width:200px;height:24px"><p id="viewport-1-a-nav-brand-text" class="text" style="left:0;top:0;font-size:20px">Verdant Row</p></div></div>
5
+ <div id="viewport-1-a-hero" class="blk" style="top:60px;height:160px"><h1 id="viewport-1-a-hero-heading" class="text" style="left:87px;top:20px;font-size:48px">Hard-to-Kill Plants.</h1><p id="viewport-1-a-hero-text" class="text" style="left:87px;top:90px;font-size:16px">Tough, beautiful, and easy to care for.</p></div>
6
+ <div id="viewport-1-a-catalog" class="blk" style="top:230px;height:540px"><p id="viewport-1-a-catalog-results" class="text" style="left:85px;top:0;font-size:12px">6 results</p><div id="viewport-1-a-catalog-filters" class="panel" style="left:78.8px;top:40px"><p id="viewport-1-a-catalog-filters-title" class="text" style="left:16px;top:12px;font-size:13px">Filter by</p></div><div id="viewport-1-a-catalog-grid" style="position:absolute;left:325px;top:40px;width:1160px;height:420px"><p id="viewport-1-a-catalog-grid-text" class="text" style="left:0;top:0">Cast Iron Plant</p></div></div>
7
+ <div id="viewport-1-a-footer" class="band" style="top:800px"><div id="viewport-1-a-footer-brand" style="position:absolute;left:118.8px;top:16px;width:140px;height:70px"><p id="viewport-1-a-footer-brand-text" class="text" style="left:0;top:0">Verdant Row</p></div><div id="viewport-1-a-footer-legal" style="position:absolute;left:129px;top:170px;width:1256px;height:12px"><p id="viewport-1-a-footer-legal-text" class="text" style="left:0;top:0;font-size:10px">© 2024 Verdant Row</p></div></div>
8
+ </section>
@@ -0,0 +1,8 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}section{position:relative;width:1536px;height:1024px;background:#fafaf8}.blk{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
+ <section id="viewport-1-a">
3
+ <!-- everything already on one rail at 80; a centered hero; an indented sub-item (+24) under an on-rail block; a grid 245 px away -->
4
+ <div id="viewport-1-a-nav" class="blk" style="top:0;height:40px"><div id="viewport-1-a-nav-brand" style="position:absolute;left:80px;top:8px;width:200px;height:24px"><p id="viewport-1-a-nav-brand-text" class="text" style="left:0;top:0;font-size:20px">Verdant Row</p></div></div>
5
+ <div id="viewport-1-a-hero" class="blk" style="top:60px;height:160px"><h1 id="viewport-1-a-hero-heading" class="text" style="left:568px;top:20px;width:400px;text-align:center;font-size:40px">Centered hero title</h1></div>
6
+ <div id="viewport-1-a-list" class="blk" style="top:240px;height:300px"><p id="viewport-1-a-list-title" class="text" style="left:80px;top:0;font-size:16px">Plants</p><p id="viewport-1-a-list-item-1" class="text" style="left:104px;top:30px">— Cast Iron Plant</p><p id="viewport-1-a-list-item-2" class="text" style="left:104px;top:56px">— Snake Plant</p><div id="viewport-1-a-list-grid" style="position:absolute;left:325px;top:0;width:1100px;height:280px"><p id="viewport-1-a-list-grid-text" class="text" style="left:0;top:0">Grid</p></div></div>
7
+ <div id="viewport-1-a-footer" class="blk" style="top:800px;height:200px;background:#f0efe9"><p id="viewport-1-a-footer-text" class="text" style="left:80px;top:20px">© 2024</p></div>
8
+ </section>
@@ -0,0 +1,4 @@
1
+ [
2
+ { "id": "viewport-1-a-intro-text", "dx": 0, "dy": 9 },
3
+ { "id": "viewport-1-a-note-text", "dx": 0, "dy": 9 }
4
+ ]
@@ -0,0 +1,26 @@
1
+ <!doctype html><meta charset="utf-8"><title>overlap/fire</title><style>
2
+ *{margin:0;padding:0;box-sizing:border-box} body{font-family:Helvetica,Arial,sans-serif}
3
+ section{position:relative;width:1000px;height:600px;background:#fff}
4
+ .group{position:absolute}
5
+ .panel{position:absolute;background:#fff;border:1px solid #e6e6e6;border-radius:12px}
6
+ .text{position:absolute;left:0;top:0;font-size:24px;line-height:40px;color:#222;white-space:nowrap}
7
+ .icon{position:absolute;left:0;top:0;width:24px;height:24px;background:#1f4d2b;border-radius:4px}
8
+ </style>
9
+ <section id="viewport-1-a">
10
+ <!-- text × text, different parents: the hero line and the panel's first line were laid out apart and
11
+ assembled 7 px into each other -->
12
+ <div id="viewport-1-a-hero" class="group" style="left:40px;top:40px;width:400px;height:60px">
13
+ <div id="viewport-1-a-hero-heading" class="text">Coastal flavours</div>
14
+ </div>
15
+ <div id="viewport-1-a-intro" class="panel" style="left:40px;top:60px;width:400px;height:200px">
16
+ <div id="viewport-1-a-intro-text" class="text" style="left:0;top:0;width:360px">Local heart, every night</div>
17
+ </div>
18
+
19
+ <!-- text × icon, different parents -->
20
+ <div id="viewport-1-a-badge" class="group" style="left:500px;top:300px;width:300px;height:40px">
21
+ <div id="viewport-1-a-badge-icon" class="icon"></div>
22
+ </div>
23
+ <div id="viewport-1-a-note" class="panel" style="left:500px;top:310px;width:300px;height:120px">
24
+ <div id="viewport-1-a-note-text" class="text" style="width:260px">Open until late</div>
25
+ </div>
26
+ </section>
@@ -0,0 +1,39 @@
1
+ <!doctype html><meta charset="utf-8"><title>overlap/silent</title><style>
2
+ *{margin:0;padding:0;box-sizing:border-box} body{font-family:Helvetica,Arial,sans-serif}
3
+ section{position:relative;width:1000px;height:800px;background:#fff}
4
+ .group{position:absolute}
5
+ .panel{position:absolute;background:#fff;border:1px solid #e6e6e6;border-radius:12px}
6
+ .text{position:absolute;left:0;top:0;font-size:24px;line-height:40px;color:#222;white-space:nowrap}
7
+ .pill{position:absolute;background:#eef4ee;border-radius:999px}
8
+ .icon{position:absolute;width:24px;height:24px;background:#1f4d2b;border-radius:4px}
9
+ </style>
10
+ <section id="viewport-1-a">
11
+ <!-- 1. two heading LINES under one parent: the overlap is the design's leading, not a collision -->
12
+ <div id="viewport-1-a-title" class="group" style="left:40px;top:40px;width:500px;height:90px">
13
+ <h2 id="viewport-1-a-title-line-1" class="text" style="font-size:44px;line-height:56px">AFTERGLOW</h2>
14
+ <h2 id="viewport-1-a-title-line-2" class="text" style="font-size:44px;line-height:56px;top:44px">NIGHT MARKET</h2>
15
+ </div>
16
+
17
+ <!-- 2. an icon and its label inside one pill -->
18
+ <div id="viewport-1-a-pill" class="pill" style="left:600px;top:40px;width:220px;height:40px">
19
+ <div id="viewport-1-a-pill-icon" class="icon" style="left:12px;top:8px"></div>
20
+ <div id="viewport-1-a-pill-label" class="text" style="left:28px;top:0;font-size:18px;line-height:40px">Live &amp; measuring</div>
21
+ </div>
22
+
23
+ <!-- 3. a 40 px overlap between different parents: that is a layout decision, not a nudge -->
24
+ <div id="viewport-1-a-slab" class="group" style="left:40px;top:300px;width:500px;height:80px">
25
+ <div id="viewport-1-a-slab-text" class="text" style="font-size:40px;line-height:60px">Deep overlap</div>
26
+ </div>
27
+ <div id="viewport-1-a-under" class="panel" style="left:40px;top:320px;width:500px;height:140px">
28
+ <div id="viewport-1-a-under-text" class="text" style="font-size:40px;line-height:60px;width:460px">Sits right through</div>
29
+ </div>
30
+
31
+ <!-- 4. a small overlap with no slack: the piece that would move has a line 2 px below it -->
32
+ <div id="viewport-1-a-tag" class="group" style="left:600px;top:520px;width:340px;height:40px">
33
+ <div id="viewport-1-a-tag-text" class="text">Sold out</div>
34
+ </div>
35
+ <div id="viewport-1-a-list" class="panel" style="left:600px;top:540px;width:340px;height:180px">
36
+ <div id="viewport-1-a-list-text-1" class="text" style="width:300px">First row label</div>
37
+ <div id="viewport-1-a-list-text-2" class="text" style="top:30px;width:300px">Second row label</div>
38
+ </div>
39
+ </section>
@@ -0,0 +1,2 @@
1
+ [{"id":"viewport-1-a-sidebar-footer","dy":-64},
2
+ {"id":"viewport-1-b","h":515}]
@@ -0,0 +1,31 @@
1
+ <!doctype html><meta charset="utf-8"><style>
2
+ *{margin:0;padding:0;box-sizing:border-box} body{font-family:Arial,Helvetica,sans-serif}
3
+ section{position:relative;width:1536px;background:#fff}
4
+ .text,h1,h2,h3{position:relative;display:block;white-space:pre;margin:0}
5
+ .image{position:absolute;background:#7a8ba0;border-radius:8px}
6
+ </style>
7
+
8
+ <!-- 1. transplanted shell: the canonical rail is 700px tall but its footer block keeps the donor page's y -->
9
+ <section id="viewport-1-a" style="height:700px">
10
+ <aside id="viewport-1-a-sidebar" data-proto-shell style="position:absolute;left:0;top:0;width:220px;height:700px;background:#0a1928">
11
+ <div id="viewport-1-a-sidebar-nav" style="position:absolute;left:20px;top:80px;width:180px;height:220px">
12
+ <p id="viewport-1-a-sidebar-nav-text" class="text" style="position:absolute;left:0;top:0;width:120px;height:16px;font-size:13px;line-height:16px;color:#cdd8e4">Overview</p>
13
+ </div>
14
+ <footer id="viewport-1-a-sidebar-footer" style="position:absolute;left:20px;top:740px;width:150px;height:16px">
15
+ <p id="viewport-1-a-sidebar-footer-text" class="text" style="position:absolute;left:0;top:0;width:150px;height:16px;font-size:12px;line-height:16px;color:#7f93a8">Kestrel v2.14.0</p>
16
+ </footer>
17
+ </aside>
18
+ <div id="viewport-1-a-body" style="position:absolute;left:260px;top:60px;width:1200px;height:400px">
19
+ <h2 id="viewport-1-a-body-heading" style="width:600px;height:40px;font-size:32px;line-height:40px">Incident overview</h2>
20
+ </div>
21
+ </section>
22
+
23
+ <!-- 2. an in-flow stacked page: the last row of the section falls off the canvas → grow the canvas -->
24
+ <section id="viewport-1-b" style="height:500px;background:#f7f8fa">
25
+ <div id="viewport-1-b-copy" style="position:absolute;left:80px;top:80px;width:900px;height:300px">
26
+ <h2 id="viewport-1-b-copy-heading" style="width:600px;height:40px;font-size:30px;line-height:40px">Everything in one place</h2>
27
+ </div>
28
+ <div id="viewport-1-b-legal" style="position:absolute;left:80px;top:492px;width:900px;height:16px">
29
+ <p id="viewport-1-b-legal-text" class="text" style="position:absolute;left:0;top:0;width:420px;height:16px;font-size:12px;line-height:16px;color:#5b6672">© 2024 Kestrel Systems. All rights reserved.</p>
30
+ </div>
31
+ </section>
@@ -0,0 +1,22 @@
1
+ <!doctype html><meta charset="utf-8"><style>
2
+ *{margin:0;padding:0;box-sizing:border-box} body{font-family:Arial,Helvetica,sans-serif}
3
+ section{position:relative;width:1536px;background:#fff}
4
+ .text,h1,h2,h3{position:relative;display:block;white-space:pre;margin:0}
5
+ .image{position:absolute;background:#7a8ba0;border-radius:8px}
6
+ </style>
7
+ <section id="viewport-1-a" style="height:400px">
8
+ <!-- a hero band that runs exactly to the canvas edge: nothing is cut -->
9
+ <div id="viewport-1-a-hero" class="image" style="left:260px;top:0;width:1276px;height:400px"></div>
10
+ <h1 id="viewport-1-a-hero-heading" style="position:absolute;left:300px;top:60px;width:600px;height:56px;font-size:44px;line-height:56px;color:#fff">Berth timeline</h1>
11
+
12
+ <!-- line-box overshoot of ~2px past the canvas: ascender space, no ink outside -->
13
+ <p id="viewport-1-a-tick-text" class="text" style="position:absolute;left:300px;top:390px;width:200px;height:12px;font-size:12px;line-height:12px;color:#fff">Updated 4 minutes ago</p>
14
+
15
+ <!-- a stranded shell block with NO room above it: lifting it would land on the panel → leave it alone -->
16
+ <aside id="viewport-1-a-sidebar" data-proto-shell style="position:absolute;left:0;top:0;width:220px;height:400px;background:#0a1928">
17
+ <div id="viewport-1-a-sidebar-panel" class="image" style="left:20px;top:300px;width:180px;height:125px"></div>
18
+ <footer id="viewport-1-a-sidebar-footer" style="position:absolute;left:20px;top:430px;width:150px;height:16px">
19
+ <p id="viewport-1-a-sidebar-footer-text" class="text" style="position:absolute;left:0;top:0;width:150px;height:16px;font-size:12px;line-height:16px;color:#7f93a8">Kestrel v2.14.0</p>
20
+ </footer>
21
+ </aside>
22
+ </section>
@@ -0,0 +1 @@
1
+ [{"id":"viewport-1-a-card-1-photo","css":{"border-radius":"14px 14px 0px 0px"}}]
@@ -0,0 +1,5 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}section{position:relative;width:1536px;height:1024px;background:#f6f4f0}.card{position:absolute;top:100px;width:180px;height:420px;background:#fff;border:1px solid #ddd;border-radius:14px}.photo{position:absolute;left:0;top:0;width:100%;height:224px;background:#8a9;display:block}.text{position:absolute;left:16px;top:250px}</style>
2
+ <section id="viewport-1-a">
3
+ <!-- photo flush in the card's top corners, square → must take the card's top radii -->
4
+ <div id="viewport-1-a-card-1" class="card" style="left:300px"><div id="viewport-1-a-card-1-photo" class="image photo"></div><p id="viewport-1-a-card-1-text" class="text">Cast Iron Plant</p></div>
5
+ </section>
@@ -0,0 +1,9 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}section{position:relative;width:1536px;height:1024px;background:#f6f4f0}.card{position:absolute;top:100px;width:180px;height:420px;background:#fff;border:1px solid #ddd;border-radius:14px}.photo{position:absolute;display:block;background:#8a9}.text{position:absolute;left:16px;top:250px}</style>
2
+ <section id="viewport-1-a">
3
+ <!-- a framed photo inset 12 px from the card edges: design, not a corner collision -->
4
+ <div id="viewport-1-a-card-1" class="card" style="left:300px"><div id="viewport-1-a-card-1-photo" class="image photo" style="left:12px;top:12px;width:154px;height:200px"></div><p id="viewport-1-a-card-1-text" class="text">Parlor Palm</p></div>
5
+ <!-- a flush photo whose card already clips -->
6
+ <div id="viewport-1-a-card-2" class="card" style="left:520px;overflow:hidden"><div id="viewport-1-a-card-2-photo" class="image photo" style="left:0;top:0;width:100%;height:224px"></div><p id="viewport-1-a-card-2-text" class="text">Snake Plant</p></div>
7
+ <!-- a flush photo that is already rounded -->
8
+ <div id="viewport-1-a-card-3" class="card" style="left:740px"><div id="viewport-1-a-card-3-photo" class="image photo" style="left:0;top:0;width:100%;height:224px;border-radius:14px 14px 0 0"></div><p id="viewport-1-a-card-3-text" class="text">ZZ Plant</p></div>
9
+ </section>
@@ -0,0 +1,6 @@
1
+ [
2
+ { "id": "viewport-1-a-nav-row-3", "dy": -4 },
3
+ { "id": "viewport-1-a-nav-row-4", "dy": -4 },
4
+ { "id": "viewport-1-a-cards-card-1", "h": 103 },
5
+ { "id": "viewport-1-a-cards-card-2", "h": 103 }
6
+ ]
@@ -0,0 +1,22 @@
1
+ <!doctype html><meta charset="utf-8"><title>rhythm/fire</title><style>
2
+ *{margin:0;padding:0;box-sizing:border-box} body{font-family:Helvetica,Arial,sans-serif}
3
+ section{position:relative;width:1000px;height:800px;background:#fff}
4
+ .panel{position:absolute;background:#fff;border:1px solid #e6e6e6}
5
+ .row{position:absolute;background:#f4f4f4;border-radius:8px}
6
+ .card{position:absolute;background:#fafafa;border:1px solid #e6e6e6;border-radius:10px}
7
+ </style>
8
+ <section id="viewport-1-a">
9
+ <!-- a nav column whose middle gap is 16 px where the rest are 12 px: the sidebar-nav mechanism -->
10
+ <div id="viewport-1-a-nav" class="panel" style="left:40px;top:40px;width:240px;height:400px">
11
+ <div id="viewport-1-a-nav-row-1" class="row" style="left:20px;top:20px;width:180px;height:40px"></div>
12
+ <div id="viewport-1-a-nav-row-2" class="row" style="left:20px;top:72px;width:180px;height:40px"></div>
13
+ <div id="viewport-1-a-nav-row-3" class="row" style="left:20px;top:128px;width:180px;height:40px"></div>
14
+ <div id="viewport-1-a-nav-row-4" class="row" style="left:20px;top:180px;width:180px;height:40px"></div>
15
+ </div>
16
+ <!-- three cards in a row, evenly spaced, whose heights are 3 px apart -->
17
+ <div id="viewport-1-a-cards" class="panel" style="left:340px;top:40px;width:600px;height:160px">
18
+ <div id="viewport-1-a-cards-card-1" class="card" style="left:20px;top:10px;width:170px;height:100px"></div>
19
+ <div id="viewport-1-a-cards-card-2" class="card" style="left:210px;top:10px;width:170px;height:102px"></div>
20
+ <div id="viewport-1-a-cards-card-3" class="card" style="left:400px;top:10px;width:170px;height:103px"></div>
21
+ </div>
22
+ </section>
@@ -0,0 +1,27 @@
1
+ <!doctype html><meta charset="utf-8"><title>rhythm/silent</title><style>
2
+ *{margin:0;padding:0;box-sizing:border-box} body{font-family:Helvetica,Arial,sans-serif}
3
+ section{position:relative;width:1000px;height:800px;background:#fff}
4
+ .panel{position:absolute;left:40px;width:600px;background:#fff;border:1px solid #e6e6e6}
5
+ .row{position:absolute;left:20px;width:500px;height:40px;background:#f4f4f4;border-radius:8px}
6
+ </style>
7
+ <section id="viewport-1-a">
8
+ <!-- deliberate step: gaps 20 / 20 / 44. The 24 px step is design (a group break) → the whole parent is off limits -->
9
+ <div id="viewport-1-a-steps" class="panel" style="top:20px;height:300px">
10
+ <div id="viewport-1-a-steps-row-1" class="row" style="top:20px"></div>
11
+ <div id="viewport-1-a-steps-row-2" class="row" style="top:80px"></div>
12
+ <div id="viewport-1-a-steps-row-3" class="row" style="top:140px"></div>
13
+ <div id="viewport-1-a-steps-row-4" class="row" style="top:224px"></div>
14
+ </div>
15
+ <!-- near threshold: gaps 20 / 26.5 — 6.5 px from the median, just outside the ±6 px snap band -->
16
+ <div id="viewport-1-a-near" class="panel" style="top:350px;height:200px">
17
+ <div id="viewport-1-a-near-row-1" class="row" style="top:20px"></div>
18
+ <div id="viewport-1-a-near-row-2" class="row" style="top:80px"></div>
19
+ <div id="viewport-1-a-near-row-3" class="row" style="top:146.5px"></div>
20
+ </div>
21
+ <!-- already regular: gaps 16 / 16, widths identical → nothing to snap and nothing to equalise -->
22
+ <div id="viewport-1-a-even" class="panel" style="top:580px;height:180px">
23
+ <div id="viewport-1-a-even-row-1" class="row" style="top:20px"></div>
24
+ <div id="viewport-1-a-even-row-2" class="row" style="top:76px"></div>
25
+ <div id="viewport-1-a-even-row-3" class="row" style="top:132px"></div>
26
+ </div>
27
+ </section>
@@ -0,0 +1,5 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1400px}section{position:absolute;left:0;width:1536px;overflow:visible}.text{position:absolute;white-space:nowrap}</style>
2
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1400px"><div id="page-flow">
3
+ <section id="viewport-1-a" style="top:0;height:700px;background:#fbf6ee"><p id="viewport-1-a-text" class="text" style="left:300px;top:660px;font-size:16px">The market ends here.</p><div id="viewport-1-a-fruit" class="decoration" style="position:absolute;left:0;bottom:0;width:180px;height:150px;background:#df8b22;border-radius:100% 100% 0 0"></div></section>
4
+ <section id="viewport-2-a" style="top:700px;height:700px;background:#fbf6ee"><p id="viewport-2-a-text" class="text" style="left:300px;top:20px;font-size:16px">The next section starts here.</p></section>
5
+ </div></div></div>
@@ -0,0 +1,5 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1400px}section{position:absolute;left:0;width:1536px;overflow:visible}.text{position:absolute;white-space:nowrap}</style>
2
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1400px"><div id="page-flow">
3
+ <section id="viewport-1-a" style="top:0;height:700px;background:#fbf6ee"><p id="viewport-1-a-text" class="text" style="left:300px;top:620px;font-size:16px">The market ends here.</p><div id="viewport-1-a-product-image" class="image" style="position:absolute;left:0;bottom:0;width:180px;height:40px;background:#925b24"></div></section>
4
+ <section id="viewport-2-a" style="top:700px;height:700px;background:#fbf6ee"><p id="viewport-2-a-text" class="text" style="left:300px;top:20px;font-size:16px">The next section starts here.</p></section>
5
+ </div></div></div>
@@ -0,0 +1 @@
1
+ [{"id":"viewport-1-a","css":{"box-shadow":"0 32px 0 0 rgb(251, 246, 238)"}},{"id":"viewport-2-a","dy":32}]
@@ -0,0 +1,6 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1400px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1400px"><div id="page-flow">
3
+ <!-- solid → solid: the hero's own cream section is the full-band ground; ink gap 24 px → one seam → +32 -->
4
+ <section id="viewport-1-a" style="top:0;height:700px;background:#fbf6ee"><h1 id="viewport-1-a-heading" class="text" style="left:80px;top:200px;font-size:48px">Give your time.</h1><p id="viewport-1-a-text" class="text" style="left:80px;top:660px;font-size:16px">Join neighbors helping pack food.</p></section>
5
+ <section id="viewport-2-a" style="top:700px;height:700px;background:#fbf6ee"><p id="viewport-2-a-eyebrow" class="text" style="left:80px;top:20px;font-size:12px">UPCOMING OPPORTUNITIES</p><h2 id="viewport-2-a-heading" class="text" style="left:80px;top:60px;font-size:32px">This week</h2></section>
6
+ </div></div></div>
@@ -0,0 +1,7 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1400px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:2100px"><div id="page-flow">
3
+ <!-- three solid sections, ink gaps 40 / 100: median 70 → target ceil8(102)=104 → additions 64 (cap) and 4 (declined: below the floor) -->
4
+ <section id="viewport-1-a" style="top:0;height:700px;background:#fff"><h1 id="viewport-1-a-heading" class="text" style="left:80px;top:200px;font-size:48px">One</h1><p id="viewport-1-a-text" class="text" style="left:80px;top:660px;font-size:16px">end of one</p></section>
5
+ <section id="viewport-2-a" style="top:700px;height:700px;background:#fff"><p id="viewport-2-a-top" class="text" style="left:80px;top:20px;font-size:16px">start of two</p><p id="viewport-2-a-text" class="text" style="left:80px;top:600px;font-size:16px">end of two</p></section>
6
+ <section id="viewport-3-a" style="top:1400px;height:700px;background:#fff"><p id="viewport-3-a-top" class="text" style="left:80px;top:20px;font-size:16px">start of three</p></section>
7
+ </div></div></div>
@@ -0,0 +1,11 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1024px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1024px"><div id="page-flow">
3
+ <section id="viewport-1-a" style="top:0;height:1024px">
4
+ <div id="viewport-1-a-page" style="position:absolute;inset:0;background:#fefefe"></div>
5
+ <div id="viewport-1-a-comparison" style="position:absolute;left:120px;top:200px;width:1296px;height:400px;border:1px solid #ddd"><p class="text" id="viewport-1-a-comparison-text" style="left:20px;top:20px">Comparison</p></div>
6
+ <div id="viewport-1-a-comparison-aside" style="position:absolute;left:816px;top:240px;width:600px;height:320px"></div>
7
+ <div style="position:absolute;left:0;top:600px;width:1536px;height:16px;background:#eee8e2"></div>
8
+ <div id="viewport-1-a-faq" style="position:absolute;left:120px;top:616px;width:1296px;height:120px"><p class="text" id="viewport-1-a-faq-text" style="left:0;top:0">Common purchase questions</p></div>
9
+ <footer id="viewport-1-a-footer" style="position:absolute;left:0;top:752px;width:1536px;height:272px;background:#f8f8f8"><p class="text" id="viewport-1-a-footer-text" style="left:120px;top:40px">Footer</p></footer>
10
+ </section>
11
+ </div></div></div>
@@ -0,0 +1,8 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1024px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1024px"><div id="page-flow">
3
+ <section id="viewport-1-a" style="top:0;height:1024px">
4
+ <div id="viewport-1-a-page" style="position:absolute;inset:0;background:#081018"></div>
5
+ <div id="viewport-1-a-dashboard" style="position:absolute;left:24px;top:100px;width:1488px;height:700px;border:1px solid #334"><p class="text" id="viewport-1-a-dashboard-text" style="left:20px;top:20px;color:white">Dense operations dashboard</p></div>
6
+ <footer id="viewport-1-a-footer" style="position:absolute;left:0;top:804px;width:1536px;height:220px;background:#0b141d"><p class="text" id="viewport-1-a-footer-text" style="left:24px;top:40px;color:white">Compact dashboard footer</p></footer>
7
+ </section>
8
+ </div></div></div>
@@ -0,0 +1,5 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1400px}section{position:absolute;left:0;width:1536px;overflow:visible}.text{position:absolute;white-space:nowrap}</style>
2
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1400px"><div id="page-flow">
3
+ <section id="viewport-1-a" style="top:0;height:700px;background:#fefefe"><div id="viewport-1-a-card" style="position:absolute;left:500px;top:500px;width:536px;height:200px;background:#fff;box-shadow:0 0 16px rgba(0,0,0,.05)"><p id="viewport-1-a-text" class="text" style="left:40px;top:80px">A card casts a soft shadow.</p></div></section>
4
+ <section id="viewport-2-a" style="top:700px;height:700px;background:#fefefe"><p id="viewport-2-a-text" class="text" style="left:80px;top:20px">The next section.</p></section>
5
+ </div></div></div>
@@ -0,0 +1,5 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1400px}section{position:absolute;left:0;width:1536px;overflow:visible}.text{position:absolute;white-space:nowrap}</style>
2
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1400px"><div id="page-flow">
3
+ <section id="viewport-1-a" style="top:0;height:700px;background:#fefefe"><div id="viewport-1-a-card" style="position:absolute;left:500px;top:500px;width:536px;height:200px;background:#fff;box-shadow:0 8px 0 #c83b3b"><p id="viewport-1-a-text" class="text" style="left:40px;top:80px">A hard coloured band is not a shadow gradient.</p></div></section>
4
+ <section id="viewport-2-a" style="top:700px;height:700px;background:#fefefe"><p id="viewport-2-a-text" class="text" style="left:80px;top:20px">The next section.</p></section>
5
+ </div></div></div>
@@ -0,0 +1,6 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1400px}section{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}</style>
2
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1400px"><div id="page-flow">
3
+ <!-- gradient → image: both grounds are experimental classes; the seam's ink gap is narrow but the page must abstain -->
4
+ <section id="viewport-1-a" style="top:0;height:700px;background-image:linear-gradient(#fbf6ee,#efe6d2)"><h1 id="viewport-1-a-heading" class="text" style="left:80px;top:200px;font-size:48px">Give your time.</h1><p id="viewport-1-a-text" class="text" style="left:80px;top:660px;font-size:16px">Join neighbors.</p></section>
5
+ <section id="viewport-2-a" style="top:700px;height:700px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==);background-size:cover;background-repeat:no-repeat"><p id="viewport-2-a-eyebrow" class="text" style="left:80px;top:20px;font-size:12px">UPCOMING</p></section>
6
+ </div></div></div>
@@ -0,0 +1,7 @@
1
+ <!doctype html><meta charset="utf-8"><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.generated-screen{position:relative}#page-flow{position:relative;width:1536px;height:1600px}section{position:absolute;left:0;width:1536px;height:400px;background:#fff}.text{position:absolute;white-space:nowrap;font-size:16px;line-height:16px}</style>
2
+ <div class="generated-screen"><div class="ly-anchor-canvas" style="position:relative;height:1600px"><div id="page-flow">
3
+ <section id="viewport-1-a" style="top:0"><p id="viewport-1-a-end" class="text" style="left:80px;top:376px">One</p></section>
4
+ <section id="viewport-2-a" style="top:400px"><p id="viewport-2-a-start" class="text" style="left:80px;top:0">Two start</p><p id="viewport-2-a-end" class="text" style="left:80px;top:376px">Two end</p></section>
5
+ <section id="viewport-3-a" style="top:800px"><p id="viewport-3-a-start" class="text" style="left:80px;top:0">Three start</p><p id="viewport-3-a-end" class="text" style="left:80px;top:370px">Three end</p></section>
6
+ <section id="viewport-4-a" style="top:1200px"><p id="viewport-4-a-start" class="text" style="left:80px;top:16px">Four</p></section>
7
+ </div></div></div>
@@ -0,0 +1 @@
1
+ [{"id":"viewport-1-c-nav-new-study","w":204},{"id":"viewport-1-c-nav-footer","dy":-38}]
@@ -0,0 +1,20 @@
1
+ <!doctype html><meta charset="utf-8"><style>
2
+ *{margin:0;padding:0;box-sizing:border-box} body{font-family:system-ui}
3
+ section{position:relative;width:1536px;height:1024px;background:#fff}
4
+ .rail{position:absolute;left:0;top:0;width:220px;height:1024px;background:#f6f4f0;padding:24px 0 0 16px}
5
+ .btn{position:relative;width:214px;height:43px;background:#245;border-radius:8px;margin-top:40px}
6
+ .btn .text{position:absolute;left:0;top:12px;width:214px;text-align:center;color:#fff;font-size:15px}
7
+ .nav{position:relative;margin-top:24px;font-size:15px;width:99px;height:18px}
8
+ .foot{position:absolute;left:16px;top:1040px;width:120px;height:14px;font-size:11px;color:#666}
9
+ .content{position:absolute;left:270px;top:40px;width:1200px;height:900px;background:#fff;border:1px solid #ddd}
10
+ </style>
11
+ <section id="viewport-1-a">
12
+ <!-- a 220 px host carrying canonical (258 px) children: the button spills 10 px past the rail, the footer 30 px below -->
13
+ <div id="viewport-1-a-rail" class="rail" data-proto-shell="viewport-1-c-nav">
14
+ <div id="viewport-1-c-nav-new-study" class="btn"><span id="viewport-1-c-nav-new-study-text" class="text">+ New study</span></div>
15
+ <div id="viewport-1-c-nav-item-overview" class="nav text">Overview</div>
16
+ <div id="viewport-1-c-nav-item-proposals" class="nav text">Proposals</div>
17
+ <div id="viewport-1-c-nav-footer" class="foot text">v2.14.0</div>
18
+ </div>
19
+ <div id="viewport-1-a-content" class="content"><span id="viewport-1-a-content-title" class="text" style="position:absolute;left:24px;top:24px;font-size:22px">Review</span></div>
20
+ </section>
@@ -0,0 +1,21 @@
1
+ <!doctype html><meta charset="utf-8"><style>
2
+ *{margin:0;padding:0;box-sizing:border-box} body{font-family:system-ui}
3
+ section{position:relative;width:1536px;height:1024px;background:#fff}
4
+ .rail{position:absolute;left:0;top:0;width:220px;height:1024px;background:#f6f4f0;padding:24px 0 0 16px}
5
+ .btn{position:relative;width:180px;height:43px;background:#245;border-radius:8px;margin-top:40px}
6
+ .btn .text{position:absolute;left:0;top:12px;width:180px;text-align:center;color:#fff;font-size:15px}
7
+ .nav{position:relative;margin-top:24px;font-size:15px;width:99px;height:18px}
8
+ .foot{position:absolute;left:16px;top:990px;width:120px;height:14px;font-size:11px;color:#666}
9
+ .content{position:absolute;left:270px;top:40px;width:1200px;height:900px;background:#fff;border:1px solid #ddd}
10
+ </style>
11
+ <section id="viewport-1-a">
12
+ <!-- the upstream fit already happened: a 220 px host (canonical was 258) whose children all fit → nothing to do;
13
+ the rail must NOT be widened and the content gutter must NOT change -->
14
+ <div id="viewport-1-a-rail" class="rail" data-proto-shell="viewport-1-c-nav">
15
+ <div id="viewport-1-c-nav-new-study" class="btn"><span id="viewport-1-c-nav-new-study-text" class="text">+ New study</span></div>
16
+ <div id="viewport-1-c-nav-item-overview" class="nav text">Overview</div>
17
+ <div id="viewport-1-c-nav-item-proposals" class="nav text">Proposals</div>
18
+ <div id="viewport-1-c-nav-footer" class="foot text">v2.14.0</div>
19
+ </div>
20
+ <div id="viewport-1-a-content" class="content"><span id="viewport-1-a-content-title" class="text" style="position:absolute;left:24px;top:24px;font-size:22px">Review</span></div>
21
+ </section>
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // Paired cases for the structure harm guard (harm.mjs): a synthetic override that breaks an icon column or collapses
3
+ // the rail/content gutter MUST be flagged; a rhythm-sized nudge (≤ 6 px) on a gap MUST NOT.
4
+ import path from 'node:path';
5
+ import { chromium } from '@playwright/test';
6
+ import { measurePage } from '../measure.mjs';
7
+ import { applyOverridesScript } from '../overrides.mjs';
8
+ import { structureHarm } from '../harm.mjs';
9
+ const here = path.dirname(new URL(import.meta.url).pathname);
10
+ const browser = await chromium.launch();
11
+ const page = await browser.newPage({ viewport: { width: 1536, height: 1024 } });
12
+ await page.goto('file://' + path.join(here, 'fixtures/harm/page.html'), { waitUntil: 'load' });
13
+ const before = await measurePage(page, {});
14
+ let failures = 0;
15
+ const check = async (name, css, expectMis, expectSp) => {
16
+ await page.evaluate(applyOverridesScript(), css);
17
+ const after = await measurePage(page, {});
18
+ const h = structureHarm(before, after);
19
+ const ok = (h.misaligned.length > 0) === expectMis && (h.spacing.length > 0) === expectSp;
20
+ if (!ok) failures++;
21
+ console.log(`${ok ? 'ok ' : 'FAIL'} harm/${name}: misaligned=${h.misaligned.length} spacing=${h.spacing.length}`);
22
+ };
23
+ await check('fire-column (icon nudged 5px out of its column)', '#viewport-1-a-nav-item-2-icon{transform:translate(-5px,0)!important}', true, false);
24
+ await check('fire-spacing (rail grown 37px into the gutter)', '#viewport-1-a-rail{width:257px!important}', false, true);
25
+ await check('silent (rhythm-sized 3px nudge, column intact)', '#viewport-1-a-nav-item-3{transform:translate(0,-3px)!important}', false, false);
26
+ // A large vertical gap change still fires unless the spacing rule declares that exact before->after seam.
27
+ await page.goto('file://' + path.join(here, 'fixtures/harm/declared-gap.html'), { waitUntil: 'load' });
28
+ const gapBefore = await measurePage(page, {});
29
+ await page.evaluate(applyOverridesScript(), '#viewport-1-a-footer{transform:translate(0,32px)!important}');
30
+ const gapAfter = await measurePage(page, {});
31
+ const undeclaredGap = structureHarm(gapBefore, gapAfter);
32
+ const declaredGap = structureHarm(gapBefore, gapAfter, new Set(), new Set(['viewport-1-a-content|viewport-1-a-footer']));
33
+ const okDeclaredGap = undeclaredGap.spacing.length > 0 && declaredGap.spacing.length === 0;
34
+ if (!okDeclaredGap) failures++;
35
+ console.log(`${okDeclaredGap ? 'ok ' : 'FAIL'} harm/declared-gap: undeclared=${undeclaredGap.spacing.length} declared=${declaredGap.spacing.length}`);
36
+ // offcenter metric: fires on ink left at its donor offset inside a fitted box, silent on re-centred / still-anchored ink
37
+ await page.goto('file://' + path.join(here, 'fixtures/harm/offcenter.html'), { waitUntil: 'load' });
38
+ const m = await measurePage(page, {});
39
+ const ids = m.offcenter.map((o) => o.id).sort();
40
+ const okOff = ids.length === 2 && ids.includes('viewport-1-a-btn-off') && ids.includes('viewport-1-a-row-off');
41
+ if (!okOff) failures++;
42
+ console.log(`${okOff ? 'ok ' : 'FAIL'} harm/offcenter: fires on ${ids.join(', ')} (expected btn-off, row-off only)`);
43
+ // overlap instrument: a control (button) landing on text from another parent must count; a button beside text must not
44
+ await page.goto('file://' + path.join(here, 'fixtures/harm/button-overlap.html'), { waitUntil: 'load' });
45
+ const mo = await measurePage(page, {});
46
+ const pairs = mo.overlap.map((o) => [o.a, o.b].sort().join('|'));
47
+ const okBtn = pairs.length === 1 && pairs[0] === 'viewport-1-a-hero-eyebrow|viewport-1-a-hero-stray';
48
+ if (!okBtn) failures++;
49
+ console.log(`${okBtn ? 'ok ' : 'FAIL'} overlap/button-over-text: ${JSON.stringify(pairs)} (expected eyebrow|stray only)`);
50
+ // contrast refinement: declared brand colour keeps its hue (orange → readable orange, not black); readable declared
51
+ // colour untouched; a hue that cannot reach the threshold falls back and records the fact
52
+ {
53
+ const rule = await import('../rules/contrast.mjs');
54
+ const hue = (rgb) => { const m = rgb.match(/\d+/g).map(Number); const [r, g, b] = m.map((v) => v / 255); const mx = Math.max(r, g, b), mn = Math.min(r, g, b); const d = mx - mn; if (!d) return null; let h = mx === r ? ((g - b) / d + (g < b ? 6 : 0)) : mx === g ? ((b - r) / d + 2) : ((r - g) / d + 4); return Math.round(h * 60); };
55
+ await page.goto('file://' + path.join(here, 'fixtures/contrast/fire.html'), { waitUntil: 'load' });
56
+ const fire = await page.evaluate(rule.browserFn(), { metrics: {}, params: rule.params });
57
+ const o = fire.find((x) => x.id === 'viewport-1-a-orange');
58
+ const okFire = o && Math.abs(hue(o.css.color) - 33) <= 6 && /4\.[5-9]|[5-9]\./.test(o.reason) && !/fallback/.test(o.reason);
59
+ if (!okFire) failures++; console.log(`${okFire ? 'ok ' : 'FAIL'} contrast/fire: orange keeps hue → ${o?.css.color} (${o?.reason})`);
60
+ await page.goto('file://' + path.join(here, 'fixtures/contrast/silent.html'), { waitUntil: 'load' });
61
+ const silent = await page.evaluate(rule.browserFn(), { metrics: {}, params: rule.params });
62
+ const okSilent = silent.length === 0;
63
+ if (!okSilent) failures++; console.log(`${okSilent ? 'ok ' : 'FAIL'} contrast/silent: readable declared orange untouched (${silent.length} overrides)`);
64
+ await page.goto('file://' + path.join(here, 'fixtures/contrast/fallback.html'), { waitUntil: 'load' });
65
+ const fb = await page.evaluate(rule.browserFn(), { metrics: {}, params: rule.params });
66
+ const okFb = fb.length === 1 && /hue unreachable/.test(fb[0].reason);
67
+ if (!okFb) failures++; console.log(`${okFb ? 'ok ' : 'FAIL'} contrast/fallback: yellow on mid grey → ${fb[0]?.css.color} (${fb[0]?.reason?.slice(0, 60)}…)`);
68
+ }
69
+ await browser.close();
70
+ if (failures) { console.log(`${failures} failure(s)`); process.exit(1); }
71
+ console.log('harm guard pairs green');