mosaic-headless 1.4.0 → 1.5.1
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.
- package/README.ja.md +1 -0
- package/README.ko.md +1 -0
- package/README.md +1 -0
- package/README.zh-TW.md +1 -0
- package/SKILL.md +14 -2
- package/assets/templates/platforms/claude-ai.json +1 -1
- package/assets/templates/platforms/claude-code.json +1 -1
- package/assets/templates/platforms/codex-cli.json +1 -1
- package/assets/templates/platforms/copilot.json +1 -1
- package/assets/templates/platforms/gemini-cli.json +1 -1
- package/bin/check-release.mjs +12 -0
- package/data/intro-verification.csv +20 -0
- package/package.json +1 -1
- package/references/failure-modes.md +15 -0
- package/sites/_moksa.py +119 -8
- package/sites/moksa.json +453 -1
- package/tools/build_site.py +43 -0
- package/tools/verify_intro.py +324 -0
package/README.ja.md
CHANGED
|
@@ -62,6 +62,7 @@ URL に対応するテンプレなし HTTP 406、未ログインには空の b
|
|
|
62
62
|
| **レスポンシブ** | 2 サイト計 576 件の `_t`/`_m` 宣言を、サイトが実際に配信したスタイルシートに対して 1 件ずつ断言 — 全件検証済み |
|
|
63
63
|
| **スタイル状態** | 53 状態のうち 52 を実ページに書き、テーブルが約束するセレクタと照合:**36 件が完全一致**、12 件 NO_HOST、3 件 SKIPPED、1 件 BROKE_PAGE。どの要素にも使えるグローバル 7 状態はすべて検証済み |
|
|
64
64
|
| **インタラクション** | JS アニメーション経路を、ネガティブコントロール付きで保存行を読み戻しながら検証:`propertyMetas` は**受理され保存される**。プロパティ値は依然としてバインドされないが、その境界は厳密になった |
|
|
65
|
+
| **入場アニメーション** | ページ読み込み後の 10 時点をサンプリングし 7 項目を断言 — 再生されること、アニメーションする `@property` カウンタが 100 に達すること、ベールがヒットテストから外れること、ビューポート内に opacity 0 のまま取り残された要素がないこと、実際のクリックが文書に届くこと、そして `prefers-reduced-motion` ではベールがそもそも存在しないこと |
|
|
65
66
|
| **ブラウザ** | 配信されたページを Chromium の 3 つのビューポートで計算スタイル 2,282 件読み取り:1,676 件が一致、606 件は比較不能として明示、**上書き 0 件** |
|
|
66
67
|
| **デザイン監査** | コントラスト、フォントフォールバック、CJK の字送り、横溢れ、テキストの切れ、1 行の文字数 — ブラウザ上で実行、**指摘 0 件** |
|
|
67
68
|
| **テーマの書き出し/読み込み** | 往復検証済み:テーマ全体を書き出し、コピーとして読み込み、そのコピーがバイト単位で同一のページを配信 |
|
package/README.ko.md
CHANGED
|
@@ -62,6 +62,7 @@ URL에 맞는 템플릿 없음 HTTP 406, 로그인하지 않은 사용자
|
|
|
62
62
|
| **반응형** | 두 사이트 합쳐 576개의 `_t`/`_m` 선언을, 사이트가 실제로 내보낸 스타일시트에 대해 하나씩 단언 — 전부 검증됨 |
|
|
63
63
|
| **스타일 상태** | 53개 상태 중 52개를 실제 페이지에 쓰고 표가 약속한 셀렉터와 대조: **36개가 정확히 일치**, 12개 NO_HOST, 3개 SKIPPED, 1개 BROKE_PAGE. 어떤 요소에나 쓸 수 있는 전역 7개 상태는 전부 검증됨 |
|
|
64
64
|
| **인터랙션** | JS 애니메이션 경로를 음성 대조군과 함께, 저장된 행을 되읽으며 검증: `propertyMetas`는 **수용되고 저장된다**. 속성 값은 여전히 바인딩되지 않지만 그 경계는 이제 정확하다 |
|
|
65
|
+
| **인트로 애니메이션** | 페이지 로드 후 10개 시점을 샘플링해 7가지를 단언 — 재생될 것, 애니메이션되는 `@property` 카운터가 100에 도달할 것, 베일이 히트 테스트에서 빠질 것, 뷰포트 안에 opacity 0으로 갇힌 요소가 없을 것, 실제 클릭이 문서에 닿을 것, 그리고 `prefers-reduced-motion`에서는 베일이 아예 존재하지 않을 것 |
|
|
65
66
|
| **브라우저** | 전달된 페이지를 Chromium의 세 가지 뷰포트에서 계산 스타일 2,282건 판독: 1,676건 일치, 606건은 비교 불가로 명시, **덮어쓰기 0건** |
|
|
66
67
|
| **디자인 감사** | 명암비, 폰트 폴백, CJK 자간, 가로 넘침, 텍스트 잘림, 한 줄 글자 수 — 브라우저에서 실행, **지적 0건** |
|
|
67
68
|
| **테마 내보내기/가져오기** | 왕복 검증: 테마 전체를 내보내 사본으로 가져왔고, 사본이 바이트 단위로 동일한 페이지를 제공 |
|
package/README.md
CHANGED
|
@@ -63,6 +63,7 @@ factories, so Pro types register and render regardless.
|
|
|
63
63
|
| **responsive** | 576 `_t`/`_m` declarations across two sites asserted against the stylesheet the site actually served — all verified |
|
|
64
64
|
| **style states** | 52 of the 53 states written to a live page and matched against the selector the table promises: **36 compiled exactly**, 12 NO_HOST, 3 SKIPPED, 1 BROKE_PAGE. All seven globally usable states verified |
|
|
65
65
|
| **interactions** | the JS animation path probed with negative controls and the row read back: `propertyMetas` **is** accepted and stored; the property values still do not bind, and the boundary is now exact |
|
|
66
|
+
| **entrance animation** | the page-load sequence sampled at ten timestamps and asserted on seven counts — it plays, its animated `@property` counter reaches 100, the veil leaves hit-testing, nothing in the viewport is stranded at opacity 0, a real click reaches the document, and under `prefers-reduced-motion` the veil never exists at all |
|
|
66
67
|
| **browser** | 2,282 computed-style readings on the delivered page in Chromium at three viewports: 1,676 compared and agreed, 606 not-comparable and labelled, **0 overridden** |
|
|
67
68
|
| **design audit** | contrast, font fallback, CJK tracking, overflow, clipped text, line measure — run in the browser, **0 findings** |
|
|
68
69
|
| **theme export/import** | round-tripped: a full theme exported, re-imported as a copy, and the copy served byte-identical pages |
|
package/README.zh-TW.md
CHANGED
|
@@ -55,6 +55,7 @@ commit 時 PHP fatal HTTP 500 (122 種型別裡有 15 種,光放在 div
|
|
|
55
55
|
| **響應式** | 兩個站共 576 條 `_t`/`_m` 宣告,對照網站實際送出的樣式表逐條斷言——全數通過 |
|
|
56
56
|
| **樣式狀態** | 53 個狀態中的 52 個寫進真實頁面,對照表格承諾的選擇器逐一比對:**36 個完全吻合**、12 個 NO_HOST、3 個 SKIPPED、1 個 BROKE_PAGE。七個可用於任何元素的狀態全數驗證 |
|
|
57
57
|
| **互動動畫** | 帶負對照組並讀回儲存列來探測 JS 動畫路徑:`propertyMetas` **確實**會被接受並儲存;屬性值仍然無法綁定,但界線現在很精確 |
|
|
58
|
+
| **入口動畫** | 在載入後十個時間點取樣並做七項斷言——它有播、被動畫的 `@property` 計數器跑到 100、遮罩退出點擊判定、視窗內沒有任何內容卡在 opacity 0、真實點擊落在文件上,而在 `prefers-reduced-motion` 下遮罩根本不存在 |
|
|
58
59
|
| **瀏覽器** | 在 Chromium 三個視窗寬度上對交付頁面做 2,282 次計算樣式讀取:1,676 條比對相符、606 條標為無法比對、**0 條被覆蓋** |
|
|
59
60
|
| **設計稽核** | 對比度、字體回退、CJK 字距、水平溢出、文字裁切、每行字數——在瀏覽器裡跑,**0 項發現** |
|
|
60
61
|
| **主題匯出/匯入** | 完整來回驗證:匯出整個主題、以副本身分匯入,副本送出的頁面逐位元組相同 |
|
package/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: |
|
|
|
4
4
|
Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface with `mo.py`, which joins every source table to the live sweeps so a lookup leads with the measured verdict rather than the declaration (122 node types, 181 properties, 98 style properties with 20 structured value shapes pinned down, 53 style states, 151 element classes, 74 dynamic variables, 12 interaction triggers, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, the design-token and element-class layers verified against compiled CSS, the @VAR() dynamic language verified against rendered output, nine designed pages built through the tables themselves, and the delivered page re-read in Chromium at three viewports so a rule that is present, correct and still wrong cannot pass.
|
|
5
5
|
license: "MIT"
|
|
6
6
|
author: "moksa (https://moksaweb.com)"
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.5.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Headless Mosaic
|
|
@@ -155,6 +155,13 @@ INTERACTION the JS animation path, probed with negative controls and the row re
|
|
|
155
155
|
written straight into the table with caches flushed.
|
|
156
156
|
data/interaction-verification.csv
|
|
157
157
|
|
|
158
|
+
INTRO the page-load sequence sampled at ten timestamps across its life and
|
|
159
|
+
asserted on seven counts - it plays, the animated `@property` counter
|
|
160
|
+
reaches 100, the veil leaves hit-testing, no in-viewport content is
|
|
161
|
+
stranded at opacity 0, a real click reaches the document, and under
|
|
162
|
+
`prefers-reduced-motion` the veil never exists at all. All pass.
|
|
163
|
+
data/intro-verification.csv
|
|
164
|
+
|
|
158
165
|
BROWSER 2,282 computed-style readings on the delivered page in Chromium, at
|
|
159
166
|
three viewports: every declared property vs `getComputedStyle` on the
|
|
160
167
|
node it targets. 1,676 compared and agreed, 606 not-comparable and
|
|
@@ -309,6 +316,7 @@ so the pattern is in the data, not just in this paragraph.
|
|
|
309
316
|
| `data/data-class-hierarchy.csv` | 121 | source - every data class and its parent, so a type's inherited properties can be resolved |
|
|
310
317
|
| `data/style-state-verification.csv` | 52 | **swept live** - each state written on a host of its own type and matched against its promised selector |
|
|
311
318
|
| `data/interaction-verification.csv` | 7 | **probed live** - interaction animation shapes, with negative controls and the stored row beside the payload |
|
|
319
|
+
| `data/intro-verification.csv` | 7 + 10 | **sampled live** - the entrance sequence over ten timestamps, plus the seven assertions about it |
|
|
312
320
|
| `data/element-classes.csv` | 151 | **live** — the built-in class metas; their IDs are what an `elementClass` record must use |
|
|
313
321
|
| `data/dynamic-variables.csv` | 74 | source — every `@VAR('ns/name')` expression, by namespace |
|
|
314
322
|
| `data/evaluator-functions.csv` | 19 | source — the `@` functions with their arity |
|
|
@@ -409,6 +417,7 @@ post — `build_all.py` resets first for that reason.
|
|
|
409
417
|
| `build_site.py` | a whole site: one master with the shell, one document per page |
|
|
410
418
|
| `verify_rwd.py` | does every `_t`/`_m` declaration reach the served stylesheet? |
|
|
411
419
|
| `verify_browser.py` | does the **browser** compute what the stylesheet promised - and does the result pass a design audit? |
|
|
420
|
+
| `verify_intro.py` | does the page-load animation play, and - the part that matters - does it END and hand the page back? |
|
|
412
421
|
| `sweep_node_types.py` | commit every node type one per document and assert the delivered HTML |
|
|
413
422
|
| `sweep_style_properties.py` | write every style property and check the compiled CSS |
|
|
414
423
|
| `sweep_node_properties.py` | probe every node property with a value from its own validator chain |
|
|
@@ -438,7 +447,10 @@ python tools/sweep_node_types.py --config sweep.json --setup
|
|
|
438
447
|
python tools/sweep_node_types.py --config sweep.json --sweep --edition all
|
|
439
448
|
python tools/sweep_properties.py --config sweep.json
|
|
440
449
|
python tools/verify_rwd.py --config sweep.json --site sites/moksa.json --csv data/rwd-verification.csv
|
|
441
|
-
python tools/verify_browser.py --config sweep.json --site sites/moksa.json
|
|
450
|
+
python tools/verify_browser.py --config sweep.json --site sites/moksa.json \
|
|
451
|
+
--csv data/browser-verification.csv --audit data/design-audit.csv
|
|
452
|
+
python tools/verify_intro.py --config sweep.json --site sites/moksa.json \
|
|
453
|
+
--csv data/intro-verification.csv --frames shots/intro/
|
|
442
454
|
python tools/sweep_style_properties.py --config sweep.json --page moksa --csv data/style-verification.csv
|
|
443
455
|
python tools/sweep_node_properties.py --config sweep.json --page moksa --csv data/node-property-verification.csv
|
|
444
456
|
python tools/sweep_style_states.py --config sweep.json --post 20 --slug probe-lab --csv data/style-state-verification.csv
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.5.1"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "Upload via Settings -> Skills -> Upload. Claude.ai parses SKILL.md frontmatter and surfaces the skill in your library. The extraction tool (extract-block-schema.php) needs a live WP-CLI connection and won't run in the sandbox; use it from a local terminal against your own site instead.",
|
|
26
26
|
"uploadSteps": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.5.1"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "Auto-loads on session start when SKILL.md frontmatter parses successfully.",
|
|
26
26
|
"verified": true,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.5.1"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "Confirmed (2026-07-11): Codex CLI natively supports the SKILL.md spec. Place SKILL.md under .codex/skills/<name>/ (project) or ~/.codex/skills/<name>/ (personal) and Codex loads the name+description at session start, then the full body on demand. A parallel, broader convention .agents/skills/ (searched from cwd up to repo root, then ~/.agents/skills/) also exists across multiple tools - if your Codex CLI version prioritizes that path instead, mirror the same SKILL.md there.",
|
|
26
26
|
"verified": true,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.5.1"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "CHANGED as of 2026-07-11: GitHub Copilot added a proper '.github/skills/' Agent Skills directory (December 2025), alongside the older single-file .github/copilot-instructions.md convention. This config targets the new skills-directory form. If your Copilot version predates this (pre Dec 2025), use the instructions-append fallback instead (see fallback below).",
|
|
26
26
|
"fallback": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.5.1"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "CHANGED as of 2026-07-11: Gemini CLI now natively supports the same SKILL.md standard as Claude Code and Codex CLI - the same directory-based skill works unmodified. Gemini CLI discovers skills in this precedence order: built-in, extension skills, ~/.gemini/skills/ (personal), .gemini/skills/ (project, shared via version control). At session start Gemini injects each discovered skill's name+description into the system prompt and calls activate_skill when a task matches.",
|
|
26
26
|
"verified": true,
|
package/bin/check-release.mjs
CHANGED
|
@@ -110,6 +110,18 @@ if (browser.includes("OVERRIDDEN"))
|
|
|
110
110
|
|
|
111
111
|
// The design audit ships even when it is empty, and empty has to mean "ran and found
|
|
112
112
|
// nothing" rather than "was never run", so the header alone is the proof.
|
|
113
|
+
// The intro table is two tables in one file - the checks and the timeline readings -
|
|
114
|
+
// so a row count says nothing useful about it. What matters is that every check
|
|
115
|
+
// passed, because a shipped example whose entrance animation never lifts is a blank
|
|
116
|
+
// page for every visitor.
|
|
117
|
+
const intro = read("data/intro-verification.csv");
|
|
118
|
+
if (!intro.startsWith("check,result,detail"))
|
|
119
|
+
fail("data/intro-verification.csv is not the table verify_intro.py writes");
|
|
120
|
+
for (const need of ["PLAYS", "ENDS", "NO_TRAP", "CLEARS", "DEGRADES"])
|
|
121
|
+
if (!new RegExp(`^${need},PASS`, "m").test(intro))
|
|
122
|
+
fail(`intro-verification.csv: ${need} did not pass`);
|
|
123
|
+
if (/,FAIL,/.test(intro)) fail("data/intro-verification.csv carries a failed check");
|
|
124
|
+
|
|
113
125
|
const audit = read("data/design-audit.csv");
|
|
114
126
|
if (!audit.startsWith("url,breakpoints,check,level"))
|
|
115
127
|
fail("data/design-audit.csv is not the audit table verify_browser.py writes");
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
check,result,detail
|
|
2
|
+
PLAYS,PASS,17 of 17 intro elements changed state
|
|
3
|
+
COUNTS,PASS,--mk-n reached 100
|
|
4
|
+
ENDS,PASS,veil is out of hit-testing from 3200ms
|
|
5
|
+
NO_TRAP,PASS,"0 content elements still at opacity 0 in the viewport (34 below the fold awaiting their own scroll timeline, 1 mid-blink)"
|
|
6
|
+
CLEARS,PASS,a real click landed on 'mk-caret-row'
|
|
7
|
+
DEGRADES,PASS,with reduced motion the veil is display:none throughout
|
|
8
|
+
READABLE,PASS,0 elements invisible at 120ms with reduced motion
|
|
9
|
+
|
|
10
|
+
t_ms,counter,veil_visibility,veil_clip,veil_covers_centre,hit_centre,invisible_content
|
|
11
|
+
120,,,,False,html.no-js,0
|
|
12
|
+
350,0,visible,none,True,mk-boot,7
|
|
13
|
+
700,56,visible,none,True,mk-boot,7
|
|
14
|
+
1100,94,visible,none,True,mk-boot,7
|
|
15
|
+
1500,100,visible,none,True,mk-boot,4
|
|
16
|
+
1750,100,visible,inset(0px 0px 2.37106%),True,mk-boot,2
|
|
17
|
+
2050,100,visible,inset(0px 0px 99.6708%),False,mk-caret-row,2
|
|
18
|
+
2400,100,hidden,inset(0px 0px 100%),False,mk-caret-row,0
|
|
19
|
+
3200,100,hidden,inset(0px 0px 100%),False,mk-caret-row,0
|
|
20
|
+
4500,100,hidden,inset(0px 0px 100%),False,mk-caret-row,0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mosaic-headless",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "AI-agent skill: build Mosaic Pro (Nextend) WordPress sites by writing the underlying data model directly - 23 custom tables, no visual editor, no DOM. Every node type, style property and node property swept against a live install and asserted on the delivered HTML and compiled CSS. Installs into Claude Code, Cursor, Codex CLI, Gemini CLI, Copilot, Continue, Windsurf and Claude.ai.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wordpress",
|
|
@@ -188,6 +188,21 @@ wrappers were sitting correctly in the database and in the committed JSON.
|
|
|
188
188
|
The tell is the byte count: if the size the builder reports and the size you fetch
|
|
189
189
|
disagree, you are not looking at what you wrote.
|
|
190
190
|
|
|
191
|
+
**And the cache cuts the other way too, which is worse.** Once every tool in the
|
|
192
|
+
toolchain cache-busts, nothing is left looking at the page a visitor receives. A
|
|
193
|
+
build finishes, every check reports green - responsive verified, computed values
|
|
194
|
+
agreed, design audit clean, the entrance animation asserted on seven counts - and
|
|
195
|
+
the site is still serving the previous document to everybody. Measured here: a
|
|
196
|
+
visitor got 141,134 bytes with none of the new work in it while a cache-busted fetch
|
|
197
|
+
of the same URL gave 148,457. Nothing was wrong except that nobody had looked.
|
|
198
|
+
|
|
199
|
+
`build_site.py` now fetches each page twice at the end of the build, plain and
|
|
200
|
+
cache-busted, and says so when they disagree. It cannot purge the cache - that needs
|
|
201
|
+
credentials a build script has no business holding - but it must not be silent. Two
|
|
202
|
+
fetches of the same live page differ by a few hundred bytes anyway, since nonces and
|
|
203
|
+
ids regenerate per request, so the tolerance is proportional: natural variance
|
|
204
|
+
measured at 0.25%, a stale document at 5%.
|
|
205
|
+
|
|
191
206
|
`tools/verify_rwd.py` now appends a unique `_v=<ms>` query string and sends
|
|
192
207
|
`Cache-Control: no-cache` on every fetch, so the assertion is always made against the
|
|
193
208
|
document that was actually committed. Do the same in anything else that checks a page
|
package/sites/_moksa.py
CHANGED
|
@@ -376,6 +376,10 @@ def motion_css():
|
|
|
376
376
|
for i, (_g, items) in enumerate(STACK)
|
|
377
377
|
for j in range(len(items)))
|
|
378
378
|
marks = ",".join("#mk-mark-%d" % i for i in range(4))
|
|
379
|
+
# the boot list lights one clause at a time
|
|
380
|
+
boot_stagger = "\n".join(
|
|
381
|
+
" #mk-boot-c%d{animation-delay:%dms}" % (i, 260 + i * 120)
|
|
382
|
+
for i in range(len(CLAUSES)))
|
|
379
383
|
# a named view timeline per section, declared on the section and consumed by its
|
|
380
384
|
# index entry - `timeline-scope` on #mk-doc is what lets the name cross between
|
|
381
385
|
# two elements that are not ancestor and descendant
|
|
@@ -528,21 +532,94 @@ def motion_css():
|
|
|
528
532
|
"@media (max-width:1079px){#mk-cue{right:28px}}",
|
|
529
533
|
"@media (max-width:767px){#mk-cue{right:18px;bottom:16px}}",
|
|
530
534
|
|
|
535
|
+
# ── the entrance sequence ────────────────────────────────────────────
|
|
536
|
+
# A page-load animation has one catastrophic failure mode: an overlay that
|
|
537
|
+
# covers the document and never leaves. So the veil is `display:none` in the
|
|
538
|
+
# BASE rule and is only switched on inside the motion query, alongside the
|
|
539
|
+
# animation that removes it. If motion is reduced, or the query never matches,
|
|
540
|
+
# or the stylesheet is truncated, the overlay does not exist at all - the
|
|
541
|
+
# failure direction is "no intro", never "no page".
|
|
542
|
+
"#mk-boot{display:none}",
|
|
543
|
+
# An integer that can be animated, and read back out of the element - which
|
|
544
|
+
# is what makes the counter verifiable rather than merely visible.
|
|
545
|
+
# `inherits:true` is load-bearing, and the reason is not obvious. The
|
|
546
|
+
# animation runs on #mk-boot-num, but the digits are drawn by its ::after
|
|
547
|
+
# through `counter(n)`. With `inherits:false` the pseudo-element never sees
|
|
548
|
+
# the animated value: measured mid-run, --mk-n was 18 on the element and 0
|
|
549
|
+
# on its ::after, so the counter sat at 0 for the whole sequence while the
|
|
550
|
+
# property underneath it animated perfectly.
|
|
551
|
+
"@property --mk-n{syntax:'<integer>';initial-value:0;inherits:true}",
|
|
552
|
+
"@keyframes mk-count{to{--mk-n:100}}",
|
|
553
|
+
"@keyframes mk-boot-out{"
|
|
554
|
+
"0%{clip-path:inset(0 0 0 0)}"
|
|
555
|
+
"100%{clip-path:inset(0 0 100% 0);visibility:hidden}}",
|
|
556
|
+
"@keyframes mk-boot-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}",
|
|
557
|
+
"@keyframes mk-boot-line{from{opacity:.14}"
|
|
558
|
+
"60%{opacity:1;color:rgb(255,90,54)}to{opacity:.55;color:rgb(107,109,113)}}",
|
|
559
|
+
"@keyframes mk-boot-scan{from{transform:translateY(0)}"
|
|
560
|
+
"to{transform:translateY(100vh)}}",
|
|
561
|
+
"@keyframes mk-boot-fade{from{opacity:0}to{opacity:1}}",
|
|
562
|
+
|
|
531
563
|
# ── motion, all of it opt-out-able ───────────────────────────────────
|
|
532
564
|
"@media (prefers-reduced-motion:no-preference){",
|
|
565
|
+
" #mk-boot{display:grid;position:fixed;inset:0;z-index:999;"
|
|
566
|
+
"background:rgb(250,250,247);align-content:center;justify-items:center;"
|
|
567
|
+
"row-gap:22px;overflow:hidden;"
|
|
568
|
+
"animation:mk-boot-out .5s cubic-bezier(.7,0,.3,1) 1.55s forwards}",
|
|
569
|
+
" #mk-boot-head,#mk-boot-foot{position:absolute;left:40px;right:40px;"
|
|
570
|
+
"display:flex;justify-content:space-between;font-family:" + MONO + ";"
|
|
571
|
+
"font-size:10px;letter-spacing:.2em;color:rgb(107,109,113);"
|
|
572
|
+
"animation:mk-boot-fade .4s ease both}",
|
|
573
|
+
" #mk-boot-head{top:34px}",
|
|
574
|
+
" #mk-boot-foot{bottom:30px;animation-delay:.1s}",
|
|
575
|
+
" #mk-boot-num{font-family:" + MONO + ";font-size:96px;font-weight:500;"
|
|
576
|
+
"line-height:1;letter-spacing:-.04em;color:rgb(22,24,28);"
|
|
577
|
+
"font-variant-numeric:tabular-nums;min-width:3ch;text-align:right;"
|
|
578
|
+
"animation:mk-count 1.2s cubic-bezier(.3,0,0,1) .12s both}",
|
|
579
|
+
' #mk-boot-num::after{counter-reset:n var(--mk-n);content:counter(n)}',
|
|
580
|
+
" #mk-boot-row{display:flex;align-items:flex-end;column-gap:8px;"
|
|
581
|
+
"line-height:1}",
|
|
582
|
+
# the darkened cut, not the fill orange: 18px of rgb(255,90,54) on paper is
|
|
583
|
+
# 2.97:1 and the design audit refuses it. The progress rule beside it keeps
|
|
584
|
+
# the bright colour, because a 1px hairline is not text.
|
|
585
|
+
" #mk-boot-pct{font-family:" + MONO + ";font-size:18px;"
|
|
586
|
+
"color:rgb(191,68,40);padding-bottom:12px}",
|
|
587
|
+
" #mk-boot-track{width:min(420px,62vw);height:1px;"
|
|
588
|
+
"background:rgba(22,24,28,.14);position:relative}",
|
|
589
|
+
" #mk-boot-fill{position:absolute;inset:0;background:rgb(255,90,54);"
|
|
590
|
+
"transform-origin:0 50%;"
|
|
591
|
+
"animation:mk-boot-fill 1.2s cubic-bezier(.3,0,0,1) .12s both}",
|
|
592
|
+
" #mk-boot-list{display:flex;column-gap:18px;row-gap:8px;"
|
|
593
|
+
"flex-wrap:wrap;justify-content:center;font-family:" + MONO + ";"
|
|
594
|
+
"font-size:10px;letter-spacing:.18em}",
|
|
595
|
+
" #mk-boot-list>*{opacity:.14;"
|
|
596
|
+
"animation:mk-boot-line .5s ease both}",
|
|
597
|
+
boot_stagger,
|
|
598
|
+
" #mk-boot::before{content:\"\";position:absolute;inset:0;"
|
|
599
|
+
"background:"
|
|
600
|
+
"repeating-linear-gradient(90deg,rgba(22,24,28,.045) 0 1px,"
|
|
601
|
+
"transparent 1px 72px),"
|
|
602
|
+
"repeating-linear-gradient(0deg,rgba(22,24,28,.045) 0 1px,"
|
|
603
|
+
"transparent 1px 72px);"
|
|
604
|
+
"animation:mk-boot-fade .5s ease both}",
|
|
605
|
+
" #mk-boot-scan{position:absolute;left:0;right:0;top:0;height:1px;"
|
|
606
|
+
"background:linear-gradient(90deg,rgba(255,90,54,0),rgba(255,90,54,.55),"
|
|
607
|
+
"rgba(255,90,54,0));animation:mk-boot-scan 1.6s linear .1s both}",
|
|
608
|
+
" @media (max-width:767px){#mk-boot-num{font-size:62px}"
|
|
609
|
+
"#mk-boot-head,#mk-boot-foot{left:18px;right:18px}}",
|
|
533
610
|
" #mk-hl1-mask>*,#mk-hl2-mask>*{transform:translateY(112%);"
|
|
534
611
|
"animation:mk-linein .95s cubic-bezier(.16,1,.3,1) forwards}",
|
|
535
|
-
" #mk-hl1-mask>*{animation-delay
|
|
536
|
-
" #mk-hl2-mask>*{animation-delay
|
|
612
|
+
" #mk-hl1-mask>*{animation-delay:1.72s}",
|
|
613
|
+
" #mk-hl2-mask>*{animation-delay:1.84s}",
|
|
537
614
|
" #mk-mast-meta,#mk-mast-lede,#mk-mast-cta{opacity:0;"
|
|
538
615
|
"animation:mk-softin .8s cubic-bezier(.16,1,.3,1) forwards}",
|
|
539
|
-
" #mk-mast-meta{animation-delay
|
|
540
|
-
" #mk-mast-lede{animation-delay
|
|
541
|
-
" #mk-mast-cta{animation-delay
|
|
616
|
+
" #mk-mast-meta{animation-delay:1.62s}",
|
|
617
|
+
" #mk-mast-lede{animation-delay:2.10s}",
|
|
618
|
+
" #mk-mast-cta{animation-delay:2.22s}",
|
|
542
619
|
" #mk-caret{animation:mk-caret 1.15s steps(1,end) infinite;"
|
|
543
|
-
"animation-delay
|
|
620
|
+
"animation-delay:2.4s}",
|
|
544
621
|
" #mk-mast-rule{animation:mk-drawx .9s cubic-bezier(.2,.7,.3,1) forwards;"
|
|
545
|
-
"animation-delay
|
|
622
|
+
"animation-delay:1.96s}",
|
|
546
623
|
" #mk-spec-rule{animation:mk-drawx .9s cubic-bezier(.2,.7,.3,1) forwards;"
|
|
547
624
|
"animation-delay:.9s}",
|
|
548
625
|
" #mk-cue{opacity:0;animation:mk-softin .8s ease forwards;"
|
|
@@ -1184,6 +1261,40 @@ CONTACT = section("contact", [wrap("mk-contact-in", [
|
|
|
1184
1261
|
]),
|
|
1185
1262
|
])])
|
|
1186
1263
|
|
|
1264
|
+
# ── the entrance sequence ─────────────────────────────────────────────────────
|
|
1265
|
+
# A boot screen, because the page is a specification document and this is what one
|
|
1266
|
+
# looks like while it is being read off a machine. It is CSS only: a counter that is
|
|
1267
|
+
# a real animated integer (`@property --mk-n`), a rule that fills with it, the seven
|
|
1268
|
+
# clauses reporting in one at a time, and a scan line. Then the whole panel clips
|
|
1269
|
+
# upward and is gone.
|
|
1270
|
+
#
|
|
1271
|
+
# Every id here is deliberate. `verify_intro.py` samples them over the first seconds
|
|
1272
|
+
# of the page's life, which is the only window in which any of this exists.
|
|
1273
|
+
BOOT = box("mk-boot", {}, [
|
|
1274
|
+
box("mk-boot-scan", {}, []),
|
|
1275
|
+
box("mk-boot-head", {}, [
|
|
1276
|
+
mono("MOKSA WEB — STUDIO PROFILE", size="10px", color="--mk-faint",
|
|
1277
|
+
track="0.2em"),
|
|
1278
|
+
mono("REV. 2026.09", size="10px", color="--mk-faint", track="0.2em"),
|
|
1279
|
+
]),
|
|
1280
|
+
box("mk-boot-row", {}, [
|
|
1281
|
+
box("mk-boot-num", {}, []),
|
|
1282
|
+
box("mk-boot-pct", {}, [T("p", "%")]),
|
|
1283
|
+
]),
|
|
1284
|
+
box("mk-boot-track", {}, [box("mk-boot-fill", {}, [])]),
|
|
1285
|
+
box("mk-boot-list", {},
|
|
1286
|
+
# each clause gets its own box because the id is what the stagger
|
|
1287
|
+
# targets, and `mono()` puts everything it is given into the style
|
|
1288
|
+
[box("mk-boot-c%d" % i, {},
|
|
1289
|
+
[mono("§" + num + " " + name, size="10px", color="--mk-faint",
|
|
1290
|
+
track="0.18em")])
|
|
1291
|
+
for i, (num, name, _h, _t) in enumerate(CLAUSES)]),
|
|
1292
|
+
box("mk-boot-foot", {}, [
|
|
1293
|
+
mono("INITIALISING", size="10px", color="--mk-faint", track="0.2em"),
|
|
1294
|
+
mono("TAICHUNG, TW", size="10px", color="--mk-faint", track="0.2em"),
|
|
1295
|
+
]),
|
|
1296
|
+
])
|
|
1297
|
+
|
|
1187
1298
|
# Registration marks. A spec sheet is a printed object and these are how one is
|
|
1188
1299
|
# trimmed; here they simply say the page has edges that were decided.
|
|
1189
1300
|
MARKS = box("mk-marks", {}, [box("mk-mark-%d" % i, {}, []) for i in range(4)])
|
|
@@ -1211,7 +1322,7 @@ CUE = box("mk-cue", {}, [
|
|
|
1211
1322
|
])
|
|
1212
1323
|
|
|
1213
1324
|
HOME_TREE = {"type": "div", "data": {"attrID": "mk-home"},
|
|
1214
|
-
"children": [box("mk-doc", {}, [
|
|
1325
|
+
"children": [BOOT, box("mk-doc", {}, [
|
|
1215
1326
|
MARKS, INDEX, CUE,
|
|
1216
1327
|
MASTHEAD, TICKER_BAND, SERVICE_SEC, PROCESS_SEC, WORK_SEC,
|
|
1217
1328
|
STACK_SEC, PRODUCT_SEC, VOICE_SEC, CONTACT,
|
package/sites/moksa.json
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"data": {
|
|
55
55
|
"attrID": "mk-motion-css",
|
|
56
56
|
"insertLocation": "head",
|
|
57
|
-
"content": "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Space+Grotesk:wght@500;600;700&family=Noto+Sans+TC:wght@300;400;500&display=swap\">\n<style id=\"mk-motion\">\n@keyframes mk-linein{from{transform:translateY(110%)}to{transform:translateY(0)}}\n@keyframes mk-softin{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}\n@keyframes mk-rise{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:none}}\n@keyframes mk-drawx{from{transform:scaleX(0)}to{transform:scaleX(1)}}\n@keyframes mk-ticker{from{transform:translateX(0)}to{transform:translateX(-50%)}}\n@keyframes mk-progress{from{width:0%}to{width:100%}}\n@keyframes mk-headrule{from{opacity:0}to{opacity:1}}\n@keyframes mk-headbg{from{background:rgba(250,250,247,0)}to{background:rgba(250,250,247,.9)}}\n@keyframes mk-caret{0%,45%{opacity:1}50%,95%{opacity:0}100%{opacity:1}}\n@keyframes mk-cuearrow{0%{transform:translateY(-3px) rotate(45deg);opacity:0}25%{opacity:1}70%{opacity:1}100%{transform:translateY(20px) rotate(45deg);opacity:0}}\n@keyframes mk-cuefade{0%,25%{opacity:1}100%{opacity:0}}\n@keyframes mk-digit{from{transform:translateY(105%)}to{transform:translateY(0)}}\n@keyframes mk-sweep{from{transform:scaleX(0)}to{transform:scaleX(1)}}\n@keyframes mk-markin{from{opacity:0;transform:scale(.4)}to{opacity:1;transform:scale(1)}}\n@keyframes mk-idxon{from,to{color:rgb(255,90,54);letter-spacing:.2em}}\n@keyframes mk-dotin{from{transform:scale(0)}to{transform:scale(1)}}\n#mk-doc{position:relative}\n#mk-doc::before,#mk-doc::after{content:\"\";position:fixed;top:0;bottom:0;width:1px;background:rgba(22,24,28,.16);z-index:1;pointer-events:none}\n#mk-doc::before{left:40px}\n#mk-doc::after{right:40px}\n@media (max-width:1079px){#mk-doc::before{left:28px}#mk-doc::after{right:28px}}\n@media (max-width:767px){#mk-doc::before,#mk-doc::after{display:none}}\n#mk-header{position:fixed;top:0;left:0;right:0;z-index:100;background:rgba(250,250,247,0);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}\n#mk-header::after{content:\"\";position:absolute;left:0;right:0;bottom:0;height:1px;background:rgba(22,24,28,.16);opacity:0}\n#mk-nav>*{position:relative}\n#mk-nav>*::after{content:\"\";position:absolute;left:0;right:100%;bottom:-5px;height:1px;background:rgb(255,90,54);transition:right .28s cubic-bezier(.2,.7,.3,1)}\n#mk-nav>*:hover::after{right:0}\n@media (hover:none){#mk-nav>*::after{right:0;opacity:.45}}\n#mk-progress-track{position:fixed;top:0;left:0;right:0;height:2px;z-index:101;pointer-events:none;background:rgba(255,90,54,.16)}\n#mk-progress{position:absolute;left:0;top:0;width:0;height:100%;background:rgb(255,90,54)}\n#mk-mast-in{padding-top:128px}\n@media (max-width:1079px){#mk-mast-in{padding-top:104px}}\n@media (max-width:767px){#mk-mast-in{padding-top:88px}}\n#mk-caret{width:13px;height:19px;background:rgb(255,90,54)}\n#mk-mast-rule,#mk-spec-rule{height:1px;background:rgba(22,24,28,.16);transform:scaleX(0);transform-origin:0 50%}\n#mk-fig-0>div>*,#mk-fig-1>div>*,#mk-fig-2>div>*,#mk-fig-3>div>*{display:block;transform:translateY(105%)}\n#mk-work-0,#mk-work-1,#mk-work-2,#mk-work-3,#mk-work-4,#mk-work-5,#mk-work-6,#mk-work-7,#mk-work-8{position:relative;transition:background-color .2s ease,padding-left .28s cubic-bezier(.2,.7,.3,1)}\n#mk-work-0::after,#mk-work-1::after,#mk-work-2::after,#mk-work-3::after,#mk-work-4::after,#mk-work-5::after,#mk-work-6::after,#mk-work-7::after,#mk-work-8::after{content:\"\";position:absolute;left:0;right:0;bottom:-1px;height:1px;background:rgb(255,90,54);transform:scaleX(0);transform-origin:0 50%}\n#mk-work-0:hover,#mk-work-1:hover,#mk-work-2:hover,#mk-work-3:hover,#mk-work-4:hover,#mk-work-5:hover,#mk-work-6:hover,#mk-work-7:hover,#mk-work-8:hover{background-color:rgba(255,90,54,.06);padding-left:14px}\n#mk-work-0:hover #mk-work-idx-0,#mk-work-1:hover #mk-work-idx-1,#mk-work-2:hover #mk-work-idx-2,#mk-work-3:hover #mk-work-idx-3,#mk-work-4:hover #mk-work-idx-4,#mk-work-5:hover #mk-work-idx-5,#mk-work-6:hover #mk-work-idx-6,#mk-work-7:hover #mk-work-idx-7,#mk-work-8:hover #mk-work-idx-8{color:rgb(255,90,54)}\n#mk-mark-0,#mk-mark-1,#mk-mark-2,#mk-mark-3{position:fixed;width:13px;height:13px;z-index:2;pointer-events:none;opacity:0}\n#mk-mark-0{top:66px;left:40px;border-left:1px solid rgba(22,24,28,.42);border-top:1px solid rgba(22,24,28,.42)}\n#mk-mark-1{top:66px;right:40px;border-right:1px solid rgba(22,24,28,.42);border-top:1px solid rgba(22,24,28,.42)}\n#mk-mark-2{bottom:20px;left:40px;border-left:1px solid rgba(22,24,28,.42);border-bottom:1px solid rgba(22,24,28,.42)}\n#mk-mark-3{bottom:20px;right:40px;border-right:1px solid rgba(22,24,28,.42);border-bottom:1px solid rgba(22,24,28,.42)}\n@media (max-width:1279px){#mk-mark-0,#mk-mark-1,#mk-mark-2,#mk-mark-3{display:none}}\n#mk-index{position:fixed;left:14px;top:50%;transform:translateY(-50%);z-index:3;display:grid;row-gap:11px;pointer-events:auto}\n@media (max-width:1439px){#mk-index{display:none}}\n#mk-proc-dot-0,#mk-proc-dot-1,#mk-proc-dot-2,#mk-proc-dot-3{width:7px;height:7px;background:rgb(255,90,54);border-radius:50%;transform:scale(0);margin-top:-4px}\n#mk-tag-0-0:hover,#mk-tag-0-1:hover,#mk-tag-0-2:hover,#mk-tag-0-3:hover,#mk-tag-0-4:hover,#mk-tag-1-0:hover,#mk-tag-1-1:hover,#mk-tag-1-2:hover,#mk-tag-1-3:hover,#mk-tag-1-4:hover,#mk-tag-2-0:hover,#mk-tag-2-1:hover,#mk-tag-2-2:hover,#mk-tag-2-3:hover,#mk-tag-2-4:hover,#mk-tag-3-0:hover,#mk-tag-3-1:hover,#mk-tag-3-2:hover,#mk-tag-3-3:hover,#mk-tag-3-4:hover,#mk-tag-4-0:hover,#mk-tag-4-1:hover,#mk-tag-4-2:hover,#mk-tag-4-3:hover,#mk-tag-4-4:hover{border-color:rgb(255,90,54)}\n#mk-ticker-track{display:flex;width:max-content;will-change:transform}\n#mk-ticker:hover #mk-ticker-track{animation-play-state:paused}\nhtml{scroll-behavior:smooth}\n#services,#works,#products,#contact{scroll-margin-top:82px}\n#mk-home{scroll-margin-top:0}\n@media (max-width:767px){#services,#works,#products,#contact{scroll-margin-top:68px}}\n#mk-cue{position:fixed;right:40px;bottom:24px;z-index:90;display:flex;align-items:center;column-gap:10px;pointer-events:none}\n#mk-cue-rail{position:relative;width:9px;height:24px}\n#mk-cue-rail::after{content:\"\";position:absolute;left:50%;top:0;width:7px;height:7px;margin-left:-4px;opacity:0;border-right:1px solid rgb(255,90,54);border-bottom:1px solid rgb(255,90,54)}\n@media (max-width:1079px){#mk-cue{right:28px}}\n@media (max-width:767px){#mk-cue{right:18px;bottom:16px}}\n@media (prefers-reduced-motion:no-preference){\n #mk-hl1-mask>*,#mk-hl2-mask>*{transform:translateY(112%);animation:mk-linein .95s cubic-bezier(.16,1,.3,1) forwards}\n #mk-hl1-mask>*{animation-delay:.2s}\n #mk-hl2-mask>*{animation-delay:.32s}\n #mk-mast-meta,#mk-mast-lede,#mk-mast-cta{opacity:0;animation:mk-softin .8s cubic-bezier(.16,1,.3,1) forwards}\n #mk-mast-meta{animation-delay:.06s}\n #mk-mast-lede{animation-delay:.66s}\n #mk-mast-cta{animation-delay:.78s}\n #mk-caret{animation:mk-caret 1.15s steps(1,end) infinite;animation-delay:.95s}\n #mk-mast-rule{animation:mk-drawx .9s cubic-bezier(.2,.7,.3,1) forwards;animation-delay:.44s}\n #mk-spec-rule{animation:mk-drawx .9s cubic-bezier(.2,.7,.3,1) forwards;animation-delay:.9s}\n #mk-cue{opacity:0;animation:mk-softin .8s ease forwards;animation-delay:1.1s}\n #mk-cue-rail::after{animation:mk-cuearrow 1.9s cubic-bezier(.4,0,.5,1) infinite}\n #mk-ticker-track{animation:mk-ticker 40s linear infinite}\n #mk-mark-0,#mk-mark-1,#mk-mark-2,#mk-mark-3{animation:mk-markin .5s cubic-bezier(.16,1,.3,1) forwards}\n #mk-mark-0{animation-delay:1.15s}\n #mk-mark-1{animation-delay:1.24s}\n #mk-mark-2{animation-delay:1.33s}\n #mk-mark-3{animation-delay:1.42s}\n @supports (animation-timeline:view()){\n #mk-spec,#mk-svc-head,#mk-svc-0,#mk-svc-1,#mk-svc-2,#mk-svc-3,#mk-proc-head,#mk-proc-0,#mk-proc-1,#mk-proc-2,#mk-proc-3,#mk-works-head,#mk-work-0,#mk-work-1,#mk-work-2,#mk-work-3,#mk-work-4,#mk-work-5,#mk-work-6,#mk-work-7,#mk-work-8,#mk-stack-head,#mk-stack-0,#mk-stack-1,#mk-stack-2,#mk-stack-3,#mk-stack-4,#mk-prod-head,#mk-prod-0,#mk-prod-1,#mk-voice-head,#mk-voice-0,#mk-voice-1,#mk-voice-2,#mk-contact-head{animation:mk-rise .01s linear both;animation-timeline:view();animation-range:entry 2% cover 36%}\n #mk-svc-0{animation-range:entry 6% cover 40%}\n #mk-svc-1{animation-range:entry 10% cover 44%}\n #mk-svc-2{animation-range:entry 14% cover 48%}\n #mk-svc-3{animation-range:entry 18% cover 52%}\n #mk-proc-0{animation-range:entry 6% cover 40%}\n #mk-proc-1{animation-range:entry 10% cover 44%}\n #mk-proc-2{animation-range:entry 14% cover 48%}\n #mk-proc-3{animation-range:entry 18% cover 52%}\n #mk-work-0{animation-range:entry 6% cover 40%}\n #mk-work-1{animation-range:entry 6% cover 40%}\n #mk-work-2{animation-range:entry 10% cover 44%}\n #mk-work-3{animation-range:entry 10% cover 44%}\n #mk-work-4{animation-range:entry 14% cover 48%}\n #mk-work-5{animation-range:entry 14% cover 48%}\n #mk-work-6{animation-range:entry 18% cover 52%}\n #mk-work-7{animation-range:entry 18% cover 52%}\n #mk-work-8{animation-range:entry 22% cover 56%}\n #mk-stack-0{animation-range:entry 6% cover 40%}\n #mk-stack-1{animation-range:entry 10% cover 44%}\n #mk-stack-2{animation-range:entry 14% cover 48%}\n #mk-stack-3{animation-range:entry 18% cover 52%}\n #mk-stack-4{animation-range:entry 22% cover 56%}\n #mk-prod-0{animation-range:entry 6% cover 40%}\n #mk-prod-1{animation-range:entry 10% cover 44%}\n #mk-voice-0{animation-range:entry 6% cover 40%}\n #mk-voice-1{animation-range:entry 10% cover 44%}\n #mk-voice-2{animation-range:entry 14% cover 48%}\n #mk-fig-0>div>*,#mk-fig-1>div>*,#mk-fig-2>div>*,#mk-fig-3>div>*{animation:mk-digit .7s cubic-bezier(.16,1,.3,1) both;animation-delay:var(--d,0ms)}\n #mk-fig-0>div,#mk-fig-1>div,#mk-fig-2>div,#mk-fig-3>div{animation:mk-rise .01s linear both;animation-timeline:view();animation-range:entry 0% entry 1%}\n #mk-work-0::after,#mk-work-1::after,#mk-work-2::after,#mk-work-3::after,#mk-work-4::after,#mk-work-5::after,#mk-work-6::after,#mk-work-7::after,#mk-work-8::after{animation:mk-sweep .01s linear both;animation-timeline:view();animation-range:entry 12% cover 24%}\n #mk-proc-dot-0,#mk-proc-dot-1,#mk-proc-dot-2,#mk-proc-dot-3{animation:mk-dotin .01s linear both;animation-timeline:view();animation-range:entry 14% cover 26%}\n }\n @supports (timeline-scope:--x){\n #mk-doc{timeline-scope:--mk-s1,--mk-s2,--mk-s3,--mk-s4,--mk-s5,--mk-s6,--mk-s7}\n#services{view-timeline-name:--mk-s1;view-timeline-inset:45% 45%}\n#process{view-timeline-name:--mk-s2;view-timeline-inset:45% 45%}\n#works{view-timeline-name:--mk-s3;view-timeline-inset:45% 45%}\n#stack{view-timeline-name:--mk-s4;view-timeline-inset:45% 45%}\n#products{view-timeline-name:--mk-s5;view-timeline-inset:45% 45%}\n#mk-voices{view-timeline-name:--mk-s6;view-timeline-inset:45% 45%}\n#contact{view-timeline-name:--mk-s7;view-timeline-inset:45% 45%}\n #mk-idx-0{animation:mk-idxon linear;animation-timeline:--mk-s1;animation-range:cover 0% cover 100%}\n #mk-idx-1{animation:mk-idxon linear;animation-timeline:--mk-s2;animation-range:cover 0% cover 100%}\n #mk-idx-2{animation:mk-idxon linear;animation-timeline:--mk-s3;animation-range:cover 0% cover 100%}\n #mk-idx-3{animation:mk-idxon linear;animation-timeline:--mk-s4;animation-range:cover 0% cover 100%}\n #mk-idx-4{animation:mk-idxon linear;animation-timeline:--mk-s5;animation-range:cover 0% cover 100%}\n #mk-idx-5{animation:mk-idxon linear;animation-timeline:--mk-s6;animation-range:cover 0% cover 100%}\n #mk-idx-6{animation:mk-idxon linear;animation-timeline:--mk-s7;animation-range:cover 0% cover 100%}\n }\n @supports (animation-timeline:scroll()){\n #mk-progress{animation:mk-progress linear both;animation-timeline:scroll(root)}\n #mk-cue{animation:mk-cuefade linear both;animation-timeline:scroll(root);animation-range:0px 260px}\n #mk-header{animation:mk-headbg linear both;animation-timeline:scroll(root);animation-range:20px 120px}\n #mk-header::after{animation:mk-headrule linear both;animation-timeline:scroll(root);animation-range:20px 120px}\n }\n}\n</style>",
|
|
57
|
+
"content": "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Space+Grotesk:wght@500;600;700&family=Noto+Sans+TC:wght@300;400;500&display=swap\">\n<style id=\"mk-motion\">\n@keyframes mk-linein{from{transform:translateY(110%)}to{transform:translateY(0)}}\n@keyframes mk-softin{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}\n@keyframes mk-rise{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:none}}\n@keyframes mk-drawx{from{transform:scaleX(0)}to{transform:scaleX(1)}}\n@keyframes mk-ticker{from{transform:translateX(0)}to{transform:translateX(-50%)}}\n@keyframes mk-progress{from{width:0%}to{width:100%}}\n@keyframes mk-headrule{from{opacity:0}to{opacity:1}}\n@keyframes mk-headbg{from{background:rgba(250,250,247,0)}to{background:rgba(250,250,247,.9)}}\n@keyframes mk-caret{0%,45%{opacity:1}50%,95%{opacity:0}100%{opacity:1}}\n@keyframes mk-cuearrow{0%{transform:translateY(-3px) rotate(45deg);opacity:0}25%{opacity:1}70%{opacity:1}100%{transform:translateY(20px) rotate(45deg);opacity:0}}\n@keyframes mk-cuefade{0%,25%{opacity:1}100%{opacity:0}}\n@keyframes mk-digit{from{transform:translateY(105%)}to{transform:translateY(0)}}\n@keyframes mk-sweep{from{transform:scaleX(0)}to{transform:scaleX(1)}}\n@keyframes mk-markin{from{opacity:0;transform:scale(.4)}to{opacity:1;transform:scale(1)}}\n@keyframes mk-idxon{from,to{color:rgb(255,90,54);letter-spacing:.2em}}\n@keyframes mk-dotin{from{transform:scale(0)}to{transform:scale(1)}}\n#mk-doc{position:relative}\n#mk-doc::before,#mk-doc::after{content:\"\";position:fixed;top:0;bottom:0;width:1px;background:rgba(22,24,28,.16);z-index:1;pointer-events:none}\n#mk-doc::before{left:40px}\n#mk-doc::after{right:40px}\n@media (max-width:1079px){#mk-doc::before{left:28px}#mk-doc::after{right:28px}}\n@media (max-width:767px){#mk-doc::before,#mk-doc::after{display:none}}\n#mk-header{position:fixed;top:0;left:0;right:0;z-index:100;background:rgba(250,250,247,0);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}\n#mk-header::after{content:\"\";position:absolute;left:0;right:0;bottom:0;height:1px;background:rgba(22,24,28,.16);opacity:0}\n#mk-nav>*{position:relative}\n#mk-nav>*::after{content:\"\";position:absolute;left:0;right:100%;bottom:-5px;height:1px;background:rgb(255,90,54);transition:right .28s cubic-bezier(.2,.7,.3,1)}\n#mk-nav>*:hover::after{right:0}\n@media (hover:none){#mk-nav>*::after{right:0;opacity:.45}}\n#mk-progress-track{position:fixed;top:0;left:0;right:0;height:2px;z-index:101;pointer-events:none;background:rgba(255,90,54,.16)}\n#mk-progress{position:absolute;left:0;top:0;width:0;height:100%;background:rgb(255,90,54)}\n#mk-mast-in{padding-top:128px}\n@media (max-width:1079px){#mk-mast-in{padding-top:104px}}\n@media (max-width:767px){#mk-mast-in{padding-top:88px}}\n#mk-caret{width:13px;height:19px;background:rgb(255,90,54)}\n#mk-mast-rule,#mk-spec-rule{height:1px;background:rgba(22,24,28,.16);transform:scaleX(0);transform-origin:0 50%}\n#mk-fig-0>div>*,#mk-fig-1>div>*,#mk-fig-2>div>*,#mk-fig-3>div>*{display:block;transform:translateY(105%)}\n#mk-work-0,#mk-work-1,#mk-work-2,#mk-work-3,#mk-work-4,#mk-work-5,#mk-work-6,#mk-work-7,#mk-work-8{position:relative;transition:background-color .2s ease,padding-left .28s cubic-bezier(.2,.7,.3,1)}\n#mk-work-0::after,#mk-work-1::after,#mk-work-2::after,#mk-work-3::after,#mk-work-4::after,#mk-work-5::after,#mk-work-6::after,#mk-work-7::after,#mk-work-8::after{content:\"\";position:absolute;left:0;right:0;bottom:-1px;height:1px;background:rgb(255,90,54);transform:scaleX(0);transform-origin:0 50%}\n#mk-work-0:hover,#mk-work-1:hover,#mk-work-2:hover,#mk-work-3:hover,#mk-work-4:hover,#mk-work-5:hover,#mk-work-6:hover,#mk-work-7:hover,#mk-work-8:hover{background-color:rgba(255,90,54,.06);padding-left:14px}\n#mk-work-0:hover #mk-work-idx-0,#mk-work-1:hover #mk-work-idx-1,#mk-work-2:hover #mk-work-idx-2,#mk-work-3:hover #mk-work-idx-3,#mk-work-4:hover #mk-work-idx-4,#mk-work-5:hover #mk-work-idx-5,#mk-work-6:hover #mk-work-idx-6,#mk-work-7:hover #mk-work-idx-7,#mk-work-8:hover #mk-work-idx-8{color:rgb(255,90,54)}\n#mk-mark-0,#mk-mark-1,#mk-mark-2,#mk-mark-3{position:fixed;width:13px;height:13px;z-index:2;pointer-events:none;opacity:0}\n#mk-mark-0{top:66px;left:40px;border-left:1px solid rgba(22,24,28,.42);border-top:1px solid rgba(22,24,28,.42)}\n#mk-mark-1{top:66px;right:40px;border-right:1px solid rgba(22,24,28,.42);border-top:1px solid rgba(22,24,28,.42)}\n#mk-mark-2{bottom:20px;left:40px;border-left:1px solid rgba(22,24,28,.42);border-bottom:1px solid rgba(22,24,28,.42)}\n#mk-mark-3{bottom:20px;right:40px;border-right:1px solid rgba(22,24,28,.42);border-bottom:1px solid rgba(22,24,28,.42)}\n@media (max-width:1279px){#mk-mark-0,#mk-mark-1,#mk-mark-2,#mk-mark-3{display:none}}\n#mk-index{position:fixed;left:14px;top:50%;transform:translateY(-50%);z-index:3;display:grid;row-gap:11px;pointer-events:auto}\n@media (max-width:1439px){#mk-index{display:none}}\n#mk-proc-dot-0,#mk-proc-dot-1,#mk-proc-dot-2,#mk-proc-dot-3{width:7px;height:7px;background:rgb(255,90,54);border-radius:50%;transform:scale(0);margin-top:-4px}\n#mk-tag-0-0:hover,#mk-tag-0-1:hover,#mk-tag-0-2:hover,#mk-tag-0-3:hover,#mk-tag-0-4:hover,#mk-tag-1-0:hover,#mk-tag-1-1:hover,#mk-tag-1-2:hover,#mk-tag-1-3:hover,#mk-tag-1-4:hover,#mk-tag-2-0:hover,#mk-tag-2-1:hover,#mk-tag-2-2:hover,#mk-tag-2-3:hover,#mk-tag-2-4:hover,#mk-tag-3-0:hover,#mk-tag-3-1:hover,#mk-tag-3-2:hover,#mk-tag-3-3:hover,#mk-tag-3-4:hover,#mk-tag-4-0:hover,#mk-tag-4-1:hover,#mk-tag-4-2:hover,#mk-tag-4-3:hover,#mk-tag-4-4:hover{border-color:rgb(255,90,54)}\n#mk-ticker-track{display:flex;width:max-content;will-change:transform}\n#mk-ticker:hover #mk-ticker-track{animation-play-state:paused}\nhtml{scroll-behavior:smooth}\n#services,#works,#products,#contact{scroll-margin-top:82px}\n#mk-home{scroll-margin-top:0}\n@media (max-width:767px){#services,#works,#products,#contact{scroll-margin-top:68px}}\n#mk-cue{position:fixed;right:40px;bottom:24px;z-index:90;display:flex;align-items:center;column-gap:10px;pointer-events:none}\n#mk-cue-rail{position:relative;width:9px;height:24px}\n#mk-cue-rail::after{content:\"\";position:absolute;left:50%;top:0;width:7px;height:7px;margin-left:-4px;opacity:0;border-right:1px solid rgb(255,90,54);border-bottom:1px solid rgb(255,90,54)}\n@media (max-width:1079px){#mk-cue{right:28px}}\n@media (max-width:767px){#mk-cue{right:18px;bottom:16px}}\n#mk-boot{display:none}\n@property --mk-n{syntax:'<integer>';initial-value:0;inherits:true}\n@keyframes mk-count{to{--mk-n:100}}\n@keyframes mk-boot-out{0%{clip-path:inset(0 0 0 0)}100%{clip-path:inset(0 0 100% 0);visibility:hidden}}\n@keyframes mk-boot-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}\n@keyframes mk-boot-line{from{opacity:.14}60%{opacity:1;color:rgb(255,90,54)}to{opacity:.55;color:rgb(107,109,113)}}\n@keyframes mk-boot-scan{from{transform:translateY(0)}to{transform:translateY(100vh)}}\n@keyframes mk-boot-fade{from{opacity:0}to{opacity:1}}\n@media (prefers-reduced-motion:no-preference){\n #mk-boot{display:grid;position:fixed;inset:0;z-index:999;background:rgb(250,250,247);align-content:center;justify-items:center;row-gap:22px;overflow:hidden;animation:mk-boot-out .5s cubic-bezier(.7,0,.3,1) 1.55s forwards}\n #mk-boot-head,#mk-boot-foot{position:absolute;left:40px;right:40px;display:flex;justify-content:space-between;font-family:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;letter-spacing:.2em;color:rgb(107,109,113);animation:mk-boot-fade .4s ease both}\n #mk-boot-head{top:34px}\n #mk-boot-foot{bottom:30px;animation-delay:.1s}\n #mk-boot-num{font-family:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:96px;font-weight:500;line-height:1;letter-spacing:-.04em;color:rgb(22,24,28);font-variant-numeric:tabular-nums;min-width:3ch;text-align:right;animation:mk-count 1.2s cubic-bezier(.3,0,0,1) .12s both}\n #mk-boot-num::after{counter-reset:n var(--mk-n);content:counter(n)}\n #mk-boot-row{display:flex;align-items:flex-end;column-gap:8px;line-height:1}\n #mk-boot-pct{font-family:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:18px;color:rgb(191,68,40);padding-bottom:12px}\n #mk-boot-track{width:min(420px,62vw);height:1px;background:rgba(22,24,28,.14);position:relative}\n #mk-boot-fill{position:absolute;inset:0;background:rgb(255,90,54);transform-origin:0 50%;animation:mk-boot-fill 1.2s cubic-bezier(.3,0,0,1) .12s both}\n #mk-boot-list{display:flex;column-gap:18px;row-gap:8px;flex-wrap:wrap;justify-content:center;font-family:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;letter-spacing:.18em}\n #mk-boot-list>*{opacity:.14;animation:mk-boot-line .5s ease both}\n #mk-boot-c0{animation-delay:260ms}\n #mk-boot-c1{animation-delay:380ms}\n #mk-boot-c2{animation-delay:500ms}\n #mk-boot-c3{animation-delay:620ms}\n #mk-boot-c4{animation-delay:740ms}\n #mk-boot-c5{animation-delay:860ms}\n #mk-boot-c6{animation-delay:980ms}\n #mk-boot::before{content:\"\";position:absolute;inset:0;background:repeating-linear-gradient(90deg,rgba(22,24,28,.045) 0 1px,transparent 1px 72px),repeating-linear-gradient(0deg,rgba(22,24,28,.045) 0 1px,transparent 1px 72px);animation:mk-boot-fade .5s ease both}\n #mk-boot-scan{position:absolute;left:0;right:0;top:0;height:1px;background:linear-gradient(90deg,rgba(255,90,54,0),rgba(255,90,54,.55),rgba(255,90,54,0));animation:mk-boot-scan 1.6s linear .1s both}\n @media (max-width:767px){#mk-boot-num{font-size:62px}#mk-boot-head,#mk-boot-foot{left:18px;right:18px}}\n #mk-hl1-mask>*,#mk-hl2-mask>*{transform:translateY(112%);animation:mk-linein .95s cubic-bezier(.16,1,.3,1) forwards}\n #mk-hl1-mask>*{animation-delay:1.72s}\n #mk-hl2-mask>*{animation-delay:1.84s}\n #mk-mast-meta,#mk-mast-lede,#mk-mast-cta{opacity:0;animation:mk-softin .8s cubic-bezier(.16,1,.3,1) forwards}\n #mk-mast-meta{animation-delay:1.62s}\n #mk-mast-lede{animation-delay:2.10s}\n #mk-mast-cta{animation-delay:2.22s}\n #mk-caret{animation:mk-caret 1.15s steps(1,end) infinite;animation-delay:2.4s}\n #mk-mast-rule{animation:mk-drawx .9s cubic-bezier(.2,.7,.3,1) forwards;animation-delay:1.96s}\n #mk-spec-rule{animation:mk-drawx .9s cubic-bezier(.2,.7,.3,1) forwards;animation-delay:.9s}\n #mk-cue{opacity:0;animation:mk-softin .8s ease forwards;animation-delay:1.1s}\n #mk-cue-rail::after{animation:mk-cuearrow 1.9s cubic-bezier(.4,0,.5,1) infinite}\n #mk-ticker-track{animation:mk-ticker 40s linear infinite}\n #mk-mark-0,#mk-mark-1,#mk-mark-2,#mk-mark-3{animation:mk-markin .5s cubic-bezier(.16,1,.3,1) forwards}\n #mk-mark-0{animation-delay:1.15s}\n #mk-mark-1{animation-delay:1.24s}\n #mk-mark-2{animation-delay:1.33s}\n #mk-mark-3{animation-delay:1.42s}\n @supports (animation-timeline:view()){\n #mk-spec,#mk-svc-head,#mk-svc-0,#mk-svc-1,#mk-svc-2,#mk-svc-3,#mk-proc-head,#mk-proc-0,#mk-proc-1,#mk-proc-2,#mk-proc-3,#mk-works-head,#mk-work-0,#mk-work-1,#mk-work-2,#mk-work-3,#mk-work-4,#mk-work-5,#mk-work-6,#mk-work-7,#mk-work-8,#mk-stack-head,#mk-stack-0,#mk-stack-1,#mk-stack-2,#mk-stack-3,#mk-stack-4,#mk-prod-head,#mk-prod-0,#mk-prod-1,#mk-voice-head,#mk-voice-0,#mk-voice-1,#mk-voice-2,#mk-contact-head{animation:mk-rise .01s linear both;animation-timeline:view();animation-range:entry 2% cover 36%}\n #mk-svc-0{animation-range:entry 6% cover 40%}\n #mk-svc-1{animation-range:entry 10% cover 44%}\n #mk-svc-2{animation-range:entry 14% cover 48%}\n #mk-svc-3{animation-range:entry 18% cover 52%}\n #mk-proc-0{animation-range:entry 6% cover 40%}\n #mk-proc-1{animation-range:entry 10% cover 44%}\n #mk-proc-2{animation-range:entry 14% cover 48%}\n #mk-proc-3{animation-range:entry 18% cover 52%}\n #mk-work-0{animation-range:entry 6% cover 40%}\n #mk-work-1{animation-range:entry 6% cover 40%}\n #mk-work-2{animation-range:entry 10% cover 44%}\n #mk-work-3{animation-range:entry 10% cover 44%}\n #mk-work-4{animation-range:entry 14% cover 48%}\n #mk-work-5{animation-range:entry 14% cover 48%}\n #mk-work-6{animation-range:entry 18% cover 52%}\n #mk-work-7{animation-range:entry 18% cover 52%}\n #mk-work-8{animation-range:entry 22% cover 56%}\n #mk-stack-0{animation-range:entry 6% cover 40%}\n #mk-stack-1{animation-range:entry 10% cover 44%}\n #mk-stack-2{animation-range:entry 14% cover 48%}\n #mk-stack-3{animation-range:entry 18% cover 52%}\n #mk-stack-4{animation-range:entry 22% cover 56%}\n #mk-prod-0{animation-range:entry 6% cover 40%}\n #mk-prod-1{animation-range:entry 10% cover 44%}\n #mk-voice-0{animation-range:entry 6% cover 40%}\n #mk-voice-1{animation-range:entry 10% cover 44%}\n #mk-voice-2{animation-range:entry 14% cover 48%}\n #mk-fig-0>div>*,#mk-fig-1>div>*,#mk-fig-2>div>*,#mk-fig-3>div>*{animation:mk-digit .7s cubic-bezier(.16,1,.3,1) both;animation-delay:var(--d,0ms)}\n #mk-fig-0>div,#mk-fig-1>div,#mk-fig-2>div,#mk-fig-3>div{animation:mk-rise .01s linear both;animation-timeline:view();animation-range:entry 0% entry 1%}\n #mk-work-0::after,#mk-work-1::after,#mk-work-2::after,#mk-work-3::after,#mk-work-4::after,#mk-work-5::after,#mk-work-6::after,#mk-work-7::after,#mk-work-8::after{animation:mk-sweep .01s linear both;animation-timeline:view();animation-range:entry 12% cover 24%}\n #mk-proc-dot-0,#mk-proc-dot-1,#mk-proc-dot-2,#mk-proc-dot-3{animation:mk-dotin .01s linear both;animation-timeline:view();animation-range:entry 14% cover 26%}\n }\n @supports (timeline-scope:--x){\n #mk-doc{timeline-scope:--mk-s1,--mk-s2,--mk-s3,--mk-s4,--mk-s5,--mk-s6,--mk-s7}\n#services{view-timeline-name:--mk-s1;view-timeline-inset:45% 45%}\n#process{view-timeline-name:--mk-s2;view-timeline-inset:45% 45%}\n#works{view-timeline-name:--mk-s3;view-timeline-inset:45% 45%}\n#stack{view-timeline-name:--mk-s4;view-timeline-inset:45% 45%}\n#products{view-timeline-name:--mk-s5;view-timeline-inset:45% 45%}\n#mk-voices{view-timeline-name:--mk-s6;view-timeline-inset:45% 45%}\n#contact{view-timeline-name:--mk-s7;view-timeline-inset:45% 45%}\n #mk-idx-0{animation:mk-idxon linear;animation-timeline:--mk-s1;animation-range:cover 0% cover 100%}\n #mk-idx-1{animation:mk-idxon linear;animation-timeline:--mk-s2;animation-range:cover 0% cover 100%}\n #mk-idx-2{animation:mk-idxon linear;animation-timeline:--mk-s3;animation-range:cover 0% cover 100%}\n #mk-idx-3{animation:mk-idxon linear;animation-timeline:--mk-s4;animation-range:cover 0% cover 100%}\n #mk-idx-4{animation:mk-idxon linear;animation-timeline:--mk-s5;animation-range:cover 0% cover 100%}\n #mk-idx-5{animation:mk-idxon linear;animation-timeline:--mk-s6;animation-range:cover 0% cover 100%}\n #mk-idx-6{animation:mk-idxon linear;animation-timeline:--mk-s7;animation-range:cover 0% cover 100%}\n }\n @supports (animation-timeline:scroll()){\n #mk-progress{animation:mk-progress linear both;animation-timeline:scroll(root)}\n #mk-cue{animation:mk-cuefade linear both;animation-timeline:scroll(root);animation-range:0px 260px}\n #mk-header{animation:mk-headbg linear both;animation-timeline:scroll(root);animation-range:20px 120px}\n #mk-header::after{animation:mk-headrule linear both;animation-timeline:scroll(root);animation-range:20px 120px}\n }\n}\n</style>",
|
|
58
58
|
"processShortcodes": "0"
|
|
59
59
|
}
|
|
60
60
|
},
|
|
@@ -995,6 +995,458 @@
|
|
|
995
995
|
"attrID": "mk-home"
|
|
996
996
|
},
|
|
997
997
|
"children": [
|
|
998
|
+
{
|
|
999
|
+
"type": "div",
|
|
1000
|
+
"data": {
|
|
1001
|
+
"attrID": "mk-boot"
|
|
1002
|
+
},
|
|
1003
|
+
"style": {
|
|
1004
|
+
"&": {
|
|
1005
|
+
"_": {}
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
"children": [
|
|
1009
|
+
{
|
|
1010
|
+
"type": "div",
|
|
1011
|
+
"data": {
|
|
1012
|
+
"attrID": "mk-boot-scan"
|
|
1013
|
+
},
|
|
1014
|
+
"style": {
|
|
1015
|
+
"&": {
|
|
1016
|
+
"_": {}
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
"children": []
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
"type": "div",
|
|
1023
|
+
"data": {
|
|
1024
|
+
"attrID": "mk-boot-head"
|
|
1025
|
+
},
|
|
1026
|
+
"style": {
|
|
1027
|
+
"&": {
|
|
1028
|
+
"_": {}
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
1031
|
+
"children": [
|
|
1032
|
+
{
|
|
1033
|
+
"type": "text",
|
|
1034
|
+
"data": {
|
|
1035
|
+
"tagName": "p"
|
|
1036
|
+
},
|
|
1037
|
+
"text": "MOKSA WEB — STUDIO PROFILE",
|
|
1038
|
+
"style": {
|
|
1039
|
+
"&": {
|
|
1040
|
+
"_": {
|
|
1041
|
+
"color": {
|
|
1042
|
+
"token": "--mk-faint"
|
|
1043
|
+
},
|
|
1044
|
+
"fontSize": "10px",
|
|
1045
|
+
"letterSpacing": "0.2em",
|
|
1046
|
+
"fontFamily": "'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
|
|
1047
|
+
"fontWeight": "400"
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
"type": "text",
|
|
1054
|
+
"data": {
|
|
1055
|
+
"tagName": "p"
|
|
1056
|
+
},
|
|
1057
|
+
"text": "REV. 2026.09",
|
|
1058
|
+
"style": {
|
|
1059
|
+
"&": {
|
|
1060
|
+
"_": {
|
|
1061
|
+
"color": {
|
|
1062
|
+
"token": "--mk-faint"
|
|
1063
|
+
},
|
|
1064
|
+
"fontSize": "10px",
|
|
1065
|
+
"letterSpacing": "0.2em",
|
|
1066
|
+
"fontFamily": "'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
|
|
1067
|
+
"fontWeight": "400"
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
]
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
"type": "div",
|
|
1076
|
+
"data": {
|
|
1077
|
+
"attrID": "mk-boot-row"
|
|
1078
|
+
},
|
|
1079
|
+
"style": {
|
|
1080
|
+
"&": {
|
|
1081
|
+
"_": {}
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
"children": [
|
|
1085
|
+
{
|
|
1086
|
+
"type": "div",
|
|
1087
|
+
"data": {
|
|
1088
|
+
"attrID": "mk-boot-num"
|
|
1089
|
+
},
|
|
1090
|
+
"style": {
|
|
1091
|
+
"&": {
|
|
1092
|
+
"_": {}
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
"children": []
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
"type": "div",
|
|
1099
|
+
"data": {
|
|
1100
|
+
"attrID": "mk-boot-pct"
|
|
1101
|
+
},
|
|
1102
|
+
"style": {
|
|
1103
|
+
"&": {
|
|
1104
|
+
"_": {}
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
"children": [
|
|
1108
|
+
{
|
|
1109
|
+
"type": "text",
|
|
1110
|
+
"data": {
|
|
1111
|
+
"tagName": "p"
|
|
1112
|
+
},
|
|
1113
|
+
"text": "%",
|
|
1114
|
+
"style": {
|
|
1115
|
+
"&": {
|
|
1116
|
+
"_": {
|
|
1117
|
+
"fontFamily": "'Noto Sans TC','PingFang TC','Hiragino Sans TC','Microsoft JhengHei',sans-serif"
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
]
|
|
1123
|
+
}
|
|
1124
|
+
]
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
"type": "div",
|
|
1128
|
+
"data": {
|
|
1129
|
+
"attrID": "mk-boot-track"
|
|
1130
|
+
},
|
|
1131
|
+
"style": {
|
|
1132
|
+
"&": {
|
|
1133
|
+
"_": {}
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
"children": [
|
|
1137
|
+
{
|
|
1138
|
+
"type": "div",
|
|
1139
|
+
"data": {
|
|
1140
|
+
"attrID": "mk-boot-fill"
|
|
1141
|
+
},
|
|
1142
|
+
"style": {
|
|
1143
|
+
"&": {
|
|
1144
|
+
"_": {}
|
|
1145
|
+
}
|
|
1146
|
+
},
|
|
1147
|
+
"children": []
|
|
1148
|
+
}
|
|
1149
|
+
]
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
"type": "div",
|
|
1153
|
+
"data": {
|
|
1154
|
+
"attrID": "mk-boot-list"
|
|
1155
|
+
},
|
|
1156
|
+
"style": {
|
|
1157
|
+
"&": {
|
|
1158
|
+
"_": {}
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
"children": [
|
|
1162
|
+
{
|
|
1163
|
+
"type": "div",
|
|
1164
|
+
"data": {
|
|
1165
|
+
"attrID": "mk-boot-c0"
|
|
1166
|
+
},
|
|
1167
|
+
"style": {
|
|
1168
|
+
"&": {
|
|
1169
|
+
"_": {}
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
"children": [
|
|
1173
|
+
{
|
|
1174
|
+
"type": "text",
|
|
1175
|
+
"data": {
|
|
1176
|
+
"tagName": "p"
|
|
1177
|
+
},
|
|
1178
|
+
"text": "§01 SERVICES",
|
|
1179
|
+
"style": {
|
|
1180
|
+
"&": {
|
|
1181
|
+
"_": {
|
|
1182
|
+
"color": {
|
|
1183
|
+
"token": "--mk-faint"
|
|
1184
|
+
},
|
|
1185
|
+
"fontSize": "10px",
|
|
1186
|
+
"letterSpacing": "0.18em",
|
|
1187
|
+
"fontFamily": "'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
|
|
1188
|
+
"fontWeight": "400"
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
]
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
"type": "div",
|
|
1197
|
+
"data": {
|
|
1198
|
+
"attrID": "mk-boot-c1"
|
|
1199
|
+
},
|
|
1200
|
+
"style": {
|
|
1201
|
+
"&": {
|
|
1202
|
+
"_": {}
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
"children": [
|
|
1206
|
+
{
|
|
1207
|
+
"type": "text",
|
|
1208
|
+
"data": {
|
|
1209
|
+
"tagName": "p"
|
|
1210
|
+
},
|
|
1211
|
+
"text": "§02 PROCESS",
|
|
1212
|
+
"style": {
|
|
1213
|
+
"&": {
|
|
1214
|
+
"_": {
|
|
1215
|
+
"color": {
|
|
1216
|
+
"token": "--mk-faint"
|
|
1217
|
+
},
|
|
1218
|
+
"fontSize": "10px",
|
|
1219
|
+
"letterSpacing": "0.18em",
|
|
1220
|
+
"fontFamily": "'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
|
|
1221
|
+
"fontWeight": "400"
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
]
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
"type": "div",
|
|
1230
|
+
"data": {
|
|
1231
|
+
"attrID": "mk-boot-c2"
|
|
1232
|
+
},
|
|
1233
|
+
"style": {
|
|
1234
|
+
"&": {
|
|
1235
|
+
"_": {}
|
|
1236
|
+
}
|
|
1237
|
+
},
|
|
1238
|
+
"children": [
|
|
1239
|
+
{
|
|
1240
|
+
"type": "text",
|
|
1241
|
+
"data": {
|
|
1242
|
+
"tagName": "p"
|
|
1243
|
+
},
|
|
1244
|
+
"text": "§03 WORKS",
|
|
1245
|
+
"style": {
|
|
1246
|
+
"&": {
|
|
1247
|
+
"_": {
|
|
1248
|
+
"color": {
|
|
1249
|
+
"token": "--mk-faint"
|
|
1250
|
+
},
|
|
1251
|
+
"fontSize": "10px",
|
|
1252
|
+
"letterSpacing": "0.18em",
|
|
1253
|
+
"fontFamily": "'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
|
|
1254
|
+
"fontWeight": "400"
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
]
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
"type": "div",
|
|
1263
|
+
"data": {
|
|
1264
|
+
"attrID": "mk-boot-c3"
|
|
1265
|
+
},
|
|
1266
|
+
"style": {
|
|
1267
|
+
"&": {
|
|
1268
|
+
"_": {}
|
|
1269
|
+
}
|
|
1270
|
+
},
|
|
1271
|
+
"children": [
|
|
1272
|
+
{
|
|
1273
|
+
"type": "text",
|
|
1274
|
+
"data": {
|
|
1275
|
+
"tagName": "p"
|
|
1276
|
+
},
|
|
1277
|
+
"text": "§04 STACK",
|
|
1278
|
+
"style": {
|
|
1279
|
+
"&": {
|
|
1280
|
+
"_": {
|
|
1281
|
+
"color": {
|
|
1282
|
+
"token": "--mk-faint"
|
|
1283
|
+
},
|
|
1284
|
+
"fontSize": "10px",
|
|
1285
|
+
"letterSpacing": "0.18em",
|
|
1286
|
+
"fontFamily": "'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
|
|
1287
|
+
"fontWeight": "400"
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
]
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
"type": "div",
|
|
1296
|
+
"data": {
|
|
1297
|
+
"attrID": "mk-boot-c4"
|
|
1298
|
+
},
|
|
1299
|
+
"style": {
|
|
1300
|
+
"&": {
|
|
1301
|
+
"_": {}
|
|
1302
|
+
}
|
|
1303
|
+
},
|
|
1304
|
+
"children": [
|
|
1305
|
+
{
|
|
1306
|
+
"type": "text",
|
|
1307
|
+
"data": {
|
|
1308
|
+
"tagName": "p"
|
|
1309
|
+
},
|
|
1310
|
+
"text": "§05 PRODUCTS",
|
|
1311
|
+
"style": {
|
|
1312
|
+
"&": {
|
|
1313
|
+
"_": {
|
|
1314
|
+
"color": {
|
|
1315
|
+
"token": "--mk-faint"
|
|
1316
|
+
},
|
|
1317
|
+
"fontSize": "10px",
|
|
1318
|
+
"letterSpacing": "0.18em",
|
|
1319
|
+
"fontFamily": "'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
|
|
1320
|
+
"fontWeight": "400"
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
]
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
"type": "div",
|
|
1329
|
+
"data": {
|
|
1330
|
+
"attrID": "mk-boot-c5"
|
|
1331
|
+
},
|
|
1332
|
+
"style": {
|
|
1333
|
+
"&": {
|
|
1334
|
+
"_": {}
|
|
1335
|
+
}
|
|
1336
|
+
},
|
|
1337
|
+
"children": [
|
|
1338
|
+
{
|
|
1339
|
+
"type": "text",
|
|
1340
|
+
"data": {
|
|
1341
|
+
"tagName": "p"
|
|
1342
|
+
},
|
|
1343
|
+
"text": "§06 VOICES",
|
|
1344
|
+
"style": {
|
|
1345
|
+
"&": {
|
|
1346
|
+
"_": {
|
|
1347
|
+
"color": {
|
|
1348
|
+
"token": "--mk-faint"
|
|
1349
|
+
},
|
|
1350
|
+
"fontSize": "10px",
|
|
1351
|
+
"letterSpacing": "0.18em",
|
|
1352
|
+
"fontFamily": "'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
|
|
1353
|
+
"fontWeight": "400"
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
]
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
"type": "div",
|
|
1362
|
+
"data": {
|
|
1363
|
+
"attrID": "mk-boot-c6"
|
|
1364
|
+
},
|
|
1365
|
+
"style": {
|
|
1366
|
+
"&": {
|
|
1367
|
+
"_": {}
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
"children": [
|
|
1371
|
+
{
|
|
1372
|
+
"type": "text",
|
|
1373
|
+
"data": {
|
|
1374
|
+
"tagName": "p"
|
|
1375
|
+
},
|
|
1376
|
+
"text": "§07 CONTACT",
|
|
1377
|
+
"style": {
|
|
1378
|
+
"&": {
|
|
1379
|
+
"_": {
|
|
1380
|
+
"color": {
|
|
1381
|
+
"token": "--mk-faint"
|
|
1382
|
+
},
|
|
1383
|
+
"fontSize": "10px",
|
|
1384
|
+
"letterSpacing": "0.18em",
|
|
1385
|
+
"fontFamily": "'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
|
|
1386
|
+
"fontWeight": "400"
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
]
|
|
1392
|
+
}
|
|
1393
|
+
]
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
"type": "div",
|
|
1397
|
+
"data": {
|
|
1398
|
+
"attrID": "mk-boot-foot"
|
|
1399
|
+
},
|
|
1400
|
+
"style": {
|
|
1401
|
+
"&": {
|
|
1402
|
+
"_": {}
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
"children": [
|
|
1406
|
+
{
|
|
1407
|
+
"type": "text",
|
|
1408
|
+
"data": {
|
|
1409
|
+
"tagName": "p"
|
|
1410
|
+
},
|
|
1411
|
+
"text": "INITIALISING",
|
|
1412
|
+
"style": {
|
|
1413
|
+
"&": {
|
|
1414
|
+
"_": {
|
|
1415
|
+
"color": {
|
|
1416
|
+
"token": "--mk-faint"
|
|
1417
|
+
},
|
|
1418
|
+
"fontSize": "10px",
|
|
1419
|
+
"letterSpacing": "0.2em",
|
|
1420
|
+
"fontFamily": "'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
|
|
1421
|
+
"fontWeight": "400"
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
},
|
|
1426
|
+
{
|
|
1427
|
+
"type": "text",
|
|
1428
|
+
"data": {
|
|
1429
|
+
"tagName": "p"
|
|
1430
|
+
},
|
|
1431
|
+
"text": "TAICHUNG, TW",
|
|
1432
|
+
"style": {
|
|
1433
|
+
"&": {
|
|
1434
|
+
"_": {
|
|
1435
|
+
"color": {
|
|
1436
|
+
"token": "--mk-faint"
|
|
1437
|
+
},
|
|
1438
|
+
"fontSize": "10px",
|
|
1439
|
+
"letterSpacing": "0.2em",
|
|
1440
|
+
"fontFamily": "'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",
|
|
1441
|
+
"fontWeight": "400"
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
]
|
|
1447
|
+
}
|
|
1448
|
+
]
|
|
1449
|
+
},
|
|
998
1450
|
{
|
|
999
1451
|
"type": "div",
|
|
1000
1452
|
"data": {
|
package/tools/build_site.py
CHANGED
|
@@ -32,6 +32,7 @@ import argparse
|
|
|
32
32
|
import json
|
|
33
33
|
import os
|
|
34
34
|
import sys
|
|
35
|
+
import urllib.request
|
|
35
36
|
import urllib.parse
|
|
36
37
|
import uuid
|
|
37
38
|
|
|
@@ -148,6 +149,45 @@ def build_page_document(client, cfg, master_id, template_id, tree, surface):
|
|
|
148
149
|
return len(records)
|
|
149
150
|
|
|
150
151
|
|
|
152
|
+
def cache_check(url, _fresh_bytes=None):
|
|
153
|
+
"""Fetch the URL twice - once as a visitor, once cache-busted - and compare.
|
|
154
|
+
|
|
155
|
+
Every check in this repo cache-busts, for good reason: a verifier that reads a
|
|
156
|
+
stale copy reports the previous build. But that means nothing here had ever
|
|
157
|
+
looked at the page an actual VISITOR receives. A full-page cache in front of
|
|
158
|
+
WordPress - Varnish on Cloudways, any CDN - keeps serving the old document after
|
|
159
|
+
a perfectly successful build, and the whole toolchain reports green while the
|
|
160
|
+
site shows yesterday's page. Measured here: 141,134 bytes to a visitor while the
|
|
161
|
+
build had just produced 148,457.
|
|
162
|
+
|
|
163
|
+
Both fetches happen here, at the same moment, because comparing against a size
|
|
164
|
+
measured earlier in the build folds in every byte that changed in between. Two
|
|
165
|
+
fetches of the same live page still differ by a few hundred bytes - nonces and
|
|
166
|
+
ids are regenerated per request - so the tolerance is proportional: natural
|
|
167
|
+
variance measured at 0.25%, a stale document at 5%.
|
|
168
|
+
|
|
169
|
+
This cannot purge the cache; that needs credentials this tool has no business
|
|
170
|
+
holding. Silence is the one thing it must not do.
|
|
171
|
+
"""
|
|
172
|
+
def get(u):
|
|
173
|
+
req = urllib.request.Request(u, headers={"User-Agent": "Mozilla/5.0"})
|
|
174
|
+
with urllib.request.urlopen(req, timeout=60) as r:
|
|
175
|
+
return r.read(), (r.headers.get("Age") or r.headers.get("X-Cache") or "")
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
sep = "&" if "?" in url else "?"
|
|
179
|
+
fresh, _ = get("%s%s_v=%d" % (url, sep, uuid.uuid4().int % 10 ** 9))
|
|
180
|
+
plain, age = get(url)
|
|
181
|
+
except Exception as exc: # noqa: BLE001
|
|
182
|
+
return "could not compare the cached page: %s" % exc
|
|
183
|
+
|
|
184
|
+
if abs(len(plain) - len(fresh)) <= max(512, len(fresh) // 100):
|
|
185
|
+
return ""
|
|
186
|
+
return ("STALE CACHE: a visitor gets %d bytes, a fresh fetch gives %d%s"
|
|
187
|
+
" - purge the page cache or the site keeps serving the old document"
|
|
188
|
+
% (len(plain), len(fresh), (" (Age %s)" % age) if age else ""))
|
|
189
|
+
|
|
190
|
+
|
|
151
191
|
def main():
|
|
152
192
|
ap = argparse.ArgumentParser()
|
|
153
193
|
ap.add_argument("--config", required=True)
|
|
@@ -168,6 +208,9 @@ def main():
|
|
|
168
208
|
ok = len(body) >= MIN_HEALTHY_BYTES
|
|
169
209
|
print(" %-16s %-7s %6d bytes %3d nodes %s" % (
|
|
170
210
|
page["slug"], "OK" if ok else "BROKEN", len(body), n, url))
|
|
211
|
+
stale = cache_check(url, len(body))
|
|
212
|
+
if stale:
|
|
213
|
+
print(" %-16s %s" % ("", stale))
|
|
171
214
|
|
|
172
215
|
|
|
173
216
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Assert a page-load animation PLAYS, and - the part that matters - that it ENDS.
|
|
3
|
+
|
|
4
|
+
pip install playwright && playwright install chromium
|
|
5
|
+
python tools/verify_intro.py --config c.json --site sites/moksa.json
|
|
6
|
+
python tools/verify_intro.py --config c.json --site sites/moksa.json \
|
|
7
|
+
--csv data/intro-verification.csv --frames shots/intro/
|
|
8
|
+
|
|
9
|
+
An entrance animation is invisible to every other check in this skill, and not by
|
|
10
|
+
oversight - by construction. `verify_rwd.py` reads the stylesheet the site served.
|
|
11
|
+
`sweep_style_properties.py` reads the compiled CSS. Even `verify_browser.py`, which
|
|
12
|
+
does open a real browser, waits for the page to settle before it reads anything,
|
|
13
|
+
because a computed value taken mid-transition is noise. An intro sequence exists only
|
|
14
|
+
in the first two seconds of a document's life, and then never again.
|
|
15
|
+
|
|
16
|
+
That window is also where the worst failure on this whole list lives:
|
|
17
|
+
|
|
18
|
+
an overlay that covers the document and never leaves
|
|
19
|
+
|
|
20
|
+
It fails silently and completely. The commit succeeds, the stylesheet is correct,
|
|
21
|
+
every declaration is present, the responsive pass is green, and a visitor gets a
|
|
22
|
+
blank screen forever. No check that reads text can see it, and a browser check that
|
|
23
|
+
waits for the page to settle will wait for a page that never does.
|
|
24
|
+
|
|
25
|
+
So this one samples. It opens the URL, takes readings at a series of timestamps
|
|
26
|
+
across the intro's life, and then asserts four things:
|
|
27
|
+
|
|
28
|
+
PLAYS something actually changed between the first and last sample -
|
|
29
|
+
otherwise the sequence is dead code and the delay is just a stall
|
|
30
|
+
ENDS by the deadline the overlay is gone from hit-testing, and every
|
|
31
|
+
element that started hidden is fully opaque
|
|
32
|
+
CLEARS a real click at the centre of the viewport reaches the document,
|
|
33
|
+
not the veil: `visibility:hidden` and `pointer-events:none` are
|
|
34
|
+
different promises and only one of them is usually kept
|
|
35
|
+
DEGRADES with `prefers-reduced-motion: reduce` the content is readable
|
|
36
|
+
immediately, and the veil never appears at all
|
|
37
|
+
|
|
38
|
+
The fourth is the one worth designing for rather than merely testing. The overlay in
|
|
39
|
+
`sites/_moksa.py` is `display:none` in its base rule and is switched on only inside
|
|
40
|
+
the motion query that also carries the animation removing it - so if the animation
|
|
41
|
+
cannot run, the overlay does not exist. The failure direction is "no intro", never
|
|
42
|
+
"no page".
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import argparse
|
|
47
|
+
import csv
|
|
48
|
+
import json
|
|
49
|
+
import os
|
|
50
|
+
import sys
|
|
51
|
+
import time
|
|
52
|
+
|
|
53
|
+
# When to read. Dense through the sequence, then two well past its end - the last
|
|
54
|
+
# two are what turn "it looked right" into "it finished".
|
|
55
|
+
SAMPLES_MS = [120, 350, 700, 1100, 1500, 1750, 2050, 2400, 3200, 4500]
|
|
56
|
+
|
|
57
|
+
# What to read at each sample. Anything whose id starts with the veil prefix is
|
|
58
|
+
# treated as part of the intro; everything else is content that must end up visible.
|
|
59
|
+
PROBE = r"""
|
|
60
|
+
(ids) => {
|
|
61
|
+
const out = {t: performance.now(), nodes: {}};
|
|
62
|
+
for (const id of ids) {
|
|
63
|
+
const el = document.getElementById(id);
|
|
64
|
+
if (!el) { out.nodes[id] = null; continue; }
|
|
65
|
+
const cs = getComputedStyle(el);
|
|
66
|
+
const r = el.getBoundingClientRect();
|
|
67
|
+
out.nodes[id] = {
|
|
68
|
+
opacity: cs.opacity,
|
|
69
|
+
visibility: cs.visibility,
|
|
70
|
+
display: cs.display,
|
|
71
|
+
pointerEvents: cs.pointerEvents,
|
|
72
|
+
clipPath: cs.clipPath,
|
|
73
|
+
transform: cs.transform,
|
|
74
|
+
counter: cs.getPropertyValue('--mk-n').trim(),
|
|
75
|
+
text: (el.textContent || '').trim().slice(0, 24),
|
|
76
|
+
w: Math.round(r.width), h: Math.round(r.height),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// What is actually under the middle of the screen? This is the question a visitor
|
|
80
|
+
// asks by clicking, and no declared value answers it.
|
|
81
|
+
const mid = document.elementFromPoint(innerWidth / 2, innerHeight / 2);
|
|
82
|
+
out.hitCentre = mid ? (mid.id || mid.tagName.toLowerCase() + '.' +
|
|
83
|
+
(mid.className || '').toString().split(' ')[0]) : null;
|
|
84
|
+
out.veilCoversCentre = !!(mid && mid.closest('#mk-boot'));
|
|
85
|
+
// Anything left invisible after the intro should be deliberate, so count it -
|
|
86
|
+
// but ONLY inside the viewport. Below the fold this page is full of elements
|
|
87
|
+
// deliberately at opacity 0, waiting for a view() timeline to bring them in as
|
|
88
|
+
// you scroll, and counting those as trapped content reports 34 failures on a
|
|
89
|
+
// page that is working exactly as designed.
|
|
90
|
+
let hidden = [], offscreen = 0, blinking = 0;
|
|
91
|
+
for (const el of document.querySelectorAll('#mk-doc *')) {
|
|
92
|
+
const cs = getComputedStyle(el);
|
|
93
|
+
if (cs.display === 'none' || cs.visibility === 'hidden') continue;
|
|
94
|
+
if (parseFloat(cs.opacity) >= 0.02) continue;
|
|
95
|
+
const r = el.getBoundingClientRect();
|
|
96
|
+
if (r.width === 0 || r.height === 0) continue;
|
|
97
|
+
if (r.top >= innerHeight || r.bottom <= 0) { offscreen++; continue; }
|
|
98
|
+
// An element in the middle of an INFINITE animation is not trapped, it is
|
|
99
|
+
// blinking. A caret sampled on its off beat reads exactly like content that
|
|
100
|
+
// never arrived, and the difference is `animation-iteration-count`.
|
|
101
|
+
if (cs.animationIterationCount.split(',').some(v => v.trim() === 'infinite')) {
|
|
102
|
+
blinking++; continue;
|
|
103
|
+
}
|
|
104
|
+
hidden.push(el.id || el.tagName.toLowerCase() + '.' +
|
|
105
|
+
(el.className || '').toString().split(' ')[0]);
|
|
106
|
+
}
|
|
107
|
+
out.invisibleContent = hidden.length;
|
|
108
|
+
out.invisibleIds = hidden.slice(0, 8);
|
|
109
|
+
out.invisibleBelowFold = offscreen;
|
|
110
|
+
out.blinking = blinking;
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def read_ids(spec, slug):
|
|
117
|
+
"""Every attrID on the page, split into veil and content."""
|
|
118
|
+
ids = []
|
|
119
|
+
|
|
120
|
+
def walk(node):
|
|
121
|
+
if isinstance(node, dict):
|
|
122
|
+
data = node.get("data")
|
|
123
|
+
if isinstance(data, dict) and data.get("attrID"):
|
|
124
|
+
ids.append(data["attrID"])
|
|
125
|
+
for v in node.values():
|
|
126
|
+
walk(v)
|
|
127
|
+
elif isinstance(node, list):
|
|
128
|
+
for v in node:
|
|
129
|
+
walk(v)
|
|
130
|
+
|
|
131
|
+
for page in spec["pages"]:
|
|
132
|
+
if page["slug"] == slug:
|
|
133
|
+
walk(page.get("tree"))
|
|
134
|
+
return ids
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def run(url, ids, reduced, frames_dir):
|
|
138
|
+
try:
|
|
139
|
+
from playwright.sync_api import sync_playwright
|
|
140
|
+
except ImportError:
|
|
141
|
+
sys.exit("verify_intro.py needs Playwright:\n"
|
|
142
|
+
" pip install playwright && playwright install chromium")
|
|
143
|
+
|
|
144
|
+
shots = []
|
|
145
|
+
with sync_playwright() as pw:
|
|
146
|
+
browser = pw.chromium.launch()
|
|
147
|
+
ctx = browser.new_context(
|
|
148
|
+
viewport={"width": 1440, "height": 900},
|
|
149
|
+
reduced_motion="reduce" if reduced else "no-preference")
|
|
150
|
+
page = ctx.new_page()
|
|
151
|
+
# `commit` rather than `load`: the clock has to start when the document
|
|
152
|
+
# starts, not when it has finished settling, or the whole sequence is over
|
|
153
|
+
# before the first reading is taken.
|
|
154
|
+
# Cache-busted, like every other check here. Without it this tool read a
|
|
155
|
+
# Varnish copy of the page from before the intro existed and reported, with
|
|
156
|
+
# complete confidence, that the sequence did not play. Third time this
|
|
157
|
+
# failure has been made in this repo; the query string is not optional.
|
|
158
|
+
sep = "&" if "?" in url else "?"
|
|
159
|
+
page.goto("%s%s_v=%d" % (url, sep, int(time.time() * 1000)),
|
|
160
|
+
wait_until="commit", timeout=60000)
|
|
161
|
+
readings = []
|
|
162
|
+
for ms in SAMPLES_MS:
|
|
163
|
+
page.wait_for_timeout(max(0, ms - (readings[-1]["t_ms"]
|
|
164
|
+
if readings else 0)))
|
|
165
|
+
rec = page.evaluate(PROBE, ids)
|
|
166
|
+
rec["t_ms"] = ms
|
|
167
|
+
readings.append(rec)
|
|
168
|
+
if frames_dir:
|
|
169
|
+
os.makedirs(frames_dir, exist_ok=True)
|
|
170
|
+
path = os.path.join(frames_dir, "t%04d.png" % ms)
|
|
171
|
+
page.screenshot(path=path)
|
|
172
|
+
shots.append(path)
|
|
173
|
+
# One real click, at the end, at the centre of the page.
|
|
174
|
+
try:
|
|
175
|
+
page.mouse.click(720, 450)
|
|
176
|
+
clicked = page.evaluate(
|
|
177
|
+
"() => { const e = document.elementFromPoint(720, 450);"
|
|
178
|
+
"return e ? (e.id || e.tagName) : null; }")
|
|
179
|
+
except Exception as exc: # noqa: BLE001
|
|
180
|
+
clicked = "click failed: %s" % exc
|
|
181
|
+
ctx.close()
|
|
182
|
+
browser.close()
|
|
183
|
+
return readings, clicked, shots
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def changed(readings, ids):
|
|
187
|
+
"""Which probes moved at all across the sequence."""
|
|
188
|
+
moved = set()
|
|
189
|
+
for i in ids:
|
|
190
|
+
seen = set()
|
|
191
|
+
for r in readings:
|
|
192
|
+
n = r["nodes"].get(i)
|
|
193
|
+
if n:
|
|
194
|
+
seen.add((n["opacity"], n["visibility"], n["clipPath"],
|
|
195
|
+
n["transform"], n["counter"]))
|
|
196
|
+
if len(seen) > 1:
|
|
197
|
+
moved.add(i)
|
|
198
|
+
return moved
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def main():
|
|
202
|
+
ap = argparse.ArgumentParser()
|
|
203
|
+
ap.add_argument("--config", required=True)
|
|
204
|
+
ap.add_argument("--site", required=True)
|
|
205
|
+
ap.add_argument("--page")
|
|
206
|
+
ap.add_argument("--veil-prefix", default="mk-boot",
|
|
207
|
+
help="ids under this prefix are the intro, not the content")
|
|
208
|
+
ap.add_argument("--deadline-ms", type=int, default=3200,
|
|
209
|
+
help="by this point the intro must be over")
|
|
210
|
+
ap.add_argument("--csv")
|
|
211
|
+
ap.add_argument("--frames")
|
|
212
|
+
a = ap.parse_args()
|
|
213
|
+
|
|
214
|
+
cfg = json.load(open(a.config, encoding="utf-8"))
|
|
215
|
+
spec = json.load(open(a.site, encoding="utf-8"))
|
|
216
|
+
slug = a.page or spec["pages"][0]["slug"]
|
|
217
|
+
url = "%s/%s/" % (cfg["base"].rstrip("/"), slug)
|
|
218
|
+
|
|
219
|
+
ids = read_ids(spec, slug)
|
|
220
|
+
veil = [i for i in ids if i.startswith(a.veil_prefix)]
|
|
221
|
+
if not veil:
|
|
222
|
+
sys.exit("no ids under %r - nothing here is an intro" % a.veil_prefix)
|
|
223
|
+
|
|
224
|
+
print("%s\n %d ids, %d of them the intro, %d samples\n"
|
|
225
|
+
% (url, len(ids), len(veil), len(SAMPLES_MS)))
|
|
226
|
+
|
|
227
|
+
readings, clicked, shots = run(url, ids, False, a.frames)
|
|
228
|
+
moved = changed(readings, ids)
|
|
229
|
+
|
|
230
|
+
print(" %-7s %-11s %-9s %-8s %-7s %s"
|
|
231
|
+
% ("t", "counter", "veil vis", "clip", "covers", "under the centre"))
|
|
232
|
+
for r in readings:
|
|
233
|
+
v = r["nodes"].get(a.veil_prefix) or {}
|
|
234
|
+
num = (r["nodes"].get(a.veil_prefix + "-num") or {}).get("counter", "")
|
|
235
|
+
print(" %-7s %-11s %-9s %-8s %-7s %s"
|
|
236
|
+
% ("%dms" % r["t_ms"], num or "-", v.get("visibility", "-"),
|
|
237
|
+
(v.get("clipPath") or "-")[:8],
|
|
238
|
+
"YES" if r["veilCoversCentre"] else "no", r["hitCentre"]))
|
|
239
|
+
|
|
240
|
+
last = readings[-1]
|
|
241
|
+
at_deadline = [r for r in readings if r["t_ms"] >= a.deadline_ms]
|
|
242
|
+
checks = []
|
|
243
|
+
|
|
244
|
+
# PLAYS
|
|
245
|
+
veil_moved = moved & set(veil)
|
|
246
|
+
checks.append(("PLAYS", bool(veil_moved),
|
|
247
|
+
"%d of %d intro elements changed state" % (len(veil_moved),
|
|
248
|
+
len(veil))))
|
|
249
|
+
# the counter is an animated integer, so it can be read rather than admired
|
|
250
|
+
counters = [(r["t_ms"], (r["nodes"].get(a.veil_prefix + "-num") or {})
|
|
251
|
+
.get("counter", "")) for r in readings]
|
|
252
|
+
nums = [int(c) for _t, c in counters if c.isdigit()]
|
|
253
|
+
checks.append(("COUNTS", bool(nums) and max(nums) >= 99,
|
|
254
|
+
"--mk-n reached %s" % (max(nums) if nums else "nothing")))
|
|
255
|
+
|
|
256
|
+
# ENDS
|
|
257
|
+
ended = all(not r["veilCoversCentre"] for r in at_deadline)
|
|
258
|
+
checks.append(("ENDS", ended,
|
|
259
|
+
"veil is out of hit-testing from %dms" % a.deadline_ms))
|
|
260
|
+
checks.append(("NO_TRAP", last["invisibleContent"] == 0,
|
|
261
|
+
"%d content elements still at opacity 0 in the viewport%s "
|
|
262
|
+
"(%d below the fold awaiting their own scroll timeline, "
|
|
263
|
+
"%d mid-blink)"
|
|
264
|
+
% (last["invisibleContent"],
|
|
265
|
+
(": " + ", ".join(last["invisibleIds"]))
|
|
266
|
+
if last["invisibleIds"] else "",
|
|
267
|
+
last["invisibleBelowFold"], last["blinking"])))
|
|
268
|
+
|
|
269
|
+
# CLEARS
|
|
270
|
+
cleared = bool(clicked) and not str(clicked).startswith(a.veil_prefix)
|
|
271
|
+
checks.append(("CLEARS", cleared, "a real click landed on %r" % clicked))
|
|
272
|
+
|
|
273
|
+
# DEGRADES
|
|
274
|
+
red, red_click, _ = run(url, ids, True, None)
|
|
275
|
+
first = red[0]
|
|
276
|
+
# A node that has not been parsed yet reads as absent, which is not a failure -
|
|
277
|
+
# the requirement is that wherever the veil EXISTS under reduced motion, it is
|
|
278
|
+
# display:none. Treating "not in the DOM at 120ms" as a broken veil is the tool
|
|
279
|
+
# marking its own sampling as a defect in the page.
|
|
280
|
+
seen_veil = [r["nodes"].get(a.veil_prefix) for r in red
|
|
281
|
+
if r["nodes"].get(a.veil_prefix)]
|
|
282
|
+
veil_absent = bool(seen_veil) and all(v.get("display") == "none"
|
|
283
|
+
for v in seen_veil)
|
|
284
|
+
checks.append(("DEGRADES", veil_absent and not first["veilCoversCentre"],
|
|
285
|
+
"with reduced motion the veil is display:none throughout"))
|
|
286
|
+
checks.append(("READABLE", red[0]["invisibleContent"] == 0,
|
|
287
|
+
"%d elements invisible at %dms with reduced motion"
|
|
288
|
+
% (red[0]["invisibleContent"], red[0]["t_ms"])))
|
|
289
|
+
|
|
290
|
+
print()
|
|
291
|
+
for name, ok, note in checks:
|
|
292
|
+
print(" %-9s %-4s %s" % (name, "PASS" if ok else "FAIL", note))
|
|
293
|
+
failed = [c for c in checks if not c[1]]
|
|
294
|
+
|
|
295
|
+
if a.csv:
|
|
296
|
+
with open(a.csv, "w", newline="", encoding="utf-8") as fh:
|
|
297
|
+
w = csv.writer(fh)
|
|
298
|
+
w.writerow(["check", "result", "detail"])
|
|
299
|
+
for name, ok, note in checks:
|
|
300
|
+
w.writerow([name, "PASS" if ok else "FAIL", note])
|
|
301
|
+
w.writerow([])
|
|
302
|
+
w.writerow(["t_ms", "counter", "veil_visibility", "veil_clip",
|
|
303
|
+
"veil_covers_centre", "hit_centre", "invisible_content"])
|
|
304
|
+
for r in readings:
|
|
305
|
+
v = r["nodes"].get(a.veil_prefix) or {}
|
|
306
|
+
w.writerow([r["t_ms"],
|
|
307
|
+
(r["nodes"].get(a.veil_prefix + "-num") or {})
|
|
308
|
+
.get("counter", ""),
|
|
309
|
+
v.get("visibility", ""), v.get("clipPath", ""),
|
|
310
|
+
r["veilCoversCentre"], r["hitCentre"],
|
|
311
|
+
r["invisibleContent"]])
|
|
312
|
+
print("\nwrote", a.csv)
|
|
313
|
+
if shots:
|
|
314
|
+
print("wrote %d frames to %s" % (len(shots), a.frames))
|
|
315
|
+
|
|
316
|
+
print("\n%s" % ("PASS - the sequence plays, finishes, hands the page back, and "
|
|
317
|
+
"does not exist at all when motion is reduced"
|
|
318
|
+
if not failed else
|
|
319
|
+
"FAIL - %s" % ", ".join(c[0] for c in failed)))
|
|
320
|
+
sys.exit(1 if failed else 0)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
if __name__ == "__main__":
|
|
324
|
+
main()
|