mosaic-headless 1.2.0 → 1.3.0

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 (49) hide show
  1. package/README.ja.md +19 -4
  2. package/README.ko.md +111 -0
  3. package/README.md +30 -5
  4. package/README.zh-TW.md +17 -4
  5. package/SKILL.md +79 -7
  6. package/assets/templates/platforms/claude-ai.json +1 -1
  7. package/assets/templates/platforms/claude-code.json +1 -1
  8. package/assets/templates/platforms/codex-cli.json +1 -1
  9. package/assets/templates/platforms/copilot.json +1 -1
  10. package/assets/templates/platforms/gemini-cli.json +1 -1
  11. package/bin/check-release.mjs +29 -2
  12. package/data/animatable-properties.csv +23 -23
  13. package/data/browser-verification.csv +2238 -0
  14. package/data/condition-comparators.csv +13 -13
  15. package/data/condition-subjects.csv +60 -60
  16. package/data/data-class-hierarchy.csv +122 -0
  17. package/data/db-columns.csv +207 -207
  18. package/data/default-children.csv +11 -11
  19. package/data/design-audit.csv +1 -0
  20. package/data/dynamic-variables.csv +75 -75
  21. package/data/element-classes.csv +152 -152
  22. package/data/evaluator-functions.csv +20 -20
  23. package/data/interaction-types.csv +13 -13
  24. package/data/node-properties.csv +182 -182
  25. package/data/node-property-verification.csv +182 -182
  26. package/data/node-types.csv +123 -123
  27. package/data/node-verification.csv +123 -123
  28. package/data/placement-rules.csv +123 -123
  29. package/data/pluggables.csv +208 -208
  30. package/data/property-verification.csv +171 -171
  31. package/data/rest-routes.csv +115 -115
  32. package/data/rwd-verification.csv +577 -570
  33. package/data/style-properties.csv +99 -99
  34. package/data/style-states.csv +54 -54
  35. package/data/style-value-shapes.csv +23 -23
  36. package/data/style-verification.csv +99 -99
  37. package/package.json +59 -59
  38. package/references/failure-modes.md +74 -0
  39. package/sites/_moksa.py +106 -44
  40. package/sites/moksa.json +9075 -8684
  41. package/tools/build_page.py +34 -1
  42. package/tools/extract_node_types.py +26 -1
  43. package/tools/mo.py +686 -0
  44. package/tools/sweep_node_properties.py +41 -2
  45. package/tools/verify_browser.py +589 -0
  46. package/tools/verify_rwd.py +14 -1
  47. package/README.zh-CN.md +0 -87
  48. package/tools/build_all.py +0 -55
  49. package/tools/build_report.py +0 -221
