From c7429c33c0d5d4c34a9098aeb119f77f0ea9dbb7 Mon Sep 17 00:00:00 2001 From: cverorg <292680828+cverorg@users.noreply.github.com> Date: Fri, 25 Sep 2026 01:38:14 +0900 Subject: [PATCH] =?UTF-8?q?host=20mode:=20page=20title=20is=20'=20=C2=B7=20Card=20=C2=B7=20feelreef'=20(placeholder=20'Card?= =?UTF-8?q?=20=C2=B7=20feelreef'=20before=20load),=20not=20the=20sandbox's?= =?UTF-8?q?=20'Try=20Card'.=20Tests=20278/278?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cardtile/serve/card-worker.mjs | 2 +- packages/cardtile/serve/card-worker.test.mjs | 3 +++ packages/cardtile/serve/edit2-assets.mjs | 4 ++-- packages/cardtile/w/sandbox-i18n.mjs | 11 +++++++++- packages/cardtile/w/sandbox-i18n.test.mjs | 2 +- packages/cardtile/w2/edit2.mjs | 21 ++++++++++++++++++++ packages/cardtile/w2/host-boot.test.mjs | 19 +++++++++++++++++- 7 files changed, 56 insertions(+), 6 deletions(-) diff --git a/packages/cardtile/serve/card-worker.mjs b/packages/cardtile/serve/card-worker.mjs index 56dea02..c966413 100644 --- a/packages/cardtile/serve/card-worker.mjs +++ b/packages/cardtile/serve/card-worker.mjs @@ -328,7 +328,7 @@ function sandboxEditorHtml(localeKey, { host = '' } = {}) { -${escLite(persona.pageTitle)} +${escLite(host ? persona.hostPageTitle : persona.pageTitle)} diff --git a/packages/cardtile/serve/card-worker.test.mjs b/packages/cardtile/serve/card-worker.test.mjs index 300b66f..da7729a 100644 --- a/packages/cardtile/serve/card-worker.test.mjs +++ b/packages/cardtile/serve/card-worker.test.mjs @@ -1057,6 +1057,9 @@ test('host mode: /edit?host=feelreef serves the editor with host opts, noindex, assert.deepEqual(optsOf(body), { host, sandbox: false, locale: 'ja', tableBase: '/try/edit/t/ja/' }); assert.ok(body.includes('id="sandbox-banner" hidden'), 'host mode ships the sandbox banner hidden'); assert.match(body, /id="host-save"/); + // ๐Ÿ”ด host mode never ships the sandbox's "Try Card ยท feelreef" โ€” before the parent hands over + // a real card there is nothing to name yet, so the tab reads the bare product name. + assert.match(body, /Card ยท feelreef<\/title>/, 'host mode ships the placeholder title, not the sandbox\'s'); assert.deepEqual(touched, []); } } diff --git a/packages/cardtile/serve/edit2-assets.mjs b/packages/cardtile/serve/edit2-assets.mjs index 4af3859..6909d62 100644 --- a/packages/cardtile/serve/edit2-assets.mjs +++ b/packages/cardtile/serve/edit2-assets.mjs @@ -3,7 +3,7 @@ // verbatim as its editing table. See that file for what is and is not committed here. export const EDIT2_BODY_HTML = "\n\n<!-- ๐Ÿ”ด SANDBOX ONLY โ€” and IDENTICAL markup to w/index.html's, ids included, because card-worker.mjs\n writes both sentences into it SERVER-SIDE with the same two string replacements it does for the\n internal editor. A visitor (or a crawler, or `curl`) who never runs the JavaScript must still be\n able to read \"nothing here is saved\". Two banners that differ by one attribute is how one of\n them stops being written to. -->\n<div class=\"ctw-sandbox-banner\" id=\"sandbox-banner\" hidden>\n <span id=\"sandbox-banner-text\"></span>\n <span id=\"sandbox-banner-status\"></span>\n <button id=\"sandbox-reset\" class=\"ctw-btn ctw-btn-ghost\" type=\"button\"></button>\n</div>\n\n<header class=\"ctw-bar\">\n <h1>cardtile-w2</h1>\n <!-- the VIEW switch: three views over the one card markdown; switching never changes content.\n The card is the default editing surface (tap to edit, drag to reorder); the board is the\n engine's tugtile; Markdown is whole-page Markdown mode. Same switch on every screen size. -->\n <div class=\"ctw-views\" role=\"group\" id=\"viewswitch\">\n <button class=\"ctw-view\" id=\"view-card\" type=\"button\" data-view-btn=\"card\" aria-pressed=\"true\"></button>\n <button class=\"ctw-view\" id=\"view-board\" type=\"button\" data-view-btn=\"board\" aria-pressed=\"false\"></button>\n <button class=\"ctw-view\" id=\"view-md\" type=\"button\" data-view-btn=\"md\" aria-pressed=\"false\"></button>\n </div>\n <div class=\"ctw-bar-spacer\"></div>\n <div class=\"ctw-actions\">\n <!-- ๐Ÿ”ด THE DOOR, and it behaves exactly as it does on the internal editor: it opens a sheet\n that says what happens next, parks the whole card for an hour, and walks. It never clears\n the draft โ€” see editor.mjs's bootSandbox() for the day that button deleted ten minutes of\n somebody's work. -->\n <button id=\"publish\" class=\"ctw-btn ctw-btn-go\" hidden></button>\n <!-- HOST MODE only (feelreef's own page embeds this on a REAL card): the save status and ใ€Œๅญ˜ใ€.\n The editor holds no credential โ€” Save is a postMessage to the parent (w2/host-bridge.mjs). -->\n <span id=\"host-status\" class=\"ctw-host-status\" role=\"status\" aria-live=\"polite\" hidden></span>\n <button id=\"host-save\" class=\"ctw-btn ctw-btn-go\" type=\"button\" hidden></button>\n </div>\n</header>\n\n<main class=\"ctw-w2\">\n <!-- the editing table: the engine's web tugtile, loaded by edit2.mjs. OPTIONAL since the card\n became the editing surface: hidden until the ใ€Œ็‰ŒๆกŒใ€ view is chosen, then the titled panel\n beside the card (wide) or in place of it (phone). Loaded and kept in sync while hidden. It is still the only place to reorder, and it lists every cell,\n including one that renders to nothing. -->\n <section class=\"ctw-w2-table\">\n <h2 class=\"ctw-w2-panel-title\" id=\"table-heading\"></h2>\n <!-- what this view is, what you can do in it, and what the other lanes are (owner rule 2) -->\n <p class=\"ctw-w2-panel-hint\" id=\"table-hint\"></p>\n <iframe id=\"table\" class=\"ctw-w2-frame\" title=\"editing table\"></iframe>\n </section>\n <!-- the card itself โ€” the PRIMARY surface: tap anything on it to edit it (edit2.mjs wireCard) -->\n <section class=\"ctw-w2-preview\">\n <iframe id=\"canvas\" class=\"ctw-w2-frame\" title=\"preview\"></iframe>\n <!-- once per browser: how the card is edited. An instruction about the EDITOR lives in the\n editor, never as a tile on the card (it used to be the seed's third cell). -->\n <div class=\"ctw-coach\" id=\"coach\" role=\"status\" hidden>\n <span id=\"coach-text\"></span>\n <button class=\"ctw-coach-close\" id=\"coach-close\" type=\"button\" aria-label=\"Close\">โœ•</button>\n </div>\n </section>\n</main>\n\n\n<!-- ใ€Œ๏ผ‹ๅŠ ไธ€ๅผต็‰Œใ€ โ†’ ONE ROW of seven icons. Not a scrolling column of cards with paragraphs of\n explanation: the seven kinds of tile a Card has, side by side, each with its name under it.\n Opens from the button at the END of a lane, and remembers which lane that was. -->\n<div class=\"ctw-modal\" id=\"picker\" hidden>\n <div class=\"ctw-modal-panel ctw-pick\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"picker-title\">\n <button class=\"ctw-modal-close\" id=\"picker-close\" aria-label=\"Close\">โœ•</button>\n <h2 id=\"picker-title\"></h2>\n <p class=\"ctw-modal-hint\" id=\"picker-lane\"></p>\n <div class=\"ctw-pick-row\" id=\"picker-row\"></div>\n </div>\n</div>\n\n<!-- The Card-typed sheet. EVERY id below is w/index.html's own, because the rows inside #modal-body\n are built by w/cell-form-core.mjs โ€” the same module, the same field ids (`f-<name>`, `e-<name>`)\n โ€” and a second set of names for the same controls is how two surfaces start disagreeing about\n what \"the address box\" is called. -->\n<div class=\"ctw-modal\" id=\"modal\" hidden>\n <div class=\"ctw-modal-panel\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"modal-title\">\n <button class=\"ctw-modal-close\" id=\"modal-close\" aria-label=\"้—œ้–‰\">โœ•</button>\n <h2 id=\"modal-title\"></h2>\n <p class=\"ctw-modal-hint\" id=\"modal-hint\"></p>\n <div id=\"modal-body\"></div>\n <!-- no \"raw parameters\" block here (ruler #3, 2026-09-24): it showed a read-only line of the\n tile's settings that nobody but a developer reads, and the Markdown view already shows the\n whole card as the file. The internal editor (w/) keeps it, under ใ€Œ้€ฒ้šŽใ€. -->\n <!-- kept from the internal editor, and for the same reason: a reorder path that cannot fail, and the\n CONTROL the drag harness measures against. On this surface the drag is the board's own. -->\n <div class=\"ctw-move\" id=\"modal-move\">\n <button class=\"ctw-btn ctw-move-btn\" id=\"modal-up\" type=\"button\">ๅพ€ไธŠ็งป</button>\n <button class=\"ctw-btn ctw-move-btn\" id=\"modal-down\" type=\"button\">ๅพ€ไธ‹็งป</button>\n </div>\n <div class=\"ctw-modal-foot\">\n <button class=\"ctw-btn ctw-btn-danger\" id=\"modal-delete\">ๅˆช้™ค้€™ๅผต็‰Œ</button>\n <button class=\"ctw-btn ctw-btn-go\" id=\"modal-save\">ๅฎŒๆˆ</button>\n </div>\n </div>\n</div>\n\n<!-- Whole-page Markdown mode. The board's own `file-text` header action opens THIS โ€” one page, two\n complete ways to edit one card, never both on screen at once. Leaving validates first\n (w/md-guard.mjs), so there is no way out of this mode having quietly lost a tile. -->\n<div class=\"ctw-modal\" id=\"mdmodal\" hidden>\n <div class=\"ctw-modal-panel ctw-md-panel\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"md-title\">\n <button class=\"ctw-modal-close\" id=\"md-close\" aria-label=\"้—œ้–‰\">โœ•</button>\n <h2 id=\"md-title\"></h2>\n <p class=\"ctw-modal-hint\" id=\"md-note\"></p>\n <p class=\"ctw-md-error\" id=\"md-error\" hidden></p>\n <textarea id=\"md\" class=\"ctw-md\" spellcheck=\"false\"></textarea>\n <div class=\"ctw-modal-foot\">\n <button class=\"ctw-btn\" id=\"md-cancel\"></button>\n <button class=\"ctw-btn ctw-btn-go\" id=\"md-done\"></button>\n </div>\n </div>\n</div>\n\n<!-- the sentence before the door โ€” identical behaviour to the internal editor's -->\n<div class=\"ctw-modal\" id=\"doormodal\" hidden>\n <div class=\"ctw-modal-panel ctw-door\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"door-title\">\n <h2 id=\"door-title\"></h2>\n <p class=\"ctw-modal-hint\" id=\"door-note\"></p>\n <div class=\"ctw-modal-foot\">\n <button class=\"ctw-btn\" id=\"door-back\"></button>\n <button class=\"ctw-btn ctw-btn-go\" id=\"door-go\"></button>\n </div>\n </div>\n</div>\n\n"; export const EDIT2_CSS = "/* cardtile-w's own chrome. Deliberately NOT the card's stylesheet โ€” the canvas is an iframe and the\n card's :root tokens stay in there, where they belong. Nothing here can reach the card, and nothing\n in the card can reach this. */\n\n:root {\n --ctw-bg: #12161c;\n --ctw-panel: #1a1f27;\n --ctw-line: #2b323d;\n --ctw-ink: #e6ecf3;\n --ctw-dim: #93a2b4;\n --ctw-go: #0556ff;\n --ctw-danger: #d3574a;\n color-scheme: dark;\n}\n@media (prefers-color-scheme: light) {\n :root { --ctw-bg:#f2f4f7; --ctw-panel:#fff; --ctw-line:#d8dee7; --ctw-ink:#111820; --ctw-dim:#5b6879; color-scheme: light; }\n}\n\n* { box-sizing: border-box; }\nbody {\n margin: 0; background: var(--ctw-bg); color: var(--ctw-ink);\n font: 15px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Noto Sans TC\", sans-serif;\n height: 100vh; display: flex; flex-direction: column;\n}\n\n/* SANDBOX ONLY โ€” see index.html's #sandbox-banner and editor.mjs's bootSandbox(). Sits above the\n bar, on purpose: the one fact a sandbox visitor must see before anything else. */\n.ctw-sandbox-banner {\n display: flex; align-items: center; justify-content: center; gap: .9rem; flex-wrap: wrap;\n padding: .55rem 1rem; text-align: center;\n background: color-mix(in srgb, var(--ctw-go) 16%, var(--ctw-panel));\n border-bottom: 1px solid var(--ctw-line); font-size: .82rem; font-weight: 600;\n}\n.ctw-btn-ghost { background: transparent; border-color: color-mix(in srgb, var(--ctw-ink) 30%, transparent); }\n/* the banner carries BOTH sentences; the media query below picks one. Desktop reads the long form. */\n#sandbox-banner-status { display: none; }\n\n.ctw-bar { display: flex; align-items: center; gap: 1rem; padding: .6rem 1rem; border-bottom: 1px solid var(--ctw-line); background: var(--ctw-panel); }\n.ctw-bar h1 { font-size: .95rem; margin: 0; letter-spacing: .02em; opacity: .8; }\n/* ๐Ÿฉธ THE SANDBOX'S MASTHEAD. The internal editor's <h1> says `cardtile-w` and should โ€” it is a\n local ops tool and that is its name. The PUBLIC sandbox got the same <h1>, so a visitor arriving\n from feelreef.com/store read an internal codename in the largest text on the page, with no\n feelreef anywhere on screen and no way back. card-worker.mjs swaps in this markup for the\n sandbox only; the link IS the way back. */\n.ctw-bar h1.ctw-brand { display: flex; align-items: baseline; gap: .38rem; opacity: 1; }\n.ctw-brand a { color: inherit; text-decoration: none; font-weight: 700; letter-spacing: -.01em; }\n.ctw-brand a:hover { text-decoration: underline; }\n.ctw-brand span { font-weight: 600; opacity: .7; }\n.ctw-brand span::before { content: 'ยท'; margin-right: .38rem; opacity: .55; }\n.ctw-lanes { display: flex; gap: .35rem; flex: 1; flex-wrap: wrap; }\n.ctw-actions { display: flex; gap: .4rem; }\n\n.ctw-tab, .ctw-btn {\n font: inherit; font-size: .85rem; padding: .35rem .7rem; border-radius: 8px;\n border: 1px solid var(--ctw-line); background: transparent; color: var(--ctw-ink); cursor: pointer;\n}\n.ctw-tab.is-on { background: color-mix(in srgb, var(--ctw-go) 20%, transparent); border-color: var(--ctw-go); }\n.ctw-tab-add { opacity: .7; }\n/* ๐Ÿฉธ A SPECIFICITY LOSS, MEASURED AS WHITE TEXT ON NOTHING. `.ctw-btn:hover` is (0,2,0) and\n `.ctw-btn-go` below is (0,1,0), so hovering the primary button replaced its solid blue with\n `rgba(17,24,32,.08)` and left `color:#fff` behind: the door out of the sandbox read as an empty\n outline with white words that vanished into it. On a phone it is worse than a desktop hover โ€”\n after tapping ๅฎŒๆˆ in the bottom sheet the finger's leftover :hover lands on the fixed bottom bar\n in the same place, so the main call to action sat there transparent with the palette showing\n through it.\n `:not(.ctw-btn-go)` costs one selector and makes the two rules disjoint instead of ranked; the\n primary button gets its own hover, which brightens the blue rather than replacing it. */\n.ctw-btn:not(.ctw-btn-go):hover, .ctw-tab:hover { background: color-mix(in srgb, var(--ctw-ink) 8%, transparent); }\n.ctw-btn[disabled] { opacity: .4; cursor: default; }\n.ctw-btn-go { background: var(--ctw-go); border-color: var(--ctw-go); color: #fff; }\n.ctw-btn-go:hover { filter: brightness(1.08); }\n.ctw-btn-go[disabled]:hover { filter: none; }\n.ctw-btn-danger { color: var(--ctw-danger); border-color: color-mix(in srgb, var(--ctw-danger) 45%, transparent); }\n.ctw-file { display: inline-flex; align-items: center; }\n/* ๐Ÿ”ด PRE-EXISTING BUG, found while building the mobile menu (2026-09-05): `.ctw-file{display:\n inline-flex}` above and the browser's own `[hidden]{display:none}` have EQUAL specificity\n (0,1,0 vs 0,1,0) โ€” a tie the author stylesheet wins over the UA one, so `el('file-block').hidden\n = true` (bootSandbox(), for the real-editor-only \"้–‹ๅ•Ÿ .md\"/Open .md button) was silently not\n hiding anything: the button stayed visible โ€” and unlocalised โ€” in every sandbox session, on\n desktop too, just easy to miss inline in a busy toolbar. It only became obvious once it showed up\n as its own row in the new mobile dropdown. `[hidden]` alone cannot out-specificity a class rule;\n this one line can, and needs nothing else to change. */\n.ctw-file[hidden] { display: none; }\n\n.ctw-main { flex: 1; display: grid; grid-template-columns: 340px 1fr; min-height: 0; }\n.ctw-side { border-right: 1px solid var(--ctw-line); padding: .9rem; overflow: auto; display: flex; flex-direction: column; gap: .6rem; background: var(--ctw-panel); }\n.ctw-side h2 { font-size: .78rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ctw-dim); margin: .4rem 0 0; }\n\n.ctw-palette { display: grid; gap: .4rem; }\n.ctw-add { text-align: left; font: inherit; padding: .5rem .6rem; border-radius: 10px; border: 1px solid var(--ctw-line); background: transparent; color: inherit; cursor: pointer; display: grid; gap: .15rem; }\n.ctw-add:hover { border-color: var(--ctw-go); }\n.ctw-add b { font-size: .9rem; }\n.ctw-add span { font-size: .76rem; color: var(--ctw-dim); }\n\n.ctw-url { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; padding: .45rem .55rem; border-radius: 8px; border: 1px solid var(--ctw-line); background: var(--ctw-bg); color: var(--ctw-ink); width: 100%; }\n.ctw-md { flex: 1; min-height: 180px; resize: vertical; font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; padding: .6rem; border-radius: 10px; border: 1px solid var(--ctw-line); background: var(--ctw-bg); color: var(--ctw-ink); }\n.ctw-note { font-size: .76rem; color: var(--ctw-dim); margin: 0; }\n\n.ctw-canvas-wrap { min-height: 0; overflow: hidden; }\n.ctw-canvas { width: 100%; height: 100%; border: 0; display: block; }\n\n.ctw-modal { position: fixed; inset: 0; z-index: 50; background: rgba(0,0,0,.5); backdrop-filter: blur(3px); display: flex; align-items: center; justify-content: center; padding: 1.2rem; }\n.ctw-modal[hidden] { display: none; }\n.ctw-modal-panel { position: relative; width: min(560px, 100%); max-height: 86vh; overflow: auto; background: var(--ctw-panel); border: 1px solid var(--ctw-line); border-radius: 16px; padding: 1.4rem 1.2rem 1rem; }\n/* top-LEFT, matching the card's own overlays โ€” one place to reach for \"close\", everywhere */\n.ctw-modal-close { position: absolute; top: .7rem; left: .7rem; width: 34px; height: 34px; border-radius: 50%; border: 1px solid var(--ctw-line); background: transparent; color: inherit; cursor: pointer; }\n.ctw-modal-panel h2 { margin: .2rem 0 .2rem 2.4rem; font-size: 1.05rem; }\n.ctw-modal-hint { margin: 0 0 1rem; color: var(--ctw-dim); font-size: .82rem; }\n\n.ctw-field { display: grid; gap: .25rem; margin-bottom: .85rem; }\n.ctw-field[hidden] { display: none; }\n.ctw-field > span { font-size: .82rem; font-weight: 600; }\n.ctw-field > em { font-size: .74rem; color: var(--ctw-dim); font-style: normal; }\n.ctw-field input[type=text], .ctw-field input[type=url], .ctw-field select, .ctw-field textarea {\n font: inherit; font-size: .88rem; padding: .45rem .55rem; border-radius: 8px;\n border: 1px solid var(--ctw-line); background: var(--ctw-bg); color: var(--ctw-ink); width: 100%;\n}\n/* ๐Ÿฉธ WAS MONOSPACE. A prose cell's content box in Menlo reads as a code editor โ€” the review's\n first-time visitor met a programming font the moment they tried to write a sentence. The form\n asks for writing now, and it looks like somewhere you write. (Raw markdown still gets a\n monospace box; that one is Markdown mode's, and it is opt-in.) */\n.ctw-field textarea { font-size: .88rem; line-height: 1.5; resize: vertical; }\n.ctw-field input[type=checkbox] { width: 20px; height: 20px; }\n/* per-field validation, empty until saveCell fills it โ€” never an alert(), never a lost sheet */\n.ctw-err { font-size: .76rem; color: var(--ctw-danger); font-style: normal; }\n.ctw-err:empty { display: none; }\n\n/* a collapsed group of advanced rows (icon settings, \"add a heading\") โ€” closed by default, so the\n short form is the one a first-time visitor meets and the long one is still one tap away */\n.ctw-group { margin: 0 0 .9rem; border: 1px solid var(--ctw-line); border-radius: 10px; padding: .55rem .7rem; }\n.ctw-group > summary { cursor: pointer; font-size: .82rem; font-weight: 600; }\n.ctw-group[open] > summary { margin-bottom: .7rem; }\n\n/* โ”€โ”€ the picture picker (CONTROL.ASSET) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n `<input type=\"file\" accept=\"image/*\">` inside the label is what opens a phone's photo library.\n The thumbnail is the confirmation โ€” before this, a picture field gave no feedback at all until\n the canvas repainted, and the canvas is below the fold on a phone. */\n.ctw-asset { display: grid; gap: .5rem; }\n.ctw-asset-shot { display: grid; place-items: center; min-height: 0; }\n.ctw-asset-shot:empty { display: none; }\n.ctw-asset-shot img { max-width: 100%; max-height: 8rem; border-radius: 10px; display: block; }\n.ctw-asset-acts { display: flex; gap: .4rem; }\n.ctw-asset-pick { display: inline-flex; align-items: center; justify-content: center; }\n.ctw-asset-orurl { display: grid; gap: .2rem; }\n.ctw-asset-orurl > span { font-size: .74rem; color: var(--ctw-dim); }\n.ctw-asset-err { font-size: .76rem; color: var(--ctw-danger); font-style: normal; }\n.ctw-asset-err:empty { display: none; }\n\n.ctw-raw { font-size: .78rem; color: var(--ctw-dim); margin-bottom: .8rem; }\n.ctw-raw input { width: 100%; font: 12px ui-monospace, Menlo, monospace; padding: .35rem .5rem; margin: .4rem 0 .2rem; border-radius: 6px; border: 1px solid var(--ctw-line); background: var(--ctw-bg); color: var(--ctw-dim); }\n.ctw-raw p { margin: 0; }\n\n/* move up / move down โ€” the reordering path that does not need a gesture (see editor.mjs) */\n.ctw-move { display: flex; gap: .5rem; margin: .2rem 0 .9rem; }\n.ctw-move[hidden] { display: none; }\n.ctw-move-btn { flex: 1; }\n\n.ctw-modal-foot { display: flex; justify-content: space-between; gap: .6rem; padding-top: .4rem; border-top: 1px solid var(--ctw-line); }\n\n/* the save confirmation โ€” deliberately plainer than the cell editor: it asks one question. */\n.ctw-pub { max-width: 30rem; }\n.ctw-pub-backup { display: flex; align-items: center; gap: .55rem; margin: .9rem 0 .5rem; font-size: .95rem; }\n.ctw-pub-backup input { width: 20px; height: 20px; }\n.ctw-pub-note { margin: 0 0 .9rem; font-size: .8rem; color: var(--ctw-dim); }\n.ctw-pub-result { font-size: .82rem; line-height: 1.55; margin-bottom: .9rem; padding: .6rem .7rem; border-radius: 8px; background: var(--ctw-bg); border: 1px solid var(--ctw-line); white-space: pre-wrap; }\n\n/* โ”€โ”€ Markdown mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n ONE page, TWO complete views, never both โ€” the same either/or as tugtile's header view toggle.\n Everything the card view owns is display:none'd rather than moved, so nothing about the form\n layout has to know this mode exists. */\n.ctw-md-toggle { display: inline-flex; align-items: center; gap: .4rem; }\n.ctw-md-ico { width: 16px; height: 16px; flex: 0 0 auto; }\n.ctw-md-ico-grid { display: none; }\n.ctw-md-toggle[aria-pressed=\"true\"] .ctw-md-ico-doc { display: none; }\n.ctw-md-toggle[aria-pressed=\"true\"] .ctw-md-ico-grid { display: inline; }\n\nbody[data-md=\"1\"] .ctw-main { grid-template-columns: 1fr; }\nbody[data-md=\"1\"] .ctw-canvas-wrap,\nbody[data-md=\"1\"] .ctw-palette,\nbody[data-md=\"1\"] #add-cell-h2,\nbody[data-md=\"1\"] #cardurl-block,\nbody[data-md=\"1\"] .ctw-lanes { display: none; }\nbody[data-md=\"1\"] .ctw-side { border-right: 0; }\nbody[data-md=\"1\"] #md-block { display: flex; flex-direction: column; flex: 1; min-height: 0; gap: .5rem; }\n/* ๐Ÿ”ด raw markdown IS allowed a monospace box โ€” this one is opt-in and it is the file. What is not\n allowed is a monospace box in a FORM, which is what a first-time visitor met instead of a\n sentence. Two different questions, two different answers. */\nbody[data-md=\"1\"] .ctw-md { flex: 1; min-height: 55vh; min-height: 55dvh; }\n.ctw-md-error { margin: 0; font-size: .82rem; color: var(--ctw-danger); font-weight: 600; }\n.ctw-md-error[hidden] { display: none; }\n\n/* โ”€โ”€ mobile disclosure menu (\"toolbar collapsed into a menu\") โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n Native <details>/<summary> โ€” no JS, so it costs nothing under prefers-reduced-motion (there is\n no animation to guard) and stays keyboard/AT operable for free. See index.html for the markup;\n this is the ONLY styling that decides whether it acts like a menu (mobile, below) or is\n invisible as a wrapper (desktop, further down). */\n.ctw-mobile-menu > summary { list-style: none; cursor: pointer; }\n.ctw-mobile-menu > summary::-webkit-details-marker { display: none; }\n.ctw-mobile-menu-body { display: flex; gap: .4rem; }\n\n/* โ”€โ”€ mobile layout (โ‰ค768px โ€” phones and small tablets in portrait; 1024px+ keeps the desktop grid\n unchanged, per the guardrail) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */\n@media (max-width: 768px) {\n html, body { overflow-x: hidden; } /* belt-and-braces: nothing below should ever need this, but a\n runaway child must never be able to open page-level h-scroll */\n\n /* ๐Ÿฉธ ONE PAGE, ONE SCROLL โ€” and the last tile in the palette was UNREACHABLE without it.\n Measured at 390ร—844: chrome 232px + canvas `min-height:45dvh` (380) + side `max-height:42dvh`\n (354) = 966px of boxes in an 844px viewport. The side panel's own frame hung 123px off the\n bottom, the fixed door button covered 55 more, and `document.scrollHeight` was exactly 844 โ€” so\n the PAGE could not scroll to bring any of it back. Scrolling inside the panel reached its own\n end while the last two buttons sat below the window. Three nested scrollers, and the one that\n mattered was the one that did not exist.\n The fix is to stop capping anything. The canvas is as tall as the card (set in JS from the\n iframe's own content โ€” see fitCanvas), the panel is as tall as its buttons, and the page\n scrolls, once, the way every other page on a phone does. */\n body { height: auto; min-height: 100vh; min-height: 100dvh; }\n .ctw-main { display: flex; flex-direction: column; min-height: 0; }\n .ctw-canvas-wrap { order: 1; flex: 0 0 auto; height: auto; min-height: 0; overflow: visible; }\n .ctw-canvas { height: auto; min-height: 40vh; min-height: 40dvh; }\n .ctw-side { order: 2; flex: 0 0 auto; max-height: none; overflow: visible; border-right: 0; border-top: 1px solid var(--ctw-line); }\n\n /* the banner drops to ONE line: 27% of the first screen was an explanation, not the card. Both\n sentences are in the markup and written server-side (a visitor with no JavaScript must still be\n able to read one), and this chooses which โ€” never a truncation. */\n #sandbox-banner-text { display: none; }\n #sandbox-banner-status { display: inline; }\n\n /* Markdown mode on a phone: the textarea IS the page. `dvh`, so an on-screen keyboard shrinks the\n space available instead of pushing the box off the bottom of it. */\n body[data-md=\"1\"] .ctw-md { min-height: 60vh; min-height: 60dvh; }\n body[data-md=\"1\"] #md-h2, body[data-md=\"1\"] #md-note { display: none; }\n .ctw-md-toggle .ctw-md-ico { width: 18px; height: 18px; }\n .ctw-sandbox-banner { padding: .45rem .8rem; font-size: .78rem; }\n\n /* top bar: title + lanes + menu can outgrow 390px in one row, so it wraps โ€” lanes get their own\n full-width, horizontally-scrollable row (never page-level h-scroll: this scrolls, the page\n does not) instead of cramming or wrapping tab text. */\n .ctw-bar { flex-wrap: wrap; row-gap: .5rem; }\n .ctw-lanes { order: 3; flex: 1 1 100%; flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: thin; }\n .ctw-tab { flex: 0 0 auto; }\n .ctw-actions { order: 2; margin-left: auto; }\n\n /* tap targets โ‰ฅ44px (buttons, tabs, the palette's add-block rows, the modal's close button) */\n .ctw-tab, .ctw-btn, .ctw-add { min-height: 44px; }\n .ctw-tab, .ctw-btn { padding: .6rem 1rem; font-size: .95rem; }\n .ctw-modal-close { width: 44px; height: 44px; }\n\n /* text inputs โ‰ฅ16px โ€” below that, iOS Safari zooms the whole page in on focus */\n .ctw-field input[type=text], .ctw-field input[type=url], .ctw-field select, .ctw-field textarea,\n .ctw-asset input[type=url],\n .ctw-url, .ctw-md, .ctw-raw input { font-size: 16px; }\n\n /* the mobile menu: a real dropdown, opened by tapping โ˜ฐ (native <details> โ€” no script) */\n .ctw-mobile-menu { position: relative; }\n .ctw-mobile-menu > summary { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; border-radius: 8px; border: 1px solid var(--ctw-line); font-size: 1.15rem; }\n .ctw-mobile-menu-body { position: absolute; top: calc(100% + 6px); right: 0; z-index: 30; flex-direction: column; min-width: 11rem; padding: .5rem; background: var(--ctw-panel); border: 1px solid var(--ctw-line); border-radius: 10px; box-shadow: 0 10px 28px rgba(0,0,0,.28); }\n .ctw-mobile-menu-body .ctw-btn, .ctw-mobile-menu-body .ctw-file { width: 100%; justify-content: flex-start; }\n\n /* the door CTA becomes a fixed bottom action bar โ€” the one thing a sandbox visitor must always be\n able to reach without hunting for it. `data-sandbox=\"1\"` (set once, in editor.mjs's\n bootSandbox()) scopes this to the sandbox: the REAL internal editor's ๅญ˜ๅˆฐ็ทšไธŠ button keeps its\n normal place in the top bar, unaffected โ€” it was never meant for a stranger's phone. */\n /* ๐Ÿ”ด the MEASURED height of the fixed bottom bar (44px floor + .9rem padding top and bottom),\n not a round number that happened to look right. Under it, the palette's last button hid behind\n the door. */\n body[data-sandbox=\"1\"] { padding-bottom: calc(3.6rem + env(safe-area-inset-bottom)); }\n body[data-sandbox=\"1\"] #publish:not([hidden]) {\n position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;\n width: 100%; margin: 0; border-radius: 0; min-height: 44px;\n padding: .9rem 1rem calc(.9rem + env(safe-area-inset-bottom));\n font-size: 1rem; box-shadow: 0 -4px 16px rgba(0,0,0,.28);\n }\n\n /* the cell editor (and the real editor's save-confirm) become a bottom sheet: anchored to the\n bottom edge instead of centred, full width, rounded top corners only. `dvh` (not `vh`) for the\n height cap so an on-screen keyboard shrinks the AVAILABLE space instead of the sheet spilling\n off-screen underneath it โ€” same fix release/obsidian-tugtile/styles.css uses for its own\n full-screen edit view (\"Dynamic viewport: shrinks with the on-screen keyboard so the editor\n stays inside the visible area\"). No open/close animation: there was none before, and adding one\n here would be exactly the kind of motion prefers-reduced-motion exists to catch โ€” easier to\n simply not add it than to guard it. */\n .ctw-modal { align-items: flex-end; padding: 0; }\n .ctw-modal-panel {\n width: 100%; max-width: 100%;\n max-height: 88vh; max-height: 88dvh;\n border-radius: 16px 16px 0 0;\n padding: 1.1rem 1rem calc(1rem + env(safe-area-inset-bottom));\n }\n /* the close button is 44px wide at this width, so 2.6rem left the heading touching it */\n .ctw-modal-panel h2 { margin-left: 3.2rem; }\n}\n\n/* desktop: the mobile-menu wrapper must add NOTHING to the layout โ€” `display: contents` makes the\n <details>/<div> boxes disappear so undo/open/download sit directly in .ctw-actions, byte-for-byte\n the row that existed before this wrapper โ€” guardrail (4), desktop layout stays intact. */\n@media (min-width: 769px) {\n .ctw-mobile-menu { display: contents; }\n .ctw-mobile-menu > summary { display: none; }\n .ctw-mobile-menu-body { display: contents; }\n}\n\n/* @cvernet/signet โ€” CVER family corner shape (squircle).\n *\n * Apple-style continuous-curvature corners via CSS `corner-shape: squircle`\n * (a real superellipse, not a circular arc), sized by each element's own\n * `border-radius`. Browsers without `corner-shape` gracefully fall back to the\n * normal rounded arc โ€” pure progressive enhancement, no @supports needed.\n *\n * Extracted from liquidframe; this is the CANONICAL CVER-family corner rule.\n * Import once at the app root and every page inherits the family look:\n * import '@cvernet/signet/corners.css';\n *\n * Covers every CVER project regardless of how the radius is applied:\n * - Tailwind `.rounded-*` utilities (cver.net)\n * - inline / bespoke `border-radius` (feelreef)\n * Any element that already has a radius becomes a squircle; radius:0 is a no-op.\n */\n\n:where(*) {\n corner-shape: squircle;\n}\n\n/* Keep genuinely circular elements circular โ€” squircling a 50% radius (avatars,\n dots, circular badges) reads wrong. Opt an element back out with\n `corner-shape: round`. */\n:where(.rounded-full, [style*=\"border-radius:50%\"], [style*=\"border-radius: 50%\"]) {\n corner-shape: round;\n}\n\n/* feelreef's PORCH skin, mirrored for the Card editor's shell (owner ruling 2026-09-24: the\n sandbox visitor stands on our porch, so the shell wears feelreef.com/dashboard's look; the CARD\n keeps its own theme).\n\n MIRRORED, NOT IMPORTED โ€” tile never imports reef code. Every literal names the reef line it was\n copied from (reef/apps/feelreef/โ€ฆ, as of 2026-09-24); w2/porch-tokens.test.mjs pins the ones that\n carry the look and, when the reef checkout is on the machine, diffs the rest against it live.\n\n ๐Ÿ”ด ONE LOOK, NO DARK COPY โ€” ON PURPOSE. reef has no `prefers-color-scheme` copy of the carrier\n block to mirror: app.css:468 ใ€ŒThere is no second skinใ€ (chodaict 2026-08-15: feelreef ships ONE\n look) and reef's own one-look.test.ts forbids asking the OS about colour. The account dashboard\n (the porch) is therefore the same pale sea-green in both OS schemes, and so is this shell. The\n only dark-scheme carrier values reef has belong to `.console.owners-home` (console.css:441) โ€”\n the owner's home, which is the OTHER dashboard, not the porch. `color-scheme: light` below keeps\n native controls (scrollbars, checkboxes, selects) from turning dark on a light ground.\n prefers-contrast is honoured, as app.css does (accessibility, not taste). */\n:root {\n color-scheme: light;\n\n /* type โ€” app.css:101-106 */\n --font-body: 'Nunito Variable', Nunito, -apple-system, 'PingFang TC', 'Hiragino Sans', 'Microsoft JhengHei', 'Noto Sans CJK TC', sans-serif;\n --font-display-cjk: 'Songti TC', 'Hiragino Mincho ProN', 'Yu Mincho', 'Noto Serif CJK TC', 'Noto Serif TC', serif;\n --font-display: 'Young Serif', var(--font-display-cjk);\n\n /* geometry โ€” app.css:172-173 */\n --console-radius: 18px;\n --console-radius-sm: 12px;\n\n /* the carrier block โ€” app.css:237-289 */\n --carrier-bg: rgba(255, 255, 255, 0.72); /* app.css:237 */\n --carrier-bg-solid: #f4f6f6; /* app.css:242 */\n --carrier-text: #0c3a44; /* app.css:243 */\n --carrier-text-muted: #557077; /* app.css:244 */\n --carrier-heading: #0c3a44; /* app.css:245 */\n --carrier-border: rgba(12, 58, 68, 0.1); /* app.css:246 */\n --carrier-shadow: 0 8px 32px rgba(12, 58, 68, 0.1); /* app.css:247 */\n --carrier-accent: #0f7c89; /* app.css:274 */\n --carrier-accent-ink: #ffffff; /* app.css:275 */\n --carrier-input-bg: rgba(12, 58, 68, 0.04); /* app.css:276 */\n --carrier-link: #0f7c89; /* app.css:279 */\n --carrier-outline-border: #0f7c89; /* app.css:289 */\n\n /* feedback โ€” app.css:431-438 */\n --carrier-feedback-success: #1d8b4f; /* app.css:431 */\n --carrier-feedback-error: #c54a3a; /* app.css:432 */\n --carrier-feedback-warning: #b67a1e; /* app.css:433 */\n\n /* derived console surfaces โ€” app.css:450-451 */\n --console-panel: var(--carrier-bg);\n --console-tint: color-mix(in srgb, var(--carrier-accent) 9%, transparent);\n\n /* buttons โ€” app.css:359-428 (only what the shell uses) */\n --btn-ink: #0c3a44; /* app.css:359 */\n --btn-secondary-ink: #00768a; /* app.css:357 */\n --btn-surface: #ffffff; /* app.css:420 */\n --btn-border: #67858a; /* app.css:422 */\n --btn-border-hover: #0f8e9c; /* app.css:423 */\n --btn-radius: 12px; /* app.css:424 */\n --btn-radius-pill: 999px; /* app.css:425 */\n --btn-weight: 800; /* app.css:427 */\n --btn-ghost-hover: rgba(20, 163, 179, 0.06); /* app.css:362 */\n --btn-danger-ink: #b02e28; /* app.css:381 */\n --btn-danger-hover: rgba(220, 38, 38, 0.14); /* app.css:364 */\n\n /* motion โ€” Button.svelte's press (\"the press answers before the server does\") */\n --porch-press: translateY(1px) scale(0.985);\n\n /* the porch ground โ€” routes/dashboard/+page.svelte:2217 (`.screen`) */\n --porch-ground: linear-gradient(180deg, #f1fbfb 0%, #e6f5f3 42%, #f4fcfa 100%);\n}\n\n/* app.css (the @media (prefers-contrast: more) block after ใ€ŒThere is no second skinใ€) */\n@media (prefers-contrast: more) {\n :root { --carrier-border: var(--carrier-heading); }\n}\n\n/* feelreef's two faces, self-hosted the way reef does it (fontsource; see fonts/PROVENANCE.md).\n Latin + latin-ext only: everything else โ€” CJK, Korean โ€” falls through to the system stacks in\n --font-body / --font-display-cjk exactly as on feelreef.com. `swap`, so words never wait on a font.\n `./fonts/` is rewritten to the Worker's immutable asset path by serve/gen-edit2-assets.mjs. */\n@font-face {\n font-family: 'Nunito Variable'; font-style: normal; font-display: swap; font-weight: 200 1000;\n src: url(/try/edit/t/en/fonts/NunitoVariable-latin-ext.woff2) format('woff2-variations');\n unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;\n}\n@font-face {\n font-family: 'Nunito Variable'; font-style: normal; font-display: swap; font-weight: 200 1000;\n src: url(/try/edit/t/en/fonts/NunitoVariable-latin.woff2) format('woff2-variations');\n unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n}\n@font-face {\n font-family: 'Young Serif'; font-style: normal; font-display: swap; font-weight: 400;\n src: url(/try/edit/t/en/fonts/YoungSerif-latin-ext.woff2) format('woff2');\n unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;\n}\n@font-face {\n font-family: 'Young Serif'; font-style: normal; font-display: swap; font-weight: 400;\n src: url(/try/edit/t/en/fonts/YoungSerif-latin.woff2) format('woff2');\n unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;\n}\n\n/* cardtile-w2's own chrome โ€” LAYERED ON TOP of w/editor.css, never a copy of it.\n The banner, the buttons, the sheet, the fields, the picture picker and the modal shell all come\n from there, because the seven sheets are the same seven sheets. What is here is only what this\n surface adds: the table/preview split, the one-row icon picker, and the phone's single toggle.\n\n ๐Ÿ”ด RWD IS CORRECTNESS, not polish. The three things this file is answerable for at 390px: the\n page never scrolls sideways, every target a finger has to hit is at least 44px, and the table and\n the finished card are never both on screen pretending to fit. */\n\n.ctw-bar-spacer { flex: 1; }\n\n/* โ”€โ”€ the split โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n ONE grid, two iframes. Below 1024 the preview is a second full-screen view the toggle swaps to;\n at 1024 and up both are on screen and the toggle is not rendered. */\n.ctw-w2 {\n flex: 1;\n min-height: 0;\n display: grid;\n grid-template-columns: 1fr;\n overflow: hidden; /* the panes scroll; the page does not */\n}\n.ctw-w2 > section { min-width: 0; min-height: 0; display: flex; }\n.ctw-w2-frame {\n flex: 1; width: 100%; height: 100%; border: 0; background: var(--ctw-bg);\n /* the table fades in once its host has published __ready and been handed the card โ€” otherwise\n the engine's own SAMPLE board is visible for a beat, which reads as somebody else's data */\n opacity: 0; transition: opacity .18s ease;\n}\n#table.is-ready, #canvas { opacity: 1; }\n.ctw-w2-preview { border-left: 1px solid var(--ctw-line); background: #fff; }\n\n/* โ‰ฅ44px targets, everywhere a finger lands. Above the phone rules below, so that shrinking a ROW\n can never quietly shrink the target inside it โ€” same specificity, and the last one would win. */\n.ctw-bar .ctw-btn, .ctw-sandbox-banner .ctw-btn { min-height: 44px; }\n\n@media (max-width: 1023px) {\n /* a phone shows ONE view: the board, when chosen, stands in place of the card */\n body[data-view=\"board\"] .ctw-w2-preview { display: none; }\n .ctw-w2-preview { border-left: 0; }\n .ctw-w2-preview .ctw-w2-frame { overflow: auto; }\n\n /* the masthead: ONE row. The brand and the door, nothing else โ€” `.ctw-bar` wraps by default\n below 769px (w/editor.css), and with two children that is a second row for no reason. */\n .ctw-bar { flex-wrap: nowrap; gap: .6rem; padding: .3rem .8rem; }\n .ctw-bar h1 { font-size: .9rem; white-space: nowrap; }\n .ctw-bar .ctw-btn { padding: .45rem .8rem; font-size: .85rem; }\n\n /* the banner: ONE row too. ๐Ÿฉธ 111px in ja (three wrapped lines plus ใ€Œใฏใ˜ใ‚ใ‹ใ‚‰ใ‚„ใ‚Š็›ดใ™ใ€ on its\n own) against 59 in zh โ€” the widest language paid the most for the least. The sentence\n ellipsises and the button keeps its own width, so nothing wraps in any of the nine. Neither\n button gets shorter than 44px: winning ten pixels off the top of the screen by shrinking the\n thing a thumb has to hit is not a trade this file makes. */\n .ctw-sandbox-banner { flex-wrap: nowrap; gap: .5rem; padding: .25rem .7rem; font-size: .75rem; }\n #sandbox-banner-status { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .ctw-sandbox-banner .ctw-btn { flex: 0 0 auto; padding: .3rem .7rem; font-size: .78rem; white-space: nowrap; }\n}\n/* โ”€โ”€ the view switch: ๅก ยท ็‰ŒๆกŒ ยท Markdown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n A segmented control; the board pane exists only in the board view. Default (no data-view yet,\n or \"card\") hides it. */\nbody:not([data-view=\"board\"]) .ctw-w2-table { display: none; }\n.ctw-views {\n display: inline-flex; flex: 0 1 auto; min-width: 0; padding: 3px; gap: 2px;\n border: 1px solid var(--ctw-line); border-radius: 12px; background: var(--ctw-bg);\n}\n.ctw-view {\n min-height: 44px; padding: .3rem .8rem; font: inherit; font-size: .82rem; font-weight: 600;\n white-space: nowrap; border: 0; border-radius: 9px; background: transparent;\n color: var(--ctw-dim); cursor: pointer;\n}\n.ctw-view[aria-pressed=\"true\"] { background: var(--ctw-panel); color: var(--ctw-ink); box-shadow: 0 1px 3px rgba(0,0,0,.15); }\n.ctw-view:focus-visible { outline: 2px solid var(--ctw-go); outline-offset: 1px; }\n@media (max-width: 640px) {\n .ctw-bar h1 { display: none; }\n .ctw-view { padding: .3rem .55rem; font-size: .76rem; min-height: 44px; }\n}\n\n/* โ”€โ”€ card first, board second โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n The card is where editing happens now (tap a tile), so on a wide screen it takes the larger,\n centre column and the board is the titled panel beside it. The board keeps every behaviour; it is\n only smaller. The card column's background frames the card so it reads as THE object on screen. */\n.ctw-w2 > section.ctw-w2-table { flex-direction: column; }\n.ctw-w2-panel-title {\n flex: 0 0 auto; margin: 0; padding: .55rem .9rem;\n font-size: .78rem; font-weight: 700; letter-spacing: .02em; color: var(--ctw-dim);\n border-bottom: 1px solid var(--ctw-line); background: var(--ctw-panel);\n}\n.ctw-w2-preview { position: relative; }\n@media (min-width: 1024px) {\n body[data-view=\"board\"] .ctw-w2 { grid-template-columns: minmax(300px, 1fr) minmax(0, 1.7fr); }\n .ctw-w2-preview { background: var(--ctw-bg); padding: 1.2rem 1.5rem 0; justify-content: center; }\n .ctw-w2-preview .ctw-w2-frame {\n max-width: 560px; margin: 0 auto; background: #fff;\n border-radius: 18px 18px 0 0; box-shadow: 0 10px 40px rgba(0,0,0,.14);\n }\n .ctw-w2-table { border-right: 1px solid var(--ctw-line); }\n .ctw-w2-preview { border-left: 0; }\n}\n\n/* the one-time coach mark: a strip ABOVE the card frame, in the shell's flow โ€” never over it.\n ๐Ÿฉธ It used to float over the top of the card (absolute, top:14px) and, at 390px in host mode,\n covered the person's name. In flow it pushes the card down instead, at every width. */\n.ctw-w2-preview { flex-direction: column; }\n.ctw-w2-preview .ctw-w2-frame { min-height: 0; }\n.ctw-coach {\n order: -1; flex: 0 0 auto; align-self: stretch; margin: .5rem .75rem; max-width: 560px; box-sizing: border-box;\n display: flex; align-items: center; justify-content: space-between; gap: .5rem;\n padding: .45rem .5rem .45rem .95rem; border-radius: 999px;\n font-size: .85rem; font-weight: 600; line-height: 1.35;\n background: rgba(20,20,20,.9); color: #fff; box-shadow: 0 6px 20px rgba(0,0,0,.25);\n}\n.ctw-coach[hidden] { display: none; }\n@media (min-width: 600px) { .ctw-coach { margin: .5rem auto; width: calc(100% - 1.5rem); } }\n.ctw-sandbox-banner[hidden] { display: none; }\n.ctw-coach-close {\n flex: 0 0 auto; min-width: 32px; min-height: 32px; border: 0; border-radius: 50%;\n background: rgba(255,255,255,.14); color: #fff; font: inherit; cursor: pointer;\n}\n\n/* โ”€โ”€ ใ€Œ๏ผ‹ๅŠ ไธ€ๅผต็‰Œใ€: ONE ROW, seven icons โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n ๐Ÿฉธ The palette this replaces was a COLUMN of seven cards, each with a title and a sentence of\n explanation โ€” 27% of a phone's first screen before the card itself began, and a scroll to reach\n the seventh. A row of icons with a name under each is the same seven choices in one glance.\n\n It WRAPS rather than scrolling sideways: a horizontally scrolling row hides its own last item,\n and the seventh kind is the one nobody would ever find. */\n.ctw-pick { width: min(520px, 100%); }\n.ctw-pick-row {\n display: flex; flex-wrap: wrap; gap: .5rem; justify-content: center; margin: .4rem 0 .6rem;\n}\n.ctw-pick-btn {\n flex: 0 0 auto; width: 4.4rem; min-height: 4.6rem;\n display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .3rem;\n font: inherit; font-size: .72rem; padding: .5rem .25rem;\n border: 1px solid var(--ctw-line); border-radius: 12px;\n background: transparent; color: var(--ctw-ink); cursor: pointer;\n}\n.ctw-pick-btn:hover, .ctw-pick-btn:focus-visible {\n background: color-mix(in srgb, var(--ctw-go) 14%, transparent); border-color: var(--ctw-go);\n}\n.ctw-pick-ico { display: block; width: 24px; height: 24px; color: var(--ctw-go); }\n.ctw-pick-ico svg { width: 100%; height: 100%; display: block; }\n.ctw-pick-name { text-align: center; line-height: 1.25; }\n/* each kind says what it is for: the row becomes a two-column list of icon ยท name ยท one line */\n.ctw-pick-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); justify-content: stretch; }\n.ctw-pick-btn { width: auto; min-height: 0; display: grid; grid-template-columns: 24px 1fr; column-gap: .6rem; row-gap: .1rem; align-items: start; justify-items: start; text-align: start; padding: .6rem .75rem; }\n.ctw-pick-btn .ctw-pick-ico { grid-row: span 2; }\n.ctw-pick-btn .ctw-pick-name { text-align: start; font-size: .85rem; }\n.ctw-pick-hint { font-weight: 400; font-size: .75rem; line-height: 1.35; color: var(--ctw-dim); }\n.ctw-w2-panel-hint {\n flex: 0 0 auto; margin: 0; padding: .45rem .9rem; font-size: .8rem; line-height: 1.4;\n color: var(--ctw-dim); border-bottom: 1px solid var(--ctw-line);\n}\n\n/* โ”€โ”€ whole-page Markdown mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n Opened by the board's OWN `file-text` header action (edit2.mjs intercepts it) โ€” one page, two\n complete ways to edit one card, never both at once. The text is the CARD, because the card is\n the file the board is a view of. */\n.ctw-md-panel { width: min(760px, 100%); }\n.ctw-md {\n width: 100%; min-height: 46vh; resize: vertical;\n font: 12.5px/1.6 ui-monospace, \"SF Mono\", Menlo, monospace;\n padding: .6rem .7rem; border-radius: 10px;\n border: 1px solid var(--ctw-line); background: var(--ctw-bg); color: var(--ctw-ink);\n}\n.ctw-md-error {\n margin: 0 0 .6rem; padding: .5rem .6rem; border-radius: 8px; font-size: .82rem;\n color: var(--ctw-danger); border: 1px solid color-mix(in srgb, var(--ctw-danger) 45%, transparent);\n}\n.ctw-md-error[hidden] { display: none; }\n\n/* the banner reads the long sentence on a wide screen and the short one on a phone โ€” same rule\n /try/edit uses, restated here because this document's banner is the same markup. */\n@media (max-width: 640px) {\n #sandbox-banner-text { display: none; }\n #sandbox-banner-status { display: inline; }\n}\n\n/* HOST MODE: the save status beside ใ€Œๅญ˜ใ€. The failure text is the parent's own words, verbatim. */\n.ctw-host-status{font:500 13px/1.3 system-ui,sans-serif;opacity:.7;margin-right:10px;max-width:40ch;overflow-wrap:anywhere}\n.ctw-host-status[data-status=\"failed\"]{opacity:1;color:#c0392b}\nbody[data-host-waiting] .ctw-w2{visibility:hidden}\n\n/* โ•โ• THE PORCH SKIN โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n Owner ruling 2026-09-24: the shell wears feelreef.com/dashboard's look (w2/porch-tokens.css); the\n CARD keeps its own theme โ€” nothing below reaches into either iframe. LAST in this file on purpose:\n it restyles w/editor.css's shared chrome for THIS surface only (w/ keeps its own look).\n\n Step one is the cheap, total one: w/editor.css paints everything from six --ctw-* tokens, so they\n are pointed at the carrier tokens here and every rule that reads them follows. Same specificity as\n editor.css's own :root (incl. its prefers-color-scheme copy) and later, so these win in BOTH OS\n schemes โ€” one look, as on the porch. Step two restyles what tokens alone cannot say (pill shapes,\n the segmented switch, Panel shadow, the display face). */\n:root {\n --ctw-bg: var(--carrier-bg-solid);\n --ctw-panel: var(--carrier-bg);\n --ctw-line: var(--carrier-border);\n --ctw-ink: var(--carrier-text);\n --ctw-dim: var(--carrier-text-muted);\n --ctw-go: var(--carrier-accent);\n --ctw-danger: var(--carrier-feedback-error);\n accent-color: var(--carrier-accent);\n}\nbody {\n font-family: var(--font-body); color: var(--carrier-text);\n background: var(--carrier-bg-solid) var(--porch-ground) no-repeat;\n background-size: 100% 100%;\n}\n::selection { background: rgba(255, 122, 92, 0.25); } /* dashboard +page.svelte:2220 */\n\n/* the banner and the bar: frosted carrier surfaces over the porch water */\n.ctw-sandbox-banner {\n background: var(--console-tint); color: var(--carrier-heading);\n border-bottom: 1px solid var(--carrier-border); font-weight: 700;\n}\n.ctw-bar {\n background: var(--carrier-bg); border-bottom: 1px solid var(--carrier-border);\n -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);\n}\n.ctw-bar h1, .ctw-bar h1.ctw-brand {\n font-family: var(--font-display); font-weight: 400; color: var(--carrier-heading);\n font-size: 1.12rem; letter-spacing: 0; opacity: 1;\n}\n.ctw-brand a { font-weight: 400; letter-spacing: 0; }\n.ctw-brand span { font-family: var(--font-body); font-weight: 700; font-size: .82rem; color: var(--carrier-text-muted); opacity: 1; }\n\n/* buttons โ€” Button.svelte: 1.5px border, weight 800, pill, the press transform */\n.ctw-btn, .ctw-tab {\n font-family: var(--font-body); font-weight: var(--btn-weight);\n border: 1.5px solid var(--carrier-outline-border); border-radius: var(--btn-radius-pill);\n background: var(--btn-surface); color: var(--btn-ink);\n transition: background .15s, border-color .15s, color .15s, transform 90ms ease;\n}\n.ctw-btn:not(.ctw-btn-go):hover, .ctw-tab:hover { background: var(--btn-ghost-hover); color: var(--btn-secondary-ink); }\n.ctw-btn-go { background: var(--carrier-accent); border-color: var(--carrier-accent); color: var(--carrier-accent-ink); }\n.ctw-btn-go:hover { filter: none; background: color-mix(in srgb, var(--carrier-accent) 86%, #000); }\n.ctw-btn-ghost { background: transparent; border-color: var(--carrier-outline-border); color: var(--carrier-link); }\n.ctw-btn-danger { background: transparent; color: var(--btn-danger-ink); border-color: color-mix(in srgb, var(--btn-danger-ink) 55%, transparent); }\n.ctw-btn-danger:not(.ctw-btn-go):hover { background: var(--btn-danger-hover); color: var(--btn-danger-ink); }\n.ctw-btn[disabled] { opacity: .55; }\n/* \"the press answers before the server does\" โ€” Button.svelte `.btn:active` */\n.ctw-btn:active:not(:disabled), .ctw-tab:active, .ctw-view:active, .ctw-pick-btn:active, .ctw-modal-close:active, .ctw-coach-close:active { transform: var(--porch-press); }\n@media (prefers-reduced-motion: reduce) {\n .ctw-btn:active:not(:disabled), .ctw-tab:active, .ctw-view:active, .ctw-pick-btn:active, .ctw-modal-close:active, .ctw-coach-close:active { transform: none; }\n}\n:where(.ctw-btn, .ctw-view, .ctw-pick-btn, .ctw-modal-close, .ctw-coach-close, .ctw-field input, .ctw-field select, .ctw-field textarea, .ctw-md):focus-visible {\n outline: 2px solid var(--btn-secondary-ink); outline-offset: 2px;\n}\n\n/* the view switch: a pill segmented control, accent fill on the chosen segment */\n.ctw-views { border: 1px solid var(--carrier-border); border-radius: var(--btn-radius-pill); background: var(--carrier-input-bg); }\n.ctw-view { border-radius: var(--btn-radius-pill); font-family: var(--font-body); font-weight: 700; color: var(--carrier-text-muted); }\n.ctw-view:hover:not([aria-pressed=\"true\"]) { color: var(--carrier-text); background: var(--btn-ghost-hover); }\n.ctw-view[aria-pressed=\"true\"] { background: var(--carrier-accent); color: var(--carrier-accent-ink); box-shadow: none; }\n\n/* sheets & modals โ€” console Panel: radius 18, carrier border + shadow, on the solid ground */\n.ctw-modal { background: rgba(12, 58, 68, .28); }\n.ctw-modal-panel {\n background: var(--carrier-bg-solid); color: var(--carrier-text);\n border: 1px solid var(--carrier-border); border-radius: var(--console-radius); box-shadow: var(--carrier-shadow);\n}\n.ctw-modal-panel h2 { font-family: var(--font-display); font-weight: 400; color: var(--carrier-heading); font-size: 1.2rem; }\n.ctw-modal-close { border: 1.5px solid var(--carrier-border); background: var(--btn-surface); color: var(--carrier-text); corner-shape: round; }\n.ctw-modal-close:hover { border-color: var(--carrier-outline-border); color: var(--carrier-link); }\n.ctw-modal-foot { border-top-color: var(--carrier-border); }\n.ctw-field > span { font-weight: 700; color: var(--carrier-heading); }\n.ctw-field input[type=text], .ctw-field input[type=url], .ctw-field select, .ctw-field textarea,\n.ctw-md, .ctw-url {\n background: var(--carrier-input-bg); border: 1px solid var(--carrier-border); border-radius: var(--console-radius-sm); color: var(--carrier-text);\n}\n.ctw-group, .ctw-pub-result { border-color: var(--carrier-border); border-radius: var(--console-radius-sm); background: var(--carrier-bg); }\n.ctw-err, .ctw-asset-err { color: var(--carrier-feedback-error); }\n.ctw-md-error { border-radius: var(--console-radius-sm); }\n.ctw-pick-btn { border-radius: var(--console-radius-sm); background: var(--btn-surface); border-color: var(--carrier-border); font-weight: 700; }\n.ctw-pick-btn:hover, .ctw-pick-btn:focus-visible { background: var(--console-tint); border-color: var(--carrier-accent); }\n.ctw-pick-ico { color: var(--carrier-accent); }\n\n/* the card column frames the card on the porch water; the board is a Panel beside it */\n.ctw-w2-preview { background: transparent; border-left-color: var(--carrier-border); }\n.ctw-w2-table { background: var(--carrier-bg); }\n.ctw-w2-panel-title {\n font-family: var(--font-display); font-weight: 400; font-size: .95rem; letter-spacing: 0;\n color: var(--carrier-heading); background: var(--carrier-bg); border-bottom-color: var(--carrier-border);\n}\n@media (min-width: 1024px) {\n .ctw-w2-preview { background: transparent; }\n .ctw-w2-preview .ctw-w2-frame { border-radius: var(--console-radius) var(--console-radius) 0 0; box-shadow: var(--carrier-shadow); }\n .ctw-w2-table { border-right-color: var(--carrier-border); }\n}\n\n/* the coach mark: a porch pill, not a black toast */\n.ctw-coach {\n background: var(--carrier-bg-solid); color: var(--carrier-heading);\n border: 1.5px solid var(--carrier-outline-border); box-shadow: var(--carrier-shadow); font-weight: 700;\n}\n.ctw-coach-close { background: var(--console-tint); color: var(--carrier-heading); corner-shape: round; }\n\n/* host-mode save status, in the same family */\n.ctw-host-status { font: 600 13px/1.3 var(--font-body); color: var(--carrier-text-muted); opacity: 1; }\n.ctw-host-status[data-status=\"failed\"] { color: var(--carrier-feedback-error); }\n\n/* โ”€โ”€ PHONE BAR: two rows at โ‰ค520px (measured 2026-09-24 at 390px: the third view segment clipped\n to ใ€ŒMarkoโ€ฆใ€, the door wrapped to two lines, the banner ellipsised). Row 1 = brand + door (one\n line, never wraps); row 2 = the view switch, full width, three equal segments that never clip.\n The banner wraps (โ‰ค3 lines) instead of ellipsising. Every target stays โ‰ฅ44px. */\n@media (max-width: 520px) {\n .ctw-bar {\n display: grid; grid-template-columns: minmax(0, 1fr) auto;\n grid-template-areas: \"brand actions\" \"views views\";\n align-items: center; gap: .45rem .6rem; padding: .45rem .75rem .55rem;\n }\n .ctw-bar h1, .ctw-bar h1.ctw-brand { display: flex; grid-area: brand; min-width: 0; overflow: hidden; white-space: nowrap; font-size: 1.05rem; }\n .ctw-bar-spacer { display: none; }\n .ctw-actions { grid-area: actions; min-width: 0; }\n .ctw-bar .ctw-btn { white-space: nowrap; font-size: 14px; padding: .4rem .9rem; min-height: 44px; }\n .ctw-views { grid-area: views; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); width: 100%; box-sizing: border-box; }\n .ctw-view { min-width: 0; padding: .3rem .25rem; font-size: .8rem; min-height: 44px; white-space: nowrap; overflow: visible; text-overflow: clip; }\n\n .ctw-sandbox-banner { flex-wrap: wrap; gap: .3rem .6rem; padding: .4rem .75rem; font-size: .78rem; line-height: 1.35; }\n #sandbox-banner-status { white-space: normal; overflow: visible; text-overflow: clip; min-width: 0; flex: 1 1 14rem;\n display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }\n .ctw-sandbox-banner .ctw-btn { min-height: 44px; }\n}\n\n/* โ”€โ”€ the card FLOATS on the porch (โ‰ฅ1024px) โ€” see fitCanvas() in edit2.mjs. The iframe takes the\n card's own height; the column scrolls. min-height covers the beat before the card has loaded. */\n@media (min-width: 1024px) {\n .ctw-w2-preview { overflow-y: auto; align-items: flex-start; padding-bottom: 2rem; }\n .ctw-w2-preview .ctw-w2-frame {\n flex: 0 1 560px; height: auto; min-height: 320px;\n border-radius: var(--console-radius); background: transparent;\n }\n}\n\n/* Scrollbars (Safari drew a thick light-grey bar on the board pane and on the card column): the\n shell's scrolling panes get the same thin translucent bar the card frame already has (edit2.mjs\n CARD_EDIT_CSS). Editor page only โ€” this stylesheet ships nowhere else. The board's own document\n gets the same rule through board-bridge's BOARD_LIGHT_CSS. */\n.ctw-w2-preview, .ctw-w2-frame, .ctw-modal-panel, .ctw-md { scrollbar-width: thin; scrollbar-color: rgba(128,128,128,.45) transparent; }\n:is(.ctw-w2-preview, .ctw-w2-frame, .ctw-modal-panel, .ctw-md)::-webkit-scrollbar { width: 8px; height: 8px; background: transparent; }\n:is(.ctw-w2-preview, .ctw-w2-frame, .ctw-modal-panel, .ctw-md)::-webkit-scrollbar-track { background: transparent; }\n:is(.ctw-w2-preview, .ctw-w2-frame, .ctw-modal-panel, .ctw-md)::-webkit-scrollbar-thumb { background: rgba(128,128,128,.45); border-radius: 8px; }\n"; -export const EDIT2_JS = "(() => {\n // packages/tugtile/board-core.js\n function tileRenderText(tileLines) {\n const out = [];\n const m = tileLines[0].match(/^- \\[.\\] ?(.*)/);\n out.push((m ? m[1] : tileLines[0]).replace(/\\s*%%tg-home:.*?%%/, \"\"));\n for (let i = 1; i < tileLines.length; i++) {\n out.push(tileLines[i].replace(/^(?:\\t| {1,4})/, \"\"));\n }\n return out.join(\"\\n\");\n }\n function parseFile(text2) {\n const eol = /\\r\\n/.test(text2) ? \"\\r\\n\" : \"\\n\";\n const lines2 = text2.split(/\\r\\n|\\r|\\n/);\n const settingsIdx = lines2.findIndex((l) => l.indexOf(\"%% kanban:settings\") === 0 || l.indexOf(\"%% tugtile:settings\") === 0);\n const end = settingsIdx === -1 ? lines2.length : settingsIdx;\n let fmEnd = 0;\n if (lines2[0] === \"---\") {\n const c = lines2.indexOf(\"---\", 1);\n if (c !== -1) fmEnd = c + 1;\n }\n const isFence = (l) => /^(```|~~~)/.test(l);\n const isHr = (l) => /^\\*\\*\\*+[ \\t]*$/.test(l);\n const isHeading = (l) => l.indexOf(\"## \") === 0;\n let archiveIdx = -1;\n {\n let fence2 = false;\n for (let i = fmEnd; i < end; i++) {\n if (isFence(lines2[i])) {\n fence2 = !fence2;\n continue;\n }\n if (fence2) continue;\n if (isHr(lines2[i])) {\n let j = i + 1;\n while (j < end && lines2[j].trim() === \"\") j++;\n if (j < end && isHeading(lines2[j])) {\n archiveIdx = i;\n break;\n }\n }\n }\n }\n const laneEnd = archiveIdx === -1 ? end : archiveIdx;\n const archive = archiveIdx === -1 ? \"\" : lines2.slice(archiveIdx, end).join(\"\\n\").replace(/^\\s+/, \"\").replace(/\\s+$/, \"\");\n let firstCol = -1;\n {\n let fence2 = false;\n for (let i = fmEnd; i < laneEnd; i++) {\n if (isFence(lines2[i])) {\n fence2 = !fence2;\n continue;\n }\n if (!fence2 && isHeading(lines2[i])) {\n firstCol = i;\n break;\n }\n }\n }\n if (firstCol === -1) firstCol = laneEnd;\n const pre = lines2.slice(0, firstCol).join(\"\\n\");\n const post = settingsIdx === -1 ? \"\" : lines2.slice(settingsIdx).join(\"\\n\");\n const columns = [];\n let col = null, tileLines = null, fence = false;\n const flush = () => {\n if (tileLines && col) {\n const cl = tileLines.slice();\n while (cl.length && cl[cl.length - 1].trim() === \"\") cl.pop();\n const cm = /^- \\[(.)\\]/.exec(cl[0]);\n const bm = /\\s+\\^([A-Za-z0-9-]+)$/.exec(cl[0]);\n const clForText = cl.slice();\n if (bm) clForText[0] = cl[0].slice(0, bm.index);\n col.tiles.push({ raw: cl.join(\"\\n\"), text: tileRenderText(clForText), check: cm ? cm[1] : \" \", block: bm ? bm[1] : null });\n }\n tileLines = null;\n };\n for (let i = firstCol; i < laneEnd; i++) {\n const ln = lines2[i];\n if (isFence(ln)) {\n fence = !fence;\n if (tileLines) tileLines.push(ln);\n else if (col) col.lead.push(ln);\n continue;\n }\n if (!fence && isHeading(ln)) {\n flush();\n col = { header: ln, title: ln.slice(3).trim(), tiles: [], lead: [] };\n columns.push(col);\n continue;\n }\n if (!col) continue;\n if (!fence && /^- \\[.\\]/.test(ln)) {\n flush();\n tileLines = [ln];\n } else if (tileLines) tileLines.push(ln);\n else col.lead.push(ln);\n }\n flush();\n for (const c of columns) {\n while (c.lead.length && c.lead[0].trim() === \"\") c.lead.shift();\n while (c.lead.length && c.lead[c.lead.length - 1].trim() === \"\") c.lead.pop();\n }\n let settings = {};\n const sm = /```\\s*\\n([\\s\\S]*?)\\n```/.exec(post);\n if (sm) {\n try {\n settings = JSON.parse(sm[1]);\n } catch (e) {\n settings = {};\n }\n }\n return { pre, columns, archive, post, settings, eol };\n }\n function serializeFile(model) {\n const pre = (model.pre || \"\").replace(/\\s+$/, \"\");\n const body = model.columns.map((c) => {\n const lead = c.lead && c.lead.length ? c.lead.join(\"\\n\") + \"\\n\\n\" : \"\";\n return c.header + \"\\n\\n\" + lead + c.tiles.map((cd) => cd.raw).join(\"\\n\");\n }).join(\"\\n\\n\\n\");\n const archive = (model.archive || \"\").replace(/^\\s+/, \"\").replace(/\\s+$/, \"\");\n const post = (model.post || \"\").replace(/^\\s+/, \"\").replace(/\\s+$/, \"\");\n let out = (pre ? pre + \"\\n\\n\" : \"\") + body;\n if (archive) out += \"\\n\\n\\n\" + archive;\n if (post) out += \"\\n\\n\\n\" + post;\n out += \"\\n\";\n return model.eol === \"\\r\\n\" ? out.replace(/\\n/g, \"\\r\\n\") : out;\n }\n\n // packages/cssmd/cssmd.js\n var ESC_RE = /[&<>]/g;\n var ESC_MAP = { \"&\": \"&\", \"<\": \"<\", \">\": \">\" };\n function escHtml(s) {\n return String(s == null ? \"\" : s).replace(ESC_RE, (c) => ESC_MAP[c]);\n }\n\n // packages/sitetile/site-core.js\n function splitFrontmatter(text2) {\n const m = /^---\\n([\\s\\S]*?)\\n---\\n?/.exec(text2);\n if (!m) return { meta: {}, body: text2 };\n const meta = {};\n const lines2 = m[1].split(\"\\n\");\n for (let i = 0; i < lines2.length; i++) {\n const kv = /^([\\w-]+):\\s*(.*)$/.exec(lines2[i]);\n if (!kv) continue;\n const key = kv[1];\n const marker = kv[2].trim();\n if (marker === \"|\" || marker === \">\") {\n const block = [];\n let base = null;\n while (i + 1 < lines2.length) {\n const nxt = lines2[i + 1];\n if (nxt.trim() === \"\") {\n block.push(\"\");\n i++;\n continue;\n }\n const ind = (nxt.match(/^[ \\t]*/)[0] || \"\").length;\n if (base === null) base = ind;\n if (ind < base) break;\n block.push(nxt.slice(base));\n i++;\n }\n while (block.length && block[block.length - 1] === \"\") block.pop();\n meta[key] = marker === \">\" ? block.join(\" \") : block.join(\"\\n\");\n } else {\n meta[key] = marker;\n }\n }\n return { meta, body: text2.slice(m[0].length) };\n }\n function parseParams(raw) {\n const map = {};\n if (!raw) return map;\n const re = /(\\w+)=(\"[^\"]*\"(?:โ†’\\S+)?|\\S+)/g;\n let m;\n while (m = re.exec(raw)) {\n const key = m[1];\n const val = m[2];\n if (val.charAt(0) === '\"') {\n const lm = /^\"([^\"]*)\"(?:โ†’(\\S+))?$/.exec(val);\n if (lm) map[key] = lm[2] !== void 0 ? { label: lm[1], href: lm[2] } : lm[1];\n else map[key] = val;\n } else map[key] = val;\n }\n const bare = raw.replace(/\"[^\"]*\"(?:โ†’\\S+)?/g, (q) => \" \".repeat(q.length));\n let bm;\n const bre = /(?:^|\\s)([a-zA-Z]\\w*)(?=\\s|$)/g;\n while (bm = bre.exec(bare)) {\n const w = bm[1];\n if (!(w in map) && !new RegExp(\"\\\\b\" + w + \"\\\\s*=\").test(bare)) map[w] = true;\n }\n return map;\n }\n var RE_CELL_BULLET = /^\\s*[-*+ใƒปโ€ข]\\s+/;\n var RE_TURN_BULLET = new RegExp(RE_CELL_BULLET.source + \"|^\\\\s*[\\u30FB\\u2022]\\\\S\");\n\n // packages/cardtile/card-core.js\n var DEFAULT_COLS = 6;\n var SPAN_MAX = 6;\n var DEFAULT_LANE = \"grid\";\n var RE_CARD_MARKER = /^%%\\s*card:\\s*(.*?)\\s*%%[ \\t]*([^\\n]*)\\n?([\\s\\S]*)$/;\n var clampSpan = (v, max) => {\n const n = Number(v);\n if (!Number.isFinite(n)) return 1;\n return Math.min(max, Math.max(1, Math.floor(n)));\n };\n function extractCell(text2) {\n const m = RE_CARD_MARKER.exec(text2 || \"\");\n if (!m) return { type: \"text\", params: {}, rawParams: \"\", body: (text2 || \"\").trim() };\n const sp = /^(\\S+)\\s*([\\s\\S]*)$/.exec(m[1].trim());\n const type = sp ? sp[1] : \"text\";\n const rawParams = sp ? sp[2].trim() : \"\";\n const body = (m[2] + (m[3] ? \"\\n\" + m[3] : \"\")).replace(/\\s+$/, \"\");\n return { type, params: parseParams(rawParams), rawParams, body: body.replace(/^\\n+/, \"\") };\n }\n function cellToRaw(cell) {\n const marker = \"%% card: \" + cell.type + (cell.rawParams ? \" \" + cell.rawParams : \"\") + \" %%\";\n const body = cell.body || \"\";\n if (!body) return \"- [ ] \" + marker;\n if (!body.includes(\"\\n\")) return \"- [ ] \" + marker + \" \" + body;\n return \"- [ ] \" + marker + body.split(\"\\n\").map((l) => \"\\n\t\" + l).join(\"\");\n }\n var RE_ASSET_LANE = /^assets$/i;\n var RE_DRAWER_LANE = /^drawer\\s*:\\s*(.+)$/i;\n var splitDrawer = (title) => {\n const m = RE_DRAWER_LANE.exec(String(title || \"\").trim());\n if (!m) return null;\n const bar = m[1].indexOf(\"|\");\n const id = (bar < 0 ? m[1] : m[1].slice(0, bar)).trim().toLowerCase().replace(/\\s+/g, \"-\");\n return { id, title: bar < 0 ? \"\" : m[1].slice(bar + 1).trim() };\n };\n function parseCard(md) {\n const m = parseFile(md);\n const { meta } = splitFrontmatter((m.pre || \"\") + \"\\n\");\n const cells = [];\n const blocks = [];\n const drawers = [];\n let faceLane = null;\n const assets = {};\n for (const col of m.columns) {\n const laneTitle = String(col.title || \"\").trim();\n if (RE_ASSET_LANE.test(laneTitle)) {\n for (const t of col.tiles) {\n const c = extractCell(t.text);\n const id = (c.params.id || \"\").trim();\n if (c.type === \"asset\" && id) assets[id] = { mime: c.params.mime || \"image/webp\", b64: (c.body || \"\").trim() };\n }\n continue;\n }\n const d = splitDrawer(col.title);\n const laneCells = col.tiles.map((t) => extractCell(t.text));\n if (d) {\n drawers.push({ id: d.id, title: d.title, cells: laneCells });\n continue;\n }\n const label2 = faceLane == null ? \"\" : String(col.title || \"\").trim();\n if (faceLane == null) faceLane = col.title;\n blocks.push({ label: label2, start: cells.length, count: laneCells.length });\n cells.push(...laneCells);\n }\n if (!blocks.length) blocks.push({ label: \"\", start: 0, count: 0 });\n return {\n pre: m.pre || \"\",\n post: m.post || \"\",\n cols: DEFAULT_COLS,\n // a Card is six columns. See the note on DEFAULT_COLS.\n lane: faceLane != null ? faceLane : DEFAULT_LANE,\n blocks,\n cells,\n drawers,\n assets\n };\n }\n function serializeCard(model) {\n const laneOf = (title, cells2) => ({\n header: \"## \" + title,\n title,\n lead: [],\n tiles: (cells2 || []).map((c) => ({ raw: cellToRaw(c), text: \"\", check: \" \", block: null }))\n });\n const cells = model.cells || [];\n const blocks = model.blocks && model.blocks.length ? model.blocks : [{ label: \"\", start: 0, count: cells.length }];\n const columns = blocks.map((b, i) => laneOf(\n i === 0 ? model.lane || DEFAULT_LANE : b.label || DEFAULT_LANE,\n cells.slice(b.start, b.start + b.count)\n ));\n for (const d of model.drawers || []) columns.push(laneOf(\"drawer: \" + d.id + (d.title ? \" | \" + d.title : \"\"), d.cells));\n const assetIds = Object.keys(model.assets || {});\n if (assetIds.length) {\n columns.push(laneOf(\"assets\", assetIds.sort().map((id) => ({\n type: \"asset\",\n rawParams: `id=${id} mime=${model.assets[id].mime}`,\n params: { id, mime: model.assets[id].mime },\n body: model.assets[id].b64\n }))));\n }\n return serializeFile({ pre: model.pre || \"\", columns, archive: \"\", post: model.post || \"\", eol: \"\\n\" });\n }\n function packGrid(model) {\n const cols = Math.max(1, model.cols || DEFAULT_COLS);\n const occ = [];\n const ensureRow = (r) => {\n while (occ.length <= r) occ.push(new Array(cols).fill(false));\n };\n const fits = (r, c, w, h) => {\n if (c + w > cols) return false;\n for (let dr = 0; dr < h; dr++) {\n ensureRow(r + dr);\n for (let dc = 0; dc < w; dc++) if (occ[r + dr][c + dc]) return false;\n }\n return true;\n };\n const mark = (r, c, w, h) => {\n for (let dr = 0; dr < h; dr++) {\n ensureRow(r + dr);\n for (let dc = 0; dc < w; dc++) occ[r + dr][c + dc] = true;\n }\n };\n const placements = [];\n (model.cells || []).forEach((cell, index) => {\n const w = Math.min(clampSpan(cell.params.w, SPAN_MAX), cols);\n const h = clampSpan(cell.params.h, SPAN_MAX);\n let r = 0, done = false;\n while (!done) {\n ensureRow(r);\n for (let c = 0; c < cols; c++) {\n if (fits(r, c, w, h)) {\n mark(r, c, w, h);\n placements.push({ cell, index, row: r, col: c, w, h, type: cell.type });\n done = true;\n break;\n }\n }\n r++;\n if (r > 1e4) {\n done = true;\n }\n }\n });\n return { placements, rows: occ.length, cols };\n }\n var VAL = '(\"[^\"]*\"(?:\\u2192\\\\S+)?|\\\\S+)';\n var tokenRe = (key) => new RegExp(\"(^|\\\\s)\" + key + \"=\" + VAL);\n function quoteParam(v) {\n const s = String(v);\n if (s === \"\") return '\"\"';\n return /[\\s\"]/.test(s) ? '\"' + s.replace(/\"/g, \"\") + '\"' : s;\n }\n function setToken(rp, key, val) {\n const re = tokenRe(key);\n if (re.test(rp)) return rp.replace(re, (m, pre) => pre + key + \"=\" + val);\n return (rp ? rp + \" \" : \"\") + key + \"=\" + val;\n }\n function removeToken(rp, key) {\n return rp.replace(tokenRe(key), \"\").replace(/^\\s+|\\s+$/g, \"\").replace(/\\s{2,}/g, \" \");\n }\n function normalizeCell(cell) {\n return parseCard(\"## \" + DEFAULT_LANE + \"\\n\" + cellToRaw(cell) + \"\\n\").cells[0];\n }\n function setTitle(model, name) {\n const pre = String(model.pre || \"\");\n const line = \"title: \" + String(name == null ? \"\" : name).trim();\n const fence = /^---\\r?\\n([\\s\\S]*?)(\\r?\\n)---[ \\t]*$/m.exec(pre);\n if (!fence) return { ...model, pre: \"---\\n\" + line + \"\\n---\\n\" + (pre ? \"\\n\" + pre.replace(/^\\n+/, \"\") : \"\") };\n const inner = fence[1];\n const next = /^title:.*$/m.test(inner) ? inner.replace(/^title:.*$/m, line) : inner ? line + \"\\n\" + inner : line;\n return { ...model, pre: pre.slice(0, fence.index) + \"---\\n\" + next + fence[2] + \"---\" + pre.slice(fence.index + fence[0].length) };\n }\n function reorder(model, from, to) {\n const cells = (model.cells || []).slice();\n if (from < 0 || from >= cells.length) return model;\n const [moved] = cells.splice(from, 1);\n const dest = Math.min(Math.max(0, to), cells.length);\n cells.splice(dest, 0, moved);\n return { ...model, cells };\n }\n\n // packages/cardtile/colour.mjs\n function parseColour(input) {\n const s = String(input == null ? \"\" : input).trim().toLowerCase();\n let m = /^#([0-9a-f]{3})$/.exec(s);\n if (m) return [...m[1]].map((c) => parseInt(c + c, 16));\n m = /^#([0-9a-f]{6})$/.exec(s);\n if (m) return [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16));\n m = /^rgba?\\(\\s*([\\d.]+)[\\s,]+([\\d.]+)[\\s,]+([\\d.]+)/.exec(s);\n if (m) return [1, 2, 3].map((i) => Math.max(0, Math.min(255, Math.round(Number(m[i])))));\n return null;\n }\n var toHex = (rgb) => \"#\" + rgb.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, \"0\")).join(\"\");\n function luminance(rgb) {\n const [r, g, b] = rgb.map((v) => {\n const c = v / 255;\n return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;\n });\n return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n }\n function contrast(a, b) {\n const la = luminance(a), lb = luminance(b);\n const [hi, lo] = la >= lb ? [la, lb] : [lb, la];\n return (hi + 0.05) / (lo + 0.05);\n }\n function mix(a, b, p) {\n const t = Math.max(0, Math.min(100, p)) / 100;\n return [0, 1, 2].map((i) => a[i] * t + b[i] * (1 - t));\n }\n var composite = (over, under, a) => mix(over, under, a * 100);\n var BODY_FLOOR = 4.5;\n var LARGE_FLOOR = 3;\n function inkFor(ground) {\n const dark = [17, 17, 17], light = [255, 255, 255];\n return contrast(ground, light) >= contrast(ground, dark) ? light : dark;\n }\n var isDarkGround = (ground) => inkFor(ground)[0] > 128;\n function scrimFloor(ground, ink, floor = BODY_FLOOR) {\n const extremes = [[0, 0, 0], [255, 255, 255]];\n for (let d = 0; d <= 100; d++) {\n const a = d / 100;\n if (extremes.every((photo) => contrast(composite(ground, photo, a), ink) >= floor)) return d;\n }\n return 100;\n }\n var DEFAULT_ACCENT = \"#0a8c8e\";\n var groundFrom = (accent) => palette(\"dark\", accent).ground;\n var inkFrom = (accent) => palette(\"dark\", accent).ink;\n var backdropDimDefault = (accent) => scrimFloor(groundFrom(accent), inkFrom(accent));\n function resolveBackdropDim(value, accent) {\n const fallback = backdropDimDefault(accent);\n if (value == null || String(value).trim() === \"\") return fallback;\n const n = Number(value);\n if (!Number.isFinite(n)) return fallback;\n return Math.max(0, Math.min(100, n));\n }\n function readableAccent(accent, grounds, floor = BODY_FLOOR) {\n const bgs = Array.isArray(grounds[0]) ? grounds : [grounds];\n const ok = (c) => bgs.every((b) => contrast(c, b) >= floor);\n if (ok(accent)) return accent;\n const target = isDarkGround(bgs[0]) ? [255, 255, 255] : [0, 0, 0];\n for (let p = 0; p <= 100; p += 2) {\n const c = mix(target, accent, p);\n if (ok(c)) return c;\n }\n return target;\n }\n var RAMP = {\n // [accent %, neutral base]\n dark: {\n ground: [8, \"#0c0c0c\"],\n surface: [10, \"#161616\"],\n \"surface-2\": [15, \"#1a1a1a\"],\n \"surface-hi\": [22, \"#1e1e1e\"],\n line: [14, \"transparent\"],\n \"line-hi\": [50, \"transparent\"],\n ink: [12, \"#ffffff\"],\n muted: [46, \"#898989\"],\n faint: [30, \"#6a6a6a\"],\n \"mist-a\": [11, \"#0c0c0c\"],\n \"mist-b\": [0, \"#0b0b0b\"],\n \"page-ink\": [28, \"#cecece\"]\n },\n light: {\n ground: [6, \"#f2f2f2\"],\n surface: [4, \"#ffffff\"],\n \"surface-2\": [7, \"#fafafa\"],\n \"surface-hi\": [12, \"#f4f4f4\"],\n line: [26, \"transparent\"],\n \"line-hi\": [60, \"transparent\"],\n ink: [10, \"#181818\"],\n muted: [34, \"#5e5e5e\"],\n faint: [26, \"#878787\"],\n \"mist-a\": [8, \"#f6f6f6\"],\n \"mist-b\": [0, \"#f8f8f8\"],\n \"page-ink\": [22, \"#3d3d3d\"]\n }\n };\n function palette(mode, accent) {\n const acc = parseColour(accent) || parseColour(DEFAULT_ACCENT);\n const out = { accent: acc };\n for (const [name, [p, base]] of Object.entries(RAMP[mode])) {\n if (base === \"transparent\") continue;\n out[name] = p === 0 ? parseColour(base) : mix(acc, parseColour(base), p);\n }\n const backs = [out.ground, out.surface, out[\"surface-2\"]];\n out[\"accent-bright\"] = readableAccent(acc, backs);\n out.muted = readableAccent(out.muted, backs);\n out[\"page-ink\"] = readableAccent(out[\"page-ink\"], [out.ground]);\n out.faint = readableAccent(out.faint, [out.surface, out[\"surface-2\"]], LARGE_FLOOR);\n return out;\n }\n var DERIVED = [\"accent-bright\", \"muted\", \"page-ink\", \"faint\"];\n function tokens(mode, accent) {\n const p = palette(mode, accent);\n const out = Object.entries(RAMP[mode]).filter(([name]) => !DERIVED.includes(name)).map(([name, [pc, base]]) => `--cp-${name}:` + (pc === 0 ? base : `color-mix(in srgb,var(--cp-accent) ${pc}%,${base})`));\n for (const name of DERIVED) out.push(`--cp-${name}:${toHex(p[name])}`);\n return out.join(\";\");\n }\n function themeCss(mode, accent) {\n const page = (m) => `html{background-color:var(--cp-ground);background-image:none}`;\n if (mode) return `:root{color-scheme:${mode};${tokens(mode, accent)}}${page(mode)}`;\n return `:root{color-scheme:light dark;${tokens(\"dark\", accent)}}${page(\"dark\")}@media(prefers-color-scheme:light){:root{${tokens(\"light\", accent)}}}`;\n }\n\n // packages/cardtile/yt.mjs\n var posterPath = (videoId) => `/_yt/${videoId}.jpg`;\n\n // packages/cardtile/marks.mjs\n var ICON_DOMAIN_RE = /^(?=.{4,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$/;\n var iconPath = (domain) => `/_icon/${domain}.ico`;\n\n // packages/dynamic-corals/shared/close-button.mjs\n var CLOSE_BUTTON_CSS = \"width:44px;height:44px;border-radius:50%;border:1px solid color-mix(in srgb,var(--cp-accent) 35%,transparent);background:color-mix(in srgb,var(--cp-ground) 66%,transparent);color:var(--cp-accent);font-size:1.2rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)\";\n var closeButtonStates = (sel) => `${sel}:hover{background:color-mix(in srgb,var(--cp-accent) 20%,transparent)}${sel}:focus-visible{outline:2px solid var(--cp-accent);outline-offset:2px}`;\n\n // packages/dynamic-corals/drawer/drawer-style.mjs\n var DRAWER_CSS = [\n // the layer is inert until a panel is targeted, so it never swallows clicks on the card\n \".dc-drawer-layer{position:fixed;inset:0;z-index:600;pointer-events:none}\",\n \".dc-drawer-layer:has(.dc-drawer:target){pointer-events:auto}\",\n \"body:has(.dc-drawer:target){overflow:hidden}\",\n // the scrim\n \".dc-drawer-scrim{position:absolute;inset:0;display:block;opacity:0;visibility:hidden;cursor:pointer;\",\n \"background:color-mix(in srgb,var(--cp-ground,#0a1628) 66%,transparent);\",\n \"-webkit-backdrop-filter:blur(16px) saturate(140%);backdrop-filter:blur(16px) saturate(140%);\",\n \"transition:opacity .3s,visibility 0s .3s}\",\n \".dc-drawer-layer:has(.dc-drawer:target) .dc-drawer-scrim{opacity:1;visibility:visible;transition:opacity .3s,visibility 0s 0s}\",\n // the panel: a bottom sheet on a phone, a centred dialog once there is room\n \".dc-drawer{position:absolute;inset:auto 0 0;max-height:88vh;max-height:88svh;overflow-y:auto;\",\n \"overscroll-behavior:contain;background:var(--cp-ground,#0a1628);\",\n \"border-top:1px solid var(--cp-accent-border,rgba(184,144,232,.25));\",\n \"border-radius:var(--r-xl,20px) var(--r-xl,20px) 0 0;\",\n \"padding:var(--s-6,1.5rem) var(--s-4,1rem) var(--s-8,2rem);\",\n \"transform:translateY(100%);visibility:hidden;\",\n \"transition:transform .4s cubic-bezier(.34,1.2,.64,1),visibility 0s .4s}\",\n \".dc-drawer:target{transform:translateY(0);visibility:visible;transition:transform .4s cubic-bezier(.34,1.2,.64,1),visibility 0s 0s}\",\n \"@media(min-width:720px){.dc-drawer{inset:50% auto auto 50%;width:min(680px,92vw);\",\n \"transform:translate(-50%,-45%) scale(.98);opacity:0;border-radius:var(--r-xl,20px);\",\n \"border:1px solid var(--cp-accent-border,rgba(184,144,232,.25));\",\n \"transition:transform .35s cubic-bezier(.34,1.2,.64,1),opacity .25s,visibility 0s .35s}\",\n \".dc-drawer:target{transform:translate(-50%,-50%) scale(1);opacity:1}}\",\n // the close control โ€” the QR popup's identity, this host's placement. It is a LINK, because\n // what it does is navigate back out of the fragment, and a link does that without script.\n //\n // ๐Ÿ”ด TOP-LEFT, matching the QR popup. chodaict: ใ€Œๆˆ‘่ฆบๅพ—่ฉฒๅ…จ้ƒจๆ”พๅทฆไธŠ่ง’,่กŒ็‚บๅฐฑๆ˜ฏ่ทŸqrไธ€ๆจฃ้—œ้–‰ๅฏ้—œใ€\n // modalๅค–ไนŸๅฏ้—œใ€. It used to float RIGHT, so a reader who learned where โœ• lives from one overlay\n // had to hunt for it in the other โ€” and every overlay on a Card is the same promise, so it should\n // be the same gesture. The dismiss half already matched: the scrim is a clickable link, and the\n // QR overlay closes on a click that lands on itself.\n // ๐Ÿ”ด It sits in the LAYER now, not in the panel, so it can be genuinely fixed to the viewport\n // corner exactly like the QR's. Inside `.dc-drawer` it could not: that element carries a\n // `transform`, which creates a containing block for fixed descendants, so `position:fixed` there\n // still resolved against the panel and still scrolled away with the content.\n //\n // Same coordinates as the QR popup's โ€” `top:1rem;left:1rem` โ€” because they are the same promise.\n // Hidden and inert until something is actually open, or it would sit over the card.\n \".dc-drawer-close{position:fixed;top:1rem;left:1rem;z-index:602;text-decoration:none;\",\n \"opacity:0;visibility:hidden;transition:opacity .3s,visibility 0s .3s;\" + CLOSE_BUTTON_CSS + \"}\",\n \".dc-drawer-layer:has(.dc-drawer:target) .dc-drawer-close{opacity:1;visibility:visible;transition:opacity .3s,visibility 0s 0s}\",\n closeButtonStates(\".dc-drawer-close\"),\n // the title no longer has to clear a button that is no longer beside it\n \".dc-drawer-title{margin:0 0 var(--s-4,1rem);font-size:1.05rem;font-weight:700;\",\n \"color:var(--cp-accent-bright,#d9c9f3);line-height:1.35}\",\n \"@media(prefers-reduced-motion:reduce){.dc-drawer,.dc-drawer-scrim{transition:none}}\"\n ].join(\"\");\n\n // packages/cardtile/card-render.mjs\n var esc = (s) => escHtml(s == null ? \"\" : s).replace(/\"/g, \""\");\n var mdInline = (s) => esc(s).replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^)\\s]+)\\)/g, '<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\">$1</a>').replace(/\\*\\*(.+?)\\*\\*/g, \"<strong>$1</strong>\").replace(/\\*(.+?)\\*/g, \"<em>$1</em>\").replace(/\\n/g, \"<br>\");\n var LABELS = {\n en: { prev: \"Previous\", next: \"Next\", slide: (n) => `Slide ${n}`, close: \"Close\", qr: \"Show QR code\", linkUnset: \"No address yet\", imgUnset: \"+ Add a picture\" },\n zh: { prev: \"\\u4E0A\\u4E00\\u5F35\", next: \"\\u4E0B\\u4E00\\u5F35\", slide: (n) => `\\u7B2C ${n} \\u5F35`, close: \"\\u95DC\\u9589\", qr: \"\\u986F\\u793A QR code\", linkUnset: \"\\u9084\\u6C92\\u586B\\u7DB2\\u5740\", imgUnset: \"\\uFF0B \\u52A0\\u4E00\\u5F35\\u5716\\u7247\" },\n ja: { prev: \"\\u524D\\u3078\", next: \"\\u6B21\\u3078\", slide: (n) => `${n} \\u679A\\u76EE`, close: \"\\u9589\\u3058\\u308B\", qr: \"QR \\u30B3\\u30FC\\u30C9\\u3092\\u8868\\u793A\", linkUnset: \"URL \\u304C\\u307E\\u3060\\u3067\\u3059\", imgUnset: \"\\uFF0B \\u753B\\u50CF\\u3092\\u8FFD\\u52A0\" },\n ko: { prev: \"\\uC774\\uC804\", next: \"\\uB2E4\\uC74C\", slide: (n) => `${n}\\uBC88\\uC9F8`, close: \"\\uB2EB\\uAE30\", qr: \"QR \\uCF54\\uB4DC \\uBCF4\\uAE30\", linkUnset: \"\\uC8FC\\uC18C\\uAC00 \\uC544\\uC9C1 \\uC5C6\\uC5B4\\uC694\", imgUnset: \"\\uFF0B \\uC0AC\\uC9C4 \\uCD94\\uAC00\" }\n };\n function label(ctx, key, ...args) {\n const override = ctx && ctx.editStrings && ctx.editStrings[key];\n if (typeof override === \"string\" && override) return override;\n const primary = String(ctx && ctx.lang || \"\").trim().toLowerCase().split(/[-_]/)[0];\n const table = LABELS[primary] || LABELS.en;\n const v = table[key] !== void 0 ? table[key] : LABELS.en[key];\n return typeof v === \"function\" ? v(...args) : v;\n }\n var mdBlock = (s) => String(s || \"\").split(/\\n{2,}/).map((para) => {\n const lines2 = para.split(\"\\n\").filter((l) => l.trim());\n if (!lines2.length) return \"\";\n if (lines2.every((l) => /^\\s*[-*]\\s+/.test(l))) {\n return \"<ul>\" + lines2.map((l) => `<li>${mdInline(l.replace(/^\\s*[-*]\\s+/, \"\"))}</li>`).join(\"\") + \"</ul>\";\n }\n if (lines2.length === 1 && /^\\s*([-*_])\\1{2,}\\s*$/.test(lines2[0])) return \"<hr>\";\n const h = /^(#{1,3})\\s+(.*)$/.exec(lines2[0]);\n if (h && lines2.length === 1) return `<h${h[1].length + 2}>${mdInline(h[2])}</h${h[1].length + 2}>`;\n return `<p>${lines2.map(mdInline).join(\"<br>\")}</p>`;\n }).join(\"\");\n var BARE_URL_RE = /^(?:https?:\\/\\/|mailto:|tel:)\\S+$/i;\n var hostLabel = (url) => {\n try {\n return new URL(url).hostname.replace(/^www\\./, \"\") || url;\n } catch {\n return url;\n }\n };\n var linkOf = (body) => {\n const m = /^\\[([^\\]]*)\\]\\(([^)\\s]+)\\)/.exec(body || \"\");\n if (m) return { label: m[1], url: m[2] };\n const bare = (body || \"\").trim();\n if (BARE_URL_RE.test(bare)) return { label: hostLabel(bare), url: bare };\n return { label: bare, url: \"\" };\n };\n var cssUrl = (u) => String(u == null ? \"\" : u).replace(/[\\\\'\"()\\s]/g, encodeURIComponent).replace(/<\\/?/g, \"\");\n var ASSET_RE = /^asset:(.+)$/;\n function resolveAsset(url, ctx) {\n const m = ASSET_RE.exec(String(url || \"\").trim());\n if (!m) return url;\n const a = ctx.assets && ctx.assets[m[1].trim()];\n return a ? `data:${a.mime};base64,${a.b64}` : \"\";\n }\n var SOCIAL_FALLBACK = '<svg class=\"st-social-bare\" viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7\"/><path d=\"M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7L12 19\"/></svg>';\n var IMG_ICON = (src, fallbacks = [], extraClass = \"\") => {\n const rest = fallbacks.map((d) => iconPath(d));\n const attr = rest.length ? ` data-iconfallback=\"${esc(rest.join(\" \"))}\"` : \"\";\n const onerr = 'var f=(this.dataset.iconfallback||\"\").split(\" \").filter(Boolean);if(f.length){this.dataset.iconfallback=f.slice(1).join(\" \");this.src=f[0]}else{this.onerror=null;this.style.display=\"none\"}';\n return `<img class=\"st-cell-icon-img${extraClass ? \" \" + extraClass : \"\"}\" src=\"${esc(src)}\" alt=\"\" width=\"22\" height=\"22\" loading=\"lazy\" referrerpolicy=\"no-referrer\"${attr} onerror='${esc(onerr)}'>`;\n };\n var hostOf = (u) => {\n try {\n return new URL(u).hostname.replace(/^www\\./, \"\");\n } catch {\n return \"\";\n }\n };\n var ICON_ALIAS = { \"twitter.com\": \"x.com\", \"fb.com\": \"facebook.com\", \"goo.gl\": \"google.com\", \"youtu.be\": \"youtube.com\" };\n function iconDomains(host) {\n if (!host) return [];\n const out = [ICON_ALIAS[host] || host];\n const parts = host.split(\".\");\n if (parts.length > 2) out.push(parts.slice(-2).join(\".\"));\n return [...new Set(out)];\n }\n function linkIcon(p, url, ctx) {\n if (p.iconimg) return IMG_ICON(resolveAsset(p.iconimg, ctx), [], \"st-cell-icon-art\");\n if (p.icon && ctx.icons && ctx.icons[p.icon]) return ctx.icons[p.icon];\n const chain = p.iconhost ? [p.iconhost] : iconDomains(hostOf(url));\n if (chain.length) return IMG_ICON(iconPath(chain[0]), chain.slice(1));\n return ctx.icons && ctx.icons._default || \"\";\n }\n var arrowOf = (ctx, px, cls = \"st-cell-arrow\", dir = \"--up-right\") => (ctx.arrows === \"on\" ? ctx.arrow || \"\" : \"\").replace(/__SIZE__/g, String(px)).replace(\"__CLASS__\", cls).replace(\"__DIR__\", dir ? \" signet-arrow\" + dir : \"\");\n function renderEmbed(p, body, size, ctx = {}) {\n if (p.kind === \"slider\") {\n const imgs = (p.images ? String(p.images).split(/\\s*,\\s*/) : []).filter(Boolean).map((u) => resolveAsset(u, ctx));\n if (!imgs.length) return `<div class=\"st-cell st-embed\" ${size} data-coral-type=\"embed\"></div>`;\n const uid = \"sl\" + Math.abs([...String(p.images || \"\")].reduce((h, c) => h * 31 + c.charCodeAt(0) | 0, 7)).toString(36);\n const n = imgs.length;\n const slides = imgs.map((src, i) => {\n const prev = `#${uid}-${(i - 1 + n) % n}`;\n const next = `#${uid}-${(i + 1) % n}`;\n return `<figure id=\"${uid}-${i}\"><img src=\"${esc(src)}\" alt=\"\" loading=\"lazy\" referrerpolicy=\"no-referrer\"><a class=\"st-slide-nav st-slide-prev\" href=\"${prev}\" aria-label=\"${esc(label(ctx, \"prev\"))}\">\\u2039</a><a class=\"st-slide-nav st-slide-next\" href=\"${next}\" aria-label=\"${esc(label(ctx, \"next\"))}\">\\u203A</a></figure>`;\n }).join(\"\");\n const dots = imgs.map((_, i) => `<a class=\"st-slide-dot\" href=\"#${uid}-${i}\" aria-label=\"${esc(label(ctx, \"slide\", i + 1))}\"></a>`).join(\"\");\n const css = [\n // ๐Ÿ”ด NO fixed aspect. chodaict read Incrediville's live page as `6x6(slide*5)` AND\n // `3x6(slide*4)` โ€” the same widget, two different heights, because the PICTURES differ. Pinning\n // 2/1 here would be ใ€Œwidth decides shapeใ€ all over again, four hours after I removed exactly\n // that rule from features. The first slide's natural height sets the track; the rest are\n // covered into it, so a set of squares is square and a set of banners is a banner.\n `.st-slider{position:relative;width:100%;overflow:hidden;border-radius:inherit}`,\n `.st-slider-track{display:flex;width:100%;overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;scrollbar-width:none}`,\n `.st-slider-track::-webkit-scrollbar{display:none}`,\n `.st-slider figure{position:relative;flex:0 0 100%;margin:0;scroll-snap-align:center}`,\n `.st-slider figure:first-child img{height:auto}`,\n `.st-slider figure img{width:100%;height:100%;object-fit:cover;display:block}`,\n `.st-slide-nav{position:absolute;top:50%;transform:translateY(-50%);width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:22px;line-height:1;text-decoration:none;color:#fff;background:rgba(0,0,0,.42);backdrop-filter:blur(2px);opacity:.85;transition:opacity .2s,background .2s}`,\n `.st-slide-nav:hover{opacity:1;background:rgba(0,0,0,.6)}`,\n `.st-slide-prev{left:10px}.st-slide-next{right:10px}`,\n `.st-slider-dots{position:absolute;left:0;right:0;bottom:9px;display:flex;gap:6px;justify-content:center}`,\n `.st-slide-dot{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,.55);box-shadow:0 0 0 1px rgba(0,0,0,.18)}`,\n // one image needs no controls, and controls on a single slide read as broken\n n < 2 ? `.st-slide-nav,.st-slider-dots{display:none}` : \"\"\n ].join(\"\");\n return `<div class=\"st-cell st-embed\" ${size} data-coral-type=\"embed\"><style>${css}</style><div class=\"st-slider\"><div class=\"st-slider-track\">${slides}</div>` + (n > 1 ? `<div class=\"st-slider-dots\">${dots}</div>` : \"\") + \"</div></div>\";\n }\n if (p.kind === \"bubbles\") {\n const imgs = (p.images ? String(p.images).split(/\\s*,\\s*/) : []).filter(Boolean).map((u) => resolveAsset(u, ctx));\n if (!imgs.length) return `<div class=\"st-cell st-embed\" ${size} data-coral-type=\"embed\"></div>`;\n const n = imgs.length;\n const hold = 5;\n const cycle = n * hold;\n const origin = p.origin || \"53% 76%\";\n const originNarrow = p.narrow || \"53% 33%\";\n const slot = 100 / n;\n const inAt = 3;\n const outAt = (slot - 3).toFixed(1);\n const figures = imgs.map((src) => `<figure><img src=\"${esc(src)}\" alt=\"\" loading=\"lazy\" referrerpolicy=\"no-referrer\"></figure>`).join(\"\");\n const bgStyle = p.bg ? ` style=\"background-image:url('${esc(resolveAsset(p.bg, ctx))}')\"` : \"\";\n const css = [\n `.st-carousel{position:relative;width:100%;aspect-ratio:1/1;overflow:hidden;background-size:contain;background-position:center;background-repeat:no-repeat}`,\n `.st-carousel figure{position:absolute;inset:0;margin:0;padding:0;display:flex;align-items:center;justify-content:center;opacity:0;transform-origin:${origin};animation:st-carousel-slide ${cycle}s infinite;animation-fill-mode:both}`,\n `.st-carousel figure img{max-width:100%;max-height:100%;object-fit:contain;display:block;margin:0}`,\n imgs.map((_, i) => `.st-carousel figure:nth-child(${i + 1}){animation-delay:${i * hold}s}`).join(\"\"),\n // ๐Ÿ”ด verbatim: scale(0.001) โ†’ scale(1) about the tail origin = \"pops out of the mouth\"\n `@keyframes st-carousel-slide{0%,${slot.toFixed(1)}%,100%{opacity:0;transform:scale(0.001)}${inAt}%,${outAt}%{opacity:1;transform:scale(1)}}`,\n `@media screen and (max-width:888px){.st-carousel figure{transform-origin:${originNarrow}}}`,\n `@media(prefers-reduced-motion:reduce){.st-carousel figure{animation:none}.st-carousel figure:nth-child(1){opacity:1;transform:scale(1)}}`\n ].join(\"\");\n return `<div class=\"st-cell st-embed\" ${size} data-coral-type=\"embed\"><style>${css}</style><div class=\"st-carousel\"${bgStyle}>${figures}</div></div>`;\n }\n return \"\";\n }\n function anchorFor(url, ctx) {\n const d = /^#(.+)$/.exec(String(url || \"\"));\n if (d) {\n if (ctx.inDrawer) return null;\n const id = esc(d[1]);\n return `href=\"#${id}\" data-drawer=\"${id}\" aria-haspopup=\"dialog\"`;\n }\n return `href=\"${esc(url)}\" target=\"_blank\" rel=\"noopener noreferrer\"`;\n }\n function renderCell(cell, place, ctx) {\n const p = cell.params || {};\n const idx = ctx.editIndex && place.abs != null ? ` data-cell=\"${place.abs}\"` : \"\";\n const size = `data-span=\"${place.w}x${place.h}\" data-w=\"${place.w}\"${idx}`;\n const b = (cell.body || \"\").trim();\n switch (cell.type) {\n case \"profile\": {\n const chips = (p.chips ? String(p.chips).split(/\\s*,\\s*/) : []).map((c) => `<span class=\"st-chip\">${esc(c)}</span>`).join(\"\");\n const avatar = resolveAsset(p.avatar, ctx);\n const qrUrl = ctx.cardUrl || \"\";\n return `<div class=\"st-cell st-hero\" ${size} data-coral-type=\"profile\"><div class=\"st-figure\" data-dynamic-coral=\"qr\" data-url=\"${esc(qrUrl)}\" data-name=\"${esc(ctx.name)}\" data-version=\"4\">${avatar ? `<img class=\"cp-icon\" src=\"${esc(avatar)}\" alt=\"${esc(ctx.name)}\" width=\"80\" height=\"80\" fetchpriority=\"high\" role=\"button\" tabindex=\"0\" aria-label=\"${esc(label(ctx, \"qr\"))}\">` : \"\"}</div><div class=\"st-hero-text\"><h1 class=\"st-hero-name\">${esc(ctx.name)}</h1><p class=\"st-hero-tagline\">${mdInline(b)}</p><div class=\"st-hero-meta\">${chips}</div></div></div>`;\n }\n case \"feature\": {\n const hasHref = !!String(p.href == null ? \"\" : p.href).trim();\n const fa = hasHref ? anchorFor(p.href, ctx) : \"\";\n if (hasHref && !fa) return \"\";\n const linkOpen = hasHref ? `<a class=\"st-gal-link\" ${fa}>` : '<div class=\"st-gal-link\">';\n const linkClose = hasHref ? \"</a>\" : \"</div>\";\n const featureImg = resolveAsset(p.img, ctx);\n if (!featureImg && !p.title && !p.cta && !b) {\n if (!ctx.editIndex) return \"\";\n return `<div class=\"st-cell st-gal-cell st-gal-cell-bare st-gal-labelled st-cell-unset\" ${size} data-coral-type=\"feature\"><div class=\"st-gal-link\"><span class=\"st-gal-img\" aria-hidden=\"true\"></span><span class=\"st-gal-label\">${esc(label(ctx, \"imgUnset\"))}</span></div></div>`;\n }\n if (!p.title && !p.cta && !b) {\n const on = String(p.label == null ? \"\" : p.label).trim().toLowerCase();\n const labelled = !!p.alt && on !== \"\" && on !== \"off\" && on !== \"false\" && on !== \"no\";\n const cls = \"st-cell st-gal-cell st-gal-cell-bare\" + (labelled ? \" st-gal-labelled\" : \"\");\n const capt = labelled ? `<span class=\"st-gal-label\">${esc(p.alt)}</span>` : \"\";\n return `<div class=\"${cls}\" ${size} data-coral-type=\"feature\">${linkOpen}<img class=\"st-gal-img\" src=\"${esc(featureImg)}\" alt=\"${esc(p.alt || \"\")}\" loading=\"lazy\" referrerpolicy=\"no-referrer\">${capt}${linkClose}</div>`;\n }\n const hasImg = !!featureImg;\n return `<div class=\"st-cell st-gal-cell${hasImg ? \"\" : \" st-gal-cell-flat\"}\" ${size} data-coral-type=\"feature\">${linkOpen}${hasImg ? `<img class=\"st-gal-img\" src=\"${esc(featureImg)}\" alt=\"\" loading=\"lazy\"><div class=\"st-gal-scrim\"></div>` : \"\"}<div class=\"st-gal-body\">${b ? `<span class=\"st-gal-eyebrow\">${esc(b)}</span>` : \"\"}${p.title ? `<span class=\"st-gal-title\">${esc(p.title)}</span>` : \"\"}${(() => {\n const arrow = arrowOf(ctx, 15, \"st-gal-arrow\", \"\");\n if (!p.cta && !arrow) return \"\";\n return `<span class=\"st-gal-cta\">${esc(p.cta)}${arrow}</span>`;\n })()}</div>${linkClose}</div>`;\n }\n case \"link\": {\n const { label: linkText, url } = linkOf(b);\n if (!url) {\n if (!ctx.editIndex) return \"\";\n const unset = label(ctx, \"linkUnset\");\n return `<div class=\"st-cell st-cell-tile st-cell-unset\" ${size} data-coral-type=\"linktile\"><div class=\"st-cell-link\"><div class=\"st-cell-head\">${arrowOf(ctx, 17)}</div><div class=\"st-cell-body\"><div class=\"st-cell-title\">${esc(linkText || unset)}</div><div class=\"st-cell-sub\">${esc(unset)}</div></div></div></div>`;\n }\n const a = anchorFor(url, ctx);\n if (!a) return \"\";\n const icon = linkIcon(p, url, ctx);\n const sub = p.sub ? `<div class=\"st-cell-sub\">${esc(p.sub)}</div>` : \"\";\n return `<div class=\"st-cell st-cell-tile\" ${size} data-coral-type=\"linktile\"><a class=\"st-cell-link\" ${a}><div class=\"st-cell-head\">${icon}${arrowOf(ctx, 17)}</div><div class=\"st-cell-body\"><div class=\"st-cell-title\">${esc(linkText)}</div>${sub}</div></a></div>`;\n }\n case \"embed\":\n return renderEmbed(p, b, size, ctx);\n case \"text\":\n return `<div class=\"st-cell st-prose\" ${size} data-coral-type=\"text\">${mdBlock(b)}</div>`;\n case \"video\": {\n const id = String(p.yt || \"\").trim();\n const chan = String(p.channel || \"\").trim();\n if (!id && !chan) return \"\";\n const hit = chan && ctx.videos ? ctx.videos[chan] : null;\n const vid = id || (hit ? hit.id : \"\");\n const poster = p.poster || (vid ? posterPath(vid) : \"\");\n const label2 = b || p.title || hit && hit.title || \"YouTube\";\n const attrs = vid ? `data-yt=\"${esc(vid)}\"` : `data-yt-channel=\"${esc(chan)}\"`;\n return `<div class=\"st-cell st-embed-video\" ${size} data-coral-type=\"video\"><button type=\"button\" class=\"st-embed-video-btn${poster ? \"\" : \" st-embed-video-noposter\"}\" ${attrs} aria-label=\"${esc(label2)}\">${poster ? `<img class=\"st-embed-video-poster\" src=\"${esc(poster)}\" alt=\"\" loading=\"lazy\" referrerpolicy=\"no-referrer\">` : \"\"}<span class=\"st-embed-video-play\" aria-hidden=\"true\"><svg viewBox=\"0 0 68 48\" width=\"54\" height=\"38\"><path d=\"M66.5 7.7a8.6 8.6 0 0 0-6-6C55.3 0 34 0 34 0S12.7 0 7.5 1.6a8.6 8.6 0 0 0-6 6.1A90 90 0 0 0 0 24a90 90 0 0 0 1.5 16.3 8.6 8.6 0 0 0 6 6C12.7 48 34 48 34 48s21.3 0 26.5-1.6a8.6 8.6 0 0 0 6-6.1A90 90 0 0 0 68 24a90 90 0 0 0-1.5-16.3z\" fill=\"#f00\"/><path d=\"M27 34l18-10-18-10z\" fill=\"#fff\"/></svg></span>${label2 ? `<span class=\"st-embed-video-label\">${esc(label2)}</span>` : \"\"}</button></div>`;\n }\n case \"social\": {\n const items = (b || \"\").split(\"\\n\").map((l) => linkOf(l.trim())).filter((x) => x.url && x.url !== \"#\");\n if (!items.length) return \"\";\n const bare = (h) => String(h || \"\").replace(/^www\\./, \"\");\n const selfHost = bare(hostOf(ctx.cardUrl || \"\"));\n const isSelf = (u) => selfHost && !u.startsWith(\"mailto:\") && bare(hostOf(u)) === selfHost;\n const marks = items.map(({ label: label2, url }) => {\n const host = url.startsWith(\"mailto:\") ? \"mail\" : hostOf(url);\n const icon = url.startsWith(\"mailto:\") ? '<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\"><rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"2\"/><path d=\"m2 7 10 6 10-6\"/></svg>' : isSelf(url) ? '<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 10.6 12 3.4l9 7.2\"/><path d=\"M5.6 9.6V19a1.6 1.6 0 0 0 1.6 1.6h9.6a1.6 1.6 0 0 0 1.6-1.6V9.6\"/></svg>' : (() => {\n const c = iconDomains(host);\n return IMG_ICON(iconPath(c[0]), c.slice(1));\n })();\n const bare2 = /^<img/.test(icon) ? SOCIAL_FALLBACK : \"\";\n return `<a class=\"st-social-btn\" href=\"${esc(url)}\"${url.startsWith(\"mailto:\") ? \"\" : ' target=\"_blank\" rel=\"noopener noreferrer\"'} aria-label=\"${esc(label2)}\" title=\"${esc(label2)}\">${icon}${bare2}</a>`;\n }).join(\"\");\n return `<div class=\"st-cell st-social\" ${size} data-coral-type=\"social\">${marks}</div>`;\n }\n default:\n return `<div class=\"st-cell\" ${size} data-coral-type=\"${esc(cell.type)}\">${mdInline(b)}</div>`;\n }\n }\n var NATIVE_CSS = [\n `.st-cell-icon-img{width:22px;height:22px;object-fit:contain;border-radius:5px;background:rgba(255,255,255,.06)}`,\n // an author's own artwork on a row โ€” 64px, `cover`, because it is a picture and not a mark.\n `.st-cell-icon-art{width:64px;height:64px;object-fit:cover;border-radius:10px;background:none}`,\n // ๐Ÿฉธ โ€ฆand that single-class rule LOSES. `.st-card [data-w=\"6\"].st-cell-tile .st-cell-icon-img` is\n // three selectors deep and was written to pin a FAVICON at 22px on exactly this cell โ€” a\n // full-width link row. Measured, not read: the source said 64px and getComputedStyle said 22px.\n // An outranked rule and a live one look identical in the file, which is why this needs a probe on\n // the computed value and not a second reading of the stylesheet.\n `.st-card .st-cell-icon-art{width:64px;height:64px}`,\n `.st-embed{overflow:hidden;padding:0}`,\n // a bento BLOCK: one section of the card, its own grid. Multiple blocks per card.\n `.st-grid{margin:0 0 var(--s-6,1.5rem)}`,\n // ๐Ÿ”ด accent-BRIGHT, not the raw accent. A lane label floats on the page ground with nothing behind\n // it, and the raw accent is a brand colour chosen to sit on a creator's artwork โ€” measured at\n // 2.17:1 on a light ground. accent-bright is the same hue moved just far enough to clear 4.5:1,\n // in whichever direction that ground requires.\n `.st-grid-label{font-weight:700;font-size:.9rem;letter-spacing:.03em;text-transform:uppercase;color:var(--cp-accent-bright,#0a8c8e);opacity:.9;margin:0 0 var(--s-3,.75rem);padding:0 var(--s-1,.25rem)}`,\n // full-bleed: escapes the card's frame + padding, spans the whole card width\n `.st-bleed{margin:0 0 var(--s-6,1.5rem);width:100%}`,\n `.st-bleed .st-cell{background:none;border:none;box-shadow:none;padding:0;border-radius:0}`,\n // prose cells (long-form text carried in from a migration, or written by hand)\n `.st-prose{display:block;font-size:.86rem;line-height:1.7;color:var(--carrier-text,#e0f0ff)}`,\n `.st-prose p{margin:0 0 .7em}.st-prose ul{margin:0 0 .7em;padding-left:1.1em}.st-prose li{margin:0 0 .3em}`,\n `.st-prose h3,.st-prose h4,.st-prose h5{margin:.2em 0 .5em;font-size:.95rem;color:var(--cp-accent-bright,#239899)}`,\n `.st-prose hr{border:0;height:1px;background:var(--cp-accent-border,rgba(10,140,142,.28));margin:1.2em 0}`,\n // ๐Ÿ”ด NOT the raw accent. A creator's brand colour is chosen to sit on their artwork, not on a\n // dark panel. Measured on a live card: #0556ff scored 3.45:1 against the drawer's ground,\n // under the 4.5:1 body-text floor. --cp-accent-bright โ€” the 72%-white mix the family already\n // derives for exactly this โ€” scores 5.57:1. The underline is not decoration either: colour alone\n // must never be the only signal that something is a link.\n `.st-prose a{color:var(--cp-accent-bright,#239899);text-decoration:underline;text-underline-offset:2px}`,\n // โ”€โ”€ text ON a picture is not themed โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // ๐Ÿ”ด A caption over a creator's artwork was wearing --cp-ink and --cp-accent-bright, which are\n // derived against the card's GROUND. The picture is not the ground. Measured on a live\n // cover: the title scored 1.76:1 in dark (already broken, today) and the CTA 1.51:1 in light.\n //\n // A picture can be any colour, so the answer is the one every platform reaches for and the same\n // one the backdrop uses: put a scrim under the caption and paint on THAT. Fixed light ink, not a\n // token โ€” the caption's context is the scrim, and the scrim is always dark whichever theme the\n // reader is in. Specificity matches card.css's `.st-card .st-gal-*` and wins on order, because\n // NATIVE_CSS is printed in the body.\n // ๐Ÿ”ด ONE gradient, not two. `.st-gal-scrim` already paints one over the whole tile; this element\n // painted a SECOND on top of it, so every title sat under two stacked scrims and the picture went\n // muddy. chodaict: ใ€Œๆผธๅฑคไธ‹ๆ‰‹ๅคช้‡,ๆˆ‘็œ‹ๅˆฐไฝ ็–Šไบ†ๅฅฝๅนพๅฑคๆผธๅฑคใ€. The scrim owns the darkening; the body\n // owns the text.\n // ๐Ÿ”ด THE SCRIM LIVES HERE, not only in card.css. Removing the caption's own gradient exposed that\n // the only bottom-heavy scrim was in the committed stylesheet โ€” so a render without it (the colour\n // suite's `renderWith`, and anything that ever forgets `css:`) had text on a photograph with no\n // backing at all. A legibility guarantee must not depend on an artifact this file does not own.\n `.st-gal-scrim{position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,.92) 6%,rgba(0,0,0,.5) 34%,rgba(0,0,0,0) 64%);pointer-events:none}`,\n // ONE gradient, not two: the scrim above darkens, the caption only holds text. Stacked, the two\n // took the bottom of every tile to ~0.99 opaque โ€” ใ€Œๆผธๅฑคไธ‹ๆ‰‹ๅคช้‡ใ€.\n `.st-gal-body{background:none;padding-top:var(--s-6,1.5rem)}`,\n `.st-card .st-gal-eyebrow{color:#f2f2f2;text-shadow:0 1px 6px rgba(0,0,0,.7)}`,\n // โ€ฆand the same three lines again for a feature that has no picture to sit on. Ground-derived\n // ink, no shadow โ€” a shadow exists to separate text from artwork, and there is no artwork here.\n // palette() has already pushed --cp-muted and --cp-ink past the body floor for every accent in\n // the space, so this is legible by construction rather than by a hardcoded pale grey.\n `.st-card .st-gal-cell-flat .st-gal-eyebrow{color:var(--cp-muted);text-shadow:none}`,\n `.st-card .st-gal-cell-flat .st-gal-title{color:var(--cp-ink);text-shadow:none}`,\n `.st-card .st-gal-cell-flat .st-gal-body{background:none}`,\n // ๐Ÿ”ด 14px. card.css sets 26px on `.st-card .st-gal-title` โ€” same specificity as this rule, so this\n // one wins only because it comes later. Measured, not assumed: see the .st-cell-icon-art incident\n // earlier today, where an identical-specificity override placed too early lost silently.\n `.st-card .st-gal-title{font-size:14px;color:#fff;text-shadow:0 2px 20px rgba(0,0,0,.75)}`,\n `.st-card .st-gal-title{font-size:14px}`,\n // ๐Ÿ”ด The card itself: no border, and PURE WHITE in light mode.\n //\n // `--cp-ground` is `color-mix(accent 6%, #f2f2f2)` โ€” a lavender off-white carrying 6% of the\n // creator's colour. On Incrediville that computes to srgb(0.936 0.916 0.937), which is what\n // chodaict was looking at when he asked for ใ€Œ็ด”็™ฝใ€. The token stays as it is (the page backdrop\n // and the QR overlay both want the tint); only the CARD goes white.\n //\n // ๐Ÿ”ด Scoped to light. `data-theme=\"dark\"` and a reader-preferred dark card must NOT turn white, so\n // this rides the same two selectors the palette does rather than a blanket rule.\n `.st-card{border:none}`,\n `:root[data-theme=\"light\"] .st-card{background:#fff}`,\n `@media(prefers-color-scheme:light){:root:not([data-theme=\"dark\"]) .st-card{background:#fff}}`,\n // ๐Ÿ”ด The avatar wears NO frame. Measured before changing anything (2026-07-30): the decoration is\n // not spread over many elements โ€” it is `img.cp-icon` carrying exactly two properties, a 2px\n // accent border and a 4px accent ring, plus `.st-figure`'s own. Two places, not a stack, which is\n // why this is two lines and not a new `avatar:` switch: ใ€Œไธ‰ไบบ่ฆโ†’่ฎŠ้–‹้—œ,ไธ€ไบบ่ฆโ†’้‚ฃๆ˜ฏ Siteใ€, and\n // there is no stacking bug here for a switch to paper over.\n //\n // (My first probe reported NINE framed elements. It read `outline-width` without reading\n // `outline-style` โ€” every element inherits a 3px width for a style of `none`, so nothing was drawn.\n // The ruler, again.)\n `.st-card .st-figure{border:none;box-shadow:none}`,\n `.st-card .st-hero .cp-icon{border:none;box-shadow:none}`,\n // ๐Ÿ”ด The social row is a CELL, so if it shows a box the box is rounded like every other one.\n // Its own rule says `background:none;border:none` โ€” but that is a single class (0,1,0) and\n // card.css's `.st-card .st-cell{background:โ€ฆ;border:โ€ฆ}` is (0,2,0), so on some cards it loses and\n // the row becomes a visible box with square corners. Found on PeachyBoys: 648ร—96, and nothing\n // above it clips. ใ€Œ่ฆๅœ“่ง’ๅฐฑๆ˜ฏๅ…จๅœ“่ง’ใ€็›ด่ง’ๅฐฑๅพ—ๅ…จ็›ด่ง’ไธ็„ถๆœƒ้†œใ€.\n `.st-card .st-cell.st-social{border-radius:18px}`,\n // ๐Ÿ”ด ZERO, not 16. The card's own 16px padding already holds every side; a margin here ADDS to it,\n // so the bottom read 32 against 16 on the other three. My first attempt set 16 ใ€Œto match the\n // sidesใ€ and measured 32 โ€” matching a number is not matching a GAP when the two stack.\n //\n // Measured on three cards after the change: ไธŠ16 ๅณ16 ไธ‹16 ๅทฆ16.\n `.st-card .st-grid{margin:0}`,\n // ๐Ÿ”ด every cell rounded, including the ones that clip their own picture. `.st-gal-img` inherits\n // the cell's radius only if the cell actually clips โ€” hence overflow:hidden alongside it.\n `.st-card .st-cell,.st-card .st-embed,.st-card .st-embed-video{border-radius:18px;overflow:hidden}`,\n `.st-card .st-gal-img,.st-card .st-slider{border-radius:inherit}`,\n `.st-card .st-gal-cta{color:#fff;background:color-mix(in srgb,var(--cp-accent,#0a8c8e) 26%,rgba(0,0,0,.62));border-color:rgba(255,255,255,.38)}`,\n // the video's caption sits on a poster frame โ€” same reasoning, same treatment\n // ๐Ÿ”ด The label sits on a POSTER โ€” a photograph we did not choose โ€” so its scrim runs to the top of\n // its own box and each glyph carries a black halo as well. The gradient alone measured 3.67:1\n // against a 4.5 floor once the harness started scoring the pixels ADJACENT to the glyphs instead\n // of the one underneath their centre: the scrim's own fade meant the upper line of a two-line\n // label sat on the picture. Not a regression โ€” a thing the previous measure structurally could\n // not see, because it sampled the background with the text lifted off.\n `.st-embed-video-label{background:linear-gradient(0deg,rgba(0,0,0,.92),rgba(0,0,0,.82) 55%,rgba(0,0,0,.6) 85%,rgba(0,0,0,.35));text-shadow:0 0 4px rgba(0,0,0,.95),0 1px 6px rgba(0,0,0,.9),0 0 14px rgba(0,0,0,.8)}`,\n // card.css carries `.st-footer a{color:var(--cp-accent)!important}` โ€” the raw brand colour, which\n // measured 2.17:1 on a light ground. Same specificity, later in the document, and the !important\n // has to come along because the rule being corrected has one.\n `.st-footer a{color:var(--cp-accent-bright,#0a8c8e)!important}`,\n // a bare image tile: the artwork IS the tile. No scrim, no CTA, no card chrome behind it.\n // ๐Ÿ”ด `.st-card .st-cell.st-gal-cell-bare`, not `.st-gal-cell-bare`. card.css declares\n // `.st-card .st-cell{border:1px solid var(--cp-line)}` at (0,2,0) and this was written at (0,1,0),\n // so the `border:none` here NEVER APPLIED โ€” every bare picture carried a 1px line and sat inset by\n // exactly 1px inside its own cell, which is the frame table's \"picture\" row quietly not holding.\n // Same shape of bug as `.st-embed-video-noposter`: a rule that reads like the fix, outranked by the\n // thing it was written to correct, therefore never once in effect. verify/frame-table.mjs found it.\n `.st-card .st-cell.st-gal-cell-bare{background:none;border:none;box-shadow:none;padding:0;overflow:hidden}`,\n `.st-gal-cell-bare .st-gal-link{display:block;width:100%;height:100%;position:relative}`,\n `.st-gal-cell-bare .st-gal-img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .3s ease}`,\n `.st-gal-cell-bare:hover .st-gal-img{transform:scale(1.02)}`,\n // click-to-load video\n `.st-embed-video{padding:0;overflow:hidden}`,\n `.st-embed-video-btn{display:block;position:relative;width:100%;height:100%;padding:0;border:0;background:#000;cursor:pointer;overflow:hidden}`,\n // โ”€โ”€ the cell layer: width is authored, height is discovered โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // The old model: the author writes w AND h, packGrid computes an (x, y), the grid hands each cell\n // a fixed box, and `overflow:hidden` cuts off anything that did not fit. `h` was a knob every card\n // had to guess at.\n //\n // ๐Ÿ”ด One model, no opt-in. This shipped for one commit scoped to cells that authored no `h`, so\n // three existing cards could keep their old path byte for byte. Nothing has been published, so\n // carrying two layout models to protect an unpublished look was cost with no payer โ€” the CARDS\n // moved instead (`h` removed from all three) and the compatibility path came out. Which also\n // removes the question every future rule would otherwise have to answer: which path does this\n // belong to?\n //\n // The new model: CSS packs (`row dense`), a\n // row is `minmax(one unit, auto)` โ€” at least a square, taller if what is in it needs more โ€” and a\n // picture carries its own `aspect-ratio` instead of being stretched into a box the grid supplied.\n // A square picture at w=N then lands on exactly N units by arithmetic, so the old rhythm survives\n // without `h` existing.\n //\n // ๐Ÿ”ด Two things this cost, both found by measuring rather than by reasoning:\n //\n // 1. `!important` on the track sizing. card.css declares `grid-auto-rows:calc(โ€ฆ)!important`, so\n // nothing without it can replace the tracks. Without it the tracks stayed a flat 43.5px while\n // cells carried a 158px minimum โ€” 15 overlapping cells and a card collapsed to under one\n // screen. The computed style said 43.5px where the source said minmax.\n // 2. It is NOT a free change for cards that stated a height. Applied to all cells it made\n // one live cardโ€™s hero 9px shorter and its cover 8px taller โ€” because a 6ร—3 span is 331ร—158.5,\n // which is 2.09:1 and not the 2:1 an `aspect-ratio` would give. Close enough to look like\n // nothing and far enough to be a change nobody asked for on a live customer's card.\n `.st-card .st-cells{--cp-unit:calc((100cqw - 5 * 14px) / 6);grid-auto-flow:row dense!important;grid-auto-rows:minmax(var(--cp-unit),auto)!important}`,\n ...[1, 2, 3, 4, 5, 6].map((w) => `.st-card [data-w=\"${w}\"]{grid-column:span ${w}}`),\n `.st-card .st-cell{grid-row:auto;align-self:start;overflow:visible;min-height:var(--cp-unit)}`,\n `.st-card .st-gal-cell,.st-card .st-embed,.st-card .st-embed-video{overflow:hidden}`,\n `.st-card .st-gal-link{height:auto;display:block;position:relative}`,\n `.st-card .st-gal-img{position:static;inset:auto;width:100%;height:auto;aspect-ratio:1;object-fit:cover;display:block}`,\n // ๐Ÿฉธ WAS: `[data-w=\"6\"] .st-gal-img{aspect-ratio:2/1}` โ€” ใ€Œa full-width picture is a banner, not a\n // 390ร—390 header photoใ€. True for the card that rule was written on, and WRONG as a general law:\n // it made WIDTH decide SHAPE, so every full-width picture became a banner whether or not it was\n // one, and `object-fit:cover` then threw away the other half.\n //\n // Measured 2026-07-30 on HTL. chodaict read his own live page off the screen as ๆ–น(6x6)ใ€ๆ–น(6x6)ใ€\n // ็ดฐ(1x6)ใ€้•ทๆ–น(3x6)ใ€้•ทๆ–น(3x6)ใ€็ดฐ(1x6) and said we had it wrong. The source images say the same\n // thing: 1200ร—1200, 1280ร—1280, then 562ร—281 and 1000ร—500. Two of his pictures are SQUARE and were\n // being cropped in half; the two that looked right were 2:1 already โ€” correct by coincidence.\n //\n // The rule that actually holds: a tile SHARING a row needs a common height so its neighbours line\n // up; a tile that OWNS its row has nothing to line up with, so the picture carries its own ratio.\n // That is ใ€Œ้ซ˜ๅบฆ็”ฑๅ…งๅฎนๆฑบๅฎšใ€ applied where it was not applied before, not a new idea.\n `.st-card [data-w=\"6\"] .st-gal-img{aspect-ratio:auto}`,\n // ๐Ÿ”ด A video and a slider have no in-flow content to be measured โ€” the play mark is\n // `position:absolute;inset:0`, and the slider's frames are stacked. Under the old model the grid\n // handed them a height from `h`; with `h` gone they collapsed to one unit and rendered as an\n // empty rounded box. A video is 16:9 because a video is 16:9, so it says so itself.\n // ๐Ÿ”ด The ratio belongs to the CELL, not to the button. The button is `replaceWith`n by the iframe\n // on click, so an `aspect-ratio` living on it leaves with it โ€” and the cell, whose `min-height:0`\n // removes the one-unit floor, collapsed to the iframe's own tiny intrinsic height. The video\n // played, at about 1ร—6. Whatever holds the shape has to be the thing that survives the swap.\n `.st-card .st-embed-video{aspect-ratio:16/9;min-height:0}`,\n `.st-card .st-embed-video .st-embed-video-btn{width:100%;height:100%}`,\n `.st-card .st-embed-video iframe{display:block;width:100%;height:100%;border:0}`,\n // ๐Ÿ”ด โ€ฆbut the slider is NOT in that situation, and pinning it to 2/1 squashed it flat.\n // `kind=slider` is the only embed there is, and it builds `.st-carousel`, which declares\n // `aspect-ratio:1/1` on ITSELF โ€” the frames stack inside a box that already knows it is square.\n // The cell asking for 2:1 on top of that crops a square into a letterbox. Its content does know\n // its own height, so the cell takes it: `min-height:0` undoes the one-unit floor `.st-cell` sets,\n // and nothing dictates a ratio from outside.\n `.st-card .st-embed:not(.st-embed-video){min-height:0}`,\n // โ”€โ”€ a picture that carries its name โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // ๐Ÿ”ด The cell's budget does not change. The picture shrinks, stays SQUARE, and gains side inset;\n // the label takes the rest. That is what iOS does to the icon โ€” measured, it loses 17.1% of its\n // side when labels are on โ€” without iOS's other half, which re-derives the whole grid metric\n // (turning labels off there moves vertical pitch โˆ’5.7% and horizontal +6.1%). We cannot afford\n // that: a Card has arbitrary content and mixed widths, and re-deriving per card destroys the\n // arithmetic that `w=N โ‡’ N units` rests on.\n //\n // The label sits OUTSIDE the picture's frame, plain, which is both the launcher's shape and the\n // rule locked earlier: the frame marks what is pressable, and here that is the picture.\n // ๐Ÿ”ด The BUDGET is on the link, and the picture takes what the label leaves. The first version\n // put `aspect-ratio:1` on the picture and let the cell size itself: labelled cells came out 150px\n // where unlabelled ones were 159, so turning the label on quietly shrank the tile and broke the\n // rhythm the whole grid rests on. `aspect-ratio` belongs to the SPACE, not to the picture.\n // ๐Ÿ”ด The picture's width is a fixed share of the cell โ€” 70%, which is what iOS gives an icon when\n // its label is showing (99.5 of a 142.5 column pitch, measured). It has to be a share and not\n // `flex:1 1 auto`: left to grow, the picture took the whole box and pushed the label out the\n // bottom, so a labelled tile came out 178px where an unlabelled one was 159. The budget is the\n // link's `aspect-ratio`; the picture takes its share of it and the label takes the rest.\n `.st-card .st-gal-labelled .st-gal-link{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;aspect-ratio:1;height:auto;padding:0}`,\n `.st-card .st-gal-labelled .st-gal-img{flex:0 0 auto;width:70%;height:auto;aspect-ratio:1;border-radius:16px}`,\n // ๐Ÿ”ด fixed small type, never scaled to the cell โ€” a w=6 banner would otherwise grow a giant\n // caption. One line, truncated in the paint; the full string stays in `alt` for assistive tech.\n `.st-gal-label{font-size:.72rem;line-height:1.25;font-weight:600;color:var(--cp-ink,#fff);max-width:92%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center;flex:0 0 auto}`,\n // A full-width picture is a banner, and a banner with a caption under it is just that โ€” no square\n // budget to divide, so the link takes its natural height instead of an aspect-ratio.\n `.st-card [data-w=\"6\"].st-gal-labelled .st-gal-link{aspect-ratio:auto;padding:0}`,\n `.st-card [data-w=\"6\"].st-gal-labelled .st-gal-img{width:100%;aspect-ratio:2/1}`,\n // โ”€โ”€ a full-width link is a ROW โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // \"A row is what a link is. A tile is what a link becomes when it has a picture worth looking at.\"\n // Five independent sources, four methods: `list` on 84% of 38 Portaly pages; Linktree 93.6% rows\n // with 109/109 links full-width and zero grids; 6 Carrd sites pure rows; and the ancestor โ€”\n // hand-written, no platform palette, no paywall โ€” 27 rows to 1 tile.\n //\n // A `w=6` link stops stacking icon-above-label and lays outๆจช like a Settings row: mark, text,\n // chevron. Nothing below applies at w<6, so a tile stays a tile and no existing card moves.\n `.st-card [data-w=\"6\"].st-cell-tile{padding:0}`,\n // ๐Ÿ”ด `flex-direction:row` explicitly. card.css sets `column` on `.st-cell-tile .st-cell-link` โ€”\n // that is what stacks icon above label in a TILE โ€” and setting only `display:flex` here inherited\n // it, so the first version of this \"row\" was 124px tall with everything still stacked. It looked\n // like a padding bug; it was a direction bug.\n `.st-card [data-w=\"6\"].st-cell-tile .st-cell-link{display:flex;flex-direction:row;align-items:center;gap:var(--s-3,.75rem);min-height:44px;padding:var(--s-3,.75rem) var(--s-4,1rem);height:auto;justify-content:flex-start}`,\n `.st-card [data-w=\"6\"].st-cell-tile .st-cell-head{flex:0 0 auto;display:contents}`,\n `.st-card [data-w=\"6\"].st-cell-tile .st-cell-body{flex:1 1 auto;margin-top:0;min-width:0}`,\n `.st-card [data-w=\"6\"].st-cell-tile .st-cell-icon,.st-card [data-w=\"6\"].st-cell-tile .st-cell-icon-img{order:0;width:22px;height:22px}`,\n // ๐Ÿฉธ IMMEDIATELY AFTER the rule above, and that placement IS the fix. Both selectors are (0,3,1) โ€”\n // identical specificity โ€” so SOURCE ORDER decides, and the art rule sitting earlier in the array\n // lost silently. Two failed attempts before this one: a single-class rule (obviously outranked),\n // then a matching-specificity rule in the wrong position (looks right, still loses).\n //\n // ๐Ÿ”ด The answer did not come from reading the stylesheet a third time. It came from asking the\n // BROWSER which rules match โ€” `el.matches(r.selectorText)` over document.styleSheets โ€” which\n // printed both rules at 64px and 22px side by side and made the tie obvious in one shot.\n `.st-card [data-w=\"6\"].st-cell-tile .st-cell-icon-art{order:0;width:64px;height:64px}`,\n `.st-card [data-w=\"6\"].st-cell-tile .st-cell-body{order:1}`,\n // ๐Ÿ”ด the chevron is the affordance, not the border โ€” so it goes last and it stays.\n `.st-card [data-w=\"6\"].st-cell-tile .st-cell-arrow{order:2;margin-left:auto;flex:0 0 auto}`,\n `.st-card [data-w=\"6\"].st-cell-tile .st-cell-title{font-size:.95rem;white-space:normal}`,\n `.st-card [data-w=\"6\"].st-cell-tile .st-cell-sub{display:block;font-size:.75rem}`,\n // โ”€โ”€ and a RUN of them is one surface, not a stack of buttons โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // The frame belongs to the LANE. iOS Settings is the proof it works: plain rows in one rounded\n // card, a hairline between each, and nobody has ever been confused about whether a row is\n // tappable. The ancestor did the same by hand โ€” which is how 27 links fit in a 1945px page.\n //\n // ๐Ÿ”ด The run is a real element (`.st-run`, see renderGrid), so the surface, the border and the\n // radius belong to IT and the rows inside are plain. This replaces closing the grid's row gap with\n // `margin-top:-14px` on every adjacent row โ€” which restated the gap as a magic number in a second\n // place and then lost to subpixel rounding: measured on a live card, row tops were 488, 549, 611,\n // 672, so row 2 ended at 610 and row 3 began at 611 and a 1px sliver of page showed through. The\n // divider read as DOUBLED from the third row on while the first two looked perfect, which is a very\n // good disguise for \"this mechanism does not actually work\".\n //\n // ใ€Œ้™ค้žๅฟ…่ฆไธ็„ถ CSS ๅˆฅๅฏซ่ฒ ๆ•ธใ€ โ€” chodaict, and the bug is the argument. A negative offset that has\n // to equal another value is a constraint the layout is not expressing.\n `.st-card .st-run{grid-column:1 / -1;display:flex;flex-direction:column;background:var(--cp-surface);border:1px solid var(--cp-line);border-radius:18px;overflow:hidden}`,\n // ๐Ÿ”ด `align-self:stretch` undoes `.st-card .st-cell{align-self:start}`, which is right for a GRID\n // item (a short cell must not stretch to its track) and means \"shrink to content width\" for a flex\n // column child. Without it every row is only as wide as its own text: the divider stops mid-card at\n // a different x per row, and the chevron's `margin-left:auto` has no space left to push into, so it\n // sits against the label instead of at the right edge.\n `.st-card .st-run > .st-cell-tile{align-self:stretch;width:100%;background:none;border:0;border-radius:0;box-shadow:none;border-top:1px solid var(--cp-line)}`,\n `.st-card .st-run > .st-cell-tile:first-child{border-top:0}`,\n // โ”€โ”€ THE RUN-CORNER RULE (ruler: tile-geometry.test.mjs + verify/tile-geometry.mjs) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // A run is a grouped list, like an inset-grouped list on iOS: the run's OUTER corners take the\n // card's tile radius (18px, what `.st-card .st-cell` resolves to), the inner seams are square, and\n // the dividers stay as they are. A tile alone in its run is therefore fully rounded โ€” it has no\n // seam to share. `.st-run` is `flex-direction:column`, so a run's ends are its top and bottom:\n // the first row rounds its two TOP corners, the last row its two BOTTOM corners, a lone row all\n // four. (All rows used to be `border-radius:0`, so a single link was a square box with nothing to\n // join, next to its own empty state drawn rounded.)\n `.st-card .st-run > .st-cell-tile:first-child{border-top-left-radius:18px;border-top-right-radius:18px}`,\n `.st-card .st-run > .st-cell-tile:last-child{border-bottom-left-radius:18px;border-bottom-right-radius:18px}`,\n // A link's head (icon + arrow) with nothing to SHOW must not hold the title down: the link is a\n // `space-between` column, so a zero-height head still pins the body to the bottom. Nothing to show\n // = no children at all (an unset link, arrows off) OR only children the favicon's onerror hid with\n // `style.display=\"none\"` (IMG_ICON) โ€” the common case, since every filled link gets a derived\n // favicon. Either way the head leaves the flow and the title sits at the top. `:has` on the\n // inline style rather than a new class: live markup must not change for this.\n `.st-card .st-cell-tile .st-cell-head:not(:has(> :not([style*=\"display: none\"]))){display:none}`,\n `.st-card .st-cell-tile .st-cell-head:not(:has(> :not([style*=\"display: none\"]))) + .st-cell-body{margin-top:0}`,\n `.st-card .st-run > .st-cell-tile:hover{background:var(--cp-surface-2)}`,\n // a row lifting on hover would break the run apart; the surface change is the feedback instead\n `.st-card [data-w=\"6\"].st-cell-tile:hover{transform:none;background:var(--cp-surface-2)}`,\n // โ”€โ”€ ไธ่ƒฝๆŒ‰็š„ไธๅ‡†้•ทๅพ—ๅƒๆŒ‰้ˆ• โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // The frame marks a region of PRESSABLE things, in both directions: what cannot be pressed must\n // not look like a button, and what can must look like one. Prose is the only cell that is neither\n // a link nor a picture, and it was wearing the identical surface, border and radius as a link โ€”\n // a paragraph shaped like something you tap.\n //\n // This is what iOS does with a grouped table's footer text: it sits OUTSIDE the rounded card,\n // plain on the background. Removing the frame here also removes a double frame inside a drawer,\n // where the panel is already the surface.\n //\n // Kept: the padding, so the text still has a margin; and `min-height:0`, or the flex centring the\n // .st-cell rule applies would vertically centre a long passage.\n `.st-card .st-cell.st-prose{background:none;border:none;box-shadow:none;backdrop-filter:none;display:block;padding:var(--s-2,.5rem) var(--s-1,.25rem)}`,\n // โ”€โ”€ the avatar is a RATIO, never a pixel count โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // Measured across 10 pages and 3 platforms: every platform picks one ratio of the card's width and\n // holds it โ€” Linktree 96px = 24.6% on 14 of 16 pages identically, Portaly 118px = 30.3%, lit.link\n // 80px = 20.5%. There is no universal figure, only a range and a choice inside it.\n //\n // Ours was `width:78px`, and on the 390px viewport all of that evidence was gathered at, 78px IS\n // 23.6% of the card โ€” the choice was already right and only the UNIT was wrong. So this is the\n // same number in a unit that travels: identical to the pixel on a phone, and it stops collapsing\n // on a desktop card, which is the whole defect. (23.6% also lands between lit.link's 20.5% and\n // Linktree's 24.6%, so it is inside the band rather than at its edge.)\n //\n // ๐Ÿ”ด The ratio is of `.st-cells-wrap` โ€” 331px inside a 390px viewport, not 358 and not 390. The\n // first attempt used 21.8, computed against the wrong box, and shrank every avatar by 6px.\n //\n // cqw resolves against `.st-cells-wrap`, the container-query context the grid already establishes\n // for its square base unit โ€” the hero is a cell inside it, so this is the card's own width and not\n // the viewport's. min-width keeps a face on the avatar if a card is ever rendered very narrow.\n `.st-card .st-hero .cp-icon{width:23.6cqw;height:23.6cqw;min-width:56px;min-height:56px}`,\n // โ”€โ”€ and the avatar's POSITION, which the ratio fix left alone โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // The survey is not ambiguous: 10 pages across 3 platforms, **10/10 centre the avatar**, median\n // header height รท card width **0.977** โ€” near square. Ours was avatar-left / text-right, measuring\n // 0.453 on a phone and 0.288 on a desktop card, with the avatar 109โ€“230px off the centreline. The\n // SIZE was corrected to a ratio and the POSITION was simply never revisited.\n //\n // A column, centred, is the whole change โ€” there is no new box and no new element. Everything here\n // overrides card.css at equal specificity by being printed in the <body>, which is also why none of\n // it carries !important.\n //\n // ๐Ÿ”ด `.st-figure` is untouched on purpose. It carries `data-dynamic-coral=\"qr\"` and the coral\n // positions its panel against that box; restyling it to help the layout would reach into the one\n // component that is explicitly not ours to move.\n // Symmetric padding again. The extra 28px at the bottom existed to give the veil's gradient a fade\n // zone to land in; with the veil gone it is a number with nothing behind it.\n `.st-card .st-hero{flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:16px;padding:24px 16px}`,\n // โ”€โ”€ and the header wears NO FRAME, which the frame table decided and nothing implemented โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // `header / avatar / tagline โ†’ none`. Prose was stripped to text on the ground (88c07c0); the\n // header never was, and kept `.st-cell`'s surface, border and radius plus its own gradient. It is\n // the same defect prose had โ€” a thing you cannot tap, shaped like a thing you can โ€” and it survived\n // because no check encodes that table. verify/frame-table.mjs is that check, added with this.\n //\n // A name and a face do not need a container to read as a heading; every platform in the survey puts\n // them straight on the page. Padding is kept, exactly as prose kept its own, so the text keeps a\n // margin instead of colliding with the first cell.\n // ๐Ÿ”ด `var(--cp-hero-veil, none)` rather than a plain `none`, and the indirection is load-bearing.\n // BACKDROP_CSS is emitted BEFORE this block and at lower specificity, so a rule it writes for\n // `.st-hero` loses twice over and silently does nothing โ€” the first attempt at the veil measured\n // 1.89:1 unchanged, because it never applied. A custom property does not care about either: the\n // page declares the veil only when it has a backdrop, and this stays the single place the header's\n // frame is decided. Cards with no backdrop resolve the fallback and are frameless, full stop.\n // ๐Ÿ”ด `border-radius:0` is not redundant. `.st-cell` sets 18px, and with no background and no border\n // a radius is invisible โ€” on a card with a BACKDROP it is not, because it rounds the veil's corners\n // and the veil becomes a panel. verify/frame-table.mjs caught this on its first run, which is the\n // argument for the file: three of us had looked at this header and none of us saw the radius.\n // ๐Ÿ”ด `background:none`, full stop โ€” no veil, no `var()` hatch for one. card.css puts a\n // `linear-gradient(120deg, surface-2, surface)` here and this is what removes it, on every card.\n // A backdrop card no longer gets a slab of ground colour over the artwork either; its text carries\n // its own legibility instead (FLOATING_INK, in BACKDROP_CSS).\n `.st-card .st-cell.st-hero{background:none;border:none;border-radius:0;box-shadow:none;backdrop-filter:none}`,\n `.st-card .st-hero-text{text-align:center;display:flex;flex-direction:column;align-items:center}`,\n `.st-card .st-hero-meta{justify-content:center}`,\n // โ”€โ”€ `header: left` โ€” the other arrangement, kept because a card may want it โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // Not a second header: the same element, the same avatar ratio (23.6cqw applies either way โ€” that\n // was a separate fix about the UNIT, not about the composition), only the axis and the alignment\n // change back. Written as overrides of the centred default so the default needs no class at all.\n `.st-card.st-card-head-left .st-hero{flex-direction:row;align-items:center;justify-content:flex-start;text-align:left;gap:18px;padding:16px}`,\n `.st-card.st-card-head-left .st-hero-text{text-align:left;align-items:flex-start}`,\n `.st-card.st-card-head-left .st-hero-meta{justify-content:flex-start}`,\n `.st-card.st-card-head-left .st-hero-tagline{margin-left:0;margin-right:0}`,\n // โ”€โ”€ `icons: mono` โ€” the fetched brand marks go grey โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // ๐Ÿ”ด Scoped to IMG marks only. Our own drawn glyphs (the envelope, the house) are already themed;\n // greying them would desaturate a colour we chose. This is why the row reads as mixed today โ€” not\n // because the brands have colour, but because ours are accent-blue while theirs are their own. In\n // mono both halves finally answer to the same rule.\n `.st-card.st-card-marks-mono .st-cell-icon-img,.st-card.st-card-marks-mono .st-social img{filter:grayscale(1)}`,\n // hover restores the brand, so the colour is one gesture away rather than gone\n `.st-card.st-card-marks-mono .st-cell-link:hover .st-cell-icon-img,.st-card.st-card-marks-mono .st-social-btn:hover img{filter:none}`,\n `@media(prefers-reduced-motion:no-preference){.st-card.st-card-marks-mono .st-cell-icon-img,.st-card.st-card-marks-mono .st-social img{transition:filter .25s}}`,\n // The tagline is the one run that must not stretch to the card's full width when centred: a\n // centred paragraph running the whole 646px of a desktop card reads as a banner, not a bio.\n // ๐Ÿ”ด `word-break:keep-all` is the whole fix, and it is about CJK rather than about any one card. A\n // Chinese or Japanese line breaks between ANY two characters by default, so a centred tagline\n // splits ใ€Œๆ’็•ซๅˆไฝœใ€ down the middle โ€” a word cut in half, which in Latin script nobody would ship.\n // keep-all confines breaks to spaces, and a tagline written as ใ€ŒA ยท B ยท Cใ€ already puts its\n // spaces exactly where the meaning divides. `overflow-wrap:break-word` stays as the escape hatch\n // for a single run genuinely longer than the line, or keep-all would overflow instead of wrapping.\n // `text-wrap:balance` then evens the remaining lines instead of leaving one orphan.\n `.st-card .st-hero-tagline{max-width:34ch;margin-left:auto;margin-right:auto;word-break:keep-all;overflow-wrap:break-word;text-wrap:balance}`,\n // โ”€โ”€ a video with no poster โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // ๐Ÿ”ด There was already a rule for this โ€” `.st-embed-video-noposter{background:linear-gradient(...)}`\n // declared one line ABOVE `.st-embed-video-btn{background:#000}`, same specificity, so the black\n // won every time. It had never once had an effect, which is why a channel-mode video rendered as a\n // black rectangle with a play button in it: not \"loading\", not \"unavailable\", just broken-looking.\n // The two-class selector below outranks the button rather than relying on source order.\n //\n // A live block \"degrades to a static state that does not look broken\" โ€” the admissibility rule.\n // So the degraded state is designed rather than absent: the card's own surface, its own line, the\n // mark, and the title read as ordinary text. It looks like a video you have not opened yet, which\n // is exactly what it is.\n `.st-embed-video-btn.st-embed-video-noposter{background:linear-gradient(160deg,var(--cp-surface-2,#1a1a1a),var(--cp-surface,#161616));border:1px solid var(--cp-line,rgba(10,140,142,.28))}`,\n `.st-embed-video-noposter .st-embed-video-play{filter:none;opacity:.94}`,\n // the label's dark scrim exists to sit on a photograph. With no photograph it is a smudge, and the\n // ink can simply be the card's ink on the card's surface โ€” which the colour sweep already covers.\n `.st-embed-video-noposter .st-embed-video-label{background:none;color:var(--cp-ink,#fff);text-shadow:none;padding:12px}`,\n `.st-embed-video-poster{width:100%;height:100%;object-fit:cover;display:block;opacity:.82;transition:opacity .25s,transform .3s}`,\n `.st-embed-video-btn:hover .st-embed-video-poster{opacity:1;transform:scale(1.02)}`,\n `.st-embed-video-play{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;filter:drop-shadow(0 2px 10px rgba(0,0,0,.5));transition:transform .2s}`,\n `.st-embed-video-btn:hover .st-embed-video-play{transform:scale(1.08)}`,\n `.st-embed-video-label{position:absolute;left:0;right:0;bottom:0;padding:22px 12px 10px;font-size:.78rem;font-weight:600;color:#fff;text-align:left;background:linear-gradient(0deg,rgba(0,0,0,.72),transparent)}`,\n `.st-embed-video iframe{width:100%;height:100%;border:0;display:block}`,\n // social mark row\n `.st-social{display:flex;flex-wrap:wrap;gap:var(--s-2,.5rem);align-items:center;justify-content:center;background:none;border:none;box-shadow:none;padding:0;min-height:0}`,\n `.st-social-btn{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;border:1px solid var(--cp-accent-border,rgba(10,140,142,.28));background:color-mix(in srgb,var(--cp-accent,#0a8c8e) 8%,transparent);color:var(--cp-accent,#0a8c8e);transition:background .2s,transform .2s}`,\n `.st-social-btn:hover{background:color-mix(in srgb,var(--cp-accent,#0a8c8e) 20%,transparent);transform:translateY(-2px)}`,\n `.st-social-btn .st-cell-icon-img{width:20px;height:20px;background:none}`,\n // the drawn last resort โ€” revealed by the sibling combinator the instant onerror hides the img,\n // so it needs no class toggle and works with JS already done running.\n `.st-social-bare{display:none}`,\n `.st-social-btn .st-cell-icon-img[style*=\"none\"] + .st-social-bare{display:block}`,\n // square-corner skin (a creator whose platform set square corners keeps them)\n `.st-card.st-card-square .st-cell,.st-card.st-card-square .st-gal-img,.st-card.st-card-square .st-embed-video-btn{border-radius:0}`\n ].join(\"\");\n var PICTURE_GLYPH = `url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='16' rx='3'/%3E%3Ccircle cx='9' cy='10' r='1.6'/%3E%3Cpath d='m21 16-5-5-9 9'/%3E%3C/svg%3E\")`;\n var EDIT_ONLY_CSS = [\n `.st-card .st-cell-unset{border-style:dashed;border-color:var(--cp-line-hi,currentColor)}`,\n // one ROW = one grid unit (`.st-cell`'s own min-height), the same height an empty link takes; the\n // gallery cell has no border of its own, so the dashed outline is drawn here in full.\n `.st-card .st-gal-labelled.st-cell-unset{min-height:var(--cp-unit);border:1px dashed var(--cp-line-hi,currentColor);cursor:pointer}`,\n `.st-card .st-gal-labelled.st-cell-unset .st-gal-link{flex-direction:row;justify-content:center;aspect-ratio:auto;height:auto;min-height:44px;gap:8px;padding:var(--s-3,.75rem) var(--s-4,1rem)}`,\n `.st-card .st-gal-labelled.st-cell-unset .st-gal-img{position:static;flex:0 0 auto;width:20px;height:20px;aspect-ratio:1;border:0;border-radius:0;background:currentColor;opacity:.75;-webkit-mask:${PICTURE_GLYPH} center/contain no-repeat;mask:${PICTURE_GLYPH} center/contain no-repeat}`,\n `.st-card .st-gal-labelled.st-cell-unset .st-gal-label{max-width:none;font-size:.85rem}`\n ].join(\"\");\n var HALO = \"text-shadow:0 0 4px var(--cp-ground),0 0 9px var(--cp-ground),0 1px 14px var(--cp-ground)\";\n var FLOATING_INK = [\n `.st-card .st-hero-name,.st-card .st-hero-tagline{color:var(--cp-ink);${HALO}}`,\n `.st-card .st-hero-tagline em{color:var(--cp-accent-bright);${HALO}}`,\n `.st-card .st-chip{color:var(--cp-accent-bright);${HALO}}`,\n // ๐Ÿ”ด `--cp-ink`, not `--cp-page-ink`. The page-ink is derived for text on the page MIST; over a\n // backdrop the footer is on artwork, and it measured 2.86 there against a 4.5 floor. Same mistake\n // the tagline had with `--cp-muted`: a colour derived for one surface, used on another.\n `.st-footer,.st-footer>div{color:var(--cp-ink)!important;${HALO}}`,\n `.st-footer a{color:var(--cp-accent-bright)!important;${HALO}}`\n ].join(\"\");\n var BACKDROP_CSS = (img, dim) => `.st-backdrop{position:fixed;inset:0;z-index:0;background-image:url(\"${cssUrl(img)}\");background-size:cover;background-position:center;background-attachment:fixed}\nhtml{scrollbar-gutter:auto}\n.st-wrap,.st-footer{position:relative;z-index:1}\n.st-backdrop::after{content:'';position:absolute;inset:0;background:linear-gradient(180deg,color-mix(in srgb,var(--cp-ground,#0a1628) ${dim}%,transparent),color-mix(in srgb,var(--cp-ground,#0a1628) ${Math.min(96, dim + 18)}%,transparent))}\n.st-grid-label{text-shadow:0 1px 3px rgba(0,0,0,.85),0 0 12px rgba(0,0,0,.6)}\n${FLOATING_INK}\n@media(max-width:720px){.st-backdrop{background-attachment:scroll}}`;\n function renderGrid(model, ctx = {}) {\n ctx = { name: \"\", icons: {}, ...ctx };\n const cols = model.cols;\n const parts = [];\n const isRow = (pl) => pl.cell.type === \"link\" && pl.w === cols;\n const groupRuns = (placements) => {\n const out = [];\n for (const pl of placements) {\n const last = out[out.length - 1];\n if (isRow(pl) && last && last.run) last.items.push(pl);\n else out.push(isRow(pl) ? { run: true, items: [pl] } : { run: false, items: [pl] });\n }\n return out;\n };\n const emit = (label2, run) => {\n if (!run.length) return;\n const g = packGrid({ cells: run.map((x) => x.cell), cols });\n const inner = groupRuns(g.placements).map((grp) => {\n const html = grp.items.map((pl) => renderCell(pl.cell, { ...pl, abs: run[pl.index].abs }, ctx)).join(\"\");\n return grp.run ? `<div class=\"st-run\">${html}</div>` : html;\n }).join(\"\");\n const head2 = label2 ? `<div class=\"st-grid-label\">${mdInline(label2)}</div>` : \"\";\n parts.push(`<section class=\"st-grid\">${head2}<div class=\"st-cells-wrap\"><div class=\"st-cells\">${inner}</div></div></section>`);\n };\n const cells = model.cells || [];\n const blocks = model.blocks && model.blocks.length ? model.blocks : [{ label: \"\", start: 0, count: cells.length }];\n for (const blk of blocks) {\n let run = [];\n let label2 = blk.label || \"\";\n for (let abs = blk.start; abs < blk.start + blk.count; abs++) {\n const cell = cells[abs];\n if (!cell) continue;\n if (cell.params && cell.params.bleed != null || cell.type === \"text\") {\n emit(label2, run);\n run = [];\n label2 = \"\";\n parts.push(`<div class=\"st-bleed\">${renderCell(cell, { w: cols, h: 1, abs }, ctx)}</div>`);\n continue;\n }\n run.push({ cell, abs });\n }\n emit(label2, run);\n }\n const skin = ctx.radius === \"square\" ? \" st-card-square\" : \"\";\n const head = String(ctx.header || \"\").trim().toLowerCase() === \"left\" ? \" st-card-head-left\" : \"\";\n const marks = /^(mono|grey|gray|monochrome)$/.test(String(ctx.iconStyle || \"\").trim().toLowerCase()) ? \" st-card-marks-mono\" : \"\";\n const editCss = ctx.editIndex ? `<style>${EDIT_ONLY_CSS}</style>` : \"\";\n return `<div class=\"st-card${skin}${head}${marks}\"><style>${NATIVE_CSS}</style>${editCss}${parts.join(\"\")}</div>`;\n }\n function renderDrawers(model, ctx = {}) {\n const drawers = model.drawers || [];\n if (!drawers.length) return \"\";\n const dctx = { ...ctx, inDrawer: true };\n const panels = drawers.map((d) => {\n const inner = renderGrid({ cells: d.cells, cols: model.cols, drawers: [] }, dctx);\n const id = esc(d.id);\n const tid = `drawer-${id}-title`;\n const head = d.title ? `<h2 class=\"dc-drawer-title\" id=\"${tid}\">${esc(d.title)}</h2>` : \"\";\n const named = d.title ? ` aria-labelledby=\"${tid}\"` : \"\";\n return `<div class=\"dc-drawer\" id=\"${id}\" data-drawer-panel=\"${id}\" role=\"dialog\" aria-modal=\"true\"${named}><div class=\"dc-drawer-body\">${head}${inner}</div></div>`;\n }).join(\"\");\n const close = `<a href=\"#\" class=\"dc-drawer-close\" role=\"button\" aria-label=\"${esc(label(ctx, \"close\"))}\">\\u2715</a>`;\n return `<style>${DRAWER_CSS}</style><div class=\"dc-drawer-layer\" data-dynamic-coral=\"drawer\"><a href=\"#\" class=\"dc-drawer-scrim\" aria-hidden=\"true\" tabindex=\"-1\"></a>${panels}${close}</div>`;\n }\n function renderPage(model, ctx = {}) {\n const backdropUrl = resolveAsset(ctx.backdrop, ctx);\n const backdrop = backdropUrl ? `<div class=\"st-backdrop\" aria-hidden=\"true\"></div><style>${BACKDROP_CSS(backdropUrl, resolveBackdropDim(ctx.backdropDim, ctx.accent))}</style>` : \"\";\n const grid = backdrop + renderGrid(model, ctx) + renderDrawers(model, ctx);\n const css = ctx.css || \"\";\n const qr = ctx.qrScriptInline ? `<script>${ctx.qrScriptInline}<\\/script>` : ctx.qrScriptSrc ? `<script src=\"${esc(ctx.qrScriptSrc)}\"><\\/script>` : \"\";\n const video = grid.includes(\"st-embed-video-btn\") ? `<script>${VIDEO_JS}<\\/script>` : \"\";\n const drawer = grid.includes('data-dynamic-coral=\"drawer\"') ? ctx.drawerScriptInline ? `<script>${ctx.drawerScriptInline}<\\/script>` : ctx.drawerScriptSrc ? `<script src=\"${esc(ctx.drawerScriptSrc)}\"><\\/script>` : \"\" : \"\";\n const year = (/* @__PURE__ */ new Date()).getFullYear();\n const since = ctx.since && String(ctx.since).trim() ? `${esc(ctx.since)}\\u2013` : \"\";\n const owner = `<span>${esc(ctx.name)} © ${since}${year}</span>`;\n const ref = ctx.handle ? `?ref=${encodeURIComponent(ctx.handle)}` : \"\";\n const powered = ctx.poweredBy ? `  |  Powered by <a href=\"https://feelreef.com/signup${ref}\" class=\"st-footer-credit\">feelreef</a>` : \"\";\n const footer = `<footer class=\"st-footer\"><div>${owner}${powered}</div></footer>`;\n const accent = ctx.accent ? `<style id=\"cp-accent\">:root{--cp-accent:${esc(ctx.accent)};--cp-accent-bg:color-mix(in srgb,${esc(ctx.accent)} 12%,transparent);--cp-accent-border:color-mix(in srgb,${esc(ctx.accent)} 25%,transparent);--cp-accent-glow:color-mix(in srgb,${esc(ctx.accent)} 35%,transparent);--cp-accent-bright:color-mix(in srgb,${esc(ctx.accent)} 72%,#fff)}.st-footer-credit{color:var(--cp-accent-bright,#0a8c8e);text-decoration:none;font-weight:600}</style>` : `<style>.st-footer-credit{color:var(--cp-accent-bright,#0a8c8e);text-decoration:none;font-weight:600}</style>`;\n const generatorTag = (() => {\n const raw = ctx.generator;\n if (raw === void 0 || raw === null || raw === \"\") return \"feelreef\";\n const g = String(raw).trim();\n if (!g || /^(off|false|no|none)$/i.test(g)) return \"\";\n return g;\n })();\n const langRaw = String(ctx.lang || \"\").trim();\n const lang = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(langRaw) ? langRaw : \"en\";\n const mode = ctx.theme === \"light\" || ctx.theme === \"dark\" ? ctx.theme : null;\n const theme = `<style id=\"cp-theme\">${themeCss(mode, ctx.accent)}</style>`;\n return `<!doctype html><html lang=\"${lang}\"${mode ? ` data-theme=\"${mode}\"` : \"\"}><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">${generatorTag ? `<meta name=\"generator\" content=\"${esc(generatorTag)}\">` : \"\"}<title>${esc(ctx.name)}${accent}${theme}
${grid}
${footer}${qr}${video}${drawer}`;\n }\n var VIDEO_JS = `(function(){document.addEventListener('click',function(e){\nvar b=e.target.closest&&e.target.closest('.st-embed-video-btn');if(!b)return;\nvar id=b.getAttribute('data-yt');var ch=b.getAttribute('data-yt-channel');if(!id&&!ch)return;\nvar f=document.createElement('iframe');\nf.src=id?('https://www.youtube-nocookie.com/embed/'+encodeURIComponent(id)+'?autoplay=1&modestbranding=1&rel=0')\n :('https://www.youtube-nocookie.com/embed/videoseries?list=UU'+encodeURIComponent(ch.replace(/^UC/,''))+'&autoplay=1&modestbranding=1&rel=0');\nf.title=b.getAttribute('aria-label')||'YouTube';f.allow='accelerometer;autoplay;encrypted-media;picture-in-picture';f.allowFullscreen=true;\nf.setAttribute('loading','lazy');b.replaceWith(f);});})();`;\n\n // packages/cardtile/serve/card-assets.mjs\n var CSS = '/* The Card\\'s stylesheet. HAND-MAINTAINED \\u2014 gen-card-css.mjs was retired 2026-07-30 because no\\n configuration of it reproduces this file. Read CSS-PROVENANCE.md next to it BEFORE editing: it has\\n the evidence, the two deliberate colour departures, and the harness that gates a change. */\\n:root{--gd-accent:var(--cp-accent);--gd-accent-ink:var(--cp-ground);--gd-bg:var(--cp-ground);--gd-surface:var(--carrier-bg);--gd-text:var(--carrier-text);--gd-muted:var(--carrier-text-muted);--gd-border:var(--cp-accent-border);--gd-radius:var(--r-md);--gd-gap:var(--s-4)}\\n:root{--s-0:0;--s-px:1px;--s-1:.25rem;--s-2:.5rem;--s-3:.75rem;--s-4:1rem;--s-5:1.25rem;--s-6:1.5rem;--s-8:2rem;--s-10:2.5rem;--s-12:3rem;--s-20:5rem;--s-24:6rem;--r-xs:4px;--r-sm:8px;--r-md:12px;--r-lg:16px;--r-xl:20px;--r-pill:9999px;--r-circle:50%;--shadow-card:0 8px 32px rgba(0,0,0,.3);--shadow-card-light:0 8px 32px rgba(0,0,0,.06);--c-teal:#7fdbca;--c-coral:#e2725b;--c-ink:#0a1628;--carrier-bg:rgba(255,255,255,.07);--carrier-bg-solid:#0d2847;--carrier-text:#e0f0ff;--carrier-text-muted:#a0c4d8;--carrier-border:rgba(127,219,202,.2);--carrier-shadow:0 8px 32px rgba(0,0,0,.3);--carrier-accent:#7fdbca;--carrier-accent-ink:#0a1628;--carrier-input-bg:rgba(0,0,0,.2);--carrier-link:var(--carrier-accent);--carrier-heading:#e0f0ff;--reef-deep:#0a1628;--reef-mid:#0f2440;--reef-surface:rgba(255,255,255,0.04);--reef-surface-hover:rgba(255,255,255,0.08);--reef-teal:#7fdbca;--reef-teal-soft:rgba(127,219,202,0.18);--reef-coral:#e2725b;--reef-coral-soft:#ff9572;--reef-lavender:#c3c3c3;--reef-gold:#f3c969;--reef-crimson:#d96666;--reef-text:#e0f0ff;--reef-text-dim:#a0c4d8;--reef-text-faint:#7a9aaa;--reef-border:rgba(127,219,202,0.15);--discord-blurple:#5865F2;--discord-online:#3ba55c;}*{margin:0;padding:0;box-sizing:border-box}:root{color-scheme:dark}::selection{background:color-mix(in srgb,var(--carrier-accent) 30%,transparent);color:var(--carrier-heading)}html{background-color:#0a1628;background-image:linear-gradient(0deg,rgba(10,22,40,.6) 0%,rgba(10,22,40,0) 100%),linear-gradient(180deg,#0a1628 0%,#0d2847 25%,#0f3460 50%,#1a5276 75%,#1e6f8e 100%);background-position:bottom,top;background-size:100% 80px,100% 100%;background-repeat:no-repeat,no-repeat;background-attachment:fixed,fixed;scroll-behavior:smooth;scrollbar-gutter:stable;overflow-x:hidden;min-height:100vh;min-height:100svh}body{font-family:\\'Nunito\\',sans-serif;color:var(--carrier-heading);min-height:100vh;min-height:100svh;display:flex;flex-direction:column;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;background:transparent;overflow-x:hidden;position:relative;animation:fadeIn .4s ease-out}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@media(prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}body{animation:none}}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--carrier-accent) 20%,transparent);border-radius:5px}::-webkit-scrollbar-thumb:hover{background:color-mix(in srgb,var(--carrier-accent) 35%,transparent)}::-webkit-scrollbar-track{background:transparent}html{scrollbar-color:color-mix(in srgb,var(--carrier-accent) 25%,transparent) transparent;scrollbar-width:thin}:focus:not(:focus-visible){outline:none}@keyframes rise{0%{transform:translateY(0) scale(1);opacity:.7}100%{transform:translateY(-110vh) scale(.3);opacity:0}}.st-footer{text-align:center;padding:var(--s-6) var(--s-4) var(--s-4);font-size:.68rem;color:#5a9aaa;letter-spacing:.05em;position:relative;z-index:10;flex-shrink:0;margin-top:auto}.st-footer a{color:var(--carrier-accent);text-decoration:none;font-weight:600}:root{--brand-accent:#7fdbca;--brand-accent-light:#0a8c8e;}:root{--cp-accent:#0a8c8e;--cp-accent-bg:color-mix(in srgb, #0a8c8e 12%, transparent);--cp-accent-border:color-mix(in srgb, #0a8c8e 25%, transparent);--cp-accent-glow:color-mix(in srgb, #0a8c8e 35%, transparent)}.st-wrap{position:relative;max-width:640px;margin:0 auto;padding:var(--s-8) var(--s-4) var(--s-12)}.cp-icon{width:80px;height:80px;border-radius:var(--r-circle);object-fit:cover;position:relative;z-index:10;pointer-events:all;box-shadow:none;border:none;position:relative;z-index:50;cursor:pointer;transition:transform .3s,filter .2s,box-shadow .2s}.cp-icon:hover{filter:brightness(1.1);box-shadow:0 0 0 4px color-mix(in srgb,var(--cp-accent) 18%,transparent)}@keyframes cpRerollHint{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--cp-accent) 0%,transparent)}50%{box-shadow:0 0 0 6px color-mix(in srgb,var(--cp-accent) 22%,transparent)}}.cp-icon{animation:cpRerollHint 2s ease-in-out 0s 3}.cp-icon:hover{animation-play-state:paused}@media(prefers-reduced-motion:reduce){.cp-icon{animation:none}}.cp-icon:focus{outline:2px solid var(--cp-accent);outline-offset:3px}.cp-icon:focus:not(:focus-visible){outline:none}@keyframes cpPulse{}.st-cells-wrap{container-type:inline-size}.st-cells{display:grid;grid-template-columns:repeat(3,1fr);grid-auto-rows:calc((100cqw - 2 * var(--s-3)) / 3);grid-auto-flow:dense;gap:var(--s-3);margin-bottom:var(--s-5)}.st-cell{background:var(--carrier-bg);backdrop-filter:blur(12px);border:1px solid var(--carrier-border);border-radius:var(--r-lg);padding:var(--s-5);min-height:0;min-width:0;display:flex;align-items:center;justify-content:center;position:relative;z-index:10;overflow:hidden}[data-span=\"2x2\"]{grid-column:span 2;grid-row:span 2}[data-span=\"3x2\"]{grid-column:span 3;grid-row:span 2}[data-span=\"3x3\"]{grid-column:span 3;grid-row:span 3}.st-cell.st-gal-cell{padding:0;overflow:hidden}.st-gal-link{display:block;width:100%;height:100%;position:relative;text-decoration:none;color:inherit}.st-gal-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center 22%;transition:transform .5s cubic-bezier(.2,.8,.2,1)}.st-cell.st-gal-cell:hover .st-gal-img{transform:scale(1.045)}.st-gal-scrim{position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,.92) 6%,rgba(0,0,0,.5) 34%,rgba(0,0,0,0) 64%)}.st-gal-body{position:absolute;left:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:flex-start;gap:var(--s-1);padding:var(--s-4);text-align:left}.st-gal-eyebrow{font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--carrier-accent)}.st-gal-title{font-size:1.15rem;font-weight:700;line-height:1.12;color:#fff;text-shadow:0 2px 16px rgba(0,0,0,.6)}.st-gal-cta{margin-top:var(--s-2);display:inline-flex;align-items:center;gap:6px;font-size:.8rem;font-weight:600;color:#fff;background:color-mix(in srgb,var(--carrier-accent) 26%,transparent);border:1px solid var(--carrier-accent);border-radius:999px;padding:5px 12px}.st-cell.st-cell-tile{padding:0;overflow:hidden;background:var(--carrier-bg);border:1px solid var(--carrier-border);border-radius:var(--r-lg);transition:transform .2s cubic-bezier(.2,.8,.2,1),border-color .2s}.st-cell.st-cell-tile:hover{transform:translateY(-3px);border-color:var(--cp-accent)}.st-cell-tile .st-cell-link{display:flex;flex-direction:column;justify-content:space-between;width:100%;height:100%;min-height:80px;box-sizing:border-box;padding:var(--s-3) var(--s-4);text-decoration:none;color:inherit}.st-cell-tile .st-cell-head{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s-2)}.st-cell-tile .st-cell-icon{width:26px;height:26px;flex:0 0 auto;color:var(--cp-accent)}.st-cell-tile .st-cell-arrow{width:17px;height:17px;flex:0 0 auto;color:var(--carrier-text-muted);transition:color .3s}.st-cell-tile .st-cell-link:hover .st-cell-arrow{color:var(--cp-accent)}.st-cell-tile .st-cell-body{display:flex;flex-direction:column;gap:2px;margin-top:var(--s-2);min-width:0}.st-cell-tile .st-cell-title{font-size:.92rem;font-weight:700;line-height:1.2;color:var(--carrier-heading);overflow:hidden;text-overflow:ellipsis}.st-cell-tile .st-cell-sub{font-size:.72rem;line-height:1.25;color:var(--carrier-text-muted);overflow:hidden;text-overflow:ellipsis}@media(prefers-reduced-motion:reduce){.st-cell.st-cell-tile,.st-cell-tile .st-cell-arrow{transition:none}.st-cell.st-cell-tile:hover{transform:none}.st-cell-tile .st-cell-link:hover .st-cell-arrow{transform:none}}@keyframes tbub{0%{opacity:.6;transform:scale(1)}100%{opacity:0;transform:translateY(-30px) scale(.3)}}.signet-arrow{position:relative;display:inline-block;flex-shrink:0;vertical-align:-2px}.signet-arrow--up-right{transform:rotate(-45deg)}.signet-arrow__tail{position:absolute;top:50%;left:12%;width:55%;height:calc(var(--signet-arrow-w,16px)/12);background:currentColor;border-radius:9999px;transform:translateY(-50%);transition:width .2s ease-out,left .2s ease-out}.signet-arrow__head{position:absolute;inset:0}a:hover .signet-arrow__tail,.signet-arrow:hover .signet-arrow__tail,.st-gal-cell:hover .signet-arrow__tail{left:67%;width:0}@media(prefers-reduced-motion:reduce){.signet-arrow__tail{transition:none}}@media(max-width:600px){.cp-icon{width:64px;height:64px}}@media(prefers-reduced-motion:reduce){*{animation:none!important;transition-duration:0.01ms!important}}@keyframes cpFeedShimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}@media print{.st-footer{display:none!important}body{background:#fff!important;color:#111!important;font-size:11pt}.st-wrap{max-width:100%!important;padding:0!important}.cp-icon{box-shadow:none!important;animation:none!important;border:2px solid #ccc!important}a{color:#333!important;text-decoration:underline!important}}:root{--cp-serif:\"Hoefler Text\",\"Iowan Old Style\",Palatino,\"Palatino Linotype\",Georgia,serif;--cp-ground:color-mix(in srgb,var(--cp-accent) 8%,#0c0c0c);--cp-surface:color-mix(in srgb,var(--cp-accent) 10%,#161616);--cp-surface-2:color-mix(in srgb,var(--cp-accent) 15%,#1a1a1a);--cp-surface-hi:color-mix(in srgb,var(--cp-accent) 22%,#1e1e1e);--cp-line:color-mix(in srgb,var(--cp-accent) 14%,transparent);--cp-line-hi:color-mix(in srgb,var(--cp-accent) 50%,transparent);--cp-ink:color-mix(in srgb,var(--cp-accent) 12%,#ffffff);--cp-muted:color-mix(in srgb,var(--cp-accent) 46%,#898989);--cp-faint:color-mix(in srgb,var(--cp-accent) 30%,#6a6a6a);--cp-accent-bright:color-mix(in srgb,var(--cp-accent) 72%,#ffffff);--cp-mist-a:color-mix(in srgb,var(--cp-accent) 11%,#0c0c0c);--cp-mist-b:#0b0b0b;--cp-page-ink:color-mix(in srgb,var(--cp-accent) 28%,#cecece);}body{background:radial-gradient(120% 90% at 50% -10%,var(--cp-mist-a),var(--cp-mist-b) 70%) fixed!important}.st-wrap{max-width:720px;width:100%}.st-card{max-width:680px;width:100%;margin:0 auto var(--s-8);background:var(--cp-ground);border:1px solid var(--cp-line);border-radius:28px;padding:16px;box-shadow:0 40px 90px -40px rgba(0,0,0,.75),0 0 0 1px rgba(0,0,0,.3)}.st-card .st-cells{grid-template-columns:repeat(6,1fr)!important;gap:14px;grid-auto-rows:calc((100cqw - 5 * 14px) / 6)!important;grid-auto-flow:dense;margin-bottom:0}.st-card [data-span=\"2x2\"]{grid-column:span 2;grid-row:span 2}.st-card [data-span=\"3x2\"]{grid-column:span 3;grid-row:span 2}.st-card [data-span=\"3x3\"]{grid-column:span 3;grid-row:span 3}.st-card [data-span=\"6x1\"]{grid-column:span 6;grid-row:span 1}.st-card [data-span=\"6x2\"]{grid-column:span 6;grid-row:span 2}.st-card [data-span=\"6x3\"]{grid-column:span 6;grid-row:span 3}.st-card [data-span=\"6x4\"]{grid-column:span 6;grid-row:span 4}.st-card .st-cell{background:var(--cp-surface);border:1px solid var(--cp-line);border-radius:18px;backdrop-filter:none;color:var(--cp-ink);transition:transform .34s cubic-bezier(.2,.8,.2,1),border-color .34s,background .34s,box-shadow .34s}.st-card .st-gal-cell:hover,.st-card .st-cell-tile:hover{transform:translateY(-4px);border-color:var(--cp-line-hi);background:var(--cp-surface-2);box-shadow:0 18px 40px -22px rgba(0,0,0,.8)}.st-card .st-hero{grid-column:1 / -1;grid-row:span 2;flex-direction:row;align-items:center;justify-content:flex-start;gap:18px;text-align:left;padding:16px;overflow:visible;background:linear-gradient(120deg,var(--cp-surface-2),var(--cp-surface))}.st-card .st-figure{position:relative;flex:none;line-height:0}.st-card .st-hero .cp-icon{width:78px;height:78px;border-radius:50%;object-fit:cover;border:2px solid var(--cp-line-hi);box-shadow:0 0 0 4px color-mix(in srgb,var(--cp-accent) 10%,transparent);animation:none;position:relative;z-index:1}.st-card .st-hero .cp-icon:hover{filter:brightness(1.09);box-shadow:0 0 0 4px color-mix(in srgb,var(--cp-accent) 16%,transparent)}.st-card .st-hero-text{min-width:0}.st-card .st-hero-name{font-family:var(--cp-serif);font-size:36px;line-height:.98;letter-spacing:.01em;margin:0;color:var(--cp-ink);font-weight:600}.st-card .st-hero-tagline{margin:7px 0 0;font-size:13px;line-height:1.4;color:var(--cp-muted)}.st-card .st-hero-tagline em{color:var(--cp-accent-bright);font-style:italic}.st-card .st-hero-meta{display:flex;align-items:center;gap:10px;margin-top:10px;flex-wrap:wrap}.st-card .st-chip{font-size:10px;letter-spacing:.09em;text-transform:uppercase;color:var(--cp-accent-bright);border:1px solid var(--cp-line-hi);border-radius:999px;padding:2.5px 9px}.st-card .st-gal-img{object-position:center 22%}.st-card .st-gal-scrim{background:linear-gradient(to top,rgba(13,13,13,.94) 8%,rgba(13,13,13,.55) 34%,rgba(13,13,13,0) 62%)}.st-card .st-gal-eyebrow{font-family:var(--cp-serif);font-size:15px;color:var(--cp-accent-bright);letter-spacing:.14em;text-transform:none}.st-card .st-gal-title{font-family:var(--cp-serif);font-size:26px;line-height:1.04;color:var(--cp-ink);text-shadow:0 2px 20px rgba(0,0,0,.6)}.st-card .st-gal-cta{color:var(--cp-ink);background:color-mix(in srgb,var(--cp-accent) 18%,transparent);border:1px solid var(--cp-line-hi);border-radius:999px;padding:7px 14px;backdrop-filter:blur(6px)}.st-card .st-cell-tile{background:var(--cp-surface);border-color:var(--cp-line)}.st-card .st-cell-tile:hover{background:var(--cp-surface-2);border-color:var(--cp-line-hi)}.st-card .st-cell-tile .st-cell-icon{color:var(--cp-accent-bright)}.st-card .st-cell-tile .st-cell-arrow{color:var(--cp-faint)}.st-card .st-cell-tile .st-cell-link:hover .st-cell-arrow{color:var(--cp-accent-bright)}.st-card .st-cell-tile .st-cell-title{color:var(--cp-ink);font-size:15px;font-weight:600}.st-card .st-cell-tile .st-cell-sub{color:var(--cp-muted);font-size:11.5px}.st-footer>div{color:var(--cp-page-ink)!important}.st-footer a{color:var(--cp-accent)!important}@media(max-width:600px){.st-card .st-hero-name{font-size:26px}.st-card .st-gal-title{font-size:22px}}@media(max-width:440px){.st-card{max-width:none;margin:0;border:none;border-radius:0;box-shadow:none;background:transparent;padding:8px max(8px,env(safe-area-inset-right)) 8px max(8px,env(safe-area-inset-left))}.st-card .st-hero{grid-row:span 3!important}.st-card .st-cell-tile .st-cell-link{padding:10px 12px;min-height:0}.st-card .st-cell-tile .st-cell-title{font-size:13px}.st-card .st-cell-tile .st-cell-sub{font-size:10px}.st-card .st-cell-tile .st-cell-icon{width:22px;height:22px}.st-card [data-span=\"2x2\"] .st-cell-sub{display:none}}@keyframes reefBodyFade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}body{animation:reefBodyFade .35s ease-out}@media (prefers-reduced-motion:reduce){body{animation:none}}button:not(.reef-toast-close):not(.reef-cp-item):active{transform:scale(.97);transition:transform .08s}[tabindex]:focus-visible{outline:2px solid var(--cp-accent,#7fdbca);outline-offset:2px;border-radius:2px}@keyframes cpLivePulse{0%{box-shadow:0 0 0 0 rgba(59,165,92,.55)}70%{box-shadow:0 0 0 8px rgba(59,165,92,0)}100%{box-shadow:0 0 0 0 rgba(59,165,92,0)}}@keyframes cbVoicePulse{0%,100%{box-shadow:0 0 0 0 rgba(59,165,92,.55);transform:scale(1)}50%{box-shadow:0 0 0 5px rgba(59,165,92,0);transform:scale(1.15)}}\\n\\n/* @cvernet/signet \\u2014 directional arrow with a tail-retract hover morph.\\n *\\n * At REST the element is a full arrow (chevron + tail). On hover of the\\n * nearest `.group` ancestor \\u2014 or the enclosing `` \\u2014 the tail retracts\\n * rightward INTO the chevron (\\u2192 collapses to >). One animation; `rotate()`\\n * adapts it to every direction:\\n * right (default) \\u2192 0deg link / next\\n * left (--left) \\u2192 180deg back / previous\\n * up-right (--up-right) \\u2192 -45deg external / outbound\\n *\\n * Geometry is the canonical CVER arrow (chevron `10 6 16 12 10 18`,\\n * stroke 2). The tail weight tracks the chevron stroke (size / 12) so the\\n * whole arrow thickens and thins as one unit at any size \\u2014 set\\n * `--signet-arrow-w` to the rendered size (the components do this for you).\\n *\\n * Pure CSS + SVG. currentColor throughout. No JS, SSR-friendly. Import once:\\n * import \\'@cvernet/signet/arrow.css\\';\\n */\\n\\n.signet-arrow {\\n position: relative;\\n display: inline-block;\\n flex-shrink: 0;\\n vertical-align: -2px;\\n}\\n\\n.signet-arrow--left {\\n transform: rotate(180deg);\\n}\\n.signet-arrow--up-right {\\n transform: rotate(-45deg);\\n}\\n\\n.signet-arrow__tail {\\n position: absolute;\\n top: 50%;\\n left: 12%;\\n width: 55%;\\n /* = the chevron\\'s rendered stroke width (stroke-width 2 in a 24 viewBox at\\n `size` px = size / 12), so head and tail are one consistent weight that\\n scales together. */\\n height: calc(var(--signet-arrow-w, 16px) / 12);\\n background: currentColor;\\n border-radius: 9999px;\\n transform: translateY(-50%);\\n transition:\\n width 200ms ease-out,\\n left 200ms ease-out;\\n}\\n\\n.signet-arrow__head {\\n position: absolute;\\n inset: 0;\\n}\\n\\n/* Hover/focus retracts the tail into the chevron. Triggers from a `.group`\\n ancestor (cards, tertiary buttons), the enclosing anchor (plain links and\\n primary/secondary buttons), or the arrow itself. */\\n.group:hover .signet-arrow__tail,\\n.group:focus-visible .signet-arrow__tail,\\na:hover .signet-arrow__tail,\\na:focus-visible .signet-arrow__tail,\\n.signet-arrow:hover .signet-arrow__tail {\\n left: 67%;\\n width: 0;\\n}\\n\\n@media (prefers-reduced-motion: reduce) {\\n .signet-arrow__tail {\\n transition: none;\\n }\\n}\\n';\n var ICONS = { \"patreon\": ``, \"conventions\": ``, \"shop\": ``, \"instagram\": ``, \"x\": `` };\n var QR_VERSION = \"0.14.7\";\n var DRAWER_VERSION = \"0.1.1\";\n var ARROW = '';\n\n // packages/cardtile/serve/card-api.mjs\n var KV_MAX_BYTES = 25 * 1024 * 1024;\n var TRY_MAX_BYTES = 1500 * 1024;\n\n // packages/cardtile/w/sandbox-i18n.mjs\n var SANDBOX_LOCALES = {\n en: {\n lang: \"en-US\",\n coachMark: \"Tap anything on the card to change it.\",\n name: \"Sam\",\n bio: \"Family doctor in a small town. I write about rest, worry, and the small things that help \\u2014 appointments and notes below.\",\n linkLabel: \"Book an appointment\",\n textBody: \"Tap any tile to edit it \\u2014 everything here is just a demo.\",\n imageAlt: \"Sample image\",\n bannerStatus: \"Trying Card \\u2014 not saved yet\",\n bannerText: \"You're trying Card \\u2014 nothing is saved until you make it yours.\",\n doorCta: \"Make this card real\",\n reset: \"Start over\",\n pageTitle: \"Try Card \\xB7 feelreef\",\n doorNote: \"Next you'll sign in with your email (first time? your account is made along the way, no password). This card comes with you.\",\n doorContinue: \"Continue\",\n doorBack: \"Keep editing\"\n },\n zh: {\n lang: \"zh-TW\",\n coachMark: \"\\u9EDE\\u5361\\u7247\\u4E0A\\u7684\\u4EFB\\u4F55\\u5730\\u65B9\\uFF0C\\u5C31\\u80FD\\u4FEE\\u6539\\u5B83\\u3002\",\n name: \"\\u5C0F\\u7F8E\",\n bio: \"\\u5C0F\\u93AE\\u7684\\u5BB6\\u5EAD\\u91AB\\u5E2B\\u3002\\u6211\\u64B0\\u5BEB\\u95DC\\u65BC\\u4F11\\u606F\\u3001\\u7169\\u60F1\\uFF0C\\u4EE5\\u53CA\\u5C0D\\u751F\\u6D3B\\u6709\\u6240\\u5E6B\\u52A9\\u7684\\u5C0F\\u4E8B\\u3002\\u4E0B\\u65B9\\u70BA\\u9810\\u7D04\\u8207\\u7B46\\u8A18\\u3002\",\n linkLabel: \"\\u9810\\u7D04\",\n textBody: \"\\u9EDE\\u4E00\\u4E0B\\u4EFB\\u4F55\\u4E00\\u5F35\\u724C\\u5C31\\u80FD\\u7DE8\\u8F2F\\u3002\\u9019\\u88E1\\u7684\\u5167\\u5BB9\\u53EA\\u662F\\u793A\\u7BC4\\u3002\",\n imageAlt: \"\\u793A\\u7BC4\\u5716\\u7247\",\n bannerStatus: \"\\u8A66\\u73A9\\u4E2D\\uFF1A\\u9084\\u6C92\\u6709\\u5132\\u5B58\",\n bannerText: \"\\u4F60\\u6B63\\u5728\\u8A66\\u73A9 Card\\u3002\\u6309\\u4E0B\\u300C\\u628A\\u9019\\u5F35\\u8B8A\\u6210\\u771F\\u7684\\u300D\\u4E4B\\u524D\\uFF0C\\u4EC0\\u9EBC\\u90FD\\u4E0D\\u6703\\u5132\\u5B58\\u3002\",\n doorCta: \"\\u628A\\u9019\\u5F35\\u8B8A\\u6210\\u771F\\u7684\",\n reset: \"\\u91CD\\u65B0\\u958B\\u59CB\",\n pageTitle: \"\\u8A66\\u73A9 Card \\xB7 feelreef\",\n doorNote: \"\\u63A5\\u4E0B\\u4F86\\u6703\\u8ACB\\u4F60\\u7528 email \\u767B\\u5165\\uFF08\\u7B2C\\u4E00\\u6B21\\u6703\\u9806\\u4FBF\\u5EFA\\u597D\\u5E33\\u865F\\uFF0C\\u4E0D\\u7528\\u5BC6\\u78BC\\uFF09\\u3002\\u9019\\u5F35\\u5361\\u6703\\u8DDF\\u8457\\u4F60\\u904E\\u53BB\\u3002\",\n doorContinue: \"\\u7E7C\\u7E8C\",\n doorBack: \"\\u518D\\u6539\\u4E00\\u4E0B\"\n },\n ja: {\n lang: \"ja-JP\",\n coachMark: \"\\u30AB\\u30FC\\u30C9\\u306E\\u3069\\u3053\\u3067\\u3082\\u30BF\\u30C3\\u30D7\\u3059\\u308B\\u3068\\u3001\\u5909\\u66F4\\u3067\\u304D\\u307E\\u3059\\u3002\",\n name: \"\\u3086\\u3044\",\n bio: \"\\u5C0F\\u3055\\u306A\\u753A\\u306E\\u5BB6\\u5EAD\\u533B\\u3067\\u3059\\u3002\\u4F11\\u606F\\u3084\\u60A9\\u307F\\u3001\\u305D\\u3057\\u3066\\u65E5\\u3005\\u306E\\u52A9\\u3051\\u306B\\u306A\\u308B\\u5C0F\\u3055\\u306A\\u3053\\u3068\\u306B\\u3064\\u3044\\u3066\\u7DB4\\u3063\\u3066\\u3044\\u307E\\u3059\\u3002\\u4E88\\u7D04\\u3084\\u30CE\\u30FC\\u30C8\\u306F\\u4EE5\\u4E0B\\u304B\\u3089\\u3002\",\n linkLabel: \"\\u4E88\\u7D04\\u3059\\u308B\",\n textBody: \"\\u3069\\u306E\\u30BF\\u30A4\\u30EB\\u3082\\u30BF\\u30C3\\u30D7\\u3059\\u308C\\u3070\\u7DE8\\u96C6\\u3067\\u304D\\u307E\\u3059\\u3002\\u3053\\u3053\\u306B\\u3042\\u308B\\u306E\\u306F\\u3059\\u3079\\u3066\\u30B5\\u30F3\\u30D7\\u30EB\\u3067\\u3059\\u3002\",\n imageAlt: \"\\u30B5\\u30F3\\u30D7\\u30EB\\u753B\\u50CF\",\n bannerStatus: \"\\u304A\\u8A66\\u3057\\u4E2D \\u2014 \\u307E\\u3060\\u4FDD\\u5B58\\u3055\\u308C\\u3066\\u3044\\u307E\\u305B\\u3093\",\n bannerText: \"Card \\u3092\\u304A\\u8A66\\u3057\\u4E2D\\u3067\\u3059\\u3002\\u300C\\u3053\\u308C\\u3092\\u672C\\u7269\\u306B\\u3059\\u308B\\u300D\\u3092\\u62BC\\u3059\\u307E\\u3067\\u3001\\u4F55\\u3082\\u4FDD\\u5B58\\u3055\\u308C\\u307E\\u305B\\u3093\\u3002\",\n doorCta: \"\\u3053\\u308C\\u3092\\u672C\\u7269\\u306B\\u3059\\u308B\",\n reset: \"\\u306F\\u3058\\u3081\\u304B\\u3089\\u3084\\u308A\\u76F4\\u3059\",\n pageTitle: \"Card \\u3092\\u8A66\\u3059 \\xB7 feelreef\",\n doorNote: \"\\u3053\\u306E\\u3042\\u3068\\u30E1\\u30FC\\u30EB\\u30A2\\u30C9\\u30EC\\u30B9\\u3067\\u30ED\\u30B0\\u30A4\\u30F3\\u3057\\u307E\\u3059\\uFF08\\u306F\\u3058\\u3081\\u3066\\u306E\\u65B9\\u306F\\u30A2\\u30AB\\u30A6\\u30F3\\u30C8\\u3082\\u4E00\\u7DD2\\u306B\\u4F5C\\u3089\\u308C\\u307E\\u3059\\u3002\\u30D1\\u30B9\\u30EF\\u30FC\\u30C9\\u306F\\u4E0D\\u8981\\uFF09\\u3002\\u3053\\u306E\\u30AB\\u30FC\\u30C9\\u306F\\u305D\\u306E\\u307E\\u307E\\u6301\\u3063\\u3066\\u3044\\u3051\\u307E\\u3059\\u3002\",\n doorContinue: \"\\u7D9A\\u3051\\u308B\",\n doorBack: \"\\u3082\\u3046\\u5C11\\u3057\\u76F4\\u3059\"\n },\n ko: {\n lang: \"ko-KR\",\n coachMark: \"\\uCE74\\uB4DC\\uC758 \\uC544\\uBB34 \\uACF3\\uC774\\uB098 \\uB20C\\uB7EC\\uC11C \\uBC14\\uAFD4 \\uBCF4\\uC138\\uC694.\",\n name: \"\\uC9C0\\uC6B0\",\n bio: \"\\uC791\\uC740 \\uB9C8\\uC744\\uC758 \\uAC00\\uC815\\uC758\\uD559\\uACFC \\uC804\\uBB38\\uC758\\uC608\\uC694. \\uD734\\uC2DD\\uACFC \\uAC71\\uC815, \\uADF8\\uB9AC\\uACE0 \\uB3C4\\uC6C0\\uC774 \\uB418\\uB294 \\uC18C\\uC18C\\uD55C \\uAC83\\uB4E4\\uC5D0 \\uAD00\\uD574 \\uAE00\\uC744 \\uC368\\uC694 \\u2014 \\uC608\\uC57D\\uACFC \\uB178\\uD2B8\\uB294 \\uC544\\uB798\\uC5D0\\uC11C.\",\n linkLabel: \"\\uC608\\uC57D\\uD558\\uAE30\",\n textBody: \"\\uC544\\uBB34 \\uD0C0\\uC77C\\uC774\\uB098 \\uB20C\\uB7EC\\uC11C \\uD3B8\\uC9D1\\uD574 \\uBCF4\\uC138\\uC694. \\uC5EC\\uAE30 \\uB0B4\\uC6A9\\uC740 \\uC804\\uBD80 \\uC608\\uC2DC\\uC608\\uC694.\",\n imageAlt: \"\\uC608\\uC2DC \\uC774\\uBBF8\\uC9C0\",\n bannerStatus: \"\\uCCB4\\uD5D8 \\uC911 \\u2014 \\uC544\\uC9C1 \\uC800\\uC7A5\\uB418\\uC9C0 \\uC54A\\uC558\\uC5B4\\uC694\",\n bannerText: \"Card\\uB97C \\uCCB4\\uD5D8\\uD558\\uB294 \\uC911\\uC774\\uC5D0\\uC694. \\u2018\\uC774 \\uCE74\\uB4DC\\uB97C \\uC9C4\\uC9DC\\uB85C \\uB9CC\\uB4E4\\uAE30\\u2019\\uB97C \\uB204\\uB974\\uAE30 \\uC804\\uC5D0\\uB294 \\uC544\\uBB34\\uAC83\\uB3C4 \\uC800\\uC7A5\\uB418\\uC9C0 \\uC54A\\uC544\\uC694.\",\n doorCta: \"\\uC774 \\uCE74\\uB4DC\\uB97C \\uC9C4\\uC9DC\\uB85C \\uB9CC\\uB4E4\\uAE30\",\n reset: \"\\uCC98\\uC74C\\uBD80\\uD130 \\uB2E4\\uC2DC\",\n pageTitle: \"Card \\uCCB4\\uD5D8 \\xB7 feelreef\",\n doorNote: \"\\uB2E4\\uC74C\\uC5D0\\uB294 \\uC774\\uBA54\\uC77C\\uB85C \\uB85C\\uADF8\\uC778\\uD574\\uC694\\uFF08\\uCC98\\uC74C\\uC774\\uB77C\\uBA74 \\uACC4\\uC815\\uB3C4 \\uD568\\uAED8 \\uB9CC\\uB4E4\\uC5B4\\uC838\\uC694. \\uBE44\\uBC00\\uBC88\\uD638\\uB294 \\uC5C6\\uC5B4\\uC694\\uFF09. \\uC774 \\uCE74\\uB4DC\\uB294 \\uADF8\\uB300\\uB85C \\uAC00\\uC838\\uAC08 \\uC218 \\uC788\\uC5B4\\uC694.\",\n doorContinue: \"\\uACC4\\uC18D\\uD558\\uAE30\",\n doorBack: \"\\uC870\\uAE08 \\uB354 \\uACE0\\uCE58\\uAE30\"\n },\n // โ”€โ”€ added 2026-09-05 (owner ruling: nine console locales, not four) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n \"zh-Hans\": {\n lang: \"zh-CN\",\n coachMark: \"\\u70B9\\u5361\\u7247\\u4E0A\\u7684\\u4EFB\\u4F55\\u5730\\u65B9\\uFF0C\\u5C31\\u80FD\\u4FEE\\u6539\\u5B83\\u3002\",\n name: \"\\u5C0F\\u7F8E\",\n bio: \"\\u5728\\u5C0F\\u9547\\u6267\\u4E1A\\u7684\\u5BB6\\u5EAD\\u533B\\u751F\\u3002\\u6211\\u5199\\u4E00\\u4E9B\\u5173\\u4E8E\\u4F11\\u606F\\u3001\\u70E6\\u607C\\uFF0C\\u4EE5\\u53CA\\u751F\\u6D3B\\u91CC\\u6709\\u5E2E\\u52A9\\u7684\\u5C0F\\u4E8B\\u3002\\u9884\\u7EA6\\u4E0E\\u7B14\\u8BB0\\u89C1\\u4E0B\\u65B9\\u3002\",\n linkLabel: \"\\u9884\\u7EA6\",\n textBody: \"\\u70B9\\u4E00\\u4E0B\\u4EFB\\u610F\\u4E00\\u5F20\\u724C\\u5373\\u53EF\\u7F16\\u8F91\\u3002\\u8FD9\\u91CC\\u7684\\u5185\\u5BB9\\u53EA\\u662F\\u793A\\u8303\\u3002\",\n imageAlt: \"\\u793A\\u4F8B\\u56FE\\u7247\",\n bannerStatus: \"\\u8BD5\\u73A9\\u4E2D\\uFF1A\\u8FD8\\u6CA1\\u6709\\u4FDD\\u5B58\",\n bannerText: \"\\u4F60\\u6B63\\u5728\\u8BD5\\u73A9 Card\\u3002\\u6309\\u4E0B\\u201C\\u628A\\u8FD9\\u5F20\\u5361\\u7247\\u53D8\\u6210\\u771F\\u7684\\u201D\\u4E4B\\u524D\\uFF0C\\u4EC0\\u4E48\\u90FD\\u4E0D\\u4F1A\\u4FDD\\u5B58\\u3002\",\n doorCta: \"\\u628A\\u8FD9\\u5F20\\u5361\\u7247\\u53D8\\u6210\\u771F\\u7684\",\n reset: \"\\u91CD\\u65B0\\u5F00\\u59CB\",\n pageTitle: \"\\u8BD5\\u73A9 Card \\xB7 feelreef\",\n doorNote: \"\\u63A5\\u4E0B\\u6765\\u4F1A\\u8BF7\\u4F60\\u7528 email \\u767B\\u5F55\\uFF08\\u7B2C\\u4E00\\u6B21\\u4F1A\\u987A\\u4FBF\\u5EFA\\u597D\\u8D26\\u53F7\\uFF0C\\u4E0D\\u7528\\u5BC6\\u7801\\uFF09\\u3002\\u8FD9\\u5F20\\u5361\\u7247\\u4F1A\\u8DDF\\u7740\\u4F60\\u8FC7\\u53BB\\u3002\",\n doorContinue: \"\\u7EE7\\u7EED\",\n doorBack: \"\\u518D\\u6539\\u4E00\\u4E0B\"\n },\n de: {\n lang: \"de-DE\",\n coachMark: \"Tippen Sie auf etwas auf der Karte, um es zu \\xE4ndern.\",\n name: \"Lea\",\n bio: \"Haus\\xE4rztin in einer Kleinstadt. Ich schreibe \\xFCber Erholung, Sorgen und die kleinen Dinge, die helfen \\u2014 Termine und Notizen unten.\",\n linkLabel: \"Termin vereinbaren\",\n textBody: \"Tippen Sie auf eine Kachel, um sie zu bearbeiten \\u2014 alles hier ist nur eine Demo.\",\n imageAlt: \"Beispielbild\",\n bannerStatus: \"Zum Ausprobieren \\u2014 noch nicht gespeichert\",\n bannerText: \"Sie probieren Card gerade aus \\u2014 gespeichert wird erst, wenn Sie die Karte zu Ihrer machen.\",\n doorCta: \"Diese Karte echt machen\",\n reset: \"Von vorn beginnen\",\n pageTitle: \"Card ausprobieren \\xB7 feelreef\",\n doorNote: \"Als N\\xE4chstes melden Sie sich mit Ihrer E-Mail-Adresse an (beim ersten Mal wird Ihr Konto gleich mit angelegt, ohne Passwort). Diese Karte kommt mit.\",\n doorContinue: \"Weiter\",\n doorBack: \"Weiter bearbeiten\"\n },\n fr: {\n lang: \"fr-FR\",\n coachMark: \"Touchez n\\u2019importe quel \\xE9l\\xE9ment de la carte pour le modifier.\",\n name: \"Camille\",\n bio: \"M\\xE9decin de famille dans une petite ville. J\\u2019\\xE9cris sur le repos, les soucis et les petites choses qui aident \\u2014 rendez-vous et notes ci-dessous.\",\n linkLabel: \"Prendre rendez-vous\",\n textBody: \"Touchez une tuile pour la modifier \\u2014 tout ceci n\\u2019est qu\\u2019une d\\xE9monstration.\",\n imageAlt: \"Image d\\u2019exemple\",\n bannerStatus: \"Essai \\u2014 rien n\\u2019est encore enregistr\\xE9\",\n bannerText: \"Vous essayez Card \\u2014 rien n\\u2019est enregistr\\xE9 tant que vous n\\u2019en faites pas la v\\xF4tre.\",\n doorCta: \"Rendre cette carte r\\xE9elle\",\n reset: \"Recommencer\",\n pageTitle: \"Essayer Card \\xB7 feelreef\",\n doorNote: \"Ensuite, vous vous connecterez avec votre e-mail (la premi\\xE8re fois, votre compte est cr\\xE9\\xE9 au passage, sans mot de passe). Cette carte vous suit.\",\n doorContinue: \"Continuer\",\n doorBack: \"Continuer \\xE0 modifier\"\n },\n es: {\n lang: \"es-ES\",\n coachMark: \"Toca cualquier parte de la tarjeta para cambiarla.\",\n name: \"Sof\\xEDa\",\n bio: \"M\\xE9dica de familia en un pueblo peque\\xF1o. Escribo sobre el descanso, las preocupaciones y las peque\\xF1as cosas que ayudan \\u2014 citas y notas abajo.\",\n linkLabel: \"Reservar una cita\",\n textBody: \"Toca cualquier ficha para editarla \\u2014 todo esto es solo una demostraci\\xF3n.\",\n imageAlt: \"Imagen de ejemplo\",\n bannerStatus: \"De prueba \\u2014 a\\xFAn no se ha guardado\",\n bannerText: \"Est\\xE1s probando Card: no se guarda nada hasta que la hagas tuya.\",\n doorCta: \"Hacer real esta tarjeta\",\n reset: \"Empezar de nuevo\",\n pageTitle: \"Probar Card \\xB7 feelreef\",\n doorNote: \"A continuaci\\xF3n iniciar\\xE1s sesi\\xF3n con tu correo (la primera vez tu cuenta se crea por el camino, sin contrase\\xF1a). Esta tarjeta se va contigo.\",\n doorContinue: \"Continuar\",\n doorBack: \"Seguir editando\"\n },\n pt: {\n lang: \"pt-BR\",\n coachMark: \"Toque em qualquer parte do cart\\xE3o para alter\\xE1-la.\",\n name: \"Beatriz\",\n bio: \"M\\xE9dica de fam\\xEDlia em uma cidade pequena. Escrevo sobre descanso, preocupa\\xE7\\xF5es e as pequenas coisas que ajudam \\u2014 consultas e notas abaixo.\",\n linkLabel: \"Agendar uma consulta\",\n textBody: \"Toque em qualquer pe\\xE7a para edit\\xE1-la \\u2014 tudo aqui \\xE9 s\\xF3 uma demonstra\\xE7\\xE3o.\",\n imageAlt: \"Imagem de exemplo\",\n bannerStatus: \"Experimentando \\u2014 ainda n\\xE3o foi salvo\",\n bannerText: \"Voc\\xEA est\\xE1 experimentando o Card \\u2014 nada \\xE9 salvo at\\xE9 voc\\xEA torn\\xE1-lo seu.\",\n doorCta: \"Tornar este cart\\xE3o real\",\n reset: \"Come\\xE7ar de novo\",\n pageTitle: \"Experimentar Card \\xB7 feelreef\",\n doorNote: \"Em seguida voc\\xEA entra com o seu e-mail (na primeira vez a conta \\xE9 criada junto, sem senha). Este cart\\xE3o vai com voc\\xEA.\",\n doorContinue: \"Continuar\",\n doorBack: \"Continuar editando\"\n }\n };\n var CHROME_STRINGS = {\n en: {\n menu: \"Menu\",\n undo: \"Undo\",\n openFile: \"Open .md\",\n download: \"Download .md\",\n addCellHeader: \"Add a tile\",\n faceTabLabel: \"Card front\",\n addDrawerButton: \"+ Drawer\",\n addDrawerTitleAttr: \"Add a drawer\",\n addDrawerPrompt: \"Drawer title (visitors will see this)\",\n addDrawerPromptDefault: \"More\",\n addDrawerDuplicateAlert: \"There's already a drawer with that name.\",\n modalCloseAria: \"Close\",\n modalRawSummary: \"Advanced\",\n modalRawNote: \"The form changes only what it knows \\u2014 anything else on this tile stays exactly as it is.\",\n modalDelete: \"Delete this tile\",\n modalSave: \"Done\",\n deleteConfirm: \"Delete this tile?\",\n selectDefaultOption: \"(default)\",\n unknownCellHint: \"This kind of tile has no form yet \\u2014 you can only change its original settings.\",\n unknownCellBodyLabel: \"Content\",\n canvasTitle: \"Card\",\n assetPick: \"Choose a picture\",\n assetReplace: \"Replace\",\n assetUrl: \"Or paste a picture address\",\n assetTooBig: \"That picture is too big \\u2014 try another, or make it smaller.\",\n assetUnreadable: \"That file couldn't be read \\u2014 try another one.\",\n targetUrl: \"An address\",\n targetDrawer: \"Drawer: \",\n needAddress: \"Needs an address\",\n needValue: \"Please fill this in\",\n linkUnset: \"No address yet\",\n imgUnset: \"+ Add a picture\",\n moveUp: \"Move up\",\n moveDown: \"Move down\",\n mdMode: \"Markdown\",\n mdBack: \"Back to the card\",\n mdBroken: \"Line {n} doesn't read as part of a card \\u2014 nothing has been changed.\",\n cardUrlHeader: \"Card address\",\n cardUrlNote: \"The QR code points here, and a social link to this same domain becomes the hand-drawn house icon. It is not kept in the file \\u2014 the address decides it when the card goes live.\",\n mdHeader: \"markdown (exactly what gets saved)\",\n mdNote: \"Change the text here and the card follows \\u2014 both directions are the same one file.\",\n pubTitle: \"Save it online\",\n pubBackup: \"Save a .md copy to the downloads folder first\",\n pubNote: \"Press this and it is exactly what\\u2019s online \\u2014 there is no other step.\",\n pubCancel: \"Cancel\",\n pubSave: \"Save\"\n },\n zh: {\n menu: \"\\u9078\\u55AE\",\n undo: \"\\u5FA9\\u539F\",\n openFile: \"\\u958B\\u555F .md\",\n download: \"\\u4E0B\\u8F09 .md\",\n addCellHeader: \"\\u52A0\\u4E00\\u5F35\\u724C\",\n faceTabLabel: \"\\u5361\\u7247\\u6B63\\u9762\",\n addDrawerButton: \"\\uFF0B\\u62BD\\u5C5C\",\n addDrawerTitleAttr: \"\\u65B0\\u589E\\u62BD\\u5C5C\",\n addDrawerPrompt: \"\\u62BD\\u5C5C\\u7684\\u6A19\\u984C\\uFF08\\u8A2A\\u5BA2\\u6703\\u770B\\u5230\\uFF09\",\n addDrawerPromptDefault: \"\\u66F4\\u591A\",\n addDrawerDuplicateAlert: \"\\u5DF2\\u7D93\\u6709\\u540C\\u540D\\u7684\\u62BD\\u5C5C\\u4E86\\u3002\",\n modalCloseAria: \"\\u95DC\\u9589\",\n modalRawSummary: \"\\u9032\\u968E\",\n modalRawNote: \"\\u8868\\u55AE\\u53EA\\u6539\\u5B83\\u8A8D\\u5F97\\u7684\\u90E8\\u5206\\uFF0C\\u9019\\u5F35\\u724C\\u4E0A\\u7684\\u5176\\u4ED6\\u6771\\u897F\\u539F\\u5C01\\u4E0D\\u52D5\\u7559\\u8457\\u3002\",\n modalDelete: \"\\u522A\\u9664\\u9019\\u5F35\\u724C\",\n modalSave: \"\\u5B8C\\u6210\",\n deleteConfirm: \"\\u522A\\u9664\\u9019\\u5F35\\u724C\\uFF1F\",\n selectDefaultOption: \"\\uFF08\\u9810\\u8A2D\\uFF09\",\n unknownCellHint: \"\\u9019\\u7A2E\\u724C\\u9084\\u6C92\\u6709\\u8868\\u55AE\\uFF0C\\u53EA\\u80FD\\u6539\\u5B83\\u539F\\u672C\\u7684\\u8A2D\\u5B9A\\u3002\",\n unknownCellBodyLabel: \"\\u5167\\u5BB9\",\n canvasTitle: \"\\u5361\\u7247\",\n assetPick: \"\\u9078\\u4E00\\u5F35\\u5716\\u7247\",\n assetReplace: \"\\u63DB\\u4E00\\u5F35\",\n assetUrl: \"\\u6216\\u8CBC\\u4E0A\\u5716\\u7247\\u7DB2\\u5740\",\n assetTooBig: \"\\u9019\\u5F35\\u592A\\u5927\\u4E86\\uFF0C\\u63DB\\u4E00\\u5F35\\u6216\\u7E2E\\u5C0F\\u518D\\u8A66\\u3002\",\n assetUnreadable: \"\\u8B80\\u4E0D\\u5230\\u9019\\u500B\\u6A94\\u6848\\uFF0C\\u63DB\\u4E00\\u5F35\\u8A66\\u8A66\\u3002\",\n targetUrl: \"\\u7DB2\\u5740\",\n targetDrawer: \"\\u62BD\\u5C5C\\uFF1A\",\n needAddress: \"\\u8981\\u6709\\u7DB2\\u5740\",\n needValue: \"\\u9019\\u4E00\\u6B04\\u8981\\u586B\",\n linkUnset: \"\\u9084\\u6C92\\u586B\\u7DB2\\u5740\",\n imgUnset: \"\\uFF0B \\u52A0\\u4E00\\u5F35\\u5716\\u7247\",\n moveUp: \"\\u5F80\\u4E0A\\u79FB\",\n moveDown: \"\\u5F80\\u4E0B\\u79FB\",\n mdMode: \"Markdown\",\n mdBack: \"\\u56DE\\u5230\\u5361\\u7247\",\n mdBroken: \"\\u7B2C {n} \\u884C\\u770B\\u4E0D\\u61C2\\uFF0C\\u5361\\u7247\\u6C92\\u6709\\u6539\\u3002\",\n cardUrlHeader: \"\\u5361\\u7247\\u7DB2\\u5740\",\n cardUrlNote: \"QR \\u78BC\\u6307\\u5411\\u9019\\u88E1\\uFF0C\\u793E\\u7FA4\\u5217\\u88E1\\u9023\\u5230\\u540C\\u4E00\\u500B\\u7DB2\\u57DF\\u7684\\u90A3\\u500B\\u4E5F\\u6703\\u63DB\\u6210\\u624B\\u7E6A\\u7684\\u623F\\u5B50\\u5716\\u793A\\u3002\\u5B83\\u4E0D\\u5B58\\u5728\\u6A94\\u6848\\u88E1 \\u2014 \\u4E0A\\u7DDA\\u6642\\u7531\\u7DB2\\u5740\\u6C7A\\u5B9A\\u3002\",\n mdHeader: \"markdown\\uFF08\\u5C31\\u662F\\u5B58\\u8D77\\u4F86\\u7684\\u90A3\\u4EFD\\uFF09\",\n mdNote: \"\\u6539\\u9019\\u88E1\\u7684\\u6587\\u5B57\\uFF0C\\u5361\\u7247\\u6703\\u8DDF\\u8457\\u8B8A \\u2014 \\u5169\\u500B\\u65B9\\u5411\\u90FD\\u662F\\u540C\\u4E00\\u4EFD\\u6A94\\u6848\\u3002\",\n pubTitle: \"\\u5B58\\u5230\\u7DDA\\u4E0A\",\n pubBackup: \"\\u5148\\u5B58\\u4E00\\u4EFD .md \\u5230\\u4E0B\\u8F09\\u8CC7\\u6599\\u593E\",\n pubNote: \"\\u6309\\u4E0B\\u53BB\\uFF0C\\u7DDA\\u4E0A\\u5C31\\u662F\\u9019\\u500B\\u6A23\\u5B50\\uFF0C\\u6C92\\u6709\\u5225\\u7684\\u6B65\\u9A5F\\u3002\",\n pubCancel: \"\\u53D6\\u6D88\",\n pubSave: \"\\u5B58\"\n },\n ja: {\n menu: \"\\u30E1\\u30CB\\u30E5\\u30FC\",\n undo: \"\\u5143\\u306B\\u623B\\u3059\",\n openFile: \".md \\u3092\\u958B\\u304F\",\n download: \".md \\u3092\\u30C0\\u30A6\\u30F3\\u30ED\\u30FC\\u30C9\",\n addCellHeader: \"\\u30BF\\u30A4\\u30EB\\u3092\\u8FFD\\u52A0\",\n faceTabLabel: \"\\u30AB\\u30FC\\u30C9\\u306E\\u8868\\u9762\",\n addDrawerButton: \"\\uFF0B\\u5F15\\u304D\\u51FA\\u3057\",\n addDrawerTitleAttr: \"\\u5F15\\u304D\\u51FA\\u3057\\u3092\\u8FFD\\u52A0\",\n addDrawerPrompt: \"\\u5F15\\u304D\\u51FA\\u3057\\u306E\\u30BF\\u30A4\\u30C8\\u30EB\\uFF08\\u8A2A\\u554F\\u8005\\u306B\\u8868\\u793A\\u3055\\u308C\\u307E\\u3059\\uFF09\",\n addDrawerPromptDefault: \"\\u3082\\u3063\\u3068\\u898B\\u308B\",\n addDrawerDuplicateAlert: \"\\u540C\\u3058\\u540D\\u524D\\u306E\\u5F15\\u304D\\u51FA\\u3057\\u304C\\u3059\\u3067\\u306B\\u3042\\u308A\\u307E\\u3059\\u3002\",\n modalCloseAria: \"\\u9589\\u3058\\u308B\",\n modalRawSummary: \"\\u8A73\\u7D30\\u8A2D\\u5B9A\",\n modalRawNote: \"\\u30D5\\u30A9\\u30FC\\u30E0\\u304C\\u5909\\u3048\\u308B\\u306E\\u306F\\u3001\\u308F\\u304B\\u308B\\u90E8\\u5206\\u3060\\u3051\\u3067\\u3059 \\u2014 \\u305D\\u308C\\u4EE5\\u5916\\u306F\\u305D\\u306E\\u307E\\u307E\\u6B8B\\u308A\\u307E\\u3059\\u3002\",\n modalDelete: \"\\u3053\\u306E\\u30BF\\u30A4\\u30EB\\u3092\\u524A\\u9664\",\n modalSave: \"\\u5B8C\\u4E86\",\n deleteConfirm: \"\\u3053\\u306E\\u30BF\\u30A4\\u30EB\\u3092\\u524A\\u9664\\u3057\\u307E\\u3059\\u304B\\uFF1F\",\n selectDefaultOption: \"\\uFF08\\u30C7\\u30D5\\u30A9\\u30EB\\u30C8\\uFF09\",\n unknownCellHint: \"\\u3053\\u306E\\u7A2E\\u985E\\u306E\\u30BF\\u30A4\\u30EB\\u306B\\u306F\\u307E\\u3060\\u30D5\\u30A9\\u30FC\\u30E0\\u304C\\u3042\\u308A\\u307E\\u305B\\u3093\\u3002\\u5143\\u306E\\u8A2D\\u5B9A\\u3060\\u3051\\u5909\\u66F4\\u3067\\u304D\\u307E\\u3059\\u3002\",\n unknownCellBodyLabel: \"\\u5185\\u5BB9\",\n canvasTitle: \"\\u30AB\\u30FC\\u30C9\",\n assetPick: \"\\u753B\\u50CF\\u3092\\u9078\\u3076\",\n assetReplace: \"\\u5DEE\\u3057\\u66FF\\u3048\\u308B\",\n assetUrl: \"\\u307E\\u305F\\u306F\\u753B\\u50CF\\u306E URL \\u3092\\u8CBC\\u308A\\u4ED8\\u3051\",\n assetTooBig: \"\\u3053\\u306E\\u753B\\u50CF\\u306F\\u5927\\u304D\\u3059\\u304E\\u307E\\u3059\\u3002\\u5225\\u306E\\u3082\\u306E\\u306B\\u3059\\u308B\\u304B\\u3001\\u5C0F\\u3055\\u304F\\u3057\\u3066\\u304A\\u8A66\\u3057\\u304F\\u3060\\u3055\\u3044\\u3002\",\n assetUnreadable: \"\\u3053\\u306E\\u30D5\\u30A1\\u30A4\\u30EB\\u306F\\u8AAD\\u307F\\u8FBC\\u3081\\u307E\\u305B\\u3093\\u3067\\u3057\\u305F\\u3002\\u5225\\u306E\\u3082\\u306E\\u3067\\u304A\\u8A66\\u3057\\u304F\\u3060\\u3055\\u3044\\u3002\",\n targetUrl: \"URL\",\n targetDrawer: \"\\u5F15\\u304D\\u51FA\\u3057\\uFF1A\",\n needAddress: \"URL \\u304C\\u5FC5\\u8981\\u3067\\u3059\",\n needValue: \"\\u3053\\u306E\\u6B04\\u306F\\u5FC5\\u9808\\u3067\\u3059\",\n linkUnset: \"URL \\u304C\\u307E\\u3060\\u3067\\u3059\",\n imgUnset: \"\\uFF0B \\u753B\\u50CF\\u3092\\u8FFD\\u52A0\",\n moveUp: \"\\u4E0A\\u3078\",\n moveDown: \"\\u4E0B\\u3078\",\n mdMode: \"Markdown\",\n mdBack: \"\\u30AB\\u30FC\\u30C9\\u306B\\u623B\\u308B\",\n mdBroken: \"{n} \\u884C\\u76EE\\u304C\\u30AB\\u30FC\\u30C9\\u306E\\u4E00\\u90E8\\u3068\\u3057\\u3066\\u8AAD\\u307F\\u53D6\\u308C\\u307E\\u305B\\u3093\\u3002\\u30AB\\u30FC\\u30C9\\u306F\\u5909\\u66F4\\u3057\\u3066\\u3044\\u307E\\u305B\\u3093\\u3002\",\n cardUrlHeader: \"\\u30AB\\u30FC\\u30C9\\u306E URL\",\n cardUrlNote: \"QR \\u30B3\\u30FC\\u30C9\\u306F\\u3053\\u3053\\u3092\\u6307\\u3057\\u307E\\u3059\\u3002\\u540C\\u3058\\u30C9\\u30E1\\u30A4\\u30F3\\u3078\\u306E\\u30BD\\u30FC\\u30B7\\u30E3\\u30EB\\u30EA\\u30F3\\u30AF\\u306F\\u3001\\u624B\\u63CF\\u304D\\u306E\\u5BB6\\u306E\\u30A2\\u30A4\\u30B3\\u30F3\\u306B\\u5909\\u308F\\u308A\\u307E\\u3059\\u3002\\u30D5\\u30A1\\u30A4\\u30EB\\u306B\\u306F\\u4FDD\\u5B58\\u3055\\u308C\\u307E\\u305B\\u3093 \\u2014 \\u516C\\u958B\\u3059\\u308B\\u3068\\u304D\\u306B URL \\u304C\\u6C7A\\u3081\\u307E\\u3059\\u3002\",\n mdHeader: \"markdown\\uFF08\\u4FDD\\u5B58\\u3055\\u308C\\u308B\\u306E\\u306F\\u3053\\u308C\\u3067\\u3059\\uFF09\",\n mdNote: \"\\u3053\\u3053\\u306E\\u6587\\u5B57\\u3092\\u5909\\u3048\\u308B\\u3068\\u30AB\\u30FC\\u30C9\\u3082\\u5909\\u308F\\u308A\\u307E\\u3059 \\u2014 \\u3069\\u3061\\u3089\\u306E\\u5411\\u304D\\u3082\\u540C\\u3058\\u4E00\\u3064\\u306E\\u30D5\\u30A1\\u30A4\\u30EB\\u3067\\u3059\\u3002\",\n pubTitle: \"\\u30AA\\u30F3\\u30E9\\u30A4\\u30F3\\u306B\\u4FDD\\u5B58\",\n pubBackup: \"\\u5148\\u306B .md \\u3092\\u30C0\\u30A6\\u30F3\\u30ED\\u30FC\\u30C9\\u30D5\\u30A9\\u30EB\\u30C0\\u3078\\u4FDD\\u5B58\\u3059\\u308B\",\n pubNote: \"\\u62BC\\u3057\\u305F\\u6642\\u70B9\\u3067\\u3001\\u305D\\u308C\\u304C\\u30AA\\u30F3\\u30E9\\u30A4\\u30F3\\u306E\\u59FF\\u3067\\u3059\\u3002\\u307B\\u304B\\u306E\\u624B\\u9806\\u306F\\u3042\\u308A\\u307E\\u305B\\u3093\\u3002\",\n pubCancel: \"\\u30AD\\u30E3\\u30F3\\u30BB\\u30EB\",\n pubSave: \"\\u4FDD\\u5B58\"\n },\n ko: {\n menu: \"\\uBA54\\uB274\",\n undo: \"\\uC2E4\\uD589 \\uCDE8\\uC18C\",\n openFile: \".md \\uC5F4\\uAE30\",\n download: \".md \\uB2E4\\uC6B4\\uB85C\\uB4DC\",\n addCellHeader: \"\\uD0C0\\uC77C \\uCD94\\uAC00\",\n faceTabLabel: \"\\uCE74\\uB4DC \\uC55E\\uBA74\",\n addDrawerButton: \"\\uFF0B\\uC11C\\uB78D\",\n addDrawerTitleAttr: \"\\uC11C\\uB78D \\uCD94\\uAC00\",\n addDrawerPrompt: \"\\uC11C\\uB78D \\uC81C\\uBAA9\\uFF08\\uBC29\\uBB38\\uC790\\uC5D0\\uAC8C \\uBCF4\\uC5EC\\uC694\\uFF09\",\n addDrawerPromptDefault: \"\\uB354 \\uBCF4\\uAE30\",\n addDrawerDuplicateAlert: \"\\uAC19\\uC740 \\uC774\\uB984\\uC758 \\uC11C\\uB78D\\uC774 \\uC774\\uBBF8 \\uC788\\uC5B4\\uC694.\",\n modalCloseAria: \"\\uB2EB\\uAE30\",\n modalRawSummary: \"\\uACE0\\uAE09\",\n modalRawNote: \"\\uC774 \\uC591\\uC2DD\\uC740 \\uC544\\uB294 \\uBD80\\uBD84\\uB9CC \\uBC14\\uAFD4\\uC694 \\u2014 \\uB098\\uBA38\\uC9C0\\uB294 \\uADF8\\uB300\\uB85C \\uB0A8\\uC544\\uC694.\",\n modalDelete: \"\\uC774 \\uD0C0\\uC77C \\uC0AD\\uC81C\",\n modalSave: \"\\uC644\\uB8CC\",\n deleteConfirm: \"\\uC774 \\uD0C0\\uC77C\\uC744 \\uC0AD\\uC81C\\uD560\\uAE4C\\uC694?\",\n selectDefaultOption: \"\\uFF08\\uAE30\\uBCF8\\uAC12\\uFF09\",\n unknownCellHint: \"\\uC774 \\uC885\\uB958\\uC758 \\uD0C0\\uC77C\\uC740 \\uC544\\uC9C1 \\uC591\\uC2DD\\uC774 \\uC5C6\\uC5B4\\uC694. \\uC6D0\\uB798 \\uC124\\uC815\\uB9CC \\uBC14\\uAFC0 \\uC218 \\uC788\\uC5B4\\uC694.\",\n unknownCellBodyLabel: \"\\uB0B4\\uC6A9\",\n canvasTitle: \"\\uCE74\\uB4DC\",\n assetPick: \"\\uC0AC\\uC9C4 \\uC120\\uD0DD\",\n assetReplace: \"\\uBC14\\uAFB8\\uAE30\",\n assetUrl: \"\\uB610\\uB294 \\uC0AC\\uC9C4 \\uC8FC\\uC18C \\uBD99\\uC5EC\\uB123\\uAE30\",\n assetTooBig: \"\\uC774 \\uC0AC\\uC9C4\\uC740 \\uB108\\uBB34 \\uCEE4\\uC694. \\uB2E4\\uB978 \\uC0AC\\uC9C4\\uC744 \\uACE0\\uB974\\uAC70\\uB098 \\uD06C\\uAE30\\uB97C \\uC904\\uC5EC \\uC8FC\\uC138\\uC694.\",\n assetUnreadable: \"\\uC774 \\uD30C\\uC77C\\uC744 \\uC77D\\uC744 \\uC218 \\uC5C6\\uC5B4\\uC694. \\uB2E4\\uB978 \\uD30C\\uC77C\\uB85C \\uD574 \\uBCF4\\uC138\\uC694.\",\n targetUrl: \"\\uC8FC\\uC18C\",\n targetDrawer: \"\\uC11C\\uB78D: \",\n needAddress: \"\\uC8FC\\uC18C\\uAC00 \\uD544\\uC694\\uD574\\uC694\",\n needValue: \"\\uC774 \\uCE78\\uC740 \\uCC44\\uC6CC \\uC8FC\\uC138\\uC694\",\n linkUnset: \"\\uC8FC\\uC18C\\uAC00 \\uC544\\uC9C1 \\uC5C6\\uC5B4\\uC694\",\n imgUnset: \"\\uFF0B \\uC0AC\\uC9C4 \\uCD94\\uAC00\",\n moveUp: \"\\uC704\\uB85C\",\n moveDown: \"\\uC544\\uB798\\uB85C\",\n mdMode: \"Markdown\",\n mdBack: \"\\uCE74\\uB4DC\\uB85C \\uB3CC\\uC544\\uAC00\\uAE30\",\n mdBroken: \"{n}\\uBC88\\uC9F8 \\uC904\\uC744 \\uCE74\\uB4DC\\uC758 \\uC77C\\uBD80\\uB85C \\uC77D\\uC744 \\uC218 \\uC5C6\\uC5B4\\uC694. \\uC544\\uBB34\\uAC83\\uB3C4 \\uBC14\\uB00C\\uC9C0 \\uC54A\\uC558\\uC5B4\\uC694.\",\n cardUrlHeader: \"\\uCE74\\uB4DC \\uC8FC\\uC18C\",\n cardUrlNote: \"QR \\uCF54\\uB4DC\\uB294 \\uC5EC\\uAE30\\uB97C \\uAC00\\uB9AC\\uCF1C\\uC694. \\uAC19\\uC740 \\uB3C4\\uBA54\\uC778\\uC73C\\uB85C \\uAC00\\uB294 \\uC18C\\uC15C \\uB9C1\\uD06C\\uB294 \\uC190\\uC73C\\uB85C \\uADF8\\uB9B0 \\uC9D1 \\uC544\\uC774\\uCF58\\uC73C\\uB85C \\uBC14\\uB00C\\uACE0\\uC694. \\uD30C\\uC77C\\uC5D0\\uB294 \\uC800\\uC7A5\\uB418\\uC9C0 \\uC54A\\uC544\\uC694 \\u2014 \\uACF5\\uAC1C\\uD560 \\uB54C \\uC8FC\\uC18C\\uAC00 \\uC815\\uD574\\uC694.\",\n mdHeader: \"markdown\\uFF08\\uC800\\uC7A5\\uB418\\uB294 \\uAC74 \\uC774\\uAC70\\uC608\\uC694\\uFF09\",\n mdNote: \"\\uC5EC\\uAE30 \\uAE00\\uC744 \\uACE0\\uCE58\\uBA74 \\uCE74\\uB4DC\\uB3C4 \\uB530\\uB77C \\uBC14\\uB00C\\uC5B4\\uC694 \\u2014 \\uC5B4\\uB290 \\uCABD\\uC774\\uB4E0 \\uAC19\\uC740 \\uD30C\\uC77C \\uD558\\uB098\\uC608\\uC694.\",\n pubTitle: \"\\uC628\\uB77C\\uC778\\uC5D0 \\uC800\\uC7A5\",\n pubBackup: \"\\uBA3C\\uC800 .md \\uC0AC\\uBCF8\\uC744 \\uB2E4\\uC6B4\\uB85C\\uB4DC \\uD3F4\\uB354\\uC5D0 \\uC800\\uC7A5\",\n pubNote: \"\\uB204\\uB974\\uB294 \\uC21C\\uAC04 \\uADF8\\uAC8C \\uC628\\uB77C\\uC778\\uC758 \\uBAA8\\uC2B5\\uC774\\uC5D0\\uC694. \\uB2E4\\uB978 \\uB2E8\\uACC4\\uB294 \\uC5C6\\uC5B4\\uC694.\",\n pubCancel: \"\\uCDE8\\uC18C\",\n pubSave: \"\\uC800\\uC7A5\"\n },\n \"zh-Hans\": {\n menu: \"\\u83DC\\u5355\",\n undo: \"\\u64A4\\u9500\",\n openFile: \"\\u6253\\u5F00 .md\",\n download: \"\\u4E0B\\u8F7D .md\",\n addCellHeader: \"\\u6DFB\\u52A0\\u4E00\\u5F20\\u724C\",\n faceTabLabel: \"\\u5361\\u7247\\u6B63\\u9762\",\n addDrawerButton: \"\\uFF0B\\u62BD\\u5C49\",\n addDrawerTitleAttr: \"\\u65B0\\u589E\\u62BD\\u5C49\",\n addDrawerPrompt: \"\\u62BD\\u5C49\\u7684\\u6807\\u9898\\uFF08\\u8BBF\\u5BA2\\u4F1A\\u770B\\u5230\\uFF09\",\n addDrawerPromptDefault: \"\\u66F4\\u591A\",\n addDrawerDuplicateAlert: \"\\u5DF2\\u7ECF\\u6709\\u540C\\u540D\\u7684\\u62BD\\u5C49\\u4E86\\u3002\",\n modalCloseAria: \"\\u5173\\u95ED\",\n modalRawSummary: \"\\u9AD8\\u7EA7\",\n modalRawNote: \"\\u8868\\u5355\\u53EA\\u6539\\u5B83\\u8BA4\\u5F97\\u7684\\u90E8\\u5206\\uFF0C\\u8FD9\\u5F20\\u724C\\u4E0A\\u7684\\u5176\\u4ED6\\u4E1C\\u897F\\u539F\\u6837\\u4FDD\\u7559\\u3002\",\n modalDelete: \"\\u5220\\u9664\\u8FD9\\u5F20\\u724C\",\n modalSave: \"\\u5B8C\\u6210\",\n deleteConfirm: \"\\u5220\\u9664\\u8FD9\\u5F20\\u724C\\uFF1F\",\n selectDefaultOption: \"\\uFF08\\u9ED8\\u8BA4\\uFF09\",\n unknownCellHint: \"\\u8FD9\\u79CD\\u724C\\u8FD8\\u6CA1\\u6709\\u8868\\u5355\\uFF0C\\u53EA\\u80FD\\u6539\\u5B83\\u539F\\u672C\\u7684\\u8BBE\\u7F6E\\u3002\",\n unknownCellBodyLabel: \"\\u5185\\u5BB9\",\n canvasTitle: \"\\u5361\\u7247\",\n assetPick: \"\\u9009\\u4E00\\u5F20\\u56FE\\u7247\",\n assetReplace: \"\\u6362\\u4E00\\u5F20\",\n assetUrl: \"\\u6216\\u7C98\\u8D34\\u56FE\\u7247\\u7F51\\u5740\",\n assetTooBig: \"\\u8FD9\\u5F20\\u592A\\u5927\\u4E86\\uFF0C\\u6362\\u4E00\\u5F20\\u6216\\u7F29\\u5C0F\\u518D\\u8BD5\\u3002\",\n assetUnreadable: \"\\u8BFB\\u4E0D\\u5230\\u8FD9\\u4E2A\\u6587\\u4EF6\\uFF0C\\u6362\\u4E00\\u5F20\\u8BD5\\u8BD5\\u3002\",\n targetUrl: \"\\u7F51\\u5740\",\n targetDrawer: \"\\u62BD\\u5C49\\uFF1A\",\n needAddress: \"\\u8981\\u6709\\u7F51\\u5740\",\n needValue: \"\\u8FD9\\u4E00\\u680F\\u8981\\u586B\",\n linkUnset: \"\\u8FD8\\u6CA1\\u586B\\u7F51\\u5740\",\n imgUnset: \"\\uFF0B \\u52A0\\u4E00\\u5F20\\u56FE\\u7247\",\n moveUp: \"\\u5F80\\u4E0A\\u79FB\",\n moveDown: \"\\u5F80\\u4E0B\\u79FB\",\n mdMode: \"Markdown\",\n mdBack: \"\\u56DE\\u5230\\u5361\\u7247\",\n mdBroken: \"\\u7B2C {n} \\u884C\\u770B\\u4E0D\\u61C2\\uFF0C\\u5361\\u7247\\u6CA1\\u6709\\u6539\\u3002\",\n cardUrlHeader: \"\\u5361\\u7247\\u7F51\\u5740\",\n cardUrlNote: \"QR \\u7801\\u6307\\u5411\\u8FD9\\u91CC\\uFF0C\\u793E\\u7FA4\\u5217\\u91CC\\u8FDE\\u5230\\u540C\\u4E00\\u4E2A\\u7F51\\u57DF\\u7684\\u90A3\\u4E2A\\u4E5F\\u4F1A\\u6362\\u6210\\u624B\\u7ED8\\u7684\\u623F\\u5B50\\u56FE\\u793A\\u3002\\u5B83\\u4E0D\\u5B58\\u5728\\u6587\\u4EF6\\u91CC \\u2014 \\u4E0A\\u7EBF\\u65F6\\u7531\\u7F51\\u5740\\u51B3\\u5B9A\\u3002\",\n mdHeader: \"markdown\\uFF08\\u5C31\\u662F\\u4FDD\\u5B58\\u8D77\\u6765\\u7684\\u90A3\\u4EFD\\uFF09\",\n mdNote: \"\\u6539\\u8FD9\\u91CC\\u7684\\u6587\\u5B57\\uFF0C\\u5361\\u7247\\u4F1A\\u8DDF\\u7740\\u53D8 \\u2014 \\u4E24\\u4E2A\\u65B9\\u5411\\u90FD\\u662F\\u540C\\u4E00\\u4EFD\\u6587\\u4EF6\\u3002\",\n pubTitle: \"\\u4FDD\\u5B58\\u5230\\u7EBF\\u4E0A\",\n pubBackup: \"\\u5148\\u4FDD\\u5B58\\u4E00\\u4EFD .md \\u5230\\u4E0B\\u8F7D\\u6587\\u4EF6\\u5939\",\n pubNote: \"\\u6309\\u4E0B\\u53BB\\uFF0C\\u7EBF\\u4E0A\\u5C31\\u662F\\u8FD9\\u4E2A\\u6837\\u5B50\\uFF0C\\u6CA1\\u6709\\u522B\\u7684\\u6B65\\u9AA4\\u3002\",\n pubCancel: \"\\u53D6\\u6D88\",\n pubSave: \"\\u4FDD\\u5B58\"\n },\n de: {\n menu: \"Men\\xFC\",\n undo: \"R\\xFCckg\\xE4ngig\",\n openFile: \".md \\xF6ffnen\",\n download: \".md herunterladen\",\n addCellHeader: \"Kachel hinzuf\\xFCgen\",\n faceTabLabel: \"Kartenvorderseite\",\n addDrawerButton: \"+ Schublade\",\n addDrawerTitleAttr: \"Schublade hinzuf\\xFCgen\",\n addDrawerPrompt: \"Titel der Schublade (f\\xFCr Besucher sichtbar)\",\n addDrawerPromptDefault: \"Mehr\",\n addDrawerDuplicateAlert: \"Es gibt bereits eine Schublade mit diesem Namen.\",\n modalCloseAria: \"Schlie\\xDFen\",\n modalRawSummary: \"Erweitert\",\n modalRawNote: \"Das Formular \\xE4ndert nur, was es kennt \\u2014 alles andere bleibt unver\\xE4ndert.\",\n modalDelete: \"Diese Kachel l\\xF6schen\",\n modalSave: \"Fertig\",\n deleteConfirm: \"Diese Kachel l\\xF6schen?\",\n selectDefaultOption: \"(Standard)\",\n unknownCellHint: \"F\\xFCr diese Art von Kachel gibt es noch kein Formular \\u2014 Sie k\\xF6nnen nur die urspr\\xFCnglichen Einstellungen \\xE4ndern.\",\n unknownCellBodyLabel: \"Inhalt\",\n canvasTitle: \"Karte\",\n assetPick: \"Bild ausw\\xE4hlen\",\n assetReplace: \"Ersetzen\",\n assetUrl: \"Oder Bildadresse einf\\xFCgen\",\n assetTooBig: \"Dieses Bild ist zu gro\\xDF \\u2014 nehmen Sie ein anderes oder verkleinern Sie es.\",\n assetUnreadable: \"Diese Datei lie\\xDF sich nicht lesen \\u2014 versuchen Sie eine andere.\",\n targetUrl: \"Eine Adresse\",\n targetDrawer: \"Schublade: \",\n needAddress: \"Braucht eine Adresse\",\n needValue: \"Bitte ausf\\xFCllen\",\n linkUnset: \"Noch keine Adresse\",\n imgUnset: \"+ Bild hinzuf\\xFCgen\",\n moveUp: \"Nach oben\",\n moveDown: \"Nach unten\",\n mdMode: \"Markdown\",\n mdBack: \"Zur\\xFCck zur Karte\",\n mdBroken: \"Zeile {n} l\\xE4sst sich nicht als Teil einer Karte lesen \\u2014 es wurde nichts ge\\xE4ndert.\",\n cardUrlHeader: \"Kartenadresse\",\n cardUrlNote: \"Der QR-Code zeigt hierher, und ein Social-Link auf dieselbe Domain wird zum handgezeichneten Haus-Symbol. Es steht nicht in der Datei \\u2014 die Adresse entscheidet das, wenn die Karte online geht.\",\n mdHeader: \"markdown (genau das, was gespeichert wird)\",\n mdNote: \"\\xC4ndern Sie hier den Text, und die Karte folgt \\u2014 beide Richtungen sind dieselbe eine Datei.\",\n pubTitle: \"Online speichern\",\n pubBackup: \"Vorher eine .md-Kopie im Download-Ordner sichern\",\n pubNote: \"Ein Druck darauf, und genau so steht sie online \\u2014 ohne weiteren Schritt.\",\n pubCancel: \"Abbrechen\",\n pubSave: \"Speichern\"\n },\n fr: {\n menu: \"Menu\",\n undo: \"Annuler\",\n openFile: \"Ouvrir un .md\",\n download: \"T\\xE9l\\xE9charger le .md\",\n addCellHeader: \"Ajouter une tuile\",\n faceTabLabel: \"Recto de la carte\",\n addDrawerButton: \"+ Tiroir\",\n addDrawerTitleAttr: \"Ajouter un tiroir\",\n addDrawerPrompt: \"Titre du tiroir (visible par les visiteurs)\",\n addDrawerPromptDefault: \"Plus\",\n addDrawerDuplicateAlert: \"Il existe d\\xE9j\\xE0 un tiroir portant ce nom.\",\n modalCloseAria: \"Fermer\",\n modalRawSummary: \"Avanc\\xE9\",\n modalRawNote: \"Le formulaire ne change que ce qu\\u2019il conna\\xEEt \\u2014 le reste ne bouge pas.\",\n modalDelete: \"Supprimer cette tuile\",\n modalSave: \"Termin\\xE9\",\n deleteConfirm: \"Supprimer cette tuile ?\",\n selectDefaultOption: \"(par d\\xE9faut)\",\n unknownCellHint: \"Ce type de tuile n\\u2019a pas encore de formulaire \\u2014 vous ne pouvez modifier que ses r\\xE9glages d\\u2019origine.\",\n unknownCellBodyLabel: \"Contenu\",\n canvasTitle: \"Carte\",\n assetPick: \"Choisir une image\",\n assetReplace: \"Remplacer\",\n assetUrl: \"Ou coller l\\u2019adresse d\\u2019une image\",\n assetTooBig: \"Cette image est trop lourde \\u2014 essayez-en une autre ou r\\xE9duisez-la.\",\n assetUnreadable: \"Ce fichier n\\u2019a pas pu \\xEAtre lu \\u2014 essayez-en un autre.\",\n targetUrl: \"Une adresse\",\n targetDrawer: \"Tiroir : \",\n needAddress: \"Il faut une adresse\",\n needValue: \"\\xC0 remplir\",\n linkUnset: \"Pas encore d\\u2019adresse\",\n imgUnset: \"+ Ajouter une image\",\n moveUp: \"Monter\",\n moveDown: \"Descendre\",\n mdMode: \"Markdown\",\n mdBack: \"Retour \\xE0 la carte\",\n mdBroken: \"La ligne {n} ne se lit pas comme une partie de carte \\u2014 rien n\\u2019a \\xE9t\\xE9 modifi\\xE9.\",\n cardUrlHeader: \"Adresse de la carte\",\n cardUrlNote: \"Le QR code pointe ici, et un lien social vers ce m\\xEAme domaine devient l\\u2019ic\\xF4ne de maison dessin\\xE9e \\xE0 la main. Ce n\\u2019est pas conserv\\xE9 dans le fichier \\u2014 c\\u2019est l\\u2019adresse qui le d\\xE9cide \\xE0 la mise en ligne.\",\n mdHeader: \"markdown (exactement ce qui est enregistr\\xE9)\",\n mdNote: \"Modifiez le texte ici et la carte suit \\u2014 les deux sens sont un seul et m\\xEAme fichier.\",\n pubTitle: \"Enregistrer en ligne\",\n pubBackup: \"Enregistrer d\\u2019abord une copie .md dans le dossier des t\\xE9l\\xE9chargements\",\n pubNote: \"Appuyez, et c\\u2019est ce qui est en ligne \\u2014 sans autre \\xE9tape.\",\n pubCancel: \"Annuler\",\n pubSave: \"Enregistrer\"\n },\n es: {\n menu: \"Men\\xFA\",\n undo: \"Deshacer\",\n openFile: \"Abrir un .md\",\n download: \"Descargar .md\",\n addCellHeader: \"A\\xF1adir una ficha\",\n faceTabLabel: \"Frente de la tarjeta\",\n addDrawerButton: \"+ Caj\\xF3n\",\n addDrawerTitleAttr: \"A\\xF1adir un caj\\xF3n\",\n addDrawerPrompt: \"T\\xEDtulo del caj\\xF3n (lo ver\\xE1n los visitantes)\",\n addDrawerPromptDefault: \"M\\xE1s\",\n addDrawerDuplicateAlert: \"Ya existe un caj\\xF3n con ese nombre.\",\n modalCloseAria: \"Cerrar\",\n modalRawSummary: \"Avanzado\",\n modalRawNote: \"El formulario solo cambia lo que conoce \\u2014 todo lo dem\\xE1s se conserva tal cual.\",\n modalDelete: \"Eliminar esta ficha\",\n modalSave: \"Listo\",\n deleteConfirm: \"\\xBFEliminar esta ficha?\",\n selectDefaultOption: \"(predeterminado)\",\n unknownCellHint: \"Este tipo de ficha todav\\xEDa no tiene formulario: solo puedes cambiar sus ajustes originales.\",\n unknownCellBodyLabel: \"Contenido\",\n canvasTitle: \"Tarjeta\",\n assetPick: \"Elegir una imagen\",\n assetReplace: \"Cambiarla\",\n assetUrl: \"O pega la direcci\\xF3n de una imagen\",\n assetTooBig: \"Esta imagen es demasiado grande: prueba con otra o red\\xFAcela.\",\n assetUnreadable: \"No se pudo leer ese archivo: prueba con otro.\",\n targetUrl: \"Una direcci\\xF3n\",\n targetDrawer: \"Caj\\xF3n: \",\n needAddress: \"Hace falta una direcci\\xF3n\",\n needValue: \"Rellena este campo\",\n linkUnset: \"Todav\\xEDa sin direcci\\xF3n\",\n imgUnset: \"+ A\\xF1adir una imagen\",\n moveUp: \"Subir\",\n moveDown: \"Bajar\",\n mdMode: \"Markdown\",\n mdBack: \"Volver a la tarjeta\",\n mdBroken: \"La l\\xEDnea {n} no se lee como parte de una tarjeta: no se ha cambiado nada.\",\n cardUrlHeader: \"Direcci\\xF3n de la tarjeta\",\n cardUrlNote: \"El c\\xF3digo QR apunta aqu\\xED, y un enlace social a este mismo dominio se convierte en el icono de casa dibujado a mano. No se guarda en el archivo: lo decide la direcci\\xF3n cuando la tarjeta sale en l\\xEDnea.\",\n mdHeader: \"markdown (exactamente lo que se guarda)\",\n mdNote: \"Cambia el texto aqu\\xED y la tarjeta lo sigue: en ambos sentidos es el mismo archivo.\",\n pubTitle: \"Guardar en l\\xEDnea\",\n pubBackup: \"Guardar antes una copia .md en la carpeta de descargas\",\n pubNote: \"Al pulsarlo, as\\xED queda en l\\xEDnea: no hay ning\\xFAn paso m\\xE1s.\",\n pubCancel: \"Cancelar\",\n pubSave: \"Guardar\"\n },\n pt: {\n menu: \"Menu\",\n undo: \"Desfazer\",\n openFile: \"Abrir .md\",\n download: \"Baixar .md\",\n addCellHeader: \"Adicionar uma pe\\xE7a\",\n faceTabLabel: \"Frente do cart\\xE3o\",\n addDrawerButton: \"+ Gaveta\",\n addDrawerTitleAttr: \"Adicionar gaveta\",\n addDrawerPrompt: \"T\\xEDtulo da gaveta (os visitantes ver\\xE3o isso)\",\n addDrawerPromptDefault: \"Mais\",\n addDrawerDuplicateAlert: \"J\\xE1 existe uma gaveta com esse nome.\",\n modalCloseAria: \"Fechar\",\n modalRawSummary: \"Avan\\xE7ado\",\n modalRawNote: \"O formul\\xE1rio s\\xF3 altera o que conhece \\u2014 o resto permanece exatamente como est\\xE1.\",\n modalDelete: \"Excluir esta pe\\xE7a\",\n modalSave: \"Conclu\\xEDdo\",\n deleteConfirm: \"Excluir esta pe\\xE7a?\",\n selectDefaultOption: \"(padr\\xE3o)\",\n unknownCellHint: \"Esse tipo de pe\\xE7a ainda n\\xE3o tem formul\\xE1rio \\u2014 voc\\xEA s\\xF3 pode alterar os ajustes originais.\",\n unknownCellBodyLabel: \"Conte\\xFAdo\",\n canvasTitle: \"Cart\\xE3o\",\n assetPick: \"Escolher uma imagem\",\n assetReplace: \"Trocar\",\n assetUrl: \"Ou cole o endere\\xE7o de uma imagem\",\n assetTooBig: \"Esta imagem \\xE9 grande demais \\u2014 escolha outra ou reduza o tamanho.\",\n assetUnreadable: \"N\\xE3o deu para ler esse arquivo \\u2014 tente outro.\",\n targetUrl: \"Um endere\\xE7o\",\n targetDrawer: \"Gaveta: \",\n needAddress: \"Precisa de um endere\\xE7o\",\n needValue: \"Preencha este campo\",\n linkUnset: \"Ainda sem endere\\xE7o\",\n imgUnset: \"+ Adicionar uma imagem\",\n moveUp: \"Subir\",\n moveDown: \"Descer\",\n mdMode: \"Markdown\",\n mdBack: \"Voltar ao cart\\xE3o\",\n mdBroken: \"A linha {n} n\\xE3o se l\\xEA como parte de um cart\\xE3o \\u2014 nada foi alterado.\",\n cardUrlHeader: \"Endere\\xE7o do cart\\xE3o\",\n cardUrlNote: \"O QR code aponta para c\\xE1, e um link social para este mesmo dom\\xEDnio vira o \\xEDcone de casa desenhado \\xE0 m\\xE3o. N\\xE3o fica guardado no arquivo \\u2014 o endere\\xE7o decide isso quando o cart\\xE3o entra no ar.\",\n mdHeader: \"markdown (exatamente o que \\xE9 salvo)\",\n mdNote: \"Altere o texto aqui e o cart\\xE3o acompanha \\u2014 os dois sentidos s\\xE3o o mesmo arquivo.\",\n pubTitle: \"Salvar on-line\",\n pubBackup: \"Salvar antes uma c\\xF3pia .md na pasta de downloads\",\n pubNote: \"Ao apertar, \\xE9 assim que ele fica on-line \\u2014 sem nenhuma outra etapa.\",\n pubCancel: \"Cancelar\",\n pubSave: \"Salvar\"\n }\n };\n var LOCALE_KEYS = Object.keys(SANDBOX_LOCALES);\n function resolveLocaleKey(tag) {\n const t = String(tag || \"\").trim().toLowerCase();\n if (!t) return null;\n const subtags = t.split(/[-_]/);\n const primary = subtags[0];\n if (primary === \"zh\") {\n return subtags.includes(\"hans\") || subtags.includes(\"cn\") || subtags.includes(\"sg\") ? \"zh-Hans\" : \"zh\";\n }\n return SANDBOX_LOCALES[primary] ? primary : null;\n }\n function primaryLocale(tag) {\n return resolveLocaleKey(tag) || \"en\";\n }\n function chromeStrings(localeKey) {\n return CHROME_STRINGS[localeKey] || CHROME_STRINGS.en;\n }\n function buildSandboxCard(localeKey) {\n const p = SANDBOX_LOCALES[primaryLocale(localeKey)];\n return [\n \"---\",\n \"card-page: try\",\n `title: ${p.name}`,\n `lang: ${p.lang}`,\n \"---\",\n \"\",\n \"## cards\",\n \"\",\n `- [ ] %% card: profile w=6 %% ${p.bio}`,\n `- [ ] %% card: link w=3 %% [${p.linkLabel}](https://example.com)`\n ].join(\"\\n\") + \"\\n\";\n }\n\n // packages/cardtile/w2/board-i18n.mjs\n var BOARD_STRINGS_BY_KEY = {\n // the one-row icon picker that opens from it\n \"board.pickTitle\": {\n zh: \"\\u8981\\u52A0\\u54EA\\u4E00\\u7A2E\\uFF1F\",\n ja: \"\\u3069\\u306E\\u7A2E\\u985E\\u306B\\u3057\\u307E\\u3059\\u304B\\uFF1F\",\n en: \"Which kind?\",\n ko: \"\\uC5B4\\uB5A4 \\uC885\\uB958\\uC778\\uAC00\\uC694?\",\n \"zh-Hans\": \"\\u8981\\u52A0\\u54EA\\u4E00\\u79CD\\uFF1F\",\n de: \"Welche Art?\",\n fr: \"Quel type ?\",\n es: \"\\xBFDe qu\\xE9 tipo?\",\n pt: \"De que tipo?\"\n },\n // โ”€โ”€ the phone's two tabs, along the bottom edge โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n //\n // ๐Ÿฉธ They replace ONE button that said ใ€Œ็œ‹ๆˆๅ“ใ€ and then ใ€Œๅ›žๆกŒไธŠใ€ โ€” ใ€ŒๆกŒใ€ is tugtile's metaphor\n // and the cold read caught it leaking into a link-page product in all three languages (ใ€Œๆœบใซๆˆปใ‚‹ใ€,\n // \"Back to the table\", ยง2.10). A toggle also has to be READ before you know where you are; two\n // tabs show it. Short by design: this is a 44px bar at the bottom of a 390px screen.\n \"board.tabEdit\": {\n zh: \"\\u7DE8\\u8F2F\",\n ja: \"\\u7DE8\\u96C6\",\n en: \"Edit\",\n ko: \"\\uD3B8\\uC9D1\",\n \"zh-Hans\": \"\\u7F16\\u8F91\",\n de: \"Bearbeiten\",\n fr: \"Modifier\",\n es: \"Editar\",\n pt: \"Editar\"\n },\n \"board.tabCard\": {\n zh: \"\\u6210\\u54C1\",\n ja: \"\\u4ED5\\u4E0A\\u304C\\u308A\",\n en: \"The card\",\n ko: \"\\uC644\\uC131\\uBCF8\",\n \"zh-Hans\": \"\\u6210\\u54C1\",\n de: \"Die Karte\",\n fr: \"La carte\",\n es: \"La tarjeta\",\n pt: \"O cart\\xE3o\"\n },\n // โ”€โ”€ the drawer: the preview's chip into it, and the board's words for which tile opens it โ”€โ”€โ”€โ”€โ”€โ”€\n //\n // ๐Ÿ”ด The drawer keeps the word ใ€ŒๆŠฝๅฑœใ€ (CARD-VOCAB-ADDENDUM-2026-09-06). The THING you touch is a\n // ็‰Œ; the LANE is a lane; the second-level card is a ๆŠฝๅฑœ. Three words, three meanings, no overlap.\n \"board.opensDrawer\": {\n zh: \"\\u727D\\u5230\\u62BD\\u5C5C\",\n ja: \"\\u5F15\\u304D\\u51FA\\u3057\\u3078\\u3064\\u306A\\u304C\\u308B\",\n en: \"Opens a drawer\",\n ko: \"\\uC11C\\uB78D\\uC73C\\uB85C \\uC774\\uC5B4\\uC838\\uC694\",\n \"zh-Hans\": \"\\u7275\\u5230\\u62BD\\u5C49\",\n de: \"\\xD6ffnet eine Schublade\",\n fr: \"Ouvre un tiroir\",\n es: \"Abre un caj\\xF3n\",\n pt: \"Abre uma gaveta\"\n },\n // โ”€โ”€ the drawer relation, said in WORDS (owner ruling 2026-09-24: \"a view answers one question\";\n // a one-to-one relation does not need a line, a word does). The dashed ็‰ฝ็ทš lines are gone; the\n // tile says where it goes and the drawer lane says who opens it. `{title}` / `{by}` are slots.\n \"board.opensLabel\": {\n zh: \"\\u2192 \\u958B\\u555F\\uFF1A{title}\",\n ja: \"\\u2192 \\u958B\\u304F\\uFF1A{title}\",\n en: \"\\u2192 Opens: {title}\",\n ko: \"\\u2192 \\uC5F4\\uAE30: {title}\",\n \"zh-Hans\": \"\\u2192 \\u6253\\u5F00\\uFF1A{title}\",\n de: \"\\u2192 \\xD6ffnet: {title}\",\n fr: \"\\u2192 Ouvre : {title}\",\n es: \"\\u2192 Abre: {title}\",\n pt: \"\\u2192 Abre: {title}\"\n },\n \"board.drawerOpenedBy\": {\n zh: \"{title}\\uFF08\\u7531\\u3008{by}\\u3009\\u6253\\u958B\\uFF09\",\n ja: \"{title}\\uFF08\\u3008{by}\\u3009\\u304B\\u3089\\u958B\\u304F\\uFF09\",\n en: \"{title} (opened by \\u201C{by}\\u201D)\",\n ko: \"{title} (\\u3008{by}\\u3009\\uC5D0\\uC11C \\uC5F4\\uB9BC)\",\n \"zh-Hans\": \"{title}\\uFF08\\u7531\\u3008{by}\\u3009\\u6253\\u5F00\\uFF09\",\n de: \"{title} (ge\\xF6ffnet von \\u201E{by}\\u201C)\",\n fr: \"{title} (ouvert par \\xAB {by} \\xBB)\",\n es: \"{title} (lo abre \\xAB{by}\\xBB)\",\n pt: \"{title} (aberto por \\u201C{by}\\u201D)\"\n },\n // a link that names a drawer nobody made. Reported, not hidden โ€” see board-bridge's drawerLinks.\n \"board.danglingDrawer\": {\n zh: \"\\u9019\\u5F35\\u724C\\u6307\\u5411\\u4E00\\u500B\\u4E0D\\u5B58\\u5728\\u7684\\u62BD\\u5C5C\",\n ja: \"\\u3053\\u306E\\u30AB\\u30FC\\u30C9\\u306F\\u5B58\\u5728\\u3057\\u306A\\u3044\\u5F15\\u304D\\u51FA\\u3057\\u3092\\u6307\\u3057\\u3066\\u3044\\u307E\\u3059\",\n en: \"This tile points at a drawer that does not exist\",\n ko: \"\\uC774 \\uCE74\\uB4DC\\uB294 \\uC5C6\\uB294 \\uC11C\\uB78D\\uC744 \\uAC00\\uB9AC\\uCF1C\\uC694\",\n \"zh-Hans\": \"\\u8FD9\\u5F20\\u724C\\u6307\\u5411\\u4E00\\u4E2A\\u4E0D\\u5B58\\u5728\\u7684\\u62BD\\u5C49\",\n de: \"Diese Kachel zeigt auf eine Schublade, die es nicht gibt\",\n fr: \"Cette tuile pointe vers un tiroir qui n\\u2019existe pas\",\n es: \"Esta ficha apunta a un caj\\xF3n que no existe\",\n pt: \"Esta pe\\xE7a aponta para uma gaveta que n\\xE3o existe\"\n },\n // โ”€โ”€ HOST MODE: the save status beside ใ€Œๅญ˜ใ€ (feelreef embeds the editor on a real card) โ”€โ”€โ”€โ”€โ”€โ”€\n // The Save button itself reuses the chrome's `pubSave`, already in all nine.\n // shown until the parent page hands the card over (host-bridge re-announces card:ready meanwhile)\n \"board.hostWaiting\": {\n zh: \"\\u7B49\\u5F85\\u9801\\u9762\\u56DE\\u61C9\\u2026\",\n ja: \"\\u30DA\\u30FC\\u30B8\\u306E\\u5FDC\\u7B54\\u3092\\u5F85\\u3063\\u3066\\u3044\\u307E\\u3059\\u2026\",\n en: \"Waiting for the page\\u2026\",\n ko: \"\\uD398\\uC774\\uC9C0\\uB97C \\uAE30\\uB2E4\\uB9AC\\uB294 \\uC911\\u2026\",\n \"zh-Hans\": \"\\u7B49\\u5F85\\u9875\\u9762\\u54CD\\u5E94\\u2026\",\n de: \"Warte auf die Seite\\u2026\",\n fr: \"En attente de la page\\u2026\",\n es: \"Esperando a la p\\xE1gina\\u2026\",\n pt: \"Aguardando a p\\xE1gina\\u2026\"\n },\n \"board.hostUnsaved\": {\n zh: \"\\u5C1A\\u672A\\u5132\\u5B58\",\n ja: \"\\u672A\\u4FDD\\u5B58\",\n en: \"Unsaved\",\n ko: \"\\uC800\\uC7A5 \\uC548 \\uB428\",\n \"zh-Hans\": \"\\u5C1A\\u672A\\u4FDD\\u5B58\",\n de: \"Nicht gespeichert\",\n fr: \"Non enregistr\\xE9\",\n es: \"Sin guardar\",\n pt: \"N\\xE3o salvo\"\n },\n \"board.hostSaving\": {\n zh: \"\\u5132\\u5B58\\u4E2D\\u2026\",\n ja: \"\\u4FDD\\u5B58\\u4E2D\\u2026\",\n en: \"Saving\\u2026\",\n ko: \"\\uC800\\uC7A5 \\uC911\\u2026\",\n \"zh-Hans\": \"\\u4FDD\\u5B58\\u4E2D\\u2026\",\n de: \"Wird gespeichert\\u2026\",\n fr: \"Enregistrement\\u2026\",\n es: \"Guardando\\u2026\",\n pt: \"Salvando\\u2026\"\n },\n \"board.hostSaved\": {\n zh: \"\\u5DF2\\u5132\\u5B58\",\n ja: \"\\u4FDD\\u5B58\\u3057\\u307E\\u3057\\u305F\",\n en: \"Saved\",\n ko: \"\\uC800\\uC7A5\\uB428\",\n \"zh-Hans\": \"\\u5DF2\\u4FDD\\u5B58\",\n de: \"Gespeichert\",\n fr: \"Enregistr\\xE9\",\n es: \"Guardado\",\n pt: \"Salvo\"\n },\n \"board.hostFailed\": {\n zh: \"\\u5132\\u5B58\\u5931\\u6557\",\n ja: \"\\u4FDD\\u5B58\\u3067\\u304D\\u307E\\u305B\\u3093\\u3067\\u3057\\u305F\",\n en: \"Not saved\",\n ko: \"\\uC800\\uC7A5 \\uC2E4\\uD328\",\n \"zh-Hans\": \"\\u4FDD\\u5B58\\u5931\\u8D25\",\n de: \"Nicht gespeichert \\u2013 Fehler\",\n fr: \"\\xC9chec de l\\u2019enregistrement\",\n es: \"No se pudo guardar\",\n pt: \"Falha ao salvar\"\n },\n // โ”€โ”€ the editor bar's view switch: ๅก ยท board ยท Markdown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n // The board word follows the engine host's own (ja ใ€Œใƒœใƒผใƒ‰ใ€, ko ใ€Œ๋ณด๋“œใ€ in its viewSwitchAction).\n \"board.viewBoard\": {\n zh: \"\\u724C\\u684C\",\n ja: \"\\u30DC\\u30FC\\u30C9\",\n en: \"Board\",\n ko: \"\\uBCF4\\uB4DC\",\n \"zh-Hans\": \"\\u724C\\u684C\",\n de: \"Board\",\n fr: \"Tableau\",\n es: \"Tablero\",\n pt: \"Quadro\"\n },\n // plain \"Markdown\": the segment is a view name; chrome `mdMode` (\"Markdown ๆจกๅผ\") is the sheet title\n \"board.viewMd\": {\n zh: \"Markdown\",\n ja: \"Markdown\",\n en: \"Markdown\",\n ko: \"Markdown\",\n \"zh-Hans\": \"Markdown\",\n de: \"Markdown\",\n fr: \"Markdown\",\n es: \"Markdown\",\n pt: \"Markdown\"\n },\n // the finished card beside the table (โ‰ฅ1024) โ€” its accessible name. Ruler #3 (2026-09-24): no\n // \"preview\" to a person; it is the card, as it will be seen.\n \"board.previewTitle\": {\n zh: \"\\u5B8C\\u6210\\u7684\\u5361\\u7247\",\n ja: \"\\u4ED5\\u4E0A\\u304C\\u3063\\u305F\\u30AB\\u30FC\\u30C9\",\n en: \"The finished card\",\n ko: \"\\uC644\\uC131\\uB41C \\uCE74\\uB4DC\",\n \"zh-Hans\": \"\\u5B8C\\u6210\\u7684\\u5361\\u7247\",\n de: \"Die fertige Karte\",\n fr: \"La carte finie\",\n es: \"La tarjeta terminada\",\n pt: \"O cart\\xE3o pronto\"\n },\n // the editing table itself โ€” the iframe's accessible name\n \"board.tableTitle\": {\n zh: \"\\u7DE8\\u8F2F\\u684C\",\n ja: \"\\u7DE8\\u96C6\\u30C6\\u30FC\\u30D6\\u30EB\",\n en: \"Editing table\",\n ko: \"\\uD3B8\\uC9D1 \\uD14C\\uC774\\uBE14\",\n \"zh-Hans\": \"\\u7F16\\u8F91\\u684C\",\n de: \"Bearbeitungstisch\",\n fr: \"Table d\\u2019\\xE9dition\",\n es: \"Mesa de edici\\xF3n\",\n pt: \"Mesa de edi\\xE7\\xE3o\"\n },\n // โ”€โ”€ the three questions (owner rule 2: every screen says what it is, what you can do, what\n // happens next). One line under each view's heading, and the picker's destination. `{front}` /\n // `{back}` are the chrome's own faceTabLabel / mdBack, so the sentence names what the screen shows.\n \"board.tableHint\": {\n zh: \"\\u6574\\u5F35\\u5361\\u7684\\u6BCF\\u4E00\\u5F35\\u724C\\u90FD\\u5728\\u9019\\u88E1\\u3002\\u62D6\\u52D5\\u53EF\\u4EE5\\u63DB\\u9806\\u5E8F\\uFF0C\\u9EDE\\u4E00\\u4E0B\\u53EF\\u4EE5\\u4FEE\\u6539\\u3002\\u300C{front}\\u300D\\u662F\\u8A2A\\u5BA2\\u5148\\u770B\\u5230\\u7684\\uFF1B\\u5176\\u4ED6\\u6B04\\u662F\\u62BD\\u5C5C\\uFF0C\\u7531\\u67D0\\u5F35\\u724C\\u6253\\u958B\\u3002\",\n ja: \"\\u30AB\\u30FC\\u30C9\\u306E\\u30BF\\u30A4\\u30EB\\u304C\\u3059\\u3079\\u3066\\u3053\\u3053\\u306B\\u4E26\\u3073\\u307E\\u3059\\u3002\\u30C9\\u30E9\\u30C3\\u30B0\\u3067\\u4E26\\u3079\\u66FF\\u3048\\u3001\\u30BF\\u30C3\\u30D7\\u3067\\u7DE8\\u96C6\\u3002\\u300C{front}\\u300D\\u306F\\u6700\\u521D\\u306B\\u898B\\u3048\\u308B\\u9762\\u3001\\u307B\\u304B\\u306E\\u5217\\u306F\\u30BF\\u30A4\\u30EB\\u304B\\u3089\\u958B\\u304F\\u5F15\\u304D\\u51FA\\u3057\\u3067\\u3059\\u3002\",\n en: \"Every tile on your card, in order. Drag to reorder; tap one to change it. \\u201C{front}\\u201D is what visitors see first; the other lanes are drawers a tile opens.\",\n ko: \"\\uCE74\\uB4DC\\uC758 \\uBAA8\\uB4E0 \\uD0C0\\uC77C\\uC774 \\uC5EC\\uAE30 \\uC788\\uC5B4\\uC694. \\uB04C\\uC5B4\\uC11C \\uC21C\\uC11C\\uB97C \\uBC14\\uAFB8\\uACE0, \\uB20C\\uB7EC\\uC11C \\uACE0\\uCCD0\\uC694. \\u300C{front}\\u300D\\uC740 \\uBC29\\uBB38\\uC790\\uAC00 \\uBA3C\\uC800 \\uBCF4\\uB294 \\uACF3, \\uB2E4\\uB978 \\uC904\\uC740 \\uD0C0\\uC77C\\uC774 \\uC5EC\\uB294 \\uC11C\\uB78D\\uC774\\uC5D0\\uC694.\",\n \"zh-Hans\": \"\\u6574\\u5F20\\u5361\\u7684\\u6BCF\\u4E00\\u5F20\\u724C\\u90FD\\u5728\\u8FD9\\u91CC\\u3002\\u62D6\\u52A8\\u53EF\\u4EE5\\u6362\\u987A\\u5E8F\\uFF0C\\u70B9\\u4E00\\u4E0B\\u53EF\\u4EE5\\u4FEE\\u6539\\u3002\\u300C{front}\\u300D\\u662F\\u8BBF\\u5BA2\\u5148\\u770B\\u5230\\u7684\\uFF1B\\u5176\\u4ED6\\u680F\\u662F\\u62BD\\u5C49\\uFF0C\\u7531\\u67D0\\u5F20\\u724C\\u6253\\u5F00\\u3002\",\n de: \"Alle Kacheln Ihrer Karte, der Reihe nach. Ziehen zum Umsortieren, antippen zum \\xC4ndern. \\u201E{front}\\u201C sehen Besucher zuerst; die anderen Spalten sind Schubladen, die eine Kachel \\xF6ffnet.\",\n fr: \"Toutes les tuiles de votre carte, dans l\\u2019ordre. Glissez pour r\\xE9ordonner, touchez pour modifier. \\xAB {front} \\xBB est ce que les visiteurs voient d\\u2019abord ; les autres colonnes sont des tiroirs qu\\u2019une tuile ouvre.\",\n es: \"Todas las fichas de tu tarjeta, en orden. Arrastra para reordenar; toca una para cambiarla. \\xAB{front}\\xBB es lo que se ve primero; las dem\\xE1s columnas son cajones que abre una ficha.\",\n pt: \"Todas as pe\\xE7as do seu cart\\xE3o, em ordem. Arraste para reordenar; toque em uma para mud\\xE1-la. \\u201C{front}\\u201D \\xE9 o que as visitas veem primeiro; as outras colunas s\\xE3o gavetas que uma pe\\xE7a abre.\"\n },\n \"board.mdLead\": {\n zh: \"\\u9019\\u662F\\u4F60\\u6574\\u5F35\\u5361\\u7247\\u5BEB\\u6210\\u6587\\u5B57\\u7684\\u6A23\\u5B50\\u3002\\u5728\\u9019\\u88E1\\u6539\\u5B57\\uFF0C\\u5361\\u7247\\u6703\\u8DDF\\u8457\\u8B8A\\uFF1B\\u6309\\u300C{back}\\u300D\\u6642\\u6703\\u5148\\u6AA2\\u67E5\\u4E00\\u904D\\uFF0C\\u770B\\u4E0D\\u61C2\\u7684\\u5730\\u65B9\\u4E0D\\u6703\\u5957\\u7528\\u3002\",\n ja: \"\\u30AB\\u30FC\\u30C9\\u5168\\u4F53\\u3092\\u6587\\u5B57\\u3067\\u66F8\\u3044\\u305F\\u3082\\u306E\\u3067\\u3059\\u3002\\u3053\\u3053\\u3067\\u66F8\\u304D\\u63DB\\u3048\\u308B\\u3068\\u30AB\\u30FC\\u30C9\\u3082\\u5909\\u308F\\u308A\\u307E\\u3059\\u3002\\u300C{back}\\u300D\\u3092\\u62BC\\u3059\\u3068\\u5148\\u306B\\u78BA\\u8A8D\\u3057\\u3001\\u8AAD\\u307F\\u53D6\\u308C\\u306A\\u3044\\u90E8\\u5206\\u306F\\u53CD\\u6620\\u3057\\u307E\\u305B\\u3093\\u3002\",\n en: \"This is your whole card, written out as text. Change it here and the card follows; \\u201C{back}\\u201D checks it first and changes nothing it can\\u2019t read.\",\n ko: \"\\uCE74\\uB4DC \\uC804\\uCCB4\\uB97C \\uAE00\\uB85C \\uC801\\uC5B4 \\uB454 \\uAC70\\uC608\\uC694. \\uC5EC\\uAE30\\uC11C \\uACE0\\uCE58\\uBA74 \\uCE74\\uB4DC\\uB3C4 \\uB530\\uB77C \\uBC14\\uB00C\\uC5B4\\uC694. \\u300C{back}\\u300D\\uB97C \\uB204\\uB974\\uBA74 \\uBA3C\\uC800 \\uD655\\uC778\\uD558\\uACE0, \\uC77D\\uC744 \\uC218 \\uC5C6\\uB294 \\uBD80\\uBD84\\uC740 \\uC801\\uC6A9\\uD558\\uC9C0 \\uC54A\\uC544\\uC694.\",\n \"zh-Hans\": \"\\u8FD9\\u662F\\u4F60\\u6574\\u5F20\\u5361\\u7247\\u5199\\u6210\\u6587\\u5B57\\u7684\\u6837\\u5B50\\u3002\\u5728\\u8FD9\\u91CC\\u6539\\u5B57\\uFF0C\\u5361\\u7247\\u4F1A\\u8DDF\\u7740\\u53D8\\uFF1B\\u6309\\u300C{back}\\u300D\\u65F6\\u4F1A\\u5148\\u68C0\\u67E5\\u4E00\\u904D\\uFF0C\\u770B\\u4E0D\\u61C2\\u7684\\u5730\\u65B9\\u4E0D\\u4F1A\\u5E94\\u7528\\u3002\",\n de: \"Das ist Ihre ganze Karte als Text. \\xC4ndern Sie ihn hier, folgt die Karte; \\u201E{back}\\u201C pr\\xFCft zuerst und \\xE4ndert nichts, was es nicht lesen kann.\",\n fr: \"Voici toute votre carte, \\xE9crite en texte. Modifiez-la ici et la carte suit ; \\xAB {back} \\xBB v\\xE9rifie d\\u2019abord et ne change rien qu\\u2019il ne sait pas lire.\",\n es: \"Esta es toda tu tarjeta escrita como texto. C\\xE1mbiala aqu\\xED y la tarjeta la sigue; \\xAB{back}\\xBB lo revisa antes y no cambia nada que no entienda.\",\n pt: \"Este \\xE9 o seu cart\\xE3o inteiro, escrito como texto. Mude aqui e o cart\\xE3o acompanha; \\u201C{back}\\u201D confere antes e n\\xE3o muda nada que n\\xE3o consiga ler.\"\n },\n // under ใ€Œ่ฆๅŠ ๅ“ชไธ€็จฎ๏ผŸใ€: where the new tile lands, so tapping one is not a leap\n \"board.pickWhere\": {\n zh: \"\\u9EDE\\u4E00\\u7A2E\\uFF0C\\u5B83\\u6703\\u52A0\\u5728\\u300C{lane}\\u300D\\u7684\\u6700\\u5F8C\\u9762\\u3002\",\n ja: \"\\u9078\\u3076\\u3068\\u300C{lane}\\u300D\\u306E\\u3044\\u3061\\u3070\\u3093\\u6700\\u5F8C\\u306B\\u8FFD\\u52A0\\u3055\\u308C\\u307E\\u3059\\u3002\",\n en: \"Tap one and it goes at the end of \\u201C{lane}\\u201D.\",\n ko: \"\\uD558\\uB098\\uB97C \\uB204\\uB974\\uBA74 \\u300C{lane}\\u300D \\uB9E8 \\uB05D\\uC5D0 \\uCD94\\uAC00\\uB3FC\\uC694.\",\n \"zh-Hans\": \"\\u70B9\\u4E00\\u79CD\\uFF0C\\u5B83\\u4F1A\\u52A0\\u5728\\u300C{lane}\\u300D\\u7684\\u6700\\u540E\\u9762\\u3002\",\n de: \"Tippen Sie eine an \\u2013 sie kommt ans Ende von \\u201E{lane}\\u201C.\",\n fr: \"Touchez-en une : elle s\\u2019ajoute \\xE0 la fin de \\xAB {lane} \\xBB.\",\n es: \"Toca una y se a\\xF1ade al final de \\xAB{lane}\\xBB.\",\n pt: \"Toque em uma e ela entra no fim de \\u201C{lane}\\u201D.\"\n }\n };\n var BOARD_KEYS = Object.keys(BOARD_STRINGS_BY_KEY);\n var CJK_LOCALES = /* @__PURE__ */ new Set([\"zh\", \"zh-Hans\", \"ja\", \"ko\"]);\n var colon = (localeKey) => CJK_LOCALES.has(localeKey) ? \"\\uFF1A\" : \": \";\n var addTileLabel = (localeKey, chrome) => {\n const words = chrome && chrome.addCellHeader || \"Add a tile\";\n return CJK_LOCALES.has(localeKey) ? `\\uFF0B${words}` : `+ ${words}`;\n };\n function boardStrings(localeKey) {\n const out = {};\n for (const [key, byLocale] of Object.entries(BOARD_STRINGS_BY_KEY)) {\n out[key] = byLocale[localeKey] != null ? byLocale[localeKey] : byLocale.en;\n }\n return out;\n }\n\n // packages/cardtile/w2/host-bridge.mjs\n var HOST_V = 1;\n var CHANGE_DEBOUNCE_MS = 150;\n var READY_EVERY_MS = 500;\n var READY_GIVE_UP_MS = 6e4;\n function validHostOrigin(raw) {\n if (typeof raw !== \"string\" || !raw) return null;\n let u;\n try {\n u = new URL(raw);\n } catch {\n return null;\n }\n if (u.username || u.password || u.pathname !== \"/\" && u.pathname !== \"\" || u.search || u.hash) return null;\n if (raw.replace(/\\/$/, \"\") !== u.origin) return null;\n if (u.origin === \"https://feelreef.com\" || u.origin === \"https://staging.feelreef.com\") return u.origin;\n if (u.protocol === \"http:\" && u.hostname === \"localhost\") return u.origin;\n return null;\n }\n function readsAsCard(md) {\n if (typeof md !== \"string\" || !md) return false;\n try {\n return parseCard(md).cells.length > 0;\n } catch {\n return false;\n }\n }\n function createHostBridge({\n host,\n post,\n onLoad,\n onStatus = () => {\n },\n setTimer = setTimeout,\n clearTimer = clearTimeout,\n debounceMs = CHANGE_DEBOUNCE_MS,\n readyEveryMs = READY_EVERY_MS,\n readyGiveUpMs = READY_GIVE_UP_MS\n }) {\n if (!validHostOrigin(host)) throw new Error(\"host-bridge: host must be an allowed origin\");\n const st = { status: \"idle\", message: \"\", loaded: false, savedMd: null, pendingMd: null, current: null, timer: null, readyTimer: null, readyStarted: 0, gaveUp: false };\n const send = (msg) => post({ v: HOST_V, ...msg }, host);\n const setStatus = (status, message = \"\") => {\n st.status = status;\n st.message = message;\n onStatus(status, message);\n };\n const dirty = () => st.loaded && st.current !== st.savedMd;\n function flushChange() {\n st.timer = null;\n if (st.current == null) return;\n send({ type: \"card:change\", md: st.current, dirty: true });\n }\n return {\n get status() {\n return st.status;\n },\n get message() {\n return st.message;\n },\n get loaded() {\n return st.loaded;\n },\n get dirty() {\n return dirty();\n },\n get gaveUp() {\n return st.gaveUp;\n },\n /**\n * Announce `card:ready`, and KEEP announcing every readyEveryMs until the first card:load.\n * ๐Ÿฉธ A one-shot ready was lost in production: the parent (a SvelteKit page) attaches its message\n * listener after hydration, and with a large inline payload the iframe was ready first โ€” the\n * announcement went to nobody, no load was ever sent, and the editor sat there. The message is\n * identical every time, so the parent may answer any one of them (a second load just reloads).\n */\n ready() {\n if (st.loaded) return;\n st.readyStarted = 0;\n st.gaveUp = false;\n setStatus(\"waiting\");\n const tick = (elapsed) => {\n st.readyTimer = null;\n if (st.loaded) return;\n send({ type: \"card:ready\" });\n if (elapsed + readyEveryMs >= readyGiveUpMs) {\n st.gaveUp = true;\n setStatus(\"waiting\");\n return;\n }\n st.readyTimer = setTimer(() => tick(elapsed + readyEveryMs), readyEveryMs);\n };\n tick(0);\n },\n /** a MessageEvent (or anything shaped like one). Returns true when it was ours and handled. */\n receive(event) {\n if (!event || event.origin !== host) return false;\n const d = event.data;\n if (!d || typeof d !== \"object\" || d.v !== HOST_V || typeof d.type !== \"string\") return false;\n if (d.type === \"card:load\") {\n if (!readsAsCard(d.md)) {\n send({ type: \"card:load-failed\", message: typeof d.md === \"string\" ? \"That does not read as a card.\" : \"card:load needs `md` as a string.\" });\n return true;\n }\n if (st.timer) {\n clearTimer(st.timer);\n st.timer = null;\n }\n if (st.readyTimer) {\n clearTimer(st.readyTimer);\n st.readyTimer = null;\n }\n st.loaded = true;\n st.current = d.md;\n st.savedMd = d.md;\n onLoad({ md: d.md, handle: typeof d.handle === \"string\" ? d.handle : \"\", cardUrl: typeof d.cardUrl === \"string\" ? d.cardUrl : \"\", locale: d.locale, version: d.version });\n setStatus(\"idle\");\n return true;\n }\n if (d.type === \"card:saved\" || d.type === \"card:save-failed\") {\n if (st.status !== \"saving\") return true;\n if (d.type === \"card:saved\") {\n st.savedMd = st.pendingMd;\n st.pendingMd = null;\n setStatus(dirty() ? \"unsaved\" : \"saved\");\n } else {\n st.pendingMd = null;\n setStatus(\"failed\", typeof d.message === \"string\" ? d.message : \"\");\n }\n return true;\n }\n return false;\n },\n /** every committed change from the editor. Ignored until the parent has loaded a card. */\n changed(md) {\n if (!st.loaded || md === st.current) return;\n st.current = md;\n if (st.status !== \"saving\") setStatus(dirty() ? \"unsaved\" : \"saved\");\n if (st.timer) clearTimer(st.timer);\n st.timer = setTimer(flushChange, debounceMs);\n },\n /** Save pressed (or Cmd/Ctrl+S). Returns false when there is nothing to do. */\n save() {\n if (!st.loaded || st.status === \"saving\") return false;\n if (st.timer) {\n clearTimer(st.timer);\n flushChange();\n }\n st.pendingMd = st.current;\n setStatus(\"saving\");\n send({ type: \"card:save\", md: st.current });\n return true;\n }\n };\n }\n\n // packages/cardtile/serve/card-worker.mjs\n var QR_PATH = `/_coral/qr-${QR_VERSION}.js`;\n var DRAWER_PATH = `/_coral/drawer-${DRAWER_VERSION}.js`;\n function renderCardHTML(cardMd, ctx = {}) {\n const model = parseCard(cardMd);\n const name = /^title:\\s*(.+)$/m.exec(cardMd)?.[1]?.trim() || ctx.handle || \"\";\n const unquote = (v) => /^(['\"]).*\\1$/.test(v) ? v.slice(1, -1).trim() : v;\n const fm = (k) => unquote(new RegExp(\"^\" + k + \":\\\\s*(.+)$\", \"m\").exec(cardMd)?.[1]?.trim() || \"\");\n return renderPage(model, {\n name,\n handle: ctx.handle || \"\",\n // โ†’ the footer's feelreef.com/signup?ref=\n icons: ICONS,\n assets: model.assets,\n // the card's own images, resolved to data: URIs at render\n css: CSS,\n arrow: ARROW,\n // @cvernet/signet's own arrow, lifted at build time โ€” never re-drawn\n // `arrows: on` puts the little arrow back on link rows and CTAs. DEFAULT OFF โ€” see arrowOf.\n arrows: fm(\"arrows\"),\n cardUrl: ctx.cardUrl || \"\",\n since: fm(\"since\"),\n accent: fm(\"accent\"),\n theme: fm(\"theme\"),\n // `theme: light|dark` pins it; absent follows the reader\n poweredBy: fm(\"powered-by\") === \"on\",\n // opt-in referral credit; default off (see card-render)\n radius: fm(\"radius\"),\n // `radius: square` keeps a creator's square-corner look\n // `header: left` puts the avatar beside the text instead of above it. Centred is the DEFAULT\n // because 10/10 surveyed platforms centre it โ€” but that evidence justifies a default, not the\n // removal of the alternative, and one live card wants the arrangement it already had.\n header: fm(\"header\"),\n // `icons: mono` greys every FETCHED brand mark on the card โ€” the social row and the link rows.\n // Colour is the default, and iOS is the precedent rather than a counter-example: its icon\n // appearance is Default / Dark / Clear / Tinted, so it ships colour and offers monochrome. Same\n // shape as every other switch here โ€” a surveyed default the author can overrule.\n // ๐Ÿ”ด Only fetched marks. Our own drawn glyphs already follow the theme; greying them would be\n // desaturating a colour we chose on purpose.\n iconStyle: fm(\"icons\"),\n // `lang: zh-TW` โ€” the document's language. Defaults to `en` in the renderer.\n lang: fm(\"lang\"),\n // `generator: off` opts out; any other value replaces it. Default ON โ€” see card-render.\n generator: fm(\"generator\"),\n backdrop: fm(\"backdrop\"),\n // a fixed page-่ƒŒๆ™ฏ image behind the card\n // ๐Ÿ”ด raw, NOT Number(). resolveBackdropDim() is the single place that decides what a bad value\n // means, and it must be able to tell \"the author wrote 48%\" from \"the author wrote nothing\" โ€”\n // coercing here would hand it a NaN with the evidence already thrown away.\n backdropDim: fm(\"backdrop-dim\"),\n // channel id โ†’ { id, title } for \"my latest video\", resolved by the fetch handler BEFORE this\n // ran. Absent is the normal case for a card with no video, and the honest case when YouTube\n // could not be reached โ€” the block falls to its degraded state either way.\n videos: ctx.videos || {},\n // ๐Ÿ”ด CALLER-supplied, not authored โ€” the only ctx field here that does not come from the card.\n // cardtile-w asks for `data-cell` indices so it can map a box on screen back to a cell in the\n // file; the Worker never sets it, so a served card is byte-identical with the flag absent.\n editIndex: !!ctx.editIndex,\n // โ€ฆand its companion: the words on the two EDIT-ONLY placeholders (a link with no address, a\n // picture cell with no picture). Only meaningful with editIndex; the Worker never sets either.\n editStrings: ctx.editStrings || null,\n qrScriptSrc: QR_PATH,\n // external script โ†’ the page consumes the coral, unmodified\n drawerScriptSrc: DRAWER_PATH\n // ditto: the overlay's behaviour, shared with any Site\n });\n }\n\n // packages/cardtile/ai-ops.mjs\n var DISPOSITION = {\n /** has a control in the editor's form, and a named field in the tool contract */\n FORM: \"form\",\n /** real and honoured, but edited through the raw param string only โ€” see each one's reason */\n RAW: \"raw\",\n /** the renderer no longer honours it. Never written; never offered. Left in files that have it. */\n RETIRED: \"retired\"\n };\n var CONTROL = { TEXT: \"text\", AREA: \"area\", URL: \"url\", SWITCH: \"switch\", ASSET: \"asset\", SELECT: \"select\" };\n var SOURCE = { PARAM: \"param\", BODY: \"body\", DERIVED: \"derived\" };\n var WIDTH = {\n disposition: DISPOSITION.FORM,\n label: \"width.label\",\n control: CONTROL.SELECT,\n options: [\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"],\n hint: \"width.hint\",\n // the two widths worth naming in words. Everything else is just its number โ€” see w/cell-i18n.mjs.\n optionLabels: { 6: \"width.6\", 3: \"width.3\" }\n };\n var UNIVERSAL = {\n w: WIDTH,\n bleed: {\n disposition: DISPOSITION.RAW,\n label: \"\\u6574\\u884C\",\n hint: \"\\u5B58\\u5728\\u5C31\\u8B93\\u9019\\u683C\\u8DF3\\u51FA bento\\u3001\\u81EA\\u5DF1\\u4F54\\u6EFF\\u4E00\\u6574\\u884C\\u3002\\u6563\\u6587(text)\\u672C\\u4F86\\u5C31\\u662F\\u9019\\u6A23,\\u4E0D\\u5FC5\\u5BEB\\u3002\"\n },\n // ๐Ÿ”ด There is ONE layout model: height comes from content and the renderer sets `grid-row:auto`.\n // `setSpan` wrote an `h` token as late as 2026-07-29, which made drag-to-resize the one path that\n // could put a dead parameter back into a creator's file โ€” where it serialises cleanly, reads as\n // \"just a span\" in review, and does nothing. An editor is exactly where this comes back to life.\n // Declared RETIRED rather than deleted so the drift test can assert it is never offered.\n h: { disposition: DISPOSITION.RETIRED, label: \"h\", hint: \"\\u5DF2\\u9000\\u5F79\\u3002renderer \\u4E0D\\u518D\\u7406\\u5B83\\u3002\" }\n };\n var CELL_TYPES = {\n profile: {\n title: \"type.profile.title\",\n hint: \"type.profile.hint\",\n body: { label: \"type.profile.body.label\", control: CONTROL.AREA, hint: \"\", rows: 2 },\n params: {\n avatar: { disposition: DISPOSITION.FORM, label: \"type.profile.avatar.label\", control: CONTROL.ASSET, hint: \"\" },\n chips: { disposition: DISPOSITION.FORM, label: \"type.profile.chips.label\", control: CONTROL.TEXT, hint: \"type.profile.chips.hint\" }\n },\n // ๐Ÿ”ด `name` is DERIVED and it is the reason this whole mechanism exists. The name a visitor sees\n // is the FILE's frontmatter `title:`, not anything on this cell โ€” so \"ๆ”นๅๅญ—\" had no field at\n // all, and the old hint told a stranger to go and edit frontmatter they could not see.\n form: [\n { source: SOURCE.DERIVED, name: \"name\", label: \"type.profile.name.label\", control: CONTROL.TEXT, hint: \"\", required: true },\n { source: SOURCE.PARAM, name: \"avatar\" },\n { source: SOURCE.BODY },\n { source: SOURCE.PARAM, name: \"chips\" }\n ]\n },\n link: {\n title: \"type.link.title\",\n hint: \"type.link.hint\",\n body: { label: \"type.link.text.label\", control: CONTROL.TEXT, hint: \"\" },\n params: {\n sub: { disposition: DISPOSITION.FORM, label: \"type.link.sub.label\", control: CONTROL.TEXT, hint: \"\" },\n icon: { disposition: DISPOSITION.FORM, label: \"type.link.icon.label\", control: CONTROL.TEXT, hint: \"type.link.icon.hint\" },\n iconhost: { disposition: DISPOSITION.FORM, label: \"type.link.iconhost.label\", control: CONTROL.URL, hint: \"\" },\n iconimg: { disposition: DISPOSITION.FORM, label: \"type.link.iconimg.label\", control: CONTROL.ASSET, hint: \"\" },\n w: WIDTH\n },\n // text + address are TWO fields and ONE body (`[text](url)`). The editor composes them; nobody\n // is ever shown, or asked to type, the square brackets.\n form: [\n { source: SOURCE.DERIVED, name: \"text\", label: \"type.link.text.label\", control: CONTROL.TEXT, hint: \"\" },\n { source: SOURCE.DERIVED, name: \"target\", label: \"type.link.target.label\", control: CONTROL.SELECT, hint: \"\", drawers: true },\n {\n source: SOURCE.DERIVED,\n name: \"url\",\n label: \"type.link.url.label\",\n control: CONTROL.URL,\n required: true,\n hint: \"type.link.url.hint\",\n showWhen: { target: \"\" }\n },\n { source: SOURCE.PARAM, name: \"sub\" },\n { source: SOURCE.PARAM, name: \"w\" },\n { group: \"type.link.advanced\", rows: [\n { source: SOURCE.PARAM, name: \"icon\" },\n { source: SOURCE.PARAM, name: \"iconhost\" },\n { source: SOURCE.PARAM, name: \"iconimg\" }\n ] }\n ]\n },\n feature: {\n title: \"type.feature.title\",\n hint: \"type.feature.hint\",\n body: { label: \"type.feature.body.label\", control: CONTROL.TEXT, hint: \"\" },\n params: {\n img: { disposition: DISPOSITION.FORM, label: \"type.feature.img.label\", control: CONTROL.ASSET, hint: \"\" },\n href: { disposition: DISPOSITION.FORM, label: \"type.feature.href.label\", control: CONTROL.URL, hint: \"\" },\n alt: { disposition: DISPOSITION.FORM, label: \"type.feature.alt.label\", control: CONTROL.TEXT, hint: \"type.feature.alt.hint\" },\n label: { disposition: DISPOSITION.FORM, label: \"type.feature.label.label\", control: CONTROL.SWITCH, hint: \"\" },\n title: { disposition: DISPOSITION.FORM, label: \"type.feature.title.label\", control: CONTROL.TEXT, hint: \"\" },\n cta: { disposition: DISPOSITION.FORM, label: \"type.feature.cta.label\", control: CONTROL.TEXT, hint: \"type.feature.cta.hint\" },\n w: WIDTH\n },\n form: [\n { source: SOURCE.PARAM, name: \"img\" },\n { source: SOURCE.PARAM, name: \"alt\" },\n { source: SOURCE.PARAM, name: \"label\" },\n { source: SOURCE.DERIVED, name: \"target\", label: \"type.feature.target.label\", control: CONTROL.SELECT, hint: \"\", drawers: true, optional: true },\n { source: SOURCE.PARAM, name: \"href\", showWhen: { target: \"\" } },\n { source: SOURCE.PARAM, name: \"w\" },\n { group: \"type.feature.advanced\", rows: [\n { source: SOURCE.PARAM, name: \"title\" },\n { source: SOURCE.BODY },\n { source: SOURCE.PARAM, name: \"cta\" }\n ] }\n ]\n },\n text: {\n title: \"type.text.title\",\n hint: \"type.text.hint\",\n body: { label: \"type.text.body.label\", control: CONTROL.AREA, hint: \"type.text.body.hint\", rows: 6 },\n params: {},\n form: [{ source: SOURCE.BODY }]\n },\n video: {\n title: \"type.video.title\",\n hint: \"type.video.hint\",\n body: { label: \"type.video.body.label\", control: CONTROL.TEXT, hint: \"type.video.body.hint\" },\n params: {\n yt: { disposition: DISPOSITION.FORM, label: \"type.video.yt.label\", control: CONTROL.TEXT, hint: \"type.video.yt.hint\" },\n channel: { disposition: DISPOSITION.FORM, label: \"type.video.channel.label\", control: CONTROL.TEXT, hint: \"type.video.channel.hint\" },\n poster: { disposition: DISPOSITION.FORM, label: \"type.video.poster.label\", control: CONTROL.ASSET, hint: \"type.video.poster.hint\" },\n // ๐Ÿ”ด The renderer reads this as a FALLBACK behind the body: `b || p.title || hit.title`. Two\n // form fields that both mean \"the label\" and silently outrank each other is a worse editor\n // than one. Honoured, editable in the raw string, not offered twice.\n title: { disposition: DISPOSITION.RAW, label: \"\\u6A19\\u984C(\\u5099\\u63F4)\", hint: \"body \\u6709\\u6771\\u897F\\u6642\\u5B83\\u4E0D\\u6703\\u751F\\u6548 \\u2014 \\u8868\\u55AE\\u7528 body,\\u9019\\u500B\\u7559\\u7D66\\u65E2\\u6709\\u6A94\\u6848\\u3002\" },\n w: WIDTH\n }\n },\n social: {\n title: \"type.social.title\",\n hint: \"type.social.hint\",\n body: { label: \"type.social.body.label\", control: CONTROL.AREA, hint: \"type.social.body.hint\" },\n params: {}\n },\n embed: {\n title: \"type.embed.title\",\n hint: \"type.embed.hint\",\n body: null,\n params: {\n // ๐Ÿ”ด `slider` is the COMMON one and holds the common name. `bubbles` was what `slider` used to\n // mean for everybody โ€” a bespoke pop built for one card (scale(0.001)โ†’scale(1) about a tail\n // origin, 1:1, no controls), so every imported carousel inherited a character's mouth.\n kind: {\n disposition: DISPOSITION.FORM,\n label: \"type.embed.kind.label\",\n control: CONTROL.SELECT,\n options: [\"slider\", \"bubbles\"],\n hint: \"type.embed.kind.hint\"\n },\n images: { disposition: DISPOSITION.FORM, label: \"type.embed.images.label\", control: CONTROL.TEXT, hint: \"type.embed.images.hint\" },\n // Positioning knobs for the slider's focal point. Real, honoured, and the kind of thing an\n // author sets once with a picture in front of them โ€” a form field for a CSS `object-position`\n // pair would be a worse UI than the string itself.\n origin: { disposition: DISPOSITION.RAW, label: \"\\u7126\\u9EDE\", hint: \"\\u9810\\u8A2D `53% 76%`\\u3002\" },\n narrow: { disposition: DISPOSITION.RAW, label: \"\\u7A84\\u87A2\\u5E55\\u7126\\u9EDE\", hint: \"\\u9810\\u8A2D `53% 33%`\\u3002\" },\n bg: { disposition: DISPOSITION.RAW, label: \"\\u80CC\\u666F\\u5716\", hint: \"\\u8F2A\\u64AD\\u5E95\\u4E0B\\u588A\\u7684\\u5716\\u3002\" },\n w: WIDTH\n }\n }\n };\n var PALETTE = [\"profile\", \"link\", \"feature\", \"text\", \"video\", \"social\", \"embed\"];\n var BLANKS = {\n profile: { type: \"profile\", rawParams: \"w=6\", body: \"blank.profile\" },\n // ๐Ÿฉธ was `[ๆ–ฐ็š„้€ฃ็ต](https://example.com)`. Two things wrong with it, and the second is the worse\n // one: the words were Chinese in all nine locales, and `example.com` is a REAL address โ€” so a\n // brand-new tile rendered as a FINISHED-looking button that went somewhere nobody meant. Words\n // and no address renders as the ใ€Œ้‚„ๆฒ’ๅกซ็ถฒๅ€ใ€ outline, which is what it honestly is.\n link: { type: \"link\", rawParams: \"w=6\", body: \"blank.link.text\" },\n feature: { type: \"feature\", rawParams: \"w=3\", body: \"\" },\n text: { type: \"text\", rawParams: \"\", body: \"blank.text\" },\n video: { type: \"video\", rawParams: \"w=6\", body: \"\" },\n // ๐Ÿ”ด NOT a key, and deliberately not translated: this one is a brand name, identical in all nine,\n // and it has to stay a whole `[name](url)` line because that is the grammar a social row reads.\n social: { type: \"social\", rawParams: \"w=6\", body: \"[Instagram](https://instagram.com/)\" },\n embed: { type: \"embed\", rawParams: \"kind=slider w=6\", body: \"\" }\n };\n function blankCell(type, strings = {}) {\n const blank = BLANKS[type];\n if (!blank) return null;\n const body = Object.prototype.hasOwnProperty.call(strings, blank.body) ? strings[blank.body] : blank.body;\n return { ...blank, body };\n }\n var SEAT = { READ: \"CO|CA|CM\", WRITE: \"CO|CA\" };\n var OPS = {\n list_cards: { seat: SEAT.READ, params: [], returns: \"handle, title, live_url, bytes, assets, version\" },\n get_card: {\n seat: SEAT.READ,\n params: [\"handle\", 'assets: \"summary\" | \"inline\"'],\n // ๐Ÿ”ด summary is the DEFAULT and inline must be asked for by name. One real card is 1,159,422\n // bytes of which 3,277 is the card; handing an agent the whole file makes \"read, change, write โ€”\n // one transaction\" cost 2.3MB per edited character. And the two are not one call with a flag:\n // a BACKUP taken over the summary exit is 99.7% missing, and what is missing is the pictures.\n returns: \"the card WITHOUT the assets lane, plus an asset manifest\"\n },\n save_card: {\n // ๐Ÿ”ด WRITE, not READ, and specifically not `save_page`'s grade โ€” which is ungated in reef-MCP\n // today. A Site edit does not go live until someone publishes; a Card has no publish step, so\n // the save IS the deploy. Copying save_page's gate would let a CM โ€” someone explicitly not\n // allowed to publish a Site โ€” push straight onto a creator's own apex domain. The parity spec's\n // Tier-1 floor lists ใ€Œpublish ไบบ้กž้–˜ใ€ as a universal hard gate.\n seat: SEAT.WRITE,\n params: [\"handle\", \"markdown (thin)\", \"base_version\", \"prompt\", \"dry_run\", \"message\"],\n returns: \"structural diff, new version, and ORPHANED assets \\u2014 reported, never collected\"\n },\n put_asset: { seat: SEAT.WRITE, params: [\"handle\", \"id\", \"base64\", \"mime\"], returns: \"id, bytes\" },\n delete_asset: { seat: SEAT.WRITE, params: [\"handle\", \"id\"], returns: \"refuses while still referenced\" },\n preview_card: { seat: SEAT.READ, params: [\"handle\", \"markdown\"], returns: \"a URL. An agent that cannot look at what it did is guessing.\" }\n };\n\n // packages/cardtile/w/cell-form-core.mjs\n var esc2 = (s) => String(s == null ? \"\" : s).replace(/[&<>\"]/g, (c) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\" })[c]);\n var splitLink = (body) => {\n const { label: label2, url } = linkOf(body);\n return { text: url ? label2 : String(body || \"\").trim(), url };\n };\n var DERIVED_READ = {\n \"link.text\": (cell) => splitLink(cell.body).text,\n \"link.url\": (cell) => {\n const u = splitLink(cell.body).url;\n return u.startsWith(\"#\") ? \"\" : u;\n },\n \"link.target\": (cell) => {\n const u = splitLink(cell.body).url;\n return u.startsWith(\"#\") ? u.slice(1) : \"\";\n },\n \"feature.target\": (cell) => {\n const h = String((cell.params || {}).href || \"\");\n return h.startsWith(\"#\") ? h.slice(1) : \"\";\n },\n \"profile.name\": (cell, ctx) => ctx && ctx.cardTitle || \"\"\n };\n function formRows(def) {\n if (def.form) return def.form;\n const rows = def.body ? [{ source: SOURCE.BODY }] : [];\n return rows.concat((def.fields || []).map((f) => ({ source: SOURCE.PARAM, name: f.name })));\n }\n var flatRows = (def) => formRows(def).flatMap((r) => r.group ? r.rows : [r]);\n function resolveRow(row, def, cell, ctx) {\n if (row.source === SOURCE.BODY) {\n const b = def.body || { label: ctx.T.unknownCellBodyLabel, control: CONTROL.AREA, hint: \"\" };\n return { ...b, ...row, name: \"__body\", value: cell.body || \"\" };\n }\n if (row.source === SOURCE.PARAM) {\n const p = def.params[row.name] || {};\n const raw = (cell.params || {})[row.name];\n const value = raw && typeof raw === \"object\" ? `\"${raw.label}\"\\u2192${raw.href}` : raw === true ? \"on\" : raw == null ? \"\" : raw;\n return { ...p, ...row, value: String(value) };\n }\n const read = DERIVED_READ[`${cell.type}.${row.name}`];\n return { ...row, value: read ? String(read(cell, ctx) ?? \"\") : \"\" };\n }\n function controlHtml(f, ctx) {\n const id = `f-${f.name}`;\n const value = f.value == null ? \"\" : String(f.value);\n switch (f.control) {\n case CONTROL.SWITCH: {\n const on = value !== \"\" && !/^(off|false|no)$/i.test(value);\n return ``;\n }\n case CONTROL.SELECT: {\n const opts = f.options ? [\"\", ...f.options].map((o) => ({ value: o, label: o ? ctx.say((f.optionLabels || {})[o] || o) : ctx.T.selectDefaultOption })) : ctx.targetOptions() || [];\n return ``;\n }\n case CONTROL.AREA:\n return ``;\n // ๐Ÿ”ด A REAL ADDRESS FIELD. `type=url` + `inputmode=url` is what puts `/` and `.com` on a phone\n // keyboard and keeps autocapitalise off โ€” the difference between pasting an address and fighting\n // one. It is not validation theatre: the check that matters happens in composeCell.\n case CONTROL.URL:\n return ``;\n // ๐Ÿ”ด THE PICKER THAT DID NOT EXIST. `CONTROL.ASSET` had no case at all, so every picture field\n // fell through to a bare text box and the answer to \"how do I put a photo on this\" was \"paste a\n // sha256 or a URL\". `` is what opens a phone's photo library;\n // the hidden input below is the value the form actually saves, so both routes (a file, a pasted\n // address) write to one place.\n case CONTROL.ASSET: {\n const isAsset = /^asset:/.test(value);\n const preview = value ? ctx.previewSrc(value) : \"\";\n return `
${preview ? `\"\"` : \"\"}
`;\n }\n default:\n return ``;\n }\n }\n function fieldHtml(f, ctx) {\n const hint = f.hint ? `${esc2(ctx.say(f.hint))}` : \"\";\n const cls = `ctw-field ctw-field-${f.control || \"text\"}`;\n const tag = f.control === CONTROL.ASSET ? \"div\" : \"label\";\n return `<${tag} class=\"${cls}\" data-field=\"${esc2(f.name)}\">${esc2(ctx.say(f.label || \"\"))}${controlHtml(f, ctx)}${hint}`;\n }\n function formBodyHtml(def, cell, ctx) {\n return formRows(def).map((row) => {\n if (row.group) {\n const inner = row.rows.map((r) => fieldHtml(resolveRow(r, def, cell, ctx), ctx)).join(\"\");\n return `
${esc2(ctx.say(row.group))}${inner}
`;\n }\n if (row.drawers && !ctx.targetOptions()) return \"\";\n return fieldHtml(resolveRow(row, def, cell, ctx), ctx);\n }).join(\"\");\n }\n var YT_ID = /^[\\w-]{11}$/;\n var YT_CH = /^UC[\\w-]{22}$/;\n function youtubeRef(input) {\n const s = String(input == null ? \"\" : input).trim();\n if (!s || YT_ID.test(s) || YT_CH.test(s)) return s;\n let u;\n try {\n u = new URL(/^[a-z][a-z0-9+.-]*:\\/\\//i.test(s) ? s : `https://${s}`);\n } catch {\n return s;\n }\n const host = u.hostname.toLowerCase().replace(/^(www|m|music)\\./, \"\");\n const seg = u.pathname.split(\"/\").filter(Boolean);\n let got = \"\";\n if (host === \"youtu.be\") got = seg[0] || \"\";\n else if (host === \"youtube.com\" || host === \"youtube-nocookie.com\") {\n if (seg[0] === \"watch\") got = u.searchParams.get(\"v\") || \"\";\n else if ([\"shorts\", \"embed\", \"live\", \"v\"].includes(seg[0])) got = seg[1] || \"\";\n else if (seg[0] === \"channel\") got = seg[1] || \"\";\n }\n return YT_ID.test(got) || YT_CH.test(got) ? got : s;\n }\n function composeCell(cell, def, io, ctx) {\n const rows = def ? flatRows(def) : [{ source: SOURCE.BODY }];\n let rawParams = cell.rawParams || \"\";\n let body = cell.body || \"\";\n const bodyRow = rows.find((r) => r.source === SOURCE.BODY);\n if (bodyRow || !def) {\n const v = io.read(\"__body\");\n if (v != null) body = v;\n }\n if (def && cell.type === \"link\") {\n const target = io.visible(\"target\") ? io.read(\"target\") || \"\" : DERIVED_READ[\"link.target\"](cell, ctx);\n const typed = io.read(\"url\") || \"\";\n const url = target ? `#${target}` : typed;\n if (!url) return { error: { field: \"url\", message: ctx.T.needAddress } };\n const text2 = (io.read(\"text\") || \"\").trim() || (target ? target : hostLabel(url));\n body = `[${text2}](${url})`;\n }\n let rename = null;\n if (def && cell.type === \"profile\") {\n const name = (io.read(\"name\") || \"\").trim();\n if (!name) return { error: { field: \"name\", message: ctx.T.needValue } };\n if (name !== (ctx.cardTitle || \"\")) rename = name;\n }\n for (const row of rows) {\n if (row.source !== SOURCE.PARAM) continue;\n let v = io.read(row.name);\n if (v == null) continue;\n if (cell.type === \"feature\" && row.name === \"href\") {\n const target = io.visible(\"target\") ? io.read(\"target\") || \"\" : DERIVED_READ[\"feature.target\"](cell, ctx);\n if (target) v = `#${target}`;\n }\n if (cell.type === \"video\" && (row.name === \"yt\" || row.name === \"channel\")) v = youtubeRef(v);\n rawParams = v === \"\" ? removeToken(rawParams, row.name) : setToken(rawParams, row.name, quoteParam(v));\n }\n return { rawParams, body, rename };\n }\n\n // packages/cardtile/w/md-guard.mjs\n function whatIsWrong(md) {\n const lines2 = String(md).split(\"\\n\");\n const orphan = lines2.findIndex((l) => /^\\s*-\\s*\\[/.test(l) && !/%%\\s*card\\s*:/.test(l));\n if (orphan >= 0) return { line: orphan + 1 };\n if (!lines2.some((l) => /^##\\s+\\S/.test(l))) return { line: 1 };\n try {\n parseCard(md);\n } catch {\n return { line: 1 };\n }\n return null;\n }\n\n // packages/cardtile/w/cell-i18n.mjs\n var CELL_STRINGS_BY_KEY = {\n // โ”€โ”€ the profile tile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n \"type.profile.title\": {\n zh: \"\\u500B\\u4EBA\\u6A94\\u6848\",\n ja: \"\\u30D7\\u30ED\\u30D5\\u30A3\\u30FC\\u30EB\",\n en: \"Profile\",\n ko: \"\\uD504\\uB85C\\uD544\",\n \"zh-Hans\": \"\\u4E2A\\u4EBA\\u6863\\u6848\",\n de: \"Profil\",\n fr: \"Profil\",\n es: \"Perfil\",\n pt: \"Perfil\"\n },\n \"type.profile.hint\": {\n zh: \"\\u5361\\u7247\\u6700\\u4E0A\\u9762\\uFF1A\\u5927\\u982D\\u8CBC\\u3001\\u540D\\u5B57\\u3001\\u4E00\\u53E5\\u8A71\\u3002\",\n ja: \"\\u30AB\\u30FC\\u30C9\\u306E\\u4E00\\u756A\\u4E0A\\uFF1A\\u5199\\u771F\\u3001\\u540D\\u524D\\u3001\\u3072\\u3068\\u3053\\u3068\\u3002\",\n en: \"The top of the card: photo, name, one line.\",\n ko: \"\\uCE74\\uB4DC \\uB9E8 \\uC704\\uFF1A\\uC0AC\\uC9C4, \\uC774\\uB984, \\uD55C \\uC904.\",\n \"zh-Hans\": \"\\u5361\\u7247\\u6700\\u4E0A\\u9762\\uFF1A\\u5934\\u50CF\\u3001\\u540D\\u5B57\\u3001\\u4E00\\u53E5\\u8BDD\\u3002\",\n de: \"Der Kopf der Karte: Foto, Name, eine Zeile.\",\n fr: \"Le haut de la carte : photo, nom, une ligne.\",\n es: \"La parte de arriba de la tarjeta: foto, nombre, una l\\xEDnea.\",\n pt: \"O topo do cart\\xE3o: foto, nome, uma linha.\"\n },\n \"type.profile.name.label\": {\n zh: \"\\u540D\\u5B57\",\n ja: \"\\u540D\\u524D\",\n en: \"Name\",\n ko: \"\\uC774\\uB984\",\n \"zh-Hans\": \"\\u540D\\u5B57\",\n de: \"Name\",\n fr: \"Nom\",\n es: \"Nombre\",\n pt: \"Nome\"\n },\n \"type.profile.body.label\": {\n zh: \"\\u4E00\\u53E5\\u8A71\\u4ECB\\u7D39\",\n ja: \"\\u3072\\u3068\\u3053\\u3068\",\n en: \"One line about you\",\n ko: \"\\uD55C \\uC904 \\uC18C\\uAC1C\",\n \"zh-Hans\": \"\\u4E00\\u53E5\\u8BDD\\u4ECB\\u7ECD\",\n de: \"Eine Zeile \\xFCber Sie\",\n fr: \"Une ligne sur vous\",\n es: \"Una l\\xEDnea sobre ti\",\n pt: \"Uma linha sobre voc\\xEA\"\n },\n \"type.profile.avatar.label\": {\n zh: \"\\u5927\\u982D\\u8CBC\",\n ja: \"\\u30D7\\u30ED\\u30D5\\u30A3\\u30FC\\u30EB\\u5199\\u771F\",\n en: \"Photo\",\n ko: \"\\uD504\\uB85C\\uD544 \\uC0AC\\uC9C4\",\n \"zh-Hans\": \"\\u5934\\u50CF\",\n de: \"Foto\",\n fr: \"Photo\",\n es: \"Foto\",\n pt: \"Foto\"\n },\n \"type.profile.chips.label\": {\n zh: \"\\u6A19\\u7C64\\uFF08\\u9078\\u586B\\uFF09\",\n ja: \"\\u30BF\\u30B0\\uFF08\\u4EFB\\u610F\\uFF09\",\n en: \"Tags (optional)\",\n ko: \"\\uD0DC\\uADF8\\uFF08\\uC120\\uD0DD\\uFF09\",\n \"zh-Hans\": \"\\u6807\\u7B7E\\uFF08\\u9009\\u586B\\uFF09\",\n de: \"Schlagw\\xF6rter (optional)\",\n fr: \"\\xC9tiquettes (facultatif)\",\n es: \"Etiquetas (opcional)\",\n pt: \"Etiquetas (opcional)\"\n },\n \"type.profile.chips.hint\": {\n zh: \"\\u7528\\u9017\\u865F\\u5206\\u958B\\uFF0C\\u4F8B\\u5982\\u300C\\u63D2\\u756B\\u5BB6, \\u9060\\u7AEF\\u300D\\u3002\",\n ja: \"\\u30AB\\u30F3\\u30DE\\u3067\\u533A\\u5207\\u308A\\u307E\\u3059\\uFF08\\u4F8B\\uFF1A\\u30A4\\u30E9\\u30B9\\u30C8\\u30EC\\u30FC\\u30BF\\u30FC, \\u30EA\\u30E2\\u30FC\\u30C8\\uFF09\\u3002\",\n en: 'Separate with commas, e.g. \"Illustrator, remote\".',\n ko: \"\\uC27C\\uD45C\\uB85C \\uAD6C\\uBD84\\uD574 \\uC8FC\\uC138\\uC694\\uFF08\\uC608\\uFF1A\\uC77C\\uB7EC\\uC2A4\\uD2B8\\uB808\\uC774\\uD130, \\uC6D0\\uACA9\\uFF09.\",\n \"zh-Hans\": \"\\u7528\\u9017\\u53F7\\u5206\\u5F00\\uFF0C\\u4F8B\\u5982\\u300C\\u63D2\\u753B\\u5BB6, \\u8FDC\\u7A0B\\u300D\\u3002\",\n de: \"Mit Kommas trennen, z. B. \\u201EIllustratorin, Remote\\u201C.\",\n fr: \"S\\xE9parez par des virgules, par exemple \\xAB Illustratrice, t\\xE9l\\xE9travail \\xBB.\",\n es: \"Sep\\xE1ralas con comas, por ejemplo \\xABIlustradora, remoto\\xBB.\",\n pt: \"Separe com v\\xEDrgulas, por exemplo \\u201CIlustradora, remoto\\u201D.\"\n },\n // โ”€โ”€ the link tile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n \"type.link.title\": {\n zh: \"\\u9023\\u7D50\",\n ja: \"\\u30EA\\u30F3\\u30AF\",\n en: \"Link\",\n ko: \"\\uB9C1\\uD06C\",\n \"zh-Hans\": \"\\u94FE\\u63A5\",\n de: \"Link\",\n fr: \"Lien\",\n es: \"Enlace\",\n pt: \"Link\"\n },\n \"type.link.hint\": {\n zh: \"\\u4E00\\u9846\\u5E36\\u5716\\u793A\\u7684\\u6309\\u9215\\uFF0C\\u9EDE\\u4E86\\u53BB\\u4F60\\u6307\\u5B9A\\u7684\\u7DB2\\u5740\\u3002\",\n ja: \"\\u30A2\\u30A4\\u30B3\\u30F3\\u4ED8\\u304D\\u306E\\u30DC\\u30BF\\u30F3\\u3002\\u62BC\\u3059\\u3068\\u6307\\u5B9A\\u3057\\u305F URL \\u3078\\u3002\",\n en: \"A button with an icon that opens the address you choose.\",\n ko: \"\\uC544\\uC774\\uCF58\\uC774 \\uC788\\uB294 \\uBC84\\uD2BC. \\uB204\\uB974\\uBA74 \\uC9C0\\uC815\\uD55C \\uC8FC\\uC18C\\uB85C \\uC774\\uB3D9\\uD574\\uC694.\",\n \"zh-Hans\": \"\\u4E00\\u9897\\u5E26\\u56FE\\u6807\\u7684\\u6309\\u94AE\\uFF0C\\u70B9\\u4E86\\u53BB\\u4F60\\u6307\\u5B9A\\u7684\\u7F51\\u5740\\u3002\",\n de: \"Eine Schaltfl\\xE4che mit Symbol, die die Adresse Ihrer Wahl \\xF6ffnet.\",\n fr: \"Un bouton avec une ic\\xF4ne qui ouvre l\\u2019adresse de votre choix.\",\n es: \"Un bot\\xF3n con icono que abre la direcci\\xF3n que elijas.\",\n pt: \"Um bot\\xE3o com \\xEDcone que abre o endere\\xE7o que voc\\xEA escolher.\"\n },\n \"type.link.text.label\": {\n zh: \"\\u6309\\u9215\\u4E0A\\u7684\\u5B57\",\n ja: \"\\u30DC\\u30BF\\u30F3\\u306E\\u6587\\u5B57\",\n en: \"Button text\",\n ko: \"\\uBC84\\uD2BC\\uC5D0 \\uC4F8 \\uAE00\\uC790\",\n \"zh-Hans\": \"\\u6309\\u94AE\\u4E0A\\u7684\\u5B57\",\n de: \"Beschriftung\",\n fr: \"Texte du bouton\",\n es: \"Texto del bot\\xF3n\",\n pt: \"Texto do bot\\xE3o\"\n },\n \"type.link.target.label\": {\n zh: \"\\u9019\\u9846\\u6309\\u9215\\u6253\\u958B\",\n ja: \"\\u3053\\u306E\\u30DC\\u30BF\\u30F3\\u3067\\u958B\\u304F\\u3082\\u306E\",\n en: \"This button opens\",\n ko: \"\\uC774 \\uBC84\\uD2BC\\uC774 \\uC5EC\\uB294 \\uAC83\",\n \"zh-Hans\": \"\\u8FD9\\u9897\\u6309\\u94AE\\u6253\\u5F00\",\n de: \"Diese Schaltfl\\xE4che \\xF6ffnet\",\n fr: \"Ce bouton ouvre\",\n es: \"Este bot\\xF3n abre\",\n pt: \"Este bot\\xE3o abre\"\n },\n \"type.link.url.label\": {\n zh: \"\\u7DB2\\u5740\",\n ja: \"URL\",\n en: \"Address\",\n ko: \"\\uC8FC\\uC18C\",\n \"zh-Hans\": \"\\u7F51\\u5740\",\n de: \"Adresse\",\n fr: \"Adresse\",\n es: \"Direcci\\xF3n\",\n pt: \"Endere\\xE7o\"\n },\n \"type.link.url.hint\": {\n zh: \"\\u8CBC\\u4E0A\\u6574\\u500B\\u7DB2\\u5740\\uFF0C\\u4F8B\\u5982 https://instagram.com/\\u4F60\\u7684\\u5E33\\u865F\",\n ja: \"URL \\u3092\\u305D\\u306E\\u307E\\u307E\\u8CBC\\u308A\\u4ED8\\u3051\\uFF08\\u4F8B\\uFF1Ahttps://instagram.com/\\u3042\\u306A\\u305F\\u306EID\\uFF09\",\n en: \"Paste the whole address, e.g. https://instagram.com/yourname\",\n ko: \"\\uC8FC\\uC18C \\uC804\\uCCB4\\uB97C \\uBD99\\uC5EC \\uB123\\uC73C\\uC138\\uC694\\uFF08\\uC608\\uFF1Ahttps://instagram.com/\\uB0B4\\uC544\\uC774\\uB514\\uFF09\",\n \"zh-Hans\": \"\\u7C98\\u8D34\\u6574\\u4E2A\\u7F51\\u5740\\uFF0C\\u4F8B\\u5982 https://instagram.com/\\u4F60\\u7684\\u8D26\\u53F7\",\n de: \"Die ganze Adresse einf\\xFCgen, z. B. https://instagram.com/ihrname\",\n fr: \"Collez l\\u2019adresse enti\\xE8re, par exemple https://instagram.com/votrenom\",\n es: \"Pega la direcci\\xF3n entera, por ejemplo https://instagram.com/tunombre\",\n pt: \"Cole o endere\\xE7o inteiro, por exemplo https://instagram.com/seunome\"\n },\n \"type.link.sub.label\": {\n zh: \"\\u5C0F\\u5B57\\uFF08\\u9078\\u586B\\uFF09\",\n ja: \"\\u88DC\\u8DB3\\uFF08\\u4EFB\\u610F\\uFF09\",\n en: \"Small print (optional)\",\n ko: \"\\uC791\\uC740 \\uAE00\\uC528\\uFF08\\uC120\\uD0DD\\uFF09\",\n \"zh-Hans\": \"\\u5C0F\\u5B57\\uFF08\\u9009\\u586B\\uFF09\",\n de: \"Kleingedrucktes (optional)\",\n fr: \"Petit texte (facultatif)\",\n es: \"Letra peque\\xF1a (opcional)\",\n pt: \"Letras mi\\xFAdas (opcional)\"\n },\n \"type.link.advanced\": {\n zh: \"\\u5716\\u793A\\u8A2D\\u5B9A\",\n ja: \"\\u30A2\\u30A4\\u30B3\\u30F3\\u8A2D\\u5B9A\",\n en: \"Icon settings\",\n ko: \"\\uC544\\uC774\\uCF58 \\uC124\\uC815\",\n \"zh-Hans\": \"\\u56FE\\u6807\\u8BBE\\u7F6E\",\n de: \"Symboleinstellungen\",\n fr: \"R\\xE9glages de l\\u2019ic\\xF4ne\",\n es: \"Ajustes del icono\",\n pt: \"Ajustes do \\xEDcone\"\n },\n \"type.link.icon.label\": {\n zh: \"\\u5167\\u5EFA\\u5716\\u793A\\u540D\\u7A31\",\n ja: \"\\u5185\\u8535\\u30A2\\u30A4\\u30B3\\u30F3\\u540D\",\n en: \"Built-in icon name\",\n ko: \"\\uAE30\\uBCF8 \\uC544\\uC774\\uCF58 \\uC774\\uB984\",\n \"zh-Hans\": \"\\u5185\\u7F6E\\u56FE\\u6807\\u540D\\u79F0\",\n de: \"Name des mitgelieferten Symbols\",\n fr: \"Nom d\\u2019une ic\\xF4ne fournie\",\n es: \"Nombre de un icono incluido\",\n pt: \"Nome de um \\xEDcone inclu\\xEDdo\"\n },\n \"type.link.icon.hint\": {\n zh: \"\\u7559\\u7A7A\\u6703\\u81EA\\u52D5\\u7528\\u90A3\\u500B\\u7DB2\\u7AD9\\u7684\\u5716\\u793A\\u3002\",\n ja: \"\\u7A7A\\u6B04\\u306A\\u3089\\u76F8\\u624B\\u30B5\\u30A4\\u30C8\\u306E\\u30A2\\u30A4\\u30B3\\u30F3\\u3092\\u81EA\\u52D5\\u3067\\u4F7F\\u3044\\u307E\\u3059\\u3002\",\n en: \"Leave empty to use that site's own icon.\",\n ko: \"\\uBE44\\uC6CC \\uB450\\uBA74 \\uADF8 \\uC0AC\\uC774\\uD2B8\\uC758 \\uC544\\uC774\\uCF58\\uC744 \\uC790\\uB3D9\\uC73C\\uB85C \\uC368\\uC694.\",\n \"zh-Hans\": \"\\u7559\\u7A7A\\u4F1A\\u81EA\\u52A8\\u7528\\u90A3\\u4E2A\\u7F51\\u7AD9\\u7684\\u56FE\\u6807\\u3002\",\n de: \"Leer lassen, um das Symbol der Website selbst zu verwenden.\",\n fr: \"Laissez vide pour utiliser l\\u2019ic\\xF4ne du site lui-m\\xEAme.\",\n es: \"D\\xE9jalo vac\\xEDo para usar el icono del propio sitio.\",\n pt: \"Deixe em branco para usar o \\xEDcone do pr\\xF3prio site.\"\n },\n \"type.link.iconhost.label\": {\n zh: \"\\u6539\\u7528\\u9019\\u500B\\u7DB2\\u7AD9\\u7684\\u5716\\u793A\",\n ja: \"\\u5225\\u30B5\\u30A4\\u30C8\\u306E\\u30A2\\u30A4\\u30B3\\u30F3\\u3092\\u4F7F\\u3046\",\n en: \"Use another site's icon\",\n ko: \"\\uB2E4\\uB978 \\uC0AC\\uC774\\uD2B8\\uC758 \\uC544\\uC774\\uCF58 \\uC4F0\\uAE30\",\n \"zh-Hans\": \"\\u6539\\u7528\\u8FD9\\u4E2A\\u7F51\\u7AD9\\u7684\\u56FE\\u6807\",\n de: \"Symbol einer anderen Website verwenden\",\n fr: \"Utiliser l\\u2019ic\\xF4ne d\\u2019un autre site\",\n es: \"Usar el icono de otro sitio\",\n pt: \"Usar o \\xEDcone de outro site\"\n },\n \"type.link.iconimg.label\": {\n zh: \"\\u81EA\\u5DF1\\u7684\\u5716\\u793A\\u5716\\u7247\",\n ja: \"\\u81EA\\u5206\\u3067\\u7528\\u610F\\u3057\\u305F\\u30A2\\u30A4\\u30B3\\u30F3\\u753B\\u50CF\",\n en: \"Your own icon image\",\n ko: \"\\uC9C1\\uC811 \\uC900\\uBE44\\uD55C \\uC544\\uC774\\uCF58 \\uC774\\uBBF8\\uC9C0\",\n \"zh-Hans\": \"\\u81EA\\u5DF1\\u7684\\u56FE\\u6807\\u56FE\\u7247\",\n de: \"Eigenes Symbolbild\",\n fr: \"Votre propre image d\\u2019ic\\xF4ne\",\n es: \"Tu propia imagen de icono\",\n pt: \"Sua pr\\xF3pria imagem de \\xEDcone\"\n },\n // โ”€โ”€ the picture tile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n \"type.feature.title\": {\n zh: \"\\u5716\\u7247\",\n ja: \"\\u753B\\u50CF\",\n en: \"Picture\",\n ko: \"\\uC0AC\\uC9C4\",\n \"zh-Hans\": \"\\u56FE\\u7247\",\n de: \"Bild\",\n fr: \"Image\",\n es: \"Imagen\",\n pt: \"Imagem\"\n },\n \"type.feature.hint\": {\n zh: \"\\u4E00\\u5F35\\u7167\\u7247\\uFF1B\\u53EF\\u4EE5\\u53EA\\u662F\\u7167\\u7247\\uFF0C\\u4E5F\\u53EF\\u4EE5\\u52A0\\u6A19\\u984C\\u548C\\u6309\\u9215\\u3002\",\n ja: \"\\u5199\\u771F 1 \\u679A\\u3002\\u5199\\u771F\\u3060\\u3051\\u3067\\u3082\\u3001\\u898B\\u51FA\\u3057\\u3084\\u30DC\\u30BF\\u30F3\\u3092\\u91CD\\u306D\\u3066\\u3082\\u3002\",\n en: \"One photo; on its own, or with a heading and a button on top.\",\n ko: \"\\uC0AC\\uC9C4 \\uD55C \\uC7A5. \\uC0AC\\uC9C4\\uB9CC\\uC73C\\uB85C\\uB3C4, \\uC81C\\uBAA9\\uACFC \\uBC84\\uD2BC\\uC744 \\uC5B9\\uC5B4\\uB3C4 \\uC88B\\uC544\\uC694.\",\n \"zh-Hans\": \"\\u4E00\\u5F20\\u7167\\u7247\\uFF1B\\u53EF\\u4EE5\\u53EA\\u662F\\u7167\\u7247\\uFF0C\\u4E5F\\u53EF\\u4EE5\\u52A0\\u6807\\u9898\\u548C\\u6309\\u94AE\\u3002\",\n de: \"Ein Foto \\u2014 f\\xFCr sich allein oder mit \\xDCberschrift und Schaltfl\\xE4che darauf.\",\n fr: \"Une photo : seule, ou avec un titre et un bouton par-dessus.\",\n es: \"Una foto: sola, o con un t\\xEDtulo y un bot\\xF3n encima.\",\n pt: \"Uma foto: sozinha, ou com um t\\xEDtulo e um bot\\xE3o por cima.\"\n },\n \"type.feature.img.label\": {\n zh: \"\\u5716\\u7247\",\n ja: \"\\u753B\\u50CF\",\n en: \"Picture\",\n ko: \"\\uC0AC\\uC9C4\",\n \"zh-Hans\": \"\\u56FE\\u7247\",\n de: \"Bild\",\n fr: \"Image\",\n es: \"Imagen\",\n pt: \"Imagem\"\n },\n \"type.feature.alt.label\": {\n zh: \"\\u9019\\u5F35\\u5716\\u662F\\u4EC0\\u9EBC\",\n ja: \"\\u3053\\u306E\\u753B\\u50CF\\u306E\\u8AAC\\u660E\",\n en: \"What this picture shows\",\n ko: \"\\uC774 \\uC0AC\\uC9C4\\uC740 \\uBB34\\uC5C7\\uC778\\uAC00\\uC694\",\n \"zh-Hans\": \"\\u8FD9\\u5F20\\u56FE\\u662F\\u4EC0\\u4E48\",\n de: \"Was auf dem Bild zu sehen ist\",\n fr: \"Ce que montre l\\u2019image\",\n es: \"Qu\\xE9 muestra la imagen\",\n pt: \"O que a imagem mostra\"\n },\n \"type.feature.alt.hint\": {\n zh: \"\\u770B\\u4E0D\\u5230\\u5716\\u7684\\u4EBA\\u6703\\u8B80\\u5230\\u9019\\u53E5\\u3002\",\n ja: \"\\u753B\\u50CF\\u304C\\u898B\\u3048\\u306A\\u3044\\u4EBA\\u306B\\u306F\\u3053\\u306E\\u6587\\u304C\\u8AAD\\u307E\\u308C\\u307E\\u3059\\u3002\",\n en: \"People who can't see the picture will read this.\",\n ko: \"\\uC0AC\\uC9C4\\uC744 \\uBCFC \\uC218 \\uC5C6\\uB294 \\uC0AC\\uB78C\\uC740 \\uC774 \\uBB38\\uC7A5\\uC744 \\uC77D\\uAC8C \\uB3FC\\uC694.\",\n \"zh-Hans\": \"\\u770B\\u4E0D\\u5230\\u56FE\\u7684\\u4EBA\\u4F1A\\u8BFB\\u5230\\u8FD9\\u53E5\\u3002\",\n de: \"Wer das Bild nicht sehen kann, liest diesen Satz.\",\n fr: \"Les personnes qui ne voient pas l\\u2019image liront cette phrase.\",\n es: \"Quien no pueda ver la imagen leer\\xE1 esta frase.\",\n pt: \"Quem n\\xE3o consegue ver a imagem vai ler esta frase.\"\n },\n \"type.feature.label.label\": {\n zh: \"\\u5716\\u7247\\u4E0B\\u65B9\\u986F\\u793A\\u9019\\u53E5\\u8AAA\\u660E\",\n ja: \"\\u3053\\u306E\\u8AAC\\u660E\\u3092\\u753B\\u50CF\\u306E\\u4E0B\\u306B\\u8868\\u793A\",\n en: \"Show this caption under the picture\",\n ko: \"\\uC774 \\uC124\\uBA85\\uC744 \\uC0AC\\uC9C4 \\uC544\\uB798\\uC5D0 \\uBCF4\\uC5EC \\uC8FC\\uAE30\",\n \"zh-Hans\": \"\\u56FE\\u7247\\u4E0B\\u65B9\\u663E\\u793A\\u8FD9\\u53E5\\u8BF4\\u660E\",\n de: \"Diesen Text unter dem Bild anzeigen\",\n fr: \"Afficher ce texte sous l\\u2019image\",\n es: \"Mostrar este texto debajo de la imagen\",\n pt: \"Mostrar este texto abaixo da imagem\"\n },\n \"type.feature.target.label\": {\n zh: \"\\u9EDE\\u5716\\u7247\\u6703\\u53BB\\u54EA\\u88E1\",\n ja: \"\\u753B\\u50CF\\u3092\\u62BC\\u3057\\u305F\\u3068\\u304D\\u306E\\u884C\\u304D\\u5148\",\n en: \"Where the picture leads\",\n ko: \"\\uC0AC\\uC9C4\\uC744 \\uB204\\uB974\\uBA74 \\uAC00\\uB294 \\uACF3\",\n \"zh-Hans\": \"\\u70B9\\u56FE\\u7247\\u4F1A\\u53BB\\u54EA\\u91CC\",\n de: \"Wohin das Bild f\\xFChrt\",\n fr: \"O\\xF9 m\\xE8ne l\\u2019image\",\n es: \"Ad\\xF3nde lleva la imagen\",\n pt: \"Para onde a imagem leva\"\n },\n \"type.feature.href.label\": {\n zh: \"\\u9EDE\\u5716\\u7247\\u6703\\u53BB\\u54EA\\u88E1\\uFF08\\u9078\\u586B\\uFF09\",\n ja: \"\\u753B\\u50CF\\u3092\\u62BC\\u3057\\u305F\\u3068\\u304D\\u306E\\u884C\\u304D\\u5148\\uFF08\\u4EFB\\u610F\\uFF09\",\n en: \"Where the picture leads (optional)\",\n ko: \"\\uC0AC\\uC9C4\\uC744 \\uB204\\uB974\\uBA74 \\uAC00\\uB294 \\uACF3\\uFF08\\uC120\\uD0DD\\uFF09\",\n \"zh-Hans\": \"\\u70B9\\u56FE\\u7247\\u4F1A\\u53BB\\u54EA\\u91CC\\uFF08\\u9009\\u586B\\uFF09\",\n de: \"Wohin das Bild f\\xFChrt (optional)\",\n fr: \"O\\xF9 m\\xE8ne l\\u2019image (facultatif)\",\n es: \"Ad\\xF3nde lleva la imagen (opcional)\",\n pt: \"Para onde a imagem leva (opcional)\"\n },\n \"type.feature.advanced\": {\n zh: \"\\u52A0\\u6A19\\u984C\",\n ja: \"\\u898B\\u51FA\\u3057\\u3092\\u4ED8\\u3051\\u308B\",\n en: \"Add a heading\",\n ko: \"\\uC81C\\uBAA9 \\uBD99\\uC774\\uAE30\",\n \"zh-Hans\": \"\\u52A0\\u6807\\u9898\",\n de: \"\\xDCberschrift hinzuf\\xFCgen\",\n fr: \"Ajouter un titre\",\n es: \"A\\xF1adir un t\\xEDtulo\",\n pt: \"Adicionar um t\\xEDtulo\"\n },\n \"type.feature.title.label\": {\n zh: \"\\u58D3\\u5728\\u5716\\u4E0A\\u7684\\u6A19\\u984C\\uFF08\\u9078\\u586B\\uFF09\",\n ja: \"\\u753B\\u50CF\\u306B\\u91CD\\u306D\\u308B\\u898B\\u51FA\\u3057\\uFF08\\u4EFB\\u610F\\uFF09\",\n en: \"Heading over the picture (optional)\",\n ko: \"\\uC0AC\\uC9C4 \\uC704\\uC5D0 \\uC5B9\\uC744 \\uC81C\\uBAA9\\uFF08\\uC120\\uD0DD\\uFF09\",\n \"zh-Hans\": \"\\u538B\\u5728\\u56FE\\u4E0A\\u7684\\u6807\\u9898\\uFF08\\u9009\\u586B\\uFF09\",\n de: \"\\xDCberschrift auf dem Bild (optional)\",\n fr: \"Titre par-dessus l\\u2019image (facultatif)\",\n es: \"T\\xEDtulo sobre la imagen (opcional)\",\n pt: \"T\\xEDtulo sobre a imagem (opcional)\"\n },\n \"type.feature.body.label\": {\n zh: \"\\u6A19\\u984C\\u4E0A\\u65B9\\u7684\\u5C0F\\u5B57\\uFF08\\u9078\\u586B\\uFF09\",\n ja: \"\\u898B\\u51FA\\u3057\\u306E\\u4E0A\\u306E\\u5C0F\\u3055\\u306A\\u6587\\u5B57\\uFF08\\u4EFB\\u610F\\uFF09\",\n en: \"Small line above the heading (optional)\",\n ko: \"\\uC81C\\uBAA9 \\uC704\\uC758 \\uC791\\uC740 \\uAE00\\uC528\\uFF08\\uC120\\uD0DD\\uFF09\",\n \"zh-Hans\": \"\\u6807\\u9898\\u4E0A\\u65B9\\u7684\\u5C0F\\u5B57\\uFF08\\u9009\\u586B\\uFF09\",\n de: \"Kleine Zeile \\xFCber der \\xDCberschrift (optional)\",\n fr: \"Petite ligne au-dessus du titre (facultatif)\",\n es: \"L\\xEDnea peque\\xF1a encima del t\\xEDtulo (opcional)\",\n pt: \"Linha pequena acima do t\\xEDtulo (opcional)\"\n },\n \"type.feature.cta.label\": {\n zh: \"\\u6309\\u9215\\u6587\\u5B57\\uFF08\\u9078\\u586B\\uFF09\",\n ja: \"\\u30DC\\u30BF\\u30F3\\u306E\\u6587\\u5B57\\uFF08\\u4EFB\\u610F\\uFF09\",\n en: \"Button text (optional)\",\n ko: \"\\uBC84\\uD2BC\\uC5D0 \\uC4F8 \\uAE00\\uC790\\uFF08\\uC120\\uD0DD\\uFF09\",\n \"zh-Hans\": \"\\u6309\\u94AE\\u6587\\u5B57\\uFF08\\u9009\\u586B\\uFF09\",\n de: \"Beschriftung der Schaltfl\\xE4che (optional)\",\n fr: \"Texte du bouton (facultatif)\",\n es: \"Texto del bot\\xF3n (opcional)\",\n pt: \"Texto do bot\\xE3o (opcional)\"\n },\n \"type.feature.cta.hint\": {\n zh: \"\\u4F8B\\u5982\\u300C\\u770B\\u66F4\\u591A\\u300D\\u3002\",\n ja: \"\\u4F8B\\uFF1A\\u300C\\u3082\\u3063\\u3068\\u898B\\u308B\\u300D\\u3002\",\n en: 'For example \"See more\".',\n ko: \"\\uC608\\uFF1A\\u300C\\uB354 \\uBCF4\\uAE30\\u300D.\",\n \"zh-Hans\": \"\\u4F8B\\u5982\\u300C\\u770B\\u66F4\\u591A\\u300D\\u3002\",\n de: \"Zum Beispiel \\u201EMehr ansehen\\u201C.\",\n fr: \"Par exemple \\xAB Voir plus \\xBB.\",\n es: \"Por ejemplo \\xABVer m\\xE1s\\xBB.\",\n pt: \"Por exemplo \\u201CVer mais\\u201D.\"\n },\n // โ”€โ”€ the text tile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n \"type.text.title\": {\n zh: \"\\u6587\\u5B57\",\n ja: \"\\u6587\\u7AE0\",\n en: \"Text\",\n ko: \"\\uAE00\",\n \"zh-Hans\": \"\\u6587\\u5B57\",\n de: \"Text\",\n fr: \"Texte\",\n es: \"Texto\",\n pt: \"Texto\"\n },\n \"type.text.hint\": {\n zh: \"\\u4E00\\u6BB5\\u8A71\\uFF0C\\u4F54\\u6EFF\\u6574\\u884C\\uFF0C\\u9AD8\\u5EA6\\u8DDF\\u8457\\u5167\\u5BB9\\u3002\",\n ja: \"\\u6587\\u7AE0\\u306E\\u304B\\u305F\\u307E\\u308A\\u3002\\u6A2A\\u3044\\u3063\\u3071\\u3044\\u306B\\u3001\\u9577\\u3055\\u306F\\u5185\\u5BB9\\u3057\\u3060\\u3044\\u3002\",\n en: \"A paragraph. Full width, as tall as it needs to be.\",\n ko: \"\\uD55C \\uB369\\uC5B4\\uB9AC\\uC758 \\uAE00. \\uAC00\\uB85C \\uC804\\uCCB4\\uB97C \\uC4F0\\uACE0, \\uAE38\\uC774\\uB294 \\uB0B4\\uC6A9\\uC5D0 \\uB530\\uB77C\\uC694.\",\n \"zh-Hans\": \"\\u4E00\\u6BB5\\u8BDD\\uFF0C\\u5360\\u6EE1\\u6574\\u884C\\uFF0C\\u9AD8\\u5EA6\\u8DDF\\u7740\\u5185\\u5BB9\\u3002\",\n de: \"Ein Absatz. Volle Breite, so hoch wie n\\xF6tig.\",\n fr: \"Un paragraphe. Pleine largeur, aussi haut qu\\u2019il le faut.\",\n es: \"Un p\\xE1rrafo. Todo el ancho, tan alto como haga falta.\",\n pt: \"Um par\\xE1grafo. Largura total, t\\xE3o alto quanto precisar.\"\n },\n \"type.text.body.label\": {\n zh: \"\\u5167\\u5BB9\",\n ja: \"\\u5185\\u5BB9\",\n en: \"Text\",\n ko: \"\\uB0B4\\uC6A9\",\n \"zh-Hans\": \"\\u5185\\u5BB9\",\n de: \"Inhalt\",\n fr: \"Contenu\",\n es: \"Contenido\",\n pt: \"Conte\\xFAdo\"\n },\n \"type.text.body.hint\": {\n zh: \"\\u7A7A\\u4E00\\u884C\\u5C31\\u662F\\u5206\\u6BB5\\u3002\",\n ja: \"\\u7A7A\\u884C\\u3092\\u5165\\u308C\\u308B\\u3068\\u6BB5\\u843D\\u304C\\u5206\\u304B\\u308C\\u307E\\u3059\\u3002\",\n en: \"Leave an empty line to start a new paragraph.\",\n ko: \"\\uBE48 \\uC904\\uC744 \\uB123\\uC73C\\uBA74 \\uBB38\\uB2E8\\uC774 \\uB098\\uB258\\uC5B4\\uC694.\",\n \"zh-Hans\": \"\\u7A7A\\u4E00\\u884C\\u5C31\\u662F\\u5206\\u6BB5\\u3002\",\n de: \"Eine Leerzeile beginnt einen neuen Absatz.\",\n fr: \"Une ligne vide commence un nouveau paragraphe.\",\n es: \"Una l\\xEDnea vac\\xEDa empieza un p\\xE1rrafo nuevo.\",\n pt: \"Uma linha em branco come\\xE7a um novo par\\xE1grafo.\"\n },\n // โ”€โ”€ the video tile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n \"type.video.title\": {\n zh: \"\\u5F71\\u7247\",\n ja: \"\\u52D5\\u753B\",\n en: \"Video\",\n ko: \"\\uC601\\uC0C1\",\n \"zh-Hans\": \"\\u89C6\\u9891\",\n de: \"Video\",\n fr: \"Vid\\xE9o\",\n es: \"V\\xEDdeo\",\n pt: \"V\\xEDdeo\"\n },\n \"type.video.hint\": {\n zh: \"\\u4E00\\u652F YouTube \\u5F71\\u7247\\uFF0C\\u8A2A\\u5BA2\\u6309\\u4E86\\u624D\\u6703\\u64AD\\u653E\\u3002\",\n ja: \"YouTube \\u306E\\u52D5\\u753B\\u3002\\u8A2A\\u554F\\u8005\\u304C\\u62BC\\u3057\\u305F\\u3068\\u304D\\u3060\\u3051\\u518D\\u751F\\u3055\\u308C\\u307E\\u3059\\u3002\",\n en: \"A YouTube video that only plays when a visitor taps it.\",\n ko: \"YouTube \\uC601\\uC0C1. \\uBC29\\uBB38\\uC790\\uAC00 \\uB20C\\uB800\\uC744 \\uB54C\\uB9CC \\uC7AC\\uC0DD\\uB3FC\\uC694.\",\n \"zh-Hans\": \"\\u4E00\\u652F YouTube \\u89C6\\u9891\\uFF0C\\u8BBF\\u5BA2\\u6309\\u4E86\\u624D\\u4F1A\\u64AD\\u653E\\u3002\",\n de: \"Ein YouTube-Video, das erst abspielt, wenn jemand darauf tippt.\",\n fr: \"Une vid\\xE9o YouTube qui ne d\\xE9marre que si un visiteur la touche.\",\n es: \"Un v\\xEDdeo de YouTube que solo se reproduce cuando alguien lo toca.\",\n pt: \"Um v\\xEDdeo do YouTube que s\\xF3 toca quando algu\\xE9m toca nele.\"\n },\n \"type.video.yt.label\": {\n zh: \"YouTube \\u5F71\\u7247\",\n ja: \"YouTube \\u306E\\u52D5\\u753B\",\n en: \"YouTube video\",\n ko: \"YouTube \\uC601\\uC0C1\",\n \"zh-Hans\": \"YouTube \\u89C6\\u9891\",\n de: \"YouTube-Video\",\n fr: \"Vid\\xE9o YouTube\",\n es: \"V\\xEDdeo de YouTube\",\n pt: \"V\\xEDdeo do YouTube\"\n },\n \"type.video.yt.hint\": {\n zh: \"\\u8CBC\\u4E0A\\u5F71\\u7247\\u7684\\u7DB2\\u5740\\uFF0C\\u6216\\u53EA\\u586B\\u7DE8\\u865F\\uFF0C\\u4F8B\\u5982 youtube.com/watch?v=dQw4w9WgXcQ \\u6216 dQw4w9WgXcQ\\u3002\\u548C\\u300C\\u983B\\u9053\\u300D\\u4E8C\\u9078\\u4E00\\u3002\",\n ja: \"\\u52D5\\u753B\\u306E\\u30EA\\u30F3\\u30AF\\u3092\\u8CBC\\u308A\\u4ED8\\u3051\\u308B\\u304B\\u3001ID \\u3060\\u3051\\u3092\\u5165\\u529B\\u3057\\u307E\\u3059\\u3002\\u4F8B\\uFF1Ayoutube.com/watch?v=dQw4w9WgXcQ \\u307E\\u305F\\u306F dQw4w9WgXcQ\\u3002\\u300C\\u30C1\\u30E3\\u30F3\\u30CD\\u30EB\\u300D\\u3068\\u3069\\u3061\\u3089\\u304B\\u4E00\\u65B9\\u3067\\u3059\\u3002\",\n en: \"Paste the video\\u2019s link, or its id \\u2014 like youtube.com/watch?v=dQw4w9WgXcQ or dQw4w9WgXcQ. Either this or the channel, not both.\",\n ko: \"\\uC601\\uC0C1 \\uB9C1\\uD06C\\uB97C \\uBD99\\uC5EC \\uB123\\uAC70\\uB098 ID\\uB9CC \\uC801\\uC5B4\\uC694. \\uC608: youtube.com/watch?v=dQw4w9WgXcQ \\uB610\\uB294 dQw4w9WgXcQ. \\u300C\\uCC44\\uB110\\u300D\\uACFC \\uB458 \\uC911 \\uD558\\uB098\\uB9CC \\uC4F0\\uC138\\uC694.\",\n \"zh-Hans\": \"\\u7C98\\u8D34\\u89C6\\u9891\\u7684\\u7F51\\u5740\\uFF0C\\u6216\\u53EA\\u586B\\u7F16\\u53F7\\uFF0C\\u4F8B\\u5982 youtube.com/watch?v=dQw4w9WgXcQ \\u6216 dQw4w9WgXcQ\\u3002\\u548C\\u300C\\u9891\\u9053\\u300D\\u4E8C\\u9009\\u4E00\\u3002\",\n de: \"F\\xFCgen Sie den Link des Videos ein oder nur seine Kennung \\u2013 etwa youtube.com/watch?v=dQw4w9WgXcQ oder dQw4w9WgXcQ. Entweder dies oder der Kanal, nicht beides.\",\n fr: \"Collez le lien de la vid\\xE9o, ou son identifiant \\u2014 par exemple youtube.com/watch?v=dQw4w9WgXcQ ou dQw4w9WgXcQ. Ceci ou la cha\\xEEne, pas les deux.\",\n es: \"Pega el enlace del v\\xEDdeo o su identificador, por ejemplo youtube.com/watch?v=dQw4w9WgXcQ o dQw4w9WgXcQ. Esto o el canal, no ambos.\",\n pt: \"Cole o link do v\\xEDdeo, ou o identificador dele \\u2014 por exemplo youtube.com/watch?v=dQw4w9WgXcQ ou dQw4w9WgXcQ. Isto ou o canal, n\\xE3o os dois.\"\n },\n \"type.video.channel.label\": {\n zh: \"\\u6216\\uFF1A\\u6C38\\u9060\\u986F\\u793A\\u9019\\u500B\\u983B\\u9053\\u6700\\u65B0\\u7684\\u4E00\\u652F\",\n ja: \"\\u307E\\u305F\\u306F\\uFF1A\\u3053\\u306E\\u30C1\\u30E3\\u30F3\\u30CD\\u30EB\\u306E\\u6700\\u65B0\\u52D5\\u753B\\u3092\\u3044\\u3064\\u3082\\u8868\\u793A\",\n en: \"Or: always show this channel's newest video\",\n ko: \"\\uB610\\uB294\\uFF1A\\uC774 \\uCC44\\uB110\\uC758 \\uCD5C\\uC2E0 \\uC601\\uC0C1\\uC744 \\uD56D\\uC0C1 \\uBCF4\\uC5EC \\uC8FC\\uAE30\",\n \"zh-Hans\": \"\\u6216\\uFF1A\\u6C38\\u8FDC\\u663E\\u793A\\u8FD9\\u4E2A\\u9891\\u9053\\u6700\\u65B0\\u7684\\u4E00\\u652F\",\n de: \"Oder: immer das neueste Video dieses Kanals zeigen\",\n fr: \"Ou : toujours montrer la derni\\xE8re vid\\xE9o de cette cha\\xEEne\",\n es: \"O: mostrar siempre el v\\xEDdeo m\\xE1s reciente de este canal\",\n pt: \"Ou: mostrar sempre o v\\xEDdeo mais recente deste canal\"\n },\n \"type.video.channel.hint\": {\n zh: \"\\u8CBC\\u4E0A\\u983B\\u9053\\u7684\\u7DB2\\u5740\\uFF0C\\u6216\\u53EA\\u586B UC \\u958B\\u982D\\u7684\\u90A3\\u4E00\\u4E32\\uFF0C\\u4F8B\\u5982 youtube.com/channel/UC\\u2026\\u3002\",\n ja: \"\\u30C1\\u30E3\\u30F3\\u30CD\\u30EB\\u306E\\u30EA\\u30F3\\u30AF\\u3092\\u8CBC\\u308A\\u4ED8\\u3051\\u308B\\u304B\\u3001UC \\u3067\\u59CB\\u307E\\u308B\\u90E8\\u5206\\u3060\\u3051\\u3092\\u5165\\u529B\\u3057\\u307E\\u3059\\u3002\\u4F8B\\uFF1Ayoutube.com/channel/UC\\u2026\",\n en: \"Paste the channel\\u2019s link, or just the part starting with UC \\u2014 like youtube.com/channel/UC\\u2026.\",\n ko: \"\\uCC44\\uB110 \\uB9C1\\uD06C\\uB97C \\uBD99\\uC5EC \\uB123\\uAC70\\uB098 UC\\uB85C \\uC2DC\\uC791\\uD558\\uB294 \\uBD80\\uBD84\\uB9CC \\uC801\\uC5B4\\uC694. \\uC608: youtube.com/channel/UC\\u2026\",\n \"zh-Hans\": \"\\u7C98\\u8D34\\u9891\\u9053\\u7684\\u7F51\\u5740\\uFF0C\\u6216\\u53EA\\u586B UC \\u5F00\\u5934\\u7684\\u90A3\\u4E00\\u4E32\\uFF0C\\u4F8B\\u5982 youtube.com/channel/UC\\u2026\\u3002\",\n de: \"F\\xFCgen Sie den Link des Kanals ein oder nur den Teil, der mit UC beginnt \\u2013 etwa youtube.com/channel/UC\\u2026.\",\n fr: \"Collez le lien de la cha\\xEEne, ou seulement la partie qui commence par UC \\u2014 par exemple youtube.com/channel/UC\\u2026.\",\n es: \"Pega el enlace del canal o solo la parte que empieza por UC, por ejemplo youtube.com/channel/UC\\u2026.\",\n pt: \"Cole o link do canal, ou s\\xF3 a parte que come\\xE7a com UC \\u2014 por exemplo youtube.com/channel/UC\\u2026.\"\n },\n \"type.video.body.label\": {\n zh: \"\\u5F71\\u7247\\u6A19\\u984C\\uFF08\\u9078\\u586B\\uFF09\",\n ja: \"\\u52D5\\u753B\\u306E\\u30BF\\u30A4\\u30C8\\u30EB\\uFF08\\u4EFB\\u610F\\uFF09\",\n en: \"Video title (optional)\",\n ko: \"\\uC601\\uC0C1 \\uC81C\\uBAA9\\uFF08\\uC120\\uD0DD\\uFF09\",\n \"zh-Hans\": \"\\u89C6\\u9891\\u6807\\u9898\\uFF08\\u9009\\u586B\\uFF09\",\n de: \"Videotitel (optional)\",\n fr: \"Titre de la vid\\xE9o (facultatif)\",\n es: \"T\\xEDtulo del v\\xEDdeo (opcional)\",\n pt: \"T\\xEDtulo do v\\xEDdeo (opcional)\"\n },\n \"type.video.body.hint\": {\n zh: \"\\u7559\\u7A7A\\u6703\\u7528\\u5F71\\u7247\\u81EA\\u5DF1\\u7684\\u6A19\\u984C\\u3002\",\n ja: \"\\u7A7A\\u6B04\\u306A\\u3089\\u52D5\\u753B\\u306E\\u30BF\\u30A4\\u30C8\\u30EB\\u3092\\u305D\\u306E\\u307E\\u307E\\u4F7F\\u3044\\u307E\\u3059\\u3002\",\n en: \"Leave empty to use the video's own title.\",\n ko: \"\\uBE44\\uC6CC \\uB450\\uBA74 \\uC601\\uC0C1\\uC758 \\uC6D0\\uB798 \\uC81C\\uBAA9\\uC744 \\uC368\\uC694.\",\n \"zh-Hans\": \"\\u7559\\u7A7A\\u4F1A\\u7528\\u89C6\\u9891\\u81EA\\u5DF1\\u7684\\u6807\\u9898\\u3002\",\n de: \"Leer lassen, um den eigenen Titel des Videos zu verwenden.\",\n fr: \"Laissez vide pour utiliser le titre de la vid\\xE9o.\",\n es: \"D\\xE9jalo vac\\xEDo para usar el t\\xEDtulo del propio v\\xEDdeo.\",\n pt: \"Deixe em branco para usar o t\\xEDtulo do pr\\xF3prio v\\xEDdeo.\"\n },\n \"type.video.poster.label\": {\n zh: \"\\u81EA\\u5DF1\\u7684\\u5C01\\u9762\\u5716\\uFF08\\u9078\\u586B\\uFF09\",\n ja: \"\\u81EA\\u5206\\u3067\\u7528\\u610F\\u3059\\u308B\\u8868\\u7D19\\u753B\\u50CF\\uFF08\\u4EFB\\u610F\\uFF09\",\n en: \"Your own cover picture (optional)\",\n ko: \"\\uC9C1\\uC811 \\uC900\\uBE44\\uD55C \\uD45C\\uC9C0 \\uC0AC\\uC9C4\\uFF08\\uC120\\uD0DD\\uFF09\",\n \"zh-Hans\": \"\\u81EA\\u5DF1\\u7684\\u5C01\\u9762\\u56FE\\uFF08\\u9009\\u586B\\uFF09\",\n de: \"Eigenes Titelbild (optional)\",\n fr: \"Votre propre image de couverture (facultatif)\",\n es: \"Tu propia imagen de portada (opcional)\",\n pt: \"Sua pr\\xF3pria imagem de capa (opcional)\"\n },\n \"type.video.poster.hint\": {\n zh: \"\\u7559\\u7A7A\\u6703\\u7528\\u5F71\\u7247\\u81EA\\u5DF1\\u7684\\u5C01\\u9762\\u3002\",\n ja: \"\\u7A7A\\u6B04\\u306A\\u3089\\u52D5\\u753B\\u306E\\u8868\\u7D19\\u3092\\u305D\\u306E\\u307E\\u307E\\u4F7F\\u3044\\u307E\\u3059\\u3002\",\n en: \"Leave empty to use the video's own cover.\",\n ko: \"\\uBE44\\uC6CC \\uB450\\uBA74 \\uC601\\uC0C1\\uC758 \\uC6D0\\uB798 \\uD45C\\uC9C0\\uB97C \\uC368\\uC694.\",\n \"zh-Hans\": \"\\u7559\\u7A7A\\u4F1A\\u7528\\u89C6\\u9891\\u81EA\\u5DF1\\u7684\\u5C01\\u9762\\u3002\",\n de: \"Leer lassen, um das Titelbild des Videos zu verwenden.\",\n fr: \"Laissez vide pour utiliser la couverture de la vid\\xE9o.\",\n es: \"D\\xE9jalo vac\\xEDo para usar la portada del propio v\\xEDdeo.\",\n pt: \"Deixe em branco para usar a capa do pr\\xF3prio v\\xEDdeo.\"\n },\n // โ”€โ”€ the social-icons tile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n \"type.social.title\": {\n zh: \"\\u793E\\u7FA4\\u5716\\u793A\",\n ja: \"SNS \\u30A2\\u30A4\\u30B3\\u30F3\",\n en: \"Social icons\",\n ko: \"SNS \\uC544\\uC774\\uCF58\",\n \"zh-Hans\": \"\\u793E\\u7FA4\\u56FE\\u6807\",\n de: \"Social-Media-Symbole\",\n fr: \"Ic\\xF4nes de r\\xE9seaux sociaux\",\n es: \"Iconos de redes sociales\",\n pt: \"\\xCDcones de redes sociais\"\n },\n \"type.social.hint\": {\n zh: \"\\u4E00\\u6392\\u5C0F\\u5716\\u793A\\uFF0C\\u6C92\\u6709\\u6587\\u5B57\\u3002\",\n ja: \"\\u5C0F\\u3055\\u306A\\u30A2\\u30A4\\u30B3\\u30F3\\u304C\\u6A2A\\u4E00\\u5217\\u3002\\u6587\\u5B57\\u306F\\u3042\\u308A\\u307E\\u305B\\u3093\\u3002\",\n en: \"A row of small icons, no text.\",\n ko: \"\\uC791\\uC740 \\uC544\\uC774\\uCF58\\uC774 \\uD55C \\uC904. \\uAE00\\uC790\\uB294 \\uC5C6\\uC5B4\\uC694.\",\n \"zh-Hans\": \"\\u4E00\\u6392\\u5C0F\\u56FE\\u6807\\uFF0C\\u6CA1\\u6709\\u6587\\u5B57\\u3002\",\n de: \"Eine Reihe kleiner Symbole, ohne Text.\",\n fr: \"Une rang\\xE9e de petites ic\\xF4nes, sans texte.\",\n es: \"Una fila de iconos peque\\xF1os, sin texto.\",\n pt: \"Uma fila de \\xEDcones pequenos, sem texto.\"\n },\n \"type.social.body.label\": {\n zh: \"\\u4F60\\u7684\\u5E33\\u865F\",\n ja: \"\\u3042\\u306A\\u305F\\u306E\\u30A2\\u30AB\\u30A6\\u30F3\\u30C8\",\n en: \"Your accounts\",\n ko: \"\\uB0B4 \\uACC4\\uC815\",\n \"zh-Hans\": \"\\u4F60\\u7684\\u8D26\\u53F7\",\n de: \"Ihre Konten\",\n fr: \"Vos comptes\",\n es: \"Tus cuentas\",\n pt: \"Suas contas\"\n },\n \"type.social.body.hint\": {\n zh: \"\\u4E00\\u884C\\u4E00\\u500B\\uFF1A\\u540D\\u7A31\\uFF0C\\u7136\\u5F8C\\u7DB2\\u5740\\u3002\",\n ja: \"1 \\u884C\\u306B 1 \\u3064\\uFF1A\\u540D\\u524D\\u306E\\u3042\\u3068\\u306B URL\\u3002\",\n en: \"One per line: the name, then the address.\",\n ko: \"\\uD55C \\uC904\\uC5D0 \\uD558\\uB098\\uFF1A\\uC774\\uB984 \\uB2E4\\uC74C\\uC5D0 \\uC8FC\\uC18C.\",\n \"zh-Hans\": \"\\u4E00\\u884C\\u4E00\\u4E2A\\uFF1A\\u540D\\u79F0\\uFF0C\\u7136\\u540E\\u7F51\\u5740\\u3002\",\n de: \"Eines pro Zeile: der Name, dann die Adresse.\",\n fr: \"Un par ligne : le nom, puis l\\u2019adresse.\",\n es: \"Uno por l\\xEDnea: el nombre y luego la direcci\\xF3n.\",\n pt: \"Um por linha: o nome e depois o endere\\xE7o.\"\n },\n // โ”€โ”€ the slideshow tile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n \"type.embed.title\": {\n zh: \"\\u8F2A\\u64AD\",\n ja: \"\\u30B9\\u30E9\\u30A4\\u30C9\",\n en: \"Slideshow\",\n ko: \"\\uC2AC\\uB77C\\uC774\\uB4DC\",\n \"zh-Hans\": \"\\u8F6E\\u64AD\",\n de: \"Diaschau\",\n fr: \"Diaporama\",\n es: \"Pase de im\\xE1genes\",\n pt: \"Apresenta\\xE7\\xE3o de imagens\"\n },\n \"type.embed.hint\": {\n zh: \"\\u597D\\u5E7E\\u5F35\\u5716\\u7247\\u8F2A\\u6D41\\u986F\\u793A\\u3002\",\n ja: \"\\u8907\\u6570\\u306E\\u753B\\u50CF\\u3092\\u9806\\u306B\\u8868\\u793A\\u3057\\u307E\\u3059\\u3002\",\n en: \"Several pictures, one after another.\",\n ko: \"\\uC5EC\\uB7EC \\uC7A5\\uC758 \\uC0AC\\uC9C4\\uC744 \\uCC28\\uB840\\uB85C \\uBCF4\\uC5EC \\uC918\\uC694.\",\n \"zh-Hans\": \"\\u597D\\u51E0\\u5F20\\u56FE\\u7247\\u8F6E\\u6D41\\u663E\\u793A\\u3002\",\n de: \"Mehrere Bilder, eines nach dem anderen.\",\n fr: \"Plusieurs images, l\\u2019une apr\\xE8s l\\u2019autre.\",\n es: \"Varias im\\xE1genes, una tras otra.\",\n pt: \"V\\xE1rias imagens, uma ap\\xF3s a outra.\"\n },\n \"type.embed.kind.label\": {\n zh: \"\\u6A23\\u5F0F\",\n ja: \"\\u5F62\\u5F0F\",\n en: \"Style\",\n ko: \"\\uD615\\uC2DD\",\n \"zh-Hans\": \"\\u6837\\u5F0F\",\n de: \"Art\",\n fr: \"Style\",\n es: \"Estilo\",\n pt: \"Estilo\"\n },\n \"type.embed.kind.hint\": {\n zh: \"\\u300Cslider\\u300D\\u5DE6\\u53F3\\u6ED1\\u52D5\\uFF0C\\u300Cbubbles\\u300D\\u6CE1\\u6CE1\\u5F48\\u51FA\\u3002\",\n ja: \"\\u300Cslider\\u300D\\u306F\\u6A2A\\u306B\\u30B9\\u30E9\\u30A4\\u30C9\\u3001\\u300Cbubbles\\u300D\\u306F\\u3075\\u308F\\u3063\\u3068\\u5207\\u308A\\u66FF\\u3048\\u3002\",\n en: '\"slider\" slides sideways; \"bubbles\" pops in and out.',\n ko: \"\\u300Cslider\\u300D\\uB294 \\uC606\\uC73C\\uB85C \\uB118\\uAE30\\uACE0,\\u300Cbubbles\\u300D\\uB294 \\uD1A1 \\uB098\\uD0C0\\uB0AC\\uB2E4 \\uC0AC\\uB77C\\uC838\\uC694.\",\n \"zh-Hans\": \"\\u300Cslider\\u300D\\u5DE6\\u53F3\\u6ED1\\u52A8\\uFF0C\\u300Cbubbles\\u300D\\u6CE1\\u6CE1\\u5F39\\u51FA\\u3002\",\n de: \"\\u201Eslider\\u201C gleitet seitw\\xE4rts, \\u201Ebubbles\\u201C blendet ein und aus.\",\n fr: \"\\xAB slider \\xBB glisse lat\\xE9ralement, \\xAB bubbles \\xBB appara\\xEEt et dispara\\xEEt.\",\n es: \"\\xABslider\\xBB se desliza de lado, \\xABbubbles\\xBB aparece y desaparece.\",\n pt: \"\\u201Cslider\\u201D desliza para o lado, \\u201Cbubbles\\u201D aparece e desaparece.\"\n },\n \"type.embed.images.label\": {\n zh: \"\\u5716\\u7247\",\n ja: \"\\u753B\\u50CF\",\n en: \"Pictures\",\n ko: \"\\uC0AC\\uC9C4\",\n \"zh-Hans\": \"\\u56FE\\u7247\",\n de: \"Bilder\",\n fr: \"Images\",\n es: \"Im\\xE1genes\",\n pt: \"Imagens\"\n },\n \"type.embed.images.hint\": {\n zh: \"\\u7528\\u9017\\u865F\\u5206\\u958B\\u6BCF\\u4E00\\u5F35\\u3002\",\n ja: \"\\u30AB\\u30F3\\u30DE\\u3067\\u533A\\u5207\\u308A\\u307E\\u3059\\u3002\",\n en: \"Separate each one with a comma.\",\n ko: \"\\uC27C\\uD45C\\uB85C \\uAD6C\\uBD84\\uD574 \\uC8FC\\uC138\\uC694.\",\n \"zh-Hans\": \"\\u7528\\u9017\\u53F7\\u5206\\u5F00\\u6BCF\\u4E00\\u5F20\\u3002\",\n de: \"Mit Kommas trennen.\",\n fr: \"S\\xE9parez chacune par une virgule.\",\n es: \"Sep\\xE1ralas con comas.\",\n pt: \"Separe cada uma com v\\xEDrgula.\"\n },\n // โ”€โ”€ width, which every tile has โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n \"width.label\": {\n zh: \"\\u5BEC\\u5EA6\",\n ja: \"\\u5E45\",\n en: \"Width\",\n ko: \"\\uB108\\uBE44\",\n \"zh-Hans\": \"\\u5BBD\\u5EA6\",\n de: \"Breite\",\n fr: \"Largeur\",\n es: \"Anchura\",\n pt: \"Largura\"\n },\n \"width.hint\": {\n zh: \"\\u4E00\\u5F35\\u5361\\u6A6B\\u5411\\u5171 6 \\u683C\\u3002\",\n ja: \"\\u30AB\\u30FC\\u30C9\\u306E\\u6A2A\\u5E45\\u306F 6 \\u30DE\\u30B9\\u3067\\u3059\\u3002\",\n en: \"A card is 6 columns wide.\",\n ko: \"\\uCE74\\uB4DC\\uB294 \\uAC00\\uB85C\\uB85C 6 \\uCE78\\uC774\\uC5D0\\uC694.\",\n \"zh-Hans\": \"\\u4E00\\u5F20\\u5361\\u6A2A\\u5411\\u5171 6 \\u683C\\u3002\",\n de: \"Eine Karte ist 6 Spalten breit.\",\n fr: \"Une carte fait 6 colonnes de large.\",\n es: \"Una tarjeta tiene 6 columnas de ancho.\",\n pt: \"Um cart\\xE3o tem 6 colunas de largura.\"\n },\n \"width.6\": {\n zh: \"6\\uFF08\\u6574\\u884C\\uFF09\",\n ja: \"6\\uFF08\\u6A2A\\u3044\\u3063\\u3071\\u3044\\uFF09\",\n en: \"6 (full row)\",\n ko: \"6\\uFF08\\uD55C \\uC904 \\uC804\\uCCB4\\uFF09\",\n \"zh-Hans\": \"6\\uFF08\\u6574\\u884C\\uFF09\",\n de: \"6 (ganze Zeile)\",\n fr: \"6 (toute la ligne)\",\n es: \"6 (fila entera)\",\n pt: \"6 (linha inteira)\"\n },\n \"width.3\": {\n zh: \"3\\uFF08\\u4E00\\u534A\\uFF09\",\n ja: \"3\\uFF08\\u534A\\u5206\\uFF09\",\n en: \"3 (half)\",\n ko: \"3\\uFF08\\uC808\\uBC18\\uFF09\",\n \"zh-Hans\": \"3\\uFF08\\u4E00\\u534A\\uFF09\",\n de: \"3 (die H\\xE4lfte)\",\n fr: \"3 (la moiti\\xE9)\",\n es: \"3 (la mitad)\",\n pt: \"3 (metade)\"\n },\n // โ”€โ”€ what a NEW tile starts as โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n //\n // ๐Ÿฉธ These were Chinese literals too, so a German visitor pressing \"Add a tile โ†’ Link\" watched\n // ใ€Œๆ–ฐ็š„้€ฃ็ตใ€ appear on their own card.\n \"blank.link.text\": {\n zh: \"\\u65B0\\u7684\\u9023\\u7D50\",\n ja: \"\\u65B0\\u3057\\u3044\\u30EA\\u30F3\\u30AF\",\n en: \"New link\",\n ko: \"\\uC0C8 \\uB9C1\\uD06C\",\n \"zh-Hans\": \"\\u65B0\\u7684\\u94FE\\u63A5\",\n de: \"Neuer Link\",\n fr: \"Nouveau lien\",\n es: \"Enlace nuevo\",\n pt: \"Novo link\"\n },\n \"blank.text\": {\n zh: \"\\u5BEB\\u9EDE\\u4EC0\\u9EBC\\u3002\",\n ja: \"\\u3053\\u3053\\u306B\\u6587\\u7AE0\\u3092\\u3002\",\n en: \"Write something.\",\n ko: \"\\uBB34\\uC5B8\\uAC00 \\uC801\\uC5B4 \\uBCF4\\uC138\\uC694.\",\n \"zh-Hans\": \"\\u5199\\u70B9\\u4EC0\\u4E48\\u3002\",\n de: \"Schreiben Sie etwas.\",\n fr: \"\\xC9crivez quelque chose.\",\n es: \"Escribe algo.\",\n pt: \"Escreva alguma coisa.\"\n },\n \"blank.profile\": {\n zh: \"\\u4E00\\u53E5\\u8A71\\u4ECB\\u7D39\\u4F60\\u81EA\\u5DF1\",\n ja: \"\\u3042\\u306A\\u305F\\u3092\\u3072\\u3068\\u3053\\u3068\\u3067\",\n en: \"One line about you\",\n ko: \"\\uB098\\uB97C \\uD55C \\uC904\\uB85C\",\n \"zh-Hans\": \"\\u4E00\\u53E5\\u8BDD\\u4ECB\\u7ECD\\u4F60\\u81EA\\u5DF1\",\n de: \"Eine Zeile \\xFCber Sie\",\n fr: \"Une ligne sur vous\",\n es: \"Una l\\xEDnea sobre ti\",\n pt: \"Uma linha sobre voc\\xEA\"\n }\n };\n var CELL_KEYS = Object.keys(CELL_STRINGS_BY_KEY);\n function cellStrings(localeKey) {\n const out = {};\n for (const [key, row] of Object.entries(CELL_STRINGS_BY_KEY)) {\n const v = row[localeKey];\n out[key] = typeof v === \"string\" ? v : row.en;\n }\n return out;\n }\n var CELL_STRINGS_ZH = cellStrings(\"zh\");\n var text = (table, value) => {\n const v = value == null ? \"\" : String(value);\n return Object.prototype.hasOwnProperty.call(table, v) ? table[v] : v;\n };\n\n // packages/cardtile/w/sandbox-door.mjs\n var HANDLE_RE = /^[a-z0-9][a-z0-9-]{0,38}$/;\n var HANDLE_MAX_LEN = 39;\n var TITLE_MAX_LEN = 120;\n var TAGLINE_MAX_LEN = 200;\n var REAL_CARD_ORIGIN = \"https://feelreef.com\";\n function slugFromName(name) {\n const slug = String(name ?? \"\").trim().toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, HANDLE_MAX_LEN).replace(/-+$/, \"\");\n return HANDLE_RE.test(slug) ? slug : \"\";\n }\n function realCardHref({ name, tagline, draft }) {\n const trimmedName = String(name ?? \"\").trim().slice(0, TITLE_MAX_LEN);\n const handle = slugFromName(name);\n const trimmedTagline = String(tagline ?? \"\").trim().slice(0, TAGLINE_MAX_LEN);\n const params = new URLSearchParams();\n if (handle) params.set(\"handle\", handle);\n if (trimmedName) params.set(\"title\", trimmedName);\n if (trimmedTagline) params.set(\"tagline\", trimmedTagline);\n if (draft) params.set(\"draft\", String(draft));\n const qs = params.toString();\n return `${REAL_CARD_ORIGIN}/dashboard/cards${qs ? `?${qs}` : \"\"}`;\n }\n function sandboxDoorFields(md) {\n const name = /^title:\\s*(.+)$/m.exec(String(md ?? \"\"))?.[1]?.trim() || \"\";\n const cellBody = /^-\\s*\\[ ?\\]\\s*%%\\s*card:\\s*profile\\b[^%]*%%[ \\t]*([^\\n]*)/m.exec(String(md ?? \"\"))?.[1];\n const tagline = (cellBody || \"\").trim();\n return { name, tagline };\n }\n function sandboxDoorHref(md, { draft } = {}) {\n return realCardHref({ ...sandboxDoorFields(md), draft });\n }\n var PARK_PATH = \"/try/park\";\n async function parkSandboxCard(md, { origin = \"\", fetchImpl } = {}) {\n const doFetch = fetchImpl || (typeof fetch === \"function\" ? fetch : null);\n if (!doFetch || !md) return \"\";\n try {\n const r = await doFetch(`${origin}${PARK_PATH}`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"text/markdown; charset=utf-8\" },\n body: md\n });\n if (!r.ok) return \"\";\n const j = await r.json();\n return typeof j.id === \"string\" ? j.id : \"\";\n } catch {\n return \"\";\n }\n }\n\n // packages/cardtile/w2/type-icons.mjs\n var TYPE_ICONS = {\n // a person\n profile: '',\n // a chain link\n link: '',\n // a framed picture โ€” the `feature` type IS \"a picture with a caption\"\n feature: '',\n // prose\n text: '',\n // play\n video: '',\n // a row of dots joined โ€” the social row\n social: '',\n // stacked frames โ€” the slideshow\n embed: ''\n };\n var typeIcon = (type) => TYPE_ICONS[type] || '';\n var typeIconSvg = (type) => '' + typeIcon(type) + \"\";\n\n // packages/cardtile/w2/board-bridge.mjs\n var cardTitle = (md) => /^title:\\s*(.*)$/m.exec(String(md || \"\"))?.[1]?.trim() || \"\";\n function boardLanes(model, ctx = {}) {\n const cells = model.cells || [];\n const blocks = model.blocks && model.blocks.length ? model.blocks : [{ label: \"\", start: 0, count: cells.length }];\n const lanes = blocks.map((b, i) => ({\n key: `face:${i}`,\n kind: \"face\",\n block: i,\n // the first block is the card's front and carries no heading of its own, so it is NAMED here\n title: i === 0 ? ctx.faceLabel || \"Card front\" : b.label || \"\",\n cells: cells.slice(b.start, b.start + b.count)\n }));\n for (const d of model.drawers || []) {\n lanes.push({\n key: `drawer:${d.id}`,\n kind: \"drawer\",\n drawerId: d.id,\n title: d.title || d.id,\n cells: d.cells || []\n });\n }\n return lanes;\n }\n function boardSlots(model, ctx = {}) {\n const out = [];\n for (const lane of boardLanes(model, ctx)) {\n lane.cells.forEach((cell, i) => out.push({ laneKey: lane.key, kind: lane.kind, block: lane.block, drawerId: lane.drawerId, cellIndex: i, cell }));\n }\n return out;\n }\n function slotOfRenderedCell(model, drawerId, index, ctx = {}) {\n const n = Number(index);\n if (!Number.isInteger(n) || n < 0) return -1;\n const blocks = model.blocks && model.blocks.length ? model.blocks : [{ start: 0, count: (model.cells || []).length }];\n return boardSlots(model, ctx).findIndex((s) => drawerId ? s.kind === \"drawer\" && s.drawerId === drawerId && s.cellIndex === n : s.kind === \"face\" && blocks[s.block] && blocks[s.block].start + s.cellIndex === n);\n }\n function cardDropOrder(model, laneKey, renderedSlots, ctx = {}) {\n const order = identityOrder(model, ctx);\n const entry = order.find((e) => e.key === laneKey);\n if (!entry) return null;\n const inLane = new Set(entry.slots);\n const moved = (renderedSlots || []).map(Number);\n const set = new Set(moved);\n if (set.size !== moved.length || moved.some((n) => !inLane.has(n))) return null;\n let k = 0;\n entry.slots = entry.slots.map((n) => set.has(n) ? moved[k++] : n);\n return order;\n }\n function laneKeyForCardAdd(model, drawerId) {\n if (drawerId) return `drawer:${drawerId}`;\n const n = model.blocks && model.blocks.length || 1;\n return `face:${n - 1}`;\n }\n function identityOrder(model, ctx = {}) {\n let n = 0;\n return boardLanes(model, ctx).map((lane) => ({ key: lane.key, slots: lane.cells.map(() => n++) }));\n }\n var firstLine = (s) => String(s || \"\").split(\"\\n\").find((l) => l.trim() !== \"\") || \"\";\n var lines = (s) => String(s || \"\").split(\"\\n\").map((l) => l.trim()).filter(Boolean);\n var flat = (s) => String(s || \"\").replace(/[#\\n]+/g, \" \").replace(/\\s+/g, \" \").trim();\n function opensText(target, ctx = {}) {\n if (!target) return \"\";\n const title = ctx.drawerTitle ? ctx.drawerTitle(target) : target;\n if (!title) return `\\u26A0 ${target}`;\n return String(ctx.opensLabel || \"\\u2192 Opens: {title}\").replace(\"{title}\", title);\n }\n function tileText(cell, ctx = {}) {\n const name = (t) => ctx.typeName ? ctx.typeName(t) : t;\n const p = cell.params || {};\n const head = (s) => `### ${flat(s)}`;\n const target = drawerTargetOf(cell);\n const opens = opensText(target, ctx);\n switch (cell.type) {\n case \"profile\":\n return [head(ctx.title || name(\"profile\")), flat(firstLine(cell.body))].filter(Boolean).join(\"\\n\");\n case \"link\": {\n const { label: label2, url } = linkOf(cell.body);\n return [head(label2 || flat(cell.body) || name(\"link\")), opens || url || \"\", flat(p.sub || \"\")].filter(Boolean).join(\"\\n\");\n }\n case \"feature\":\n return [\n head(flat(p.title) || flat(cell.body) || flat(p.alt) || name(\"feature\")),\n opens || String(p.href || \"\")\n ].filter(Boolean).join(\"\\n\");\n case \"text\":\n return head(flat(firstLine(cell.body)) || name(\"text\"));\n case \"video\":\n return [head(flat(cell.body) || name(\"video\")), String(p.yt || p.channel || \"\")].filter(Boolean).join(\"\\n\");\n case \"social\":\n return [\n head(name(\"social\")),\n String(cell.body || \"\").split(\"\\n\").map((l) => linkOf(l).label || \"\").filter(Boolean).join(\" \\xB7 \")\n ].filter(Boolean).join(\"\\n\");\n case \"embed\":\n return [\n head(name(\"embed\")),\n String(p.images || \"\").split(\",\").filter(Boolean).length ? `${String(p.images || \"\").split(\",\").filter(Boolean).length}` : \"\"\n ].filter(Boolean).join(\"\\n\");\n default:\n return head(flat(firstLine(cell.body)) || cell.type);\n }\n }\n function tileFace(cell, ctx = {}) {\n const name = (t) => ctx.typeName ? ctx.typeName(t) : t;\n const p = cell && cell.params || {};\n const target = drawerTargetOf(cell);\n const opens = opensText(target, ctx);\n const face = { kind: cell && cell.type || \"text\", title: \"\", sub: \"\", img: \"\", icons: [] };\n const marks = (url) => url && !url.startsWith(\"#\") && !url.startsWith(\"mailto:\") ? iconDomains(hostOf(url)).slice(0, 1) : [];\n switch (face.kind) {\n case \"profile\":\n face.title = flat(ctx.title) || name(\"profile\");\n face.sub = flat(firstLine(cell.body));\n face.img = String(p.avatar || \"\");\n return face;\n case \"link\": {\n const { label: label2, url } = linkOf(cell.body);\n face.title = flat(label2) || flat(cell.body) || name(\"link\");\n face.sub = opens || (url ? hostLabel(url) : ctx.unsetLink || \"\");\n face.icons = marks(url);\n return face;\n }\n case \"feature\":\n face.title = flat(p.title) || flat(cell.body) || flat(p.alt) || name(\"feature\");\n face.img = String(p.img || \"\");\n face.sub = opens || (face.img ? flat(p.alt) || flat(cell.body) : ctx.unsetImg || \"\");\n face.icons = opens ? [] : marks(String(p.href || \"\"));\n return face;\n case \"text\": {\n const ls = lines(cell.body);\n face.title = flat(ls[0] || \"\") || name(\"text\");\n face.sub = flat(ls[1] || \"\");\n return face;\n }\n case \"video\": {\n const id = String(p.yt || \"\").trim();\n face.title = flat(cell.body) || flat(p.title) || name(\"video\");\n face.sub = id || String(p.channel || \"\").trim();\n face.img = String(p.poster || \"\") || (id ? posterPath(id) : \"\");\n return face;\n }\n case \"social\": {\n const items = lines(cell.body).map((l) => linkOf(l)).filter((x) => x.url && x.url !== \"#\");\n face.title = name(\"social\");\n face.sub = items.map((x) => x.label || hostLabel(x.url)).join(\" \\xB7 \");\n face.icons = items.flatMap((x) => marks(x.url)).slice(0, 6);\n return face;\n }\n case \"embed\": {\n const imgs = String(p.images || \"\").split(\",\").map((s) => s.trim()).filter(Boolean);\n face.title = name(\"embed\");\n face.sub = imgs.length ? String(imgs.length) : \"\";\n face.img = imgs[0] || \"\";\n return face;\n }\n default:\n face.title = flat(firstLine(cell.body)) || face.kind;\n return face;\n }\n }\n function boardMd(model, ctx = {}) {\n const lines2 = [\"---\", \"kanban-plugin: board\", \"---\", \"\"];\n for (const lane of boardLanes(model, ctx)) {\n lines2.push(`## ${lane.title}`, \"\");\n for (const cell of lane.cells) {\n const text2 = tileText(cell, { ...ctx, title: ctx.title });\n const [head, ...rest] = text2.split(\"\\n\");\n lines2.push(`- [ ] ${head}${rest.map((l) => `\n\t${l}`).join(\"\")}`);\n }\n lines2.push(\"\");\n }\n return lines2.join(\"\\n\");\n }\n function applyBoardOrder(model, order, ctx = {}) {\n const slots = boardSlots(model, ctx);\n const lanes = boardLanes(model, ctx);\n const byKey = new Map(lanes.map((l) => [l.key, l]));\n const seen = /* @__PURE__ */ new Set();\n const pick = (n) => {\n const s = slots[n];\n if (!s || seen.has(n)) return null;\n seen.add(n);\n return s.cell;\n };\n const faceLanes = [];\n const drawerCells = /* @__PURE__ */ new Map();\n for (const entry of order) {\n const lane = byKey.get(entry.key);\n if (!lane) continue;\n const cells2 = (entry.slots || []).map(pick).filter(Boolean);\n if (lane.kind === \"face\") faceLanes.push({ block: lane.block, label: lane.title, cells: cells2 });\n else drawerCells.set(lane.drawerId, cells2);\n }\n for (const lane of lanes) {\n if (order.some((e) => e.key === lane.key)) continue;\n if (lane.kind === \"face\") faceLanes.push({ block: lane.block, label: lane.title, cells: lane.cells });\n else if (!drawerCells.has(lane.drawerId)) drawerCells.set(lane.drawerId, lane.cells);\n }\n const cells = [];\n const blocks = [];\n for (const lane of faceLanes) {\n blocks.push({ label: lane.block === 0 ? \"\" : lane.label, start: cells.length, count: lane.cells.length });\n cells.push(...lane.cells);\n }\n if (!blocks.length) blocks.push({ label: \"\", start: 0, count: 0 });\n const drawers = (model.drawers || []).map((d) => ({ ...d, cells: drawerCells.has(d.id) ? drawerCells.get(d.id) : d.cells }));\n return { ...model, cells, blocks, drawers };\n }\n function drawerTargetOf(cell) {\n if (!cell) return \"\";\n if (cell.type === \"link\") {\n const u = linkOf(cell.body).url || \"\";\n return u.startsWith(\"#\") ? u.slice(1) : \"\";\n }\n const h = String((cell.params || {}).href || \"\");\n return h.startsWith(\"#\") ? h.slice(1) : \"\";\n }\n function drawerLinks(model, ctx = {}) {\n const slots = boardSlots(model, ctx);\n const drawers = new Map((model.drawers || []).map((d) => [d.id, d]));\n const links = [];\n const dangling = [];\n slots.forEach((s, slot) => {\n const id = drawerTargetOf(s.cell);\n if (!id) return;\n const d = drawers.get(id);\n const row = { slot, fromLane: s.laneKey, drawerId: id, drawerLane: `drawer:${id}`, drawerTitle: d ? d.title || d.id : \"\" };\n (d ? links : dangling).push(row);\n });\n return { links, dangling };\n }\n function drawerLabels(model, ctx = {}, strings = {}) {\n const openedBy = strings[\"board.drawerOpenedBy\"] || \"{title} (opened by \\u201C{by}\\u201D)\";\n const { links } = drawerLinks(model, ctx);\n const slots = boardSlots(model, ctx);\n const firstOpener = /* @__PURE__ */ new Map();\n for (const l of links) if (!firstOpener.has(l.drawerId)) firstOpener.set(l.drawerId, l.slot);\n const lanes = (model.drawers || []).map((d) => {\n const title = d.title || d.id;\n const slot = firstOpener.get(d.id);\n const cell = slot == null ? null : (slots[slot] || {}).cell;\n const by = cell ? tileFace(cell, ctx).title : \"\";\n return { laneKey: `drawer:${d.id}`, heading: by ? openedBy.replace(\"{title}\", title).replace(\"{by}\", by) : title };\n });\n return { lanes };\n }\n var BOARD_LIGHT_CSS = `\n :root { color-scheme: light;\n --background-primary: #ffffff; --background-secondary: #f6f6f6;\n --background-modifier-border: #e0e0e0; --background-modifier-border-hover: #d4d4d4;\n --background-modifier-form-field: #ffffff; --background-modifier-hover: rgba(0,0,0,.067);\n --text-normal: #222222; --text-muted: #5c5c5c; --text-faint: #ababab;\n --text-accent: hsl(258, 68%, 52%); --text-error: #c0392b; --text-success: #2f9e5e;\n }\n * { scrollbar-width: thin; scrollbar-color: rgba(128,128,128,.45) transparent; }\n ::-webkit-scrollbar { width: 8px; height: 8px; background: transparent; }\n ::-webkit-scrollbar-track { background: transparent; }\n ::-webkit-scrollbar-thumb { background: rgba(128,128,128,.45); border-radius: 8px; }\n`;\n\n // packages/cardtile/w2/edit2.mjs\n var el = (id) => document.getElementById(id);\n var TYPES = Object.fromEntries(Object.entries(CELL_TYPES).map(([type, def]) => [type, {\n ...def,\n fields: Object.entries(def.params).filter(([, p]) => p.disposition === DISPOSITION.FORM).map(([name, p]) => ({ name, ...p }))\n }]));\n var T = chromeStrings(\"en\");\n var TX = CELL_STRINGS_ZH;\n var TB = boardStrings(\"en\");\n var COLON = colon(\"en\");\n var say = (key) => text(TX, key);\n var S = {\n md: \"\",\n model: null,\n locale: \"en\",\n sandbox: false,\n handle: \"try\",\n cardUrl: \"\",\n host: \"\",\n // HOST MODE: the parent origin (feelreef) โ€” see host-bridge.mjs\n bridge: null,\n tableBase: \"\",\n undo: [],\n // slot โ†” the board's own `data-tid`. Invariant: immediately after `__load`, tid n is slot n,\n // because the host pushes tiles onto one flat array in the reading order of the md we gave it.\n slotOfTid: [],\n wired: false,\n // the iframe document's intercepts are installed once per LOAD\n syncing: false,\n // suppress the observer while WE are the ones rewriting the board\n addLane: null,\n // which lane ใ€Œ๏ผ‹ๅŠ ไธ€ๅผต็‰Œใ€ was pressed in\n openSlot: null,\n // the tile whose sheet is open\n pendingAssets: {},\n // the tile `addCell` just created, if its sheet is still the one open and Done has not fired yet.\n // `{ slot, preMd, undoFloor }` โ€” see `newTileCancelTarget` and `closeModal`. Cleared the moment the\n // sheet closes any other way (Done, Delete) or a different tile's sheet opens.\n newTile: null\n };\n function newTileCancelTarget(track, slot) {\n if (!track || track.slot !== slot) return null;\n return track.preMd;\n }\n var SANDBOX_STORAGE_KEY = \"cardtile:try:draft:v1\";\n var readDraft = () => {\n try {\n return typeof window !== \"undefined\" && window.localStorage.getItem(SANDBOX_STORAGE_KEY) || \"\";\n } catch {\n return \"\";\n }\n };\n var writeDraft = (md) => {\n try {\n if (typeof window !== \"undefined\") window.localStorage.setItem(SANDBOX_STORAGE_KEY, md);\n } catch {\n }\n };\n var clearDraft = () => {\n try {\n if (typeof window !== \"undefined\") window.localStorage.removeItem(SANDBOX_STORAGE_KEY);\n } catch {\n }\n };\n var bridgeCtx = () => ({\n faceLabel: T.faceTabLabel,\n title: cardTitle(S.md),\n typeName: (t) => TYPES[t] ? say(TYPES[t].title) : t,\n opensLabel: TB[\"board.opensLabel\"],\n drawerTitle: (id) => {\n const d = (S.model.drawers || []).find((x) => x.id === id);\n return d ? d.title || d.id : \"\";\n },\n // what the RENDERER writes on a tile whose address or picture is not filled in yet. The table\n // says the same sentence, because the tile is a mini-render of that element and not a label for it.\n unsetLink: T.linkUnset,\n unsetImg: T.imgUnset\n });\n var formCtx = () => ({ T, say, targetOptions, previewSrc, cardTitle: cardTitle(S.md) });\n function commit(nextMd, { undoable = true, reloadTable = true } = {}) {\n if (undoable) S.undo.push(S.md);\n S.md = nextMd;\n S.model = parseCard(S.md);\n if (S.sandbox) writeDraft(S.md);\n if (S.bridge) S.bridge.changed(S.md);\n paintPreview();\n if (reloadTable) loadTable();\n else decorate();\n }\n var edit = (fn) => {\n const next = fn(structuredClone({ ...S.model }));\n commit(serializeCard(next));\n };\n var PREVIEW_GUARD = `\n\n\n\n","tugtile.css":"/* tugtile โ€” markdown kanban (prototype, read-only renderer) */\n\n/* Squircle (continuous-curvature) corners โ€” progressive enhancement.\n corner-shape is Chromium-only as of mid-2026; Safari/WebKit & Firefox ignore it and keep the\n border-radius arc, so this is ZERO visual regression. Requires a non-zero border-radius (still\n set per-surface below). Token lives here because corner-shape is NOT inherited but a custom\n property IS. Applied only to finite-radius card/button/panel/input surfaces โ€” never pills/circles. */\n:root { --corner: squircle; }\n:where(\n .tugtile-brand, .tugtile__lane, .tugtile__tile, .tugtile__num, .tugtile__popup,\n .tugtile__popup-d, .tugtile__popup-t, .tugtile__add-btn,\n .tugtile-prompt-field, .tugtile-iconbtn, .tugtile-ed-find-i,\n .tugtile-viewcycle, .tugtile__archivebar, .tugtile__lane-chevron,\n .tugtile-archive-row, .tugtile__addcol-btn\n) { corner-shape: var(--corner); }\n\n.tugtile {\n height: 100%;\n padding: 0;\n position: relative; /* Contain the view-swap snapshot overlay */\n}\n/* Search/Undo/Redo live in the native back/forward container (repurposed by setupLeftActions), so they line up with the left-sidebar toggle above. */\n.tugtile-leftacts { display: flex; align-items: center; gap: 2px; padding-left: 4px; } /* Nudge right to line up the first icon with the left-sidebar toggle above (auto-measure couldn't find the toggle on iPad) */\n/* iPad only, Obsidian sets `.is-mobile .view-action { margin: auto 0 }` (specificity (0,2,0)) on these addAction\n icons, which misaligns Search/Undo/Redo. This (0,3,0) compound out-specifies it and resets the margin without\n !important โ€” the children always carry both classes (addAction โ†’ .clickable-icon.view-action). */\n.tugtile-leftacts > .clickable-icon.view-action { margin: 0; }\n/* Boardโ‡„table swap: the outgoing snapshot slides up & out while the new view rises from below (two panels shuffling). */\n.tugtile-viewsnap {\n position: absolute; inset: 0; z-index: 3;\n background: var(--background-primary); overflow: hidden; pointer-events: none;\n animation: tugtile-view-out 0.38s cubic-bezier(.7, 0, .3, 1) forwards; /* ease-in-out: slow โ†’ fast โ†’ slow */\n}\n@keyframes tugtile-view-out { from { transform: translateY(0); } to { transform: translateY(-100%); } }\n.tugtile--switching .tugtile__board,\n.tugtile--switching .tugtile__table-wrap { animation: tugtile-view-in 0.38s cubic-bezier(.7, 0, .3, 1); }\n@keyframes tugtile-view-in { from { transform: translateY(100%); } to { transform: translateY(0); } }\n/* Phone: the centered control (view-cycle ยท lock) sits in a top-bar in the content, not the cramped header */\n/* Solid bg + z-index above the view-swap snapshot (z-index 3): the bar stays put on top while board/table\n slide beneath it, so the up-slide animation never leaks through the control row. */\n.tugtile__ctlbar { flex: 0 0 auto; width: 100%; box-sizing: border-box; display: flex; justify-content: center; padding: 2px 8px 8px; position: relative; z-index: 4; background: var(--background-primary); }\n/* Phone: kill the view-content top padding so the control bar sits flush against the header โ€” otherwise that\n strip sits ABOVE the bar and the view-swap snapshot (which covers the whole content) leaks through it. */\nbody.is-phone .workspace-leaf-content[data-type=\"tugtile-board\"] .view-content { padding: 0; } /* All sides: the left/right padding inset the content so the full-width bar still left gaps on both edges */\n/* Greyed-out (disabled-looking) header action โ€” used for undo/redo when their stack is empty, instead of hiding the button */\n.tugtile-act-off { opacity: 0.35; pointer-events: none; }\n/* Dimmed brand suffix appended to the centered header title: \" ยท tugtile-ing\" */\n/* Centered header controls: [view-cycle] ยท [lock]. Sized up (font-size) to roughly match the right-hand toolbar icons. */\n.tugtile-headerctl {\n display: inline-flex; align-items: center; gap: 6px; vertical-align: middle;\n font-size: 18px;\n}\n.tugtile-sep { color: var(--text-faint); } /* Neutral separator, NOT a button โ€” equal button padding + the flex gap keep its two sides symmetric */\n/* Brand suffix + lock icon = one button (t('brandSuffix') + padlock) that toggles the read-only lock */\n.tugtile-brand {\n display: inline-flex; align-items: center; gap: 5px;\n color: var(--text-faint); font-weight: var(--font-normal);\n cursor: pointer; border-radius: var(--radius-s); padding: 2px 6px;\n}\n.tugtile-brand:hover { background: var(--background-modifier-hover); color: var(--text-muted); }\n\n/* TileEditModal (tugtile's big editor) control strip โ€” scoped to the modal so marktile's own strip is untouched.\n A uniform gray bar spanning the full width: โœ• pinned far left, โœ“ far right, the modeยทlock centered between. */\n.tugtile-edit-modal-full .tugtile__ctlbar { background: var(--background-secondary); }\n.tugtile-edit-modal-full .tugtile-headerctl { display: flex; width: 100%; }\n.tugtile-edit-modal-full .tugtile-headerctl > .tugtile-iconbtn:first-child { margin-right: auto; } /* โœ• โ†’ far left */\n.tugtile-edit-modal-full .tugtile-headerctl > .tugtile-iconbtn:last-child { margin-left: auto; } /* โœ“ โ†’ far right */\n.tugtile-lock-icon { display: inline-flex; }\n.tugtile-lock-icon svg { width: 0.9em; height: 0.9em; }\n/* Locked board (read-only): hide add/edit affordances, disable checkboxes, drop drag cursors. View-only actions (collapse, view switch, search) still work. */\n.tugtile--locked .tugtile__add,\n.tugtile--locked .tugtile__addcol,\n.tugtile--locked .tugtile__tile-more,\n.tugtile--locked .tugtile__lane-more { display: none; }\n.tugtile--locked .tugtile__check,\n.tugtile--locked .tugtile__row-title input { pointer-events: none; }\n.tugtile--locked .tugtile__tile,\n.tugtile--locked .tugtile__lane-head { cursor: default; }\n.tugtile__empty {\n padding: 16px;\n color: var(--text-muted);\n}\n.tugtile__board {\n display: flex;\n gap: 12px;\n align-items: flex-start;\n height: 100%;\n overflow-x: auto;\n padding: 12px;\n}\n.tugtile__lane {\n flex: 0 0 auto;\n width: var(--tugtile-lane-width, 300px); /* Uniform fixed column width (configurable in settings); all lanes line up evenly */\n max-height: 100%;\n display: flex;\n flex-direction: column;\n background: var(--background-secondary);\n border-radius: var(--radius-m);\n padding: 8px;\n overflow: hidden; /* Cleanly clips content into the changing lane width during collapse/expand (dragging uses a clone on body and is unaffected) */\n --tg-d1: 0.26s; /* Stage 1: lane narrows + horizontal title tucks into the chevron (collapse) / widens + title grows back (expand) */\n --tg-d2: 0.24s; /* Stage 2: vertical title grows down out of the chevron (collapse) / retracts up into it (expand) */\n transition: width var(--tg-d1) cubic-bezier(.4, 0, .2, 1); /* Smooth width change on collapse/expand */\n}\n.tugtile__lane-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 4px 0 8px; /* Horizontal 0 โ†’ header rails line up with the card column (cards sit at the lane's own 8px padding on both sides) */\n font-weight: 700;\n cursor: grab; /* Header acts as the drag handle for the lane */\n}\n.tugtile__lane-head:active { cursor: grabbing; }\n.tugtile__lane--ghost { opacity: 0.4; }\n.tugtile__lane--chosen { box-shadow: 0 0 0 2px var(--interactive-accent); }\n.tugtile__lane--drag { /* Slightly tilts and lifts the lane when dragging the entire column */\n rotate: 2deg;\n box-shadow: 0 16px 36px rgba(0, 0, 0, 0.5);\n}\n.tugtile__lane-count {\n flex: 0 0 auto;\n opacity: 0.55;\n font-size: 0.85em;\n transition: opacity var(--tg-d1) ease;\n}\n.tugtile__lane-count--over { /* WIP: Warning indicator when exceeding limits */\n color: var(--text-error);\n opacity: 1;\n font-weight: 700;\n}\n.tugtile--hide-counts .tugtile__lane-count {\n display: none;\n}\n.tugtile__list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n overflow-y: auto;\n /* Scrollbar gutter without changing the card layout: the list extends 8px to the right (eating the lane's right padding) via the negative margin, while padding-right keeps the cards in their original position. The iOS/iPadOS overlay scrollbar then sits in that right padding instead of on top of the cards. */\n margin-right: -8px;\n padding-right: 8px;\n}\n.tugtile__tile:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--interactive-accent); } /* keyboard-focused card (arrow keys move it) */\n.tugtile__tile {\n position: relative;\n background: var(--background-primary);\n border: 1px solid var(--background-modifier-border);\n border-radius: var(--radius-m);\n padding: 8px;\n cursor: grab;\n}\n.tugtile__tile:active {\n cursor: grabbing;\n}\n.tugtile__tile:hover {\n border-color: var(--background-modifier-border-hover, var(--interactive-accent));\n}\n/* Dragging state (corresponding to the Sortable class in main.js) */\n/* Placeholder left behind: faded out with a dashed border, similar to a Trello card slot */\n.tugtile__tile--ghost {\n opacity: 0.4;\n background: var(--background-modifier-hover);\n border-style: dashed;\n}\n.tugtile__tile--ghost > * { visibility: hidden; }\n.tugtile__tile--chosen {\n box-shadow: inset 0 0 0 2px var(--interactive-accent); /* Inner ring โ€” an outer ring's left/right 2px gets clipped by the list's overflow (cards are full-width with no horizontal room) */\n}\n/* The card following the cursor feels \"picked up\": slightly tilted, scaled up, with a floating shadow.\n Uses standalone rotate/scale properties (not transform) to avoid overwriting translations applied by Sortable. */\n.tugtile__tile--drag {\n opacity: 0.97;\n rotate: 3deg;\n scale: 1.03;\n box-shadow: 0 14px 32px rgba(0, 0, 0, 0.5);\n cursor: grabbing;\n}\n/* Index number: a rounded inline chip prepended to the card text.\n Key detail: keeping font-size unscaled (matching body text size) aligns the number and adjacent text to the same baseline,\n achieving natural vertical alignment without magic numbers, font, line-height, or scale issues. The background chip sits snug\n around the number (slight horizontal, minimal vertical padding), centering itself on the first line and wrapping naturally. */\n.tugtile__num {\n display: inline-block;\n margin-right: 0.4em;\n padding: 0 0.4em;\n background: var(--interactive-accent);\n color: var(--text-on-accent);\n border-radius: var(--radius-s);\n font-weight: 700;\n line-height: 1.25;\n}\n/* Floating control area in the top-right: checkbox + โ‹ฏ. Using float allows the first line of text to wrap around it, keeping content aligned to the top without wasting space. */\n.tugtile__tile-ctrls {\n float: right;\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0 0 2px 8px;\n}\n.tugtile__tile-body {\n padding-top: 0.15em; /* Subtle top padding for breathing room (kept smaller than the control height so the first line still wraps around it) */\n overflow-wrap: anywhere; /* Alphanumeric text/URLs do not overflow the lane */\n contain: layout paint; /* The body's internal layout is self-contained, so animating the tile's height doesn't re-flow the rendered markdown every frame โ€” kills the stutter on content-heavy tiles */\n}\n/* Long card collapse: hides the full text when folded, showing only the first line of the title (Approach C, without clipping) */\n.tugtile__tile-title {\n display: none;\n padding-top: 0.15em;\n font-weight: 600;\n}\n.tugtile__tile-title > :first-child { margin-top: 0; }\n.tugtile__tile-title > :last-child { margin-bottom: 0; }\n.tugtile__tile--folded .tugtile__tile-body { display: none; }\n.tugtile__tile--folded .tugtile__tile-title { display: block; }\n/* On expand the tile height snaps (one reflow), then the body slides + fades in on the GPU โ€” no per-frame\n layout, so it stays smooth no matter how much content the tile holds. */\n.tugtile__tile--revealing .tugtile__tile-body { animation: tugtile-body-reveal 0.24s cubic-bezier(.4, 0, .2, 1) both; }\n@keyframes tugtile-body-reveal {\n from { opacity: 0; transform: translateY(-6px); }\n to { opacity: 1; transform: none; }\n}.tugtile__tile-body > :first-child {\n margin-top: 0;\n}\n.tugtile__tile-body > :last-child {\n margin-bottom: 0;\n}\n/* Date and time picker popup */\n.tugtile__popup {\n position: fixed;\n z-index: 9999;\n background: var(--background-primary);\n border: 1px solid var(--background-modifier-border);\n border-radius: var(--radius-m);\n box-shadow: var(--shadow-l);\n padding: 8px;\n user-select: none;\n}\n.tugtile__popup-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-weight: 600;\n padding: 2px 4px 8px;\n}\n.tugtile__popup-nav {\n cursor: pointer;\n padding: 0 8px;\n display: inline-flex;\n align-items: center;\n}\n.tugtile__popup-nav svg { width: 1.15em; height: 1.15em; }\n.tugtile__popup-nav:hover { color: var(--text-accent); }\n.tugtile__popup-grid {\n display: grid;\n grid-template-columns: repeat(7, 2em);\n gap: 2px;\n text-align: center;\n}\n.tugtile__popup-w { opacity: 0.5; font-size: 0.8em; }\n.tugtile__popup-d {\n cursor: pointer;\n border-radius: var(--radius-s);\n padding: 3px 0;\n line-height: 1.4;\n}\n.tugtile__popup-d:hover { background: var(--background-modifier-hover); }\n.tugtile__popup-d--today { color: var(--text-accent); font-weight: 700; }\n.tugtile__popup--time {\n max-height: 240px;\n overflow-y: auto;\n padding: 4px;\n}\n.tugtile__popup-t {\n cursor: pointer;\n padding: 4px 16px;\n border-radius: var(--radius-s);\n}\n.tugtile__popup-t:hover { background: var(--background-modifier-hover); }\n/* Tag chips at the bottom of the card (when move-tags-to-card-footer is enabled) */\n.tugtile__tags {\n margin-top: 6px;\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n}\n.tugtile__tag {\n font-size: 0.78em;\n padding: 1px 7px;\n border-radius: 999px;\n background: var(--background-modifier-hover);\n color: var(--text-accent);\n cursor: pointer;\n}\n.tugtile__tag:hover { filter: brightness(1.1); }\n/* Date/time chips at the bottom of the card */\n.tugtile__date {\n margin-top: 6px;\n font-size: 0.78em;\n color: var(--text-muted);\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px; /* separates the date and time chips (replaces the old full-width-space spacer) */\n}\n.tugtile__date-d, .tugtile__date-t { display: inline-flex; align-items: center; gap: 3px; }\n.tugtile__date svg { width: 1em; height: 1em; } /* Lucide calendar/clock, sized to the chip text */\n.tugtile__archivebar-i { display: inline-flex; }\n.tugtile__archivebar svg { width: 1.1em; height: 1.1em; } /* Lucide archive icon (was a ๐Ÿ“ฅ emoji) */\n/* Date status colors: red for overdue, accent for today, yellow for upcoming (โ‰ค7 days), and green for safe (>7 days); colored whenever a date exists */\n.tugtile__date--overdue { color: var(--color-red); font-weight: 600; }\n.tugtile__date--today { color: var(--text-accent); font-weight: 600; }\n.tugtile__date--soon { color: var(--color-yellow); font-weight: 600; }\n.tugtile__date--safe { color: var(--color-green); }\n.tugtile__date--unknown { color: var(--text-muted); } /* Neutral color used when the date cannot be parsed */\n\n/* Freeze container scrolling during dragging (adding .is-dragging in main.js onStart) to prevent native mobile scrolling from fighting the drag gesture */\n.tugtile.is-dragging .tugtile__board,\n.tugtile.is-dragging .tugtile__list {\n /* (0,2,0) already out-specifies the base .tugtile__board/__list (0,1,0) in this file, so no !important needed */\n overflow: hidden;\n touch-action: none;\n}\n/* Add card button and input */\n.tugtile__add {\n padding-top: 8px; /* Match the 8px gap between cards so the add button keeps the same vertical rhythm */\n}\n.tugtile__add-btn {\n width: 100%;\n text-align: left;\n background: transparent;\n border: 1px dashed var(--background-modifier-border);\n border-radius: var(--radius-m);\n color: var(--text-muted);\n padding: 8px; /* Match card padding and the add-lane button (consistent 8px rhythm); left-aligned \"+\" lines up with the card number chip */\n cursor: pointer;\n}\n.tugtile__add-btn:hover {\n background: var(--background-modifier-hover);\n color: var(--text-normal);\n}\n/* โ‹ฏ More actions */\n.tugtile__tile-more {\n color: var(--text-faint);\n line-height: 1;\n cursor: pointer;\n opacity: 0.3;\n display: inline-flex;\n align-items: center;\n}\n.tugtile__tile-more svg { width: 1em; height: 1em; }\n.tugtile__tile-more:hover {\n opacity: 1;\n color: var(--text-normal);\n}\n/* Card checkbox */\n.tugtile__check {\n margin: 0;\n flex: none;\n cursor: pointer;\n}\n/* \"Undo\" button for toast notifications (to restore deleted/archived items in one tap) */\n.tugtile-toast-undo {\n margin-left: 10px;\n font-weight: 700;\n text-decoration: underline;\n cursor: pointer;\n color: var(--text-accent);\n}\n/* Focused editing sheet (integrated with iOS 26 rounded sheet design; hides native โœ• to use our own left โŒ and right โœ“) */\n.tugtile-edit-modal-full {\n width: 100vw;\n max-width: 100vw;\n height: 100dvh; /* Dynamic viewport: shrinks with the on-screen keyboard so the editor stays inside the visible area and the textarea can scroll. dvh is supported on every Obsidian-supported renderer (Electron + iOS/Android webview), so no 100vh fallback is needed. */\n max-height: 100dvh;\n border-radius: 0; /* Full-screen focus editor โ€” nothing of the board shows behind */\n /* Inset content past the iPad status bar / notch / home indicator; env() = 0 on desktop. The solid background still bleeds into these strips. */\n padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);\n overflow: hidden;\n transform-origin: center;\n animation: tugtile-pop-in 0.4s cubic-bezier(.34, 1.56, .64, 1); /* Keep the bounce โ€” full-screen โ‰  unrefined */\n}\n/* Backdrop darkens to solid FAST (so the board vanishes quickly, not lingering behind the bounce); zero container padding */\n.modal-container.tugtile-edit-host { padding: 0; align-items: flex-start; } /* Top-pinned, so shrinking the editor to the visual viewport (keyboard) keeps it above the keyboard. Class set in TileEditModal.onOpen โ€” a direct container target, no parent-match selector. */\n/* The status-bar / home-indicator strips show --background-secondary (white in light, grey in dark) and sit\n outside the modal's reach. So instead of fighting to cover them, dye the WHOLE editor --background-secondary\n to match โ€” seamless in both modes (the old --background-primary only matched in light mode). */\n.modal-container.tugtile-edit-host .modal-bg { background: var(--background-secondary); opacity: 1; animation: tugtile-bg-in 0.12s ease both; }\n.tugtile-edit-modal-full,\n.tugtile-edit-modal-full .tugtile-edit-modal { background: var(--background-secondary); }\n/* macOS: push the editor down so its top bar (โœ•) isn't covered by the window traffic lights (class is .mod-macos) */\n.mod-macos .tugtile-edit-modal-full { padding-top: 28px; }\n@keyframes tugtile-bg-in { from { opacity: 0; } to { opacity: 1; } }\n.tugtile-edit-modal-full.tugtile-ed-closing {\n animation: tugtile-pop-out 0.28s ease-in forwards; /* Exit transition: reverse scaling animation */\n}\n/* Opacity reaches 1 early (~0.1s) so the editor covers the board fast โ€” the scale still bounces to settle. Fixes the \"laggy / see-through during the animation\" feel. */\n@keyframes tugtile-pop-in {\n 0% { opacity: 0; transform: scale(0.92); }\n 35% { opacity: 1; }\n 100% { opacity: 1; transform: scale(1); }\n}\n@keyframes tugtile-pop-out {\n from { opacity: 1; transform: scale(1); }\n to { opacity: 0; transform: scale(0.92); }\n}\n/* In-content text bars (search ยท rename ยท add-lane) โ€” none of these use an Obsidian Modal any more. The view\n renders a strip at the top of its content: .tugtile-searchbar is persistent (toggled by .tugtile--searching),\n .tugtile-promptbar is transient (built on demand for rename / add-lane). Both reuse the .tugtile-prompt-row/\n -field/-input styles below, and the JS freezes the board height while they're open so the virtual keyboard\n can't collapse it into a black gap. On phone they take over the .tugtile__ctlbar slot. */\n.tugtile-searchbar { display: none; }\n.tugtile--searching .tugtile-searchbar,\n.tugtile-promptbar {\n flex: 0 0 auto;\n padding: 4px 8px 8px;\n background: var(--background-primary);\n}\n.tugtile--searching .tugtile-searchbar { display: block; }\nbody.is-phone .tugtile--searching .tugtile__ctlbar,\nbody.is-phone .tugtile--prompting .tugtile__ctlbar { display: none; }\n.tugtile-prompt-row { display: flex; align-items: center; gap: 8px; }\n/* The input is wrapped in a rounded \"field\" so a leading icon (search) can sit inside it */\n.tugtile-prompt-field {\n flex: 1; min-width: 0;\n display: flex; align-items: center; gap: 8px;\n box-sizing: border-box; height: 40px; padding: 0 12px;\n background: var(--background-modifier-form-field);\n border: 1px solid var(--background-modifier-border);\n border-radius: var(--radius-m);\n overflow: hidden; /* Clip the input to the rounded box so it can never poke out of the corners */\n}\n.tugtile-prompt-field:focus-within { border-color: var(--interactive-accent); }\n.tugtile-prompt-fieldicon { flex: 0 0 auto; display: inline-flex; align-items: center; color: var(--text-muted); }\n.tugtile-prompt-fieldicon svg { width: 1.15em; height: 1.15em; }\n/* The input is our OWN contenteditable element (not a native ), so Obsidian's input chrome never\n applies โ€” no overrides, no !important. The .tugtile-prompt-field IS the visible box; this is the bare\n single-line text surface inside it. plaintext-only keeps paste/IME clean (no rich markup, CJK-safe). */\n.tugtile-prompt-input {\n flex: 1; min-width: 0;\n outline: none;\n white-space: pre; overflow-x: auto; overflow-y: hidden; /* single line: scroll sideways, never wrap */\n font-size: 16px; color: var(--text-normal); /* 16px prevents iOS auto-zoom on focus */\n -webkit-user-modify: read-write-plaintext-only;\n}\n.tugtile-prompt-input::-webkit-scrollbar { display: none; } /* hide the horizontal scrollbar on the one-line field */\n.tugtile-prompt-input:empty::before {\n content: attr(data-placeholder);\n color: var(--text-faint);\n pointer-events: none; /* placeholder text isn't selectable / doesn't block the caret */\n}\n/* .tugtile-prompt-x / -ok inherit from .tugtile-iconbtn (the โœ“ colour override lives up there) */\n.tugtile-edit-modal-full .modal-close-button { display: none; } /* Hides the native top-right โœ• button in favor of our custom โŒ button with safety checks */\n.tugtile-edit-modal {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n padding: 0;\n background: var(--background-primary); /* Opaque: in the marktile pane there's no modal-bg behind it, so without this the area the iOS keyboard reveals shows through as a black gap */\n}\n/* marktile lives in a leaf, not a modal โ€” fill the pane opaquely and use the dynamic viewport so the editor\n shrinks WITH the on-screen keyboard instead of leaving a black void below it. */\n.workspace-leaf-content[data-type=\"marktile-editor\"] .view-content {\n padding: 0;\n background: var(--background-primary);\n}\n.workspace-leaf-content[data-type=\"marktile-editor\"] .tugtile-ed-scroll { max-height: 100dvh; }\n.tugtile-ed-bar {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 10px;\n border-bottom: 1px solid var(--background-modifier-border);\n}\n/* โ”€โ”€ Single source of truth for EVERY icon button โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n editor โœ•/โœ“, toolbar tools, find/replace, search โœ•/โœ“. 40ร—40, flat, non-flex centering. Two hard\n rules baked in here so they can never drift again and re-break iPad:\n 1) NO display:flex on the button โ€” iPad WebKit blanks an inline svg that's a flex item.\n 2) the svg is nested in a (done in JS) โ€” iPad WebKit won't render an svg that's a direct\n button child. So a size rule on the svg is safe.\n Per-button classes below only override what's genuinely unique (colour of โœ“, hover colour of โœ•). */\n.tugtile-iconbtn {\n flex: 0 0 auto;\n -webkit-appearance: none; appearance: none;\n position: relative;\n width: 40px; height: 40px;\n border: none; border-radius: var(--radius-m);\n background: transparent; color: var(--text-muted);\n cursor: pointer;\n}\n/* The icon's svg is wrapped in a (the iPad svg-in-button fix). Absolutely-centre that span โ€” true\n pixel-centre, and NO flex on the button (flex on a