package/package.json CHANGED
@@ -1,59 +1,59 @@
1
- {
2
- "name": "mosaic-headless",
3
- "version": "1.2.0",
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
- "keywords": [
6
- "wordpress",
7
- "mosaic",
8
- "mosaic-pro",
9
- "nextend",
10
- "page-builder",
11
- "claude-code",
12
- "agent-skill",
13
- "ai-agent",
14
- "headless",
15
- "woocommerce",
16
- "wp-cli"
17
- ],
18
- "homepage": "https://github.com/Moksa1123/mosaic-headless#readme",
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/Moksa1123/mosaic-headless.git"
22
- },
23
- "bugs": {
24
- "url": "https://github.com/Moksa1123/mosaic-headless/issues"
25
- },
26
- "license": "MIT",
27
- "author": "moksa (https://moksaweb.com)",
28
- "type": "module",
29
- "bin": {
30
- "mosaic-headless": "bin/install.mjs"
31
- },
32
- "files": [
33
- "bin/",
34
- "assets/",
35
- "data/*.csv",
36
- "references/",
37
- "sites/_moksa.py",
38
- "sites/moksa.json",
39
- "tools/*.py",
40
- "tools/*.php",
41
- "SKILL.md",
42
- "README.md",
43
- "README.zh-TW.md",
44
- "README.zh-CN.md",
45
- "README.ja.md",
46
- "LICENSE"
47
- ],
48
- "engines": {
49
- "node": ">=18"
50
- },
51
- "scripts": {
52
- "install-skill": "node bin/install.mjs",
53
- "check": "node bin/check-release.mjs",
54
- "preversion": "npm run check",
55
- "version": "node bin/sync-version.mjs && git add SKILL.md assets/templates/platforms",
56
- "postversion": "git push && git push --tags",
57
- "prepublishOnly": "npm run check"
58
- }
59
- }
1
+ {
2
+ "name": "mosaic-headless",
3
+ "version": "1.3.0",
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
+ "keywords": [
6
+ "wordpress",
7
+ "mosaic",
8
+ "mosaic-pro",
9
+ "nextend",
10
+ "page-builder",
11
+ "claude-code",
12
+ "agent-skill",
13
+ "ai-agent",
14
+ "headless",
15
+ "woocommerce",
16
+ "wp-cli"
17
+ ],
18
+ "homepage": "https://github.com/Moksa1123/mosaic-headless#readme",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/Moksa1123/mosaic-headless.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/Moksa1123/mosaic-headless/issues"
25
+ },
26
+ "license": "MIT",
27
+ "author": "moksa (https://moksaweb.com)",
28
+ "type": "module",
29
+ "bin": {
30
+ "mosaic-headless": "bin/install.mjs"
31
+ },
32
+ "files": [
33
+ "bin/",
34
+ "assets/",
35
+ "data/*.csv",
36
+ "references/",
37
+ "sites/_moksa.py",
38
+ "sites/moksa.json",
39
+ "tools/*.py",
40
+ "tools/*.php",
41
+ "SKILL.md",
42
+ "README.md",
43
+ "README.zh-TW.md",
44
+ "README.ja.md",
45
+ "README.ko.md",
46
+ "LICENSE"
47
+ ],
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
51
+ "scripts": {
52
+ "install-skill": "node bin/install.mjs",
53
+ "check": "node bin/check-release.mjs",
54
+ "preversion": "npm run check",
55
+ "version": "node bin/sync-version.mjs && git add SKILL.md assets/templates/platforms",
56
+ "postversion": "git push && git push --tags",
57
+ "prepublishOnly": "npm run check"
58
+ }
59
+ }
@@ -171,6 +171,80 @@ required children on commit.
171
171
  A record committed with `"modified_gmt": "2026-09-05 17:00:00"` came back stored as
172
172
  `16:59:16`. The server overwrites it. Do not use a value you sent as a local cache key.
173
173
 
174
+ ## A page cache turns "it worked" into "the node does not exist"
175
+
176
+ Not a Mosaic failure - a failure of *checking* Mosaic, and it produces the exact
177
+ result this skill exists to prevent: a confident negative about something that is
178
+ actually there.
179
+
180
+ Commit succeeds. `build_site.py` reports the byte size the renderer produced. Then
181
+ the verifier fetches the URL, a full-page cache in front of WordPress (Varnish on
182
+ Cloudways, or any CDN) answers with the pre-commit HTML, and every declaration on a
183
+ node you have just added comes back `no-element`. Measured on this install: the
184
+ builder saw 138878 bytes, `curl` on the same URL a second later got 136021, and
185
+ seven declarations on two brand-new grid wrappers were reported missing while the
186
+ wrappers were sitting correctly in the database and in the committed JSON.
187
+
188
+ The tell is the byte count: if the size the builder reports and the size you fetch
189
+ disagree, you are not looking at what you wrote.
190
+
191
+ `tools/verify_rwd.py` now appends a unique `_v=<ms>` query string and sends
192
+ `Cache-Control: no-cache` on every fetch, so the assertion is always made against the
193
+ document that was actually committed. Do the same in anything else that checks a page
194
+ - including a browser: hard-reload is not always enough, a query string always is.
195
+
196
+ ## Changing a design token's VALUE leaves the old one in `:root`
197
+
198
+ A collection variable is a row, and a row is identified by its ID. If the writer
199
+ mints a fresh ID each run - which `build_page.py` did, because its `_varIDs` cache
200
+ starts empty every time - then changing a token's value does not change the token.
201
+ It adds a second one.
202
+
203
+ The rows hang off the theme's collection, and `build_site.py` makes a fresh master
204
+ per build while the collection persists, so nothing ever cleans them up. Both
205
+ declarations reach `:root` and the later one wins:
206
+
207
+ ```css
208
+ :root{ ... --mk-faint: rgb(107, 109, 113); ... --mk-faint: rgb(160, 162, 168); ... }
209
+ ```
210
+
211
+ Measured: after changing `--mk-faint`, a rebuild reported OK, the new variable
212
+ existed with the correct value, `verify_rwd.py` passed, and the page kept rendering
213
+ the old grey. Ten stale rows had accumulated across earlier builds. **Every
214
+ server-side check agreed with the intent and the page still disagreed** - only
215
+ `verify_browser.py`, reading the computed colour off the element, could see it.
216
+
217
+ The fix is that a token's row ID must be a function of its name -
218
+ `uuid5(VAR_NAMESPACE, "--mk-faint")` - so a rebuild rebinds the row instead of
219
+ adding one, plus a reap of anything on the collection claiming a managed custom
220
+ property under an ID we did not derive. Both are in `theme_records()`.
221
+
222
+ The general shape is worth remembering beyond tokens: **anything keyed by a random
223
+ ID that you write repeatedly will accumulate**, and duplicates in a cascade fail
224
+ silently in favour of whichever happens to be last.
225
+
226
+ ## A declaration can be present, correct, and still wrong
227
+
228
+ The fourth failure mode in SKILL.md - "wrong value SHAPE, HTTP 200, and the CSS rule
229
+ is simply absent" - has a quieter sibling: the rule is *present*, the value is what
230
+ you asked for, and what the browser does with it is not what you meant.
231
+
232
+ Three ways, all measured on the example page:
233
+
234
+ - **The unit resolves against something you did not think about.**
235
+ `letter-spacing: -0.035em` is a normal amount of tightening for a Latin display
236
+ face. On a 62px headline it is -2.17px, and the headline was Chinese.
237
+ - **The face cannot render the text.** `font-family: 'Space Grotesk', 'Noto Sans TC'`
238
+ is honoured exactly: Space Grotesk has no CJK coverage, so Han characters come from
239
+ Noto Sans TC - while still carrying the tracking that was chosen for the Latin
240
+ face. Nothing reports a fallback.
241
+ - **The property was never in play.** `display: inline-block` on a flex item is
242
+ blockified by the spec. The declaration was in the stylesheet, correct, and
243
+ computed to `block`.
244
+
245
+ None of these is visible to anything that reads the stylesheet, because the
246
+ stylesheet is right. `verify_browser.py` asks the element instead.
247
+
174
248
  ## Reproducing all of this
175
249
 
176
250
  ```bash
package/sites/_moksa.py CHANGED
@@ -41,9 +41,10 @@ TOKENS = {
41
41
  "--mk-paper": {"type": "color", "value": "rgb(250,250,247)"},
42
42
  "--mk-panel": {"type": "color", "value": "rgb(243,243,239)"},
43
43
  "--mk-accent": {"type": "color", "value": "rgb(255,90,54)"},
44
- "--mk-muted": {"type": "color", "value": "rgb(111,113,120)"},
44
+ "--mk-accent-ink": {"type": "color", "value": "rgb(191,68,40)"},
45
+ "--mk-muted": {"type": "color", "value": "rgb(90,92,97)"},
45
46
  "--mk-rule": {"type": "color", "value": "rgb(214,214,206)"},
46
- "--mk-faint": {"type": "color", "value": "rgb(160,162,168)"},
47
+ "--mk-faint": {"type": "color", "value": "rgb(107,109,113)"},
47
48
  }
48
49
 
49
50
  RULE = "rgba(22,24,28,.16)"
@@ -131,7 +132,7 @@ def mono(text, size="11px", color="--mk-muted", track="0.14em", **st):
131
132
  fontSize=size, letterSpacing=track, **st)
132
133
 
133
134
 
134
- def figure(attr, num, size="40px", t="34px", m="27px"):
135
+ def figure(attr, num, size="56px", t="44px", m="34px"):
135
136
  """A number set as one masked box per digit.
136
137
 
137
138
  A count-up needs JavaScript; a digit rolling out from behind its own edge does
@@ -145,12 +146,17 @@ def figure(attr, num, size="40px", t="34px", m="27px"):
145
146
  "font-variant-numeric:tabular-nums;"}}},
146
147
  "children": [
147
148
  {"type": "div", "data": {"attrID": attr + "-d%d" % j},
149
+ # No `display:inline-block` here: these are flex items, and the
150
+ # spec blockifies a flex item's display value. The declaration was
151
+ # in the stylesheet, correct, and computed to `block` anyway -
152
+ # which is exactly the class of dead code verify_browser.py exists
153
+ # to find, and it found this one.
148
154
  "style": {"&": {"_": {"customStyles":
149
- "overflow:hidden;display:inline-block;"
155
+ "overflow:hidden;"
150
156
  "--d:%dms;" % (j * 70)}}},
151
157
  "children": [T("h3", ch, color={"token": "--mk-ink"}, fontSize=size,
152
- fontWeight="500", letterSpacing="-0.04em",
153
- lineHeight="1.05", fontFamily=MONO,
158
+ fontWeight="600", letterSpacing="-0.03em",
159
+ lineHeight="1.02", fontFamily=DISPLAY,
154
160
  _t={"fontSize": t}, _m={"fontSize": m})]}
155
161
  for j, ch in enumerate(num)
156
162
  ]}
@@ -168,11 +174,12 @@ def clause(num, en, zh, attr):
168
174
  "customStyles": "border-top:1px solid " + RULE + ";"},
169
175
  _m={"gridCols": "48px 1fr"},
170
176
  children=[
171
- mono("§" + num, size="12px", color="--mk-accent", track="0.06em"),
177
+ mono("§" + num, size="12px", color="--mk-accent-ink",
178
+ track="0.06em"),
172
179
  box(attr + "-t", {}, [
173
180
  mono(en, size="11px", color="--mk-muted"),
174
181
  T("h2", zh, color={"token": "--mk-ink"}, fontSize="34px",
175
- fontWeight="600", letterSpacing="-0.025em", lineHeight="1.3",
182
+ fontWeight="600", letterSpacing="0.01em", lineHeight="1.35",
176
183
  fontFamily=DISPLAY, marginTop="10px",
177
184
  _t={"fontSize": "29px"}, _m={"fontSize": "23px"}),
178
185
  ]),
@@ -234,6 +241,16 @@ SPEC = [
234
241
  ("SUPPORT", "876", "技術支援與服務"),
235
242
  ]
236
243
 
244
+ # The masthead's right-hand column. A profile document states its terms in its own
245
+ # header, and it is the content that fills the frame the headline leaves empty.
246
+ SIDE = [
247
+ ("DISCIPLINE", "Web / AI / Automation"),
248
+ ("STACK", "WordPress · n8n · Claude"),
249
+ ("ENGAGEMENT", "專案制 / 長期維運"),
250
+ ("LEAD TIME", "3–8 週"),
251
+ ("RESPONSE", "1 個工作天"),
252
+ ]
253
+
237
254
  SERVICES = [
238
255
  ("01", "WEB & E-COMMERCE", "網站與電商",
239
256
  "品牌官網、電商平台與 WordPress 開發,乾淨的結構與好管理的後台。"),
@@ -624,8 +641,12 @@ HEADER = box("mk-shell-top", {}, [
624
641
  "url": "#contact"},
625
642
  "style": {"&": {"_m": {"display": "none"},
626
643
  "_t": {"fontSize": "12px"},
644
+ # 13px of the bright orange on the glass header is
645
+ # 3.10:1; the darkened cut is 4.9:1. The BORDER
646
+ # stays bright, because a rule is not text and
647
+ # nobody has to read it.
627
648
  "_": {"backgroundColor": "rgba(0,0,0,0)",
628
- "color": {"token": "--mk-accent"},
649
+ "color": {"token": "--mk-accent-ink"},
629
650
  "fontSize": "13px", "fontFamily": MONO,
630
651
  "letterSpacing": "0.06em",
631
652
  "paddingTop": "6px", "paddingBottom": "6px",
@@ -635,7 +656,7 @@ HEADER = box("mk-shell-top", {}, [
635
656
  "color": "rgb(255,90,54)"},
636
657
  "transitionAll": "180ms ease"}},
637
658
  "hover": {"_": {"backgroundColor": {"token": "--mk-accent"},
638
- "color": {"token": "--mk-paper"}}}},
659
+ "color": {"token": "--mk-ink"}}}},
639
660
  "text": "START A PROJECT"},
640
661
  ]}
641
662
  ])]),
@@ -659,7 +680,7 @@ FOOTER = box("mk-footer",
659
680
  mono("MAKE WEB MEANINGFUL", size="11px", color="--mk-accent",
660
681
  track="0.2em"),
661
682
  T("h2", "打造有價值的網站體驗。", color={"token": "--mk-paper"},
662
- fontSize="30px", fontWeight="500", letterSpacing="-0.01em",
683
+ fontSize="30px", fontWeight="500", letterSpacing="0.02em",
663
684
  lineHeight="1.5", fontFamily=CJK, marginTop="16px",
664
685
  _m={"fontSize": "22px"}),
665
686
  box("mk-f-contact",
@@ -674,7 +695,7 @@ FOOTER = box("mk-footer",
674
695
  ]),
675
696
  grid("mk-f-links", 2, "28px", tcols=2, mcols=2, children=[
676
697
  box("mk-f-col-0", {}, [
677
- mono("PAGES", size="10px", color="rgb(122,124,132)",
698
+ mono("PAGES", size="10px", color="rgb(128,130,138)",
678
699
  track="0.2em"),
679
700
  box("mk-f-col-0-list",
680
701
  {"marginTop": "16px", "display": "grid", "rowGap": "9px"},
@@ -683,7 +704,7 @@ FOOTER = box("mk-footer",
683
704
  for t in ["作品集", "團隊成員", "關於我們", "服務報價"]]),
684
705
  ]),
685
706
  box("mk-f-col-1", {}, [
686
- mono("LEARN", size="10px", color="rgb(122,124,132)",
707
+ mono("LEARN", size="10px", color="rgb(128,130,138)",
687
708
  track="0.2em"),
688
709
  box("mk-f-col-1-list",
689
710
  {"marginTop": "16px", "display": "grid", "rowGap": "9px"},
@@ -703,17 +724,17 @@ FOOTER = box("mk-footer",
703
724
  "customStyles": "flex-wrap:wrap;"}, None, None),
704
725
  "children": [
705
726
  mono("© 2026 MOKSA WEB — ALL RIGHTS RESERVED", size="10px",
706
- color="rgb(112,114,122)", track="0.1em"),
727
+ color="rgb(128,130,138)", track="0.1em"),
707
728
  mono("BUILT HEADLESS ON MOSAIC", size="10px",
708
- color="rgb(112,114,122)", track="0.1em"),
729
+ color="rgb(128,130,138)", track="0.1em"),
709
730
  ]}]),
710
731
  ])])
711
732
 
712
733
 
713
734
  # ── the page ──────────────────────────────────────────────────────────────────
714
735
  HERO_LINE = dict(color={"token": "--mk-ink"}, fontSize="62px", fontWeight="600",
715
- lineHeight="1.12", letterSpacing="-0.035em", fontFamily=DISPLAY,
716
- _t={"fontSize": "46px"}, _m={"fontSize": "31px"})
736
+ lineHeight="1.14", letterSpacing="0.005em", fontFamily=DISPLAY,
737
+ _t={"fontSize": "48px"}, _m={"fontSize": "32px"})
717
738
 
718
739
  MASTHEAD = section("mk-mast", [wrap("mk-mast-in", [
719
740
  # the document's own header block: what this is, where it is from, which revision
@@ -733,11 +754,19 @@ MASTHEAD = section("mk-mast", [wrap("mk-mast-in", [
733
754
  {"display": "flex", "alignItems": "center", "marginTop": "10px"},
734
755
  [box("mk-caret", {}, [])]),
735
756
  ]),
757
+ box("mk-mast-body",
758
+ {"display": "grid", "gridCols": "1.35fr .65fr", "columnGap": "56px",
759
+ "rowGap": "32px", "alignItems": "start", "marginTop": "26px"},
760
+ _t={"gridCols": "1.2fr .8fr", "columnGap": "36px"},
761
+ _m={"gridCols": "repeat(1, 1fr)", "columnGap": "0px", "rowGap": "26px",
762
+ "marginTop": "20px"},
763
+ children=[
764
+ box("mk-mast-left", {}, [
736
765
  {"type": "text", "data": {"tagName": "p", "attrID": "mk-mast-lede"},
737
766
  "style": bp({"color": {"token": "--mk-muted"}, "fontSize": "16px",
738
- "lineHeight": "2", "marginTop": "24px", "maxWidth": "34em"},
767
+ "lineHeight": "2", "marginTop": "0px", "maxWidth": "30em"},
739
768
  None,
740
- {"fontSize": "14px", "lineHeight": "1.95", "marginTop": "20px"}),
769
+ {"fontSize": "14px", "lineHeight": "1.95", "marginTop": "0px"}),
741
770
  "text": "我打造網站、開發軟體、導入 AI、串起自動化流程,"
742
771
  "讓技術不只是工具,而是幫你省下時間、長出業績的數位夥伴。"},
743
772
  {"type": "div", "data": {"attrID": "mk-mast-cta"},
@@ -747,19 +776,46 @@ MASTHEAD = section("mk-mast", [wrap("mk-mast-in", [
747
776
  "children": [
748
777
  # underlined links, not buttons: a document points, it does not sell
749
778
  {"type": "button", "data": {"attrID": "mk-cta-%d" % n, "url": href},
750
- "style": {"&": {"_": {"backgroundColor": "rgba(0,0,0,0)",
751
- "color": {"token": "--mk-ink"}, "fontSize": "14px",
752
- "fontFamily": MONO, "letterSpacing": "0.06em",
753
- "paddingTop": "8px", "paddingBottom": "8px",
754
- "radius": "0px", "cursor": "pointer",
755
- "customStyles":
756
- "border-bottom:1px solid rgb(22,24,28);",
757
- "transitionAll": "180ms ease"}},
758
- "hover": {"_": {"color": {"token": "--mk-accent"}}}},
779
+ "style": {"&": {"_": dict(
780
+ {"backgroundColor": {"token": "--mk-accent"},
781
+ "color": {"token": "--mk-ink"},
782
+ "paddingLeft": "22px", "paddingRight": "22px",
783
+ "paddingTop": "13px", "paddingBottom": "13px",
784
+ "customStyles": "border:1px solid rgb(255,90,54);"}
785
+ if n == 1 else
786
+ {"backgroundColor": "rgba(0,0,0,0)", "color": {"token": "--mk-ink"},
787
+ "paddingLeft": "0px", "paddingRight": "0px",
788
+ "paddingTop": "13px", "paddingBottom": "13px",
789
+ "customStyles": "border:1px solid rgba(0,0,0,0);"
790
+ "border-bottom-color:rgb(22,24,28);"},
791
+ fontSize="13px", fontFamily=MONO, letterSpacing="0.1em",
792
+ radius="0px", cursor="pointer", transitionAll="180ms ease")},
793
+ "hover": {"_": ({"backgroundColor": {"token": "--mk-ink"},
794
+ "customStyles": "border:1px solid rgb(22,24,28);"}
795
+ if n == 1
796
+ else {"color": {"token": "--mk-accent"}})}},
759
797
  "text": text}
760
- for n, (text, href) in enumerate([("→ 服務項目", "#services"),
761
- ("→ 精選作品", "#works")], start=1)
798
+ for n, (text, href) in enumerate([("服務項目 →", "#services"),
799
+ ("精選作品 →", "#works")], start=1)
762
800
  ]},
801
+ ]),
802
+ # the right-hand column: what a spec sheet puts in its header block
803
+ box("mk-mast-side",
804
+ {"display": "grid", "rowGap": "0px",
805
+ "customStyles": "border-top:1px solid " + RULE_INK + ";"},
806
+ _m={"customStyles": "border-top:1px solid " + RULE + ";"},
807
+ children=[
808
+ box("mk-mast-side-%d" % i,
809
+ {"display": "grid", "gridCols": "1fr auto",
810
+ "columnGap": "14px", "alignItems": "baseline",
811
+ "paddingTop": "11px", "paddingBottom": "11px",
812
+ "customStyles": ("" if i == 0
813
+ else "border-top:1px solid " + RULE + ";")},
814
+ [mono(k, size="10px", color="--mk-faint", track="0.18em"),
815
+ mono(v, size="12px", color="--mk-ink", track="0.02em")])
816
+ for i, (k, v) in enumerate(SIDE)
817
+ ]),
818
+ ]),
763
819
  box("mk-spec-rule", {"marginTop": "56px"}, [], _m={"marginTop": "40px"}),
764
820
  # the figures as a datasheet, not as four big numbers looking for attention
765
821
  grid("mk-spec", 4, "0px", tcols=2, mcols=2,
@@ -786,8 +842,9 @@ MASTHEAD = section("mk-mast", [wrap("mk-mast-in", [
786
842
  {"display": "flex", "alignItems": "baseline",
787
843
  "columnGap": "3px", "marginTop": "12px"},
788
844
  [figure("mk-fig-%d" % i, num),
789
- T("p", "+", color={"token": "--mk-accent"},
790
- fontSize="17px", fontFamily=MONO, fontWeight="500")]),
845
+ T("p", "+", color={"token": "--mk-accent-ink"},
846
+ fontSize="22px", fontFamily=DISPLAY, fontWeight="600",
847
+ _m={"fontSize": "16px"})]),
791
848
  T("p", zh, color={"token": "--mk-muted"}, fontSize="12px",
792
849
  marginTop="10px", lineHeight="1.7", _m={"fontSize": "11px"}),
793
850
  ])
@@ -822,7 +879,8 @@ SERVICE_SEC = section("services", [wrap("mk-svc-in", [
822
879
  children=[
823
880
  mono(num, size="12px", color="--mk-faint", track="0.06em"),
824
881
  box("mk-svc-t-%d" % i, {}, [
825
- mono(en, size="10px", color="--mk-accent", track="0.16em"),
882
+ mono(en, size="10px", color="--mk-accent-ink",
883
+ track="0.16em"),
826
884
  T("h3", zh, color={"token": "--mk-ink"}, fontSize="19px",
827
885
  fontWeight="500", marginTop="9px", fontFamily=CJK,
828
886
  letterSpacing="0.01em"),
@@ -857,7 +915,8 @@ PROCESS_SEC = section("process", [wrap("mk-proc-in", [
857
915
  T("h3", zh, color={"token": "--mk-ink"}, fontSize="19px",
858
916
  fontWeight="500", fontFamily=CJK, marginTop="10px",
859
917
  letterSpacing="0.01em"),
860
- mono(en, size="10px", color="--mk-accent", track="0.16em",
918
+ mono(en, size="10px", color="--mk-accent-ink",
919
+ track="0.16em",
861
920
  marginTop="7px"),
862
921
  T("p", body, color={"token": "--mk-muted"},
863
922
  fontSize="13px", lineHeight="2", marginTop="12px"),
@@ -946,7 +1005,8 @@ WORK_SEC = section("works", [wrap("mk-works-in", [
946
1005
  fontWeight="500", fontFamily=CJK, letterSpacing="0.01em",
947
1006
  _m={"fontSize": "16px"}),
948
1007
  mono(cat, size="11px", color="--mk-muted", track="0.1em"),
949
- mono(domain, size="12px", color="--mk-accent", track="0.02em"),
1008
+ mono(domain, size="12px", color="--mk-accent-ink",
1009
+ track="0.02em"),
950
1010
  ])
951
1011
  for i, (name, cat, domain) in enumerate(WORKS)
952
1012
  ]),
@@ -970,8 +1030,8 @@ PRODUCT_SEC = section("products", [wrap("mk-prod-in", [
970
1030
  mono("OUR PRODUCTS", size="11px", color="rgb(140,142,150)"),
971
1031
  T("h2", "不只接案,也開發自己的產品",
972
1032
  color={"token": "--mk-paper"}, fontSize="34px",
973
- fontWeight="600", letterSpacing="-0.025em",
974
- lineHeight="1.3", fontFamily=DISPLAY, marginTop="10px",
1033
+ fontWeight="600", letterSpacing="0.01em",
1034
+ lineHeight="1.35", fontFamily=DISPLAY, marginTop="10px",
975
1035
  _t={"fontSize": "29px"}, _m={"fontSize": "23px"}),
976
1036
  ]),
977
1037
  ]),
@@ -988,14 +1048,14 @@ PRODUCT_SEC = section("products", [wrap("mk-prod-in", [
988
1048
  "paddingTop": "20px", "paddingBottom": "20px"},
989
1049
  children=[
990
1050
  mono("%02d" % (i + 1), size="12px",
991
- color="rgb(122,124,132)", track="0.06em"),
1051
+ color="rgb(128,130,138)", track="0.06em"),
992
1052
  box("mk-prod-t-%d" % i, {}, [
993
1053
  mono(en, size="10px", color="--mk-accent",
994
1054
  track="0.16em"),
995
1055
  T("h3", name, color={"token": "--mk-paper"},
996
1056
  fontSize="24px", fontWeight="600",
997
1057
  fontFamily=DISPLAY, marginTop="9px",
998
- letterSpacing="-0.02em", _m={"fontSize": "21px"}),
1058
+ letterSpacing="0.01em", _m={"fontSize": "21px"}),
999
1059
  # a property of the product, not a column of the
1000
1060
  # table: as a fourth grid child it landed alone in
1001
1061
  # the 72px number gutter on row two at tablet
@@ -1035,7 +1095,7 @@ VOICE_SEC = section("mk-voices", [wrap("mk-voice-in", [
1035
1095
  "customStyles": "border-top:1px solid " + RULE + ";"
1036
1096
  "border-left:0;"},
1037
1097
  children=[
1038
- mono("“", size="24px", color="--mk-accent", track="0"),
1098
+ mono("“", size="24px", color="--mk-accent-ink", track="0"),
1039
1099
  T("p", quote, color={"token": "--mk-ink"}, fontSize="14px",
1040
1100
  lineHeight="2.05", marginTop="4px"),
1041
1101
  box("mk-voice-a-%d" % i, {"marginTop": "22px"}, [
@@ -1058,13 +1118,14 @@ CONTACT = section("contact", [wrap("mk-contact-in", [
1058
1118
  "customStyles": "border-top:1px solid " + RULE + ";"},
1059
1119
  _m={"gridCols": "48px 1fr"},
1060
1120
  children=[
1061
- mono("§07", size="12px", color="--mk-accent", track="0.06em"),
1121
+ mono("§07", size="12px", color="--mk-accent-ink",
1122
+ track="0.06em"),
1062
1123
  box("mk-contact-head-t", {}, [
1063
1124
  mono("START A PROJECT", size="11px", color="--mk-muted"),
1064
1125
  ml("h2", "準備好升級\n你的數位競爭力了嗎?",
1065
1126
  color={"token": "--mk-ink"}, fontSize="44px",
1066
- fontWeight="600", letterSpacing="-0.03em",
1067
- lineHeight="1.24", fontFamily=DISPLAY, marginTop="12px",
1127
+ fontWeight="600", letterSpacing="0.01em",
1128
+ lineHeight="1.3", fontFamily=DISPLAY, marginTop="12px",
1068
1129
  _t={"fontSize": "36px"}, _m={"fontSize": "26px"}),
1069
1130
  T("p", "先看方案抓預算,或直接告訴我們你想解決的問題,"
1070
1131
  "一個工作天內回覆。",
@@ -1089,7 +1150,8 @@ CONTACT = section("contact", [wrap("mk-contact-in", [
1089
1150
  _m={"fontSize": "13px"}),
1090
1151
  ])
1091
1152
  for i, (k, v, vc) in enumerate([
1092
- ("EMAIL", "services@moksaweb.com", "--mk-accent"),
1153
+ ("EMAIL", "services@moksaweb.com",
1154
+ "--mk-accent-ink"),
1093
1155
  ("PHONE", "+886-958-839-939", "--mk-ink"),
1094
1156
  ("LOCATION", "TAICHUNG, TAIWAN", "--mk-ink"),
1095
1157
  ("RESPONSE", "WITHIN 1 BUSINESS DAY", "--mk-ink"),