@wenathlan/cobaia 0.0.0-stage → 2.0.158

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/App.tsx +52 -0
  2. package/Dockerfile +48 -0
  3. package/_headers +4 -0
  4. package/_redirects +1 -0
  5. package/adsconfig.ts +59 -0
  6. package/biome.jsonc +134 -0
  7. package/bun.lock +2451 -0
  8. package/capacitor.config.ts +51 -0
  9. package/checkoutlinks.ts +80 -0
  10. package/cli.ts +114 -0
  11. package/cobaia.csproj +22 -0
  12. package/cobaia.gemspec +14 -0
  13. package/cobaia.java +16 -0
  14. package/content.ts +425 -0
  15. package/db.ts +219 -0
  16. package/devthink.toml +5 -0
  17. package/docs/features.md +58 -0
  18. package/docs/monetization.md +61 -0
  19. package/familyurl.ts +22 -0
  20. package/favicon.svg +1 -0
  21. package/icon.svg +1 -0
  22. package/index.html +26 -0
  23. package/labels.ts +291 -0
  24. package/manifest.webmanifest +56 -0
  25. package/mime.types +12 -0
  26. package/netlify.toml +2 -0
  27. package/package.json +207 -4
  28. package/pnpm-workspace.yaml +101 -0
  29. package/pnpmboot.package-lock.json +157 -0
  30. package/pnpmboot.package.json +6 -0
  31. package/pom.xml +29 -0
  32. package/posts.ts +54 -0
  33. package/robots.txt +90 -0
  34. package/roster.ts +29 -0
  35. package/sandboxmodality.ts +138 -0
  36. package/schema.prisma +31 -0
  37. package/sitemap.xml +42 -0
  38. package/sol/Sol.tsx +91 -0
  39. package/sol/apps/apps.tsx +66 -0
  40. package/sol/blog/blog.tsx +86 -0
  41. package/sol/home/home.tsx +140 -0
  42. package/sol/intro/intro.tsx +187 -0
  43. package/sol/legal/legal.tsx +76 -0
  44. package/sol/notfound/notfound.tsx +32 -0
  45. package/sol/pricing/pricing.tsx +98 -0
  46. package/sol/shell/Shell.tsx +161 -0
  47. package/sol/sol.css +1951 -0
  48. package/sol/support/support.tsx +102 -0
  49. package/sol/wingets/IntroFooter.tsx +69 -0
  50. package/sol/wingets/IntroPlans.tsx +93 -0
  51. package/sol/wingets/IntroSplash.tsx +105 -0
  52. package/sol/wingets/ShellAdsslot.tsx +47 -0
  53. package/sol/wingets/ShellBrand.tsx +86 -0
  54. package/sol/wingets/index.ts +18 -0
  55. package/tests/adsconfig.test.ts +51 -0
  56. package/tests/checkoutlinks.test.ts +73 -0
  57. package/tests/familyurl.test.ts +40 -0
  58. package/tests/posts.test.ts +76 -0
  59. package/tests/sandboxmodality.test.ts +77 -0
  60. package/tsconfig.json +22 -0
  61. package/tsconfig.node.json +22 -0
  62. package/vercel.json +3 -0
  63. package/vite.config.ts +44 -0
  64. package/vitest.config.ts +42 -0
  65. package/wrangler.toml +7 -0
  66. package/README.md +0 -3
@@ -0,0 +1,187 @@
1
+ /**
2
+ * intro page anchor — layer 3 of the anchor architecture. The file carrying
3
+ * the folder's own name is the path manager of the page: it imports the
4
+ * loose components beside it, mounts the page and re-exports the public
5
+ * component surface. Only the theme anchor (sol/Sol.tsx) consumes this file.
6
+ *
7
+ * The PRESENTATION page of cobaia — the family grammar's last seat (every
8
+ * app of the family carries one): the page mounts BARE at /intro, no window
9
+ * chrome, riding the site voice, not the frame. The akash splash opens over
10
+ * the landing (the mark spring on the overshoot curve, the name rise, the
11
+ * role fade; click/tap/key skip; reduced motion never plays; the no storage
12
+ * law holds — no seen flag, the splash plays per visit and writes nothing),
13
+ * then the landing answers: the hero says what the surface is (the
14
+ * monetization face kept apart so the main sites stay clean), the plans
15
+ * zone renders the real offer from the one checkoutlinks table (the not
16
+ * configured state stays honest), the doors panes open the app directory
17
+ * and the legal page, and the footer carries the doors (the site row, the
18
+ * family row, the legal line). The entrance rides the cb-in stagger under
19
+ * the second, the layers ride the surface-ladder bands, the door panes ride
20
+ * the silk law. Public-site rules: zero network calls from the page itself,
21
+ * React state only; zero decorative dots; zero invented numbers and zero
22
+ * fake testimonials — the real offer, read from the data modules.
23
+ */
24
+
25
+ import type { CSSProperties } from "react";
26
+ import { useCallback, useEffect, useState } from "react";
27
+ import { Link } from "wouter";
28
+ import { label } from "../../labels";
29
+ import { BrandMark, IntroFooter, IntroPlans, IntroSplash } from "../wingets";
30
+
31
+ /** the lifecycle of the opening act: resolving (the motion preference
32
+ * decides) → playing (the akash floor over the landing) → done (the
33
+ * landing alone). No seen state is consulted — the no storage law. */
34
+ type SplashPhase = "resolving" | "playing" | "done";
35
+
36
+ /** the door glyph of the panes: the inline 10×10 chevron the chrome's
37
+ * caption grammar draws (the app carries no icon dependency). */
38
+ function DoorGlyph() {
39
+ return (
40
+ <svg
41
+ className="cb-intropane__glyph"
42
+ width="10"
43
+ height="10"
44
+ viewBox="0 0 10 10"
45
+ aria-hidden="true"
46
+ focusable="false"
47
+ >
48
+ <path d="M3 0l5 5-5 5" fill="none" stroke="currentColor" strokeWidth="1.5" />
49
+ </svg>
50
+ );
51
+ }
52
+
53
+ /**
54
+ * The intro page: the splash plays over the landing, then the bare
55
+ * presentation answers — hero, plans, doors, footer.
56
+ *
57
+ * @returns the intro element (null while the motion preference decides).
58
+ */
59
+ export function Intro() {
60
+ const [splash, setSplash] = useState<SplashPhase>("resolving");
61
+
62
+ // the splash decision rides the motion preference alone — nothing is read
63
+ // from the visitor machine and nothing is written back (the no storage law)
64
+ useEffect(() => {
65
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
66
+ setSplash("done");
67
+ return;
68
+ }
69
+ setSplash("playing");
70
+ }, []);
71
+
72
+ /** the slide completed: the splash unmounts, the landing owns the page. */
73
+ const onSplashDone = useCallback(() => {
74
+ setSplash("done");
75
+ }, []);
76
+
77
+ /** the quiet cue: walks the page to the plans band. */
78
+ const cuePlans = useCallback(() => {
79
+ const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
80
+ document.getElementById("intro-plans")?.scrollIntoView({ behavior: reduced ? "auto" : "smooth", block: "start" });
81
+ }, []);
82
+
83
+ // the phase decision holds the paint: nothing renders while it resolves —
84
+ // no flash of the landing under a splash that will never play
85
+ if (splash === "resolving") return null;
86
+
87
+ return (
88
+ <div className="cb-intro" data-page="intro">
89
+ {/* the skip link rides outside no frame — the bare page answers the
90
+ * viewport directly; the splash sits above it while it plays */}
91
+ <a className="cb-skip" href="#intro-content">
92
+ {label("shell.skip")}
93
+ </a>
94
+
95
+ {/* the akash floor: the opening act rides over the landing until its
96
+ * slide completes; reduced motion never mounts it */}
97
+ {splash === "playing" && <IntroSplash onDone={onSplashDone} />}
98
+
99
+ {/* the intro bar: the wordmark and the two quiet doors — the ONE
100
+ * primary cta lives in the hero */}
101
+ <header className="cb-introbar">
102
+ <div className="cb-introbar__in">
103
+ <span className="cb-introbar__brand">
104
+ <BrandMark size={20} />
105
+ <span className="cb-introbar__word">{label("shell.name")}</span>
106
+ </span>
107
+ <span className="cb-introbar__spacer" />
108
+ <nav className="cb-introbar__nav" aria-label={label("intro.bar.aria")}>
109
+ <Link className="cb-introdoor" href="/apps">
110
+ {label("shell.foot.apps")}
111
+ </Link>
112
+ <Link className="cb-introdoor" href="/pricing">
113
+ {label("shell.foot.pricing")}
114
+ </Link>
115
+ </nav>
116
+ </div>
117
+ </header>
118
+
119
+ <main id="intro-content">
120
+ {/* the hero: what the surface is, said directly */}
121
+ <section className="cb-introhero" aria-label={label("intro.aria.page")}>
122
+ <div className="cb-introwrap">
123
+ <span className="cb-introhero__mark cb-in" style={{ "--cb-d": 0 } as CSSProperties} aria-hidden="true">
124
+ <BrandMark size={64} />
125
+ </span>
126
+ <p className="cb-eyebrow cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
127
+ {label("intro.hero.eyebrow")}
128
+ </p>
129
+ <h1 className="cb-title cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
130
+ {label("intro.hero.title")}
131
+ </h1>
132
+ <p className="cb-lede cb-in" style={{ "--cb-d": 3 } as CSSProperties}>
133
+ {label("intro.hero.lede")}
134
+ </p>
135
+ <div className="cb-btnrow cb-in" style={{ "--cb-d": 4 } as CSSProperties}>
136
+ <Link className="cb-btn" href="/">
137
+ {label("intro.hero.cta")}
138
+ </Link>
139
+ <button type="button" className="cb-btn cb-btn--ghost" onClick={cuePlans}>
140
+ {label("intro.hero.cue")}
141
+ </button>
142
+ </div>
143
+ <p className="cb-note cb-in" style={{ "--cb-d": 5 } as CSSProperties}>
144
+ {label("intro.hero.note")}
145
+ </p>
146
+ </div>
147
+ </section>
148
+
149
+ {/* the plans as they really are (the one checkoutlinks table) */}
150
+ <IntroPlans />
151
+
152
+ {/* the doors: the app directory and the legal page, on silk panes */}
153
+ <section className="cb-introband cb-introband--deep" aria-label={label("intro.doors.aria")}>
154
+ <div className="cb-introwrap">
155
+ <div className="cb-secthead cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
156
+ <p className="cb-eyebrow">{label("intro.doors.eyebrow")}</p>
157
+ <h2 className="cb-secttitle">{label("intro.doors.title")}</h2>
158
+ </div>
159
+ <div className="cb-introdoorgrid">
160
+ <article className="cb-intropane cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
161
+ <p className="cb-cardmeta">{label("intro.doors.apps")}</p>
162
+ <p className="cb-intropane__text">{label("intro.doors.appsText")}</p>
163
+ <Link className="cb-intropane__door" href="/apps">
164
+ {label("intro.doors.appsCta")}
165
+ <DoorGlyph />
166
+ </Link>
167
+ </article>
168
+ <article className="cb-intropane cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
169
+ <p className="cb-cardmeta">{label("intro.doors.legal")}</p>
170
+ <p className="cb-intropane__text">{label("intro.doors.legalText")}</p>
171
+ <Link className="cb-intropane__door" href="/legal">
172
+ {label("intro.doors.legalCta")}
173
+ <DoorGlyph />
174
+ </Link>
175
+ </article>
176
+ </div>
177
+ </div>
178
+ </section>
179
+ </main>
180
+
181
+ {/* the footer carries the doors: the site row, the family row, the line */}
182
+ <IntroFooter />
183
+ </div>
184
+ );
185
+ }
186
+
187
+ export default Intro;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * legal page anchor — layer 3 of the anchor architecture.
3
+ * The file carrying the folder's own name is the path manager of the page:
4
+ * it imports the loose components beside it, mounts the page and re-exports
5
+ * the public component surface. Only the theme anchor (sol/Sol.tsx) consumes
6
+ * this file. The legal page of the cobaia content site: the privacy policy
7
+ * and the terms, written seriously and directly. The page discloses the
8
+ * advertising plan (labeled, reserved slots, third-party cookies only once
9
+ * the network is approved and live) and the analytics rule (none by default,
10
+ * opt-in and disclosed here first if that ever changes). No ad slot lives on
11
+ * this page: it is the compliance and health-check surface of the site.
12
+ * every paragraph rides the label accessor — the store keeps the sentence,
13
+ * the anchor keeps the rendering.
14
+ */
15
+ import type { CSSProperties } from "react";
16
+ import { label } from "../../labels";
17
+
18
+ /** one legal paragraph: the strong lead and its body, both from the store. */
19
+ function Clause({ head, text }: { head: string; text: string }) {
20
+ return (
21
+ <p>
22
+ <strong>{head}</strong> {text}
23
+ </p>
24
+ );
25
+ }
26
+
27
+ export function Legal() {
28
+ return (
29
+ <div className="cb-page">
30
+ <section className="cb-hero" aria-label={label("legal.aria.hero")}>
31
+ <p className="cb-eyebrow cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
32
+ {label("legal.eyebrow")}
33
+ </p>
34
+ <h1 className="cb-title cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
35
+ {label("legal.title")}
36
+ </h1>
37
+ <p className="cb-lede cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
38
+ {label("legal.lede")}
39
+ </p>
40
+ </section>
41
+
42
+ <article
43
+ className="cb-article cb-in"
44
+ style={{ "--cb-d": 1 } as CSSProperties}
45
+ aria-label={label("legal.privacy.aria")}
46
+ >
47
+ <h2 className="cb-secttitle">{label("legal.privacy.title")}</h2>
48
+ <div className="cb-articlebody">
49
+ <Clause head={label("legal.privacy.collect.head")} text={label("legal.privacy.collect.text")} />
50
+ <Clause head={label("legal.privacy.logs.head")} text={label("legal.privacy.logs.text")} />
51
+ <Clause head={label("legal.privacy.ads.head")} text={label("legal.privacy.ads.text")} />
52
+ <Clause head={label("legal.privacy.analytics.head")} text={label("legal.privacy.analytics.text")} />
53
+ <Clause head={label("legal.privacy.payments.head")} text={label("legal.privacy.payments.text")} />
54
+ <Clause head={label("legal.privacy.contact.head")} text={label("legal.privacy.contact.text")} />
55
+ </div>
56
+ </article>
57
+
58
+ <article
59
+ className="cb-article cb-in"
60
+ style={{ "--cb-d": 2 } as CSSProperties}
61
+ aria-label={label("legal.terms.aria")}
62
+ >
63
+ <h2 className="cb-secttitle">{label("legal.terms.title")}</h2>
64
+ <div className="cb-articlebody">
65
+ <Clause head={label("legal.terms.service.head")} text={label("legal.terms.service.text")} />
66
+ <Clause head={label("legal.terms.use.head")} text={label("legal.terms.use.text")} />
67
+ <Clause head={label("legal.terms.data.head")} text={label("legal.terms.data.text")} />
68
+ <Clause head={label("legal.terms.ip.head")} text={label("legal.terms.ip.text")} />
69
+ <Clause head={label("legal.terms.changes.head")} text={label("legal.terms.changes.text")} />
70
+ </div>
71
+ </article>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ export default Legal;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * notfound page anchor — layer 3 of the anchor architecture.
3
+ * The file carrying the folder's own name is the path manager of the page:
4
+ * it imports the loose components beside it, mounts the page and re-exports
5
+ * the public component surface. Only the theme anchor (sol/Sol.tsx) consumes
6
+ * this file. The cobaia copy answers for the family site: the address is not
7
+ * part of the family — the way out is the home and the directory.
8
+ */
9
+
10
+ import { Link } from "wouter";
11
+
12
+ import { label } from "../../labels";
13
+
14
+ export function NotFound() {
15
+ return (
16
+ <div className="cb-page cb-missing">
17
+ <p className="cb-eyebrow">{label("notfound.eyebrow")}</p>
18
+ <h1 className="cb-title">{label("notfound.title")}</h1>
19
+ <p className="cb-lede">{label("notfound.lede")}</p>
20
+ <div className="cb-btnrow">
21
+ <Link className="cb-btn" href="/">
22
+ {label("notfound.action.home")}
23
+ </Link>
24
+ <Link className="cb-btn cb-btn--ghost" href="/apps">
25
+ {label("notfound.action.apps")}
26
+ </Link>
27
+ </div>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ export default NotFound;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * pricing page anchor — layer 3 of the anchor architecture.
3
+ * The file carrying the folder's own name is the path manager of the page:
4
+ * it imports the loose components beside it, mounts the page and re-exports
5
+ * the public component surface. Only the theme anchor (sol/Sol.tsx) consumes
6
+ * this file.
7
+ *
8
+ * The pricing table of the cobaia content site: three honest plans (free,
9
+ * pro, studio — us$0 / 5 / 19 a month, placeholders until billing lands).
10
+ * The billing values answer from the pure config at the app root
11
+ * (checkoutlinks.ts — ids, prices, checkout hrefs) and every plan sentence
12
+ * (name, blurb, features) rides the label store: a configured link renders
13
+ * a real anchor, an empty one renders the disabled button with the honest
14
+ * note "checkout not configured yet". No checkout page carries an ad — the
15
+ * ad slots live only on the content pages. Zero network calls: the links
16
+ * are plain hrefs the visitor chooses to follow.
17
+ */
18
+ import type { CSSProperties } from "react";
19
+ import { checkoutlinks, type checkoutplan, isconfigured } from "../../checkoutlinks";
20
+ import { useContent } from "../../content.ts";
21
+ import { label, labelfill } from "../../labels";
22
+
23
+ /** renders one plan's checkout control: a live anchor when the owner
24
+ * configured the link, the honest disabled state until then. */
25
+ function PlanAction({ plan }: { plan: checkoutplan }) {
26
+ if (isconfigured(plan.href)) {
27
+ return (
28
+ <a className="cb-btn" href={plan.href} rel="noreferrer">
29
+ {labelfill("pricing.action.choose", { name: label(plan.nameKey) })}
30
+ </a>
31
+ );
32
+ }
33
+ return (
34
+ <>
35
+ <button className="cb-btn" type="button" disabled>
36
+ {labelfill("pricing.action.choose", { name: label(plan.nameKey) })}
37
+ </button>
38
+ <p className="cb-note">{label("pricing.action.notConfigured")}</p>
39
+ </>
40
+ );
41
+ }
42
+
43
+ export function Pricing() {
44
+ useContent();
45
+ return (
46
+ <div className="cb-page">
47
+ <section className="cb-hero" aria-label={label("pricing.aria.hero")}>
48
+ <p className="cb-eyebrow cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
49
+ {label("pricing.eyebrow")}
50
+ </p>
51
+ <h1 className="cb-title cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
52
+ {label("pricing.title")}
53
+ </h1>
54
+ <p className="cb-lede cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
55
+ {label("pricing.lede")}
56
+ </p>
57
+ </section>
58
+
59
+ <section className="cb-section" aria-label={label("pricing.section.plansAria")}>
60
+ <div className="cb-plans">
61
+ {checkoutlinks().map((plan, index) => (
62
+ <article
63
+ key={plan.id}
64
+ className={plan.id === "studio" ? "cb-plan cb-plan--lead cb-in" : "cb-plan cb-in"}
65
+ style={{ "--cb-d": index + 1 } as CSSProperties}
66
+ aria-label={labelfill("pricing.card.planAria", { name: label(plan.nameKey) })}
67
+ >
68
+ <p className="cb-planname">{label(plan.nameKey)}</p>
69
+ <p className="cb-planprice">
70
+ ${plan.priceusd}
71
+ <span className="cb-planper"> / {label(plan.perKey)}</span>
72
+ </p>
73
+ <p className="cb-planblurb">{label(plan.blurbKey)}</p>
74
+ <ul className="cb-planfeats">
75
+ {plan.featureKeys.map((featureKey) => (
76
+ <li key={featureKey} className="cb-planfeat">
77
+ {label(featureKey)}
78
+ </li>
79
+ ))}
80
+ </ul>
81
+ <PlanAction plan={plan} />
82
+ </article>
83
+ ))}
84
+ </div>
85
+ </section>
86
+
87
+ <section className="cb-section" aria-label={label("pricing.section.moneyAria")}>
88
+ <div className="cb-secthead">
89
+ <h2 className="cb-secttitle">{label("pricing.section.moneyTitle")}</h2>
90
+ <p className="cb-sectlede">{label("pricing.section.moneyLede")}</p>
91
+ </div>
92
+ <p className="cb-note is-live">{label("pricing.section.moneyNote")}</p>
93
+ </section>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ export default Pricing;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * shell.tsx — the ONE chrome of the cobaia content site under the family
3
+ * fullscreen law: the direct url owns the whole viewport (no floating
4
+ * window, no title bar, no captions — the window chrome exists only inside
5
+ * the DevThink OS power clip, and the OS draws that window itself), so the
6
+ * site renders full-bleed with its own voice — the sticky section bar (the
7
+ * acrylic nav pins over the drawn home mark), the page surface and the
8
+ * worked footer carrying the family row: every deploy unit of the family
9
+ * linked through familyurl(), because the family redirects both ways. The
10
+ * bar rides the trylle canvas with the Windows hands: the section links
11
+ * are the pins — the acrylic bar, the hover wash, the press bounce (scale
12
+ * 0.7, 100ms), the dtPopIntro mount pop and the 3px ladder indicator that
13
+ * grows under the active section; the focus underline and the active pin
14
+ * answer the Windows accent — the identity amber lives only in the brand
15
+ * mark. The active section link reads the location so the visitor always
16
+ * knows where they stand. Inside the OS power clip the embedded contract
17
+ * renders the SAME site surface under the shell--embedded seat: the
18
+ * sticky section bar, the page surface and the worked footer answer in
19
+ * both seats so the design is IDENTICAL at the direct url and inside the
20
+ * os window (the window-era law) — the host window owns the chrome
21
+ * there. Styled entirely by sol/sol.css (the fullscreen law
22
+ * block carries the two contracts).
23
+ */
24
+ import type { CSSProperties, ReactNode } from "react";
25
+ import { Link, useLocation } from "wouter";
26
+ import { useContent } from "../../content.ts";
27
+ import { familyurl } from "../../familyurl";
28
+ import { type LabelKey, label } from "../../labels";
29
+ import { roster } from "../../roster.ts";
30
+ import { BrandMark } from "../wingets";
31
+
32
+ /** the section links of the top bar, in reading order (home is the mark).
33
+ * the href rides the route table, the text rides the label accessor. */
34
+ const NAVLINKS: readonly { href: string; key: LabelKey }[] = [
35
+ { href: "/apps", key: "shell.nav.apps" },
36
+ { href: "/pricing", key: "shell.nav.pricing" },
37
+ { href: "/blog", key: "shell.nav.blog" },
38
+ { href: "/support", key: "shell.nav.support" },
39
+ ];
40
+
41
+ /** the shared chrome: the full-bleed site surface — the sticky section bar
42
+ * (with the ONE logo slot as the home mark), the page surface and the
43
+ * footer row; the embedded pass renders the SAME site surface inside the
44
+ * os power clip (the window-era law: one design in both seats). */
45
+ export function Shell({ children }: { children: ReactNode }) {
46
+ useContent();
47
+ const [location] = useLocation();
48
+
49
+ // the embedded contract of the family fullscreen law: inside the os
50
+ // power clip the app rides the host window chrome — the seat class
51
+ // answers the mount, the tree never changes between the two doors
52
+ // (the window-era law: one design in both seats)
53
+ const framed = (() => {
54
+ try {
55
+ return window.self !== window.top;
56
+ } catch {
57
+ return true;
58
+ }
59
+ })();
60
+
61
+ const isActive = (href: string): boolean =>
62
+ href === "/" ? location === "/" : location === href || location.startsWith(`${href}/`);
63
+
64
+ // the embedded seat: inside the os power clip the app rides the host
65
+ // window chrome, but the SURFACE stays the same — the seat class answers
66
+ // the mount so differential styles (if any) can find it, the tree never
67
+ // changes between the two doors (the window-era law: one design)
68
+ const seat = framed ? "shell--embedded" : "shell--standalone";
69
+
70
+ // the surface pass: the direct url answers full-bleed — the window
71
+ // chrome exists only inside the os power clip, so the site owns the whole
72
+ // viewport; the sticky section bar stays as the site's own navigation,
73
+ // the page surface fills the middle and the worked footer closes it
74
+ return (
75
+ <div className={`shell ${seat}`}>
76
+ {/* the skip link answers the viewport: the surface owns the whole
77
+ * document, the skip jumps straight to the content anchor */}
78
+ <a className="cb-skip" href="#content">
79
+ {label("shell.skip")}
80
+ </a>
81
+ <div className="cb-root">
82
+ <header className="cb-top">
83
+ <nav className="cb-nav" aria-label={label("shell.nav.aria")}>
84
+ <Link
85
+ className="cb-brand"
86
+ href="/"
87
+ aria-label={label("shell.brand.aria")}
88
+ aria-current={isActive("/") ? "page" : undefined}
89
+ >
90
+ <BrandMark size={18} />
91
+ <strong>{label("shell.name")}</strong>
92
+ <span className="cb-brandtag">{label("shell.role")}</span>
93
+ </Link>
94
+ {NAVLINKS.map((link) => (
95
+ <Link
96
+ key={link.href}
97
+ className="cb-navlink"
98
+ href={link.href}
99
+ aria-current={isActive(link.href) ? "page" : undefined}
100
+ >
101
+ {label(link.key)}
102
+ </Link>
103
+ ))}
104
+ </nav>
105
+ </header>
106
+
107
+ <main id="content" className="cb-main">
108
+ {children}
109
+ </main>
110
+
111
+ <footer className="cb-foot">
112
+ <div className="cb-footgrid">
113
+ <div className="cb-footcol cb-footabout">
114
+ <p className="cb-foothead">{label("shell.foot.aboutHead")}</p>
115
+ <p className="cb-foottext">{label("shell.foot.aboutText")}</p>
116
+ </div>
117
+ <div className="cb-footcol">
118
+ <p className="cb-foothead">{label("shell.foot.siteHead")}</p>
119
+ <Link className="cb-footlink" href="/apps">
120
+ {label("shell.foot.apps")}
121
+ </Link>
122
+ <Link className="cb-footlink" href="/pricing">
123
+ {label("shell.foot.pricing")}
124
+ </Link>
125
+ <Link className="cb-footlink" href="/blog">
126
+ {label("shell.foot.blog")}
127
+ </Link>
128
+ <Link className="cb-footlink" href="/support">
129
+ {label("shell.foot.support")}
130
+ </Link>
131
+ <Link className="cb-footlink" href="/legal">
132
+ {label("shell.foot.legal")}
133
+ </Link>
134
+ </div>
135
+ <div className="cb-footcol cb-footfamily">
136
+ <p className="cb-foothead">{label("shell.foot.familyHead")}</p>
137
+ <div className="cb-famgrid">
138
+ {roster().map((member) => (
139
+ <a
140
+ key={member.slug}
141
+ className="cb-famlink"
142
+ href={familyurl(member.slug)}
143
+ target="_blank"
144
+ rel="noreferrer"
145
+ style={{ "--cb-dot": member.accent } as CSSProperties}
146
+ >
147
+ <span className="cb-famdot" aria-hidden="true" />
148
+ {member.slug}
149
+ </a>
150
+ ))}
151
+ </div>
152
+ </div>
153
+ </div>
154
+ <p className="cb-colophon">{label("shell.colophon")}</p>
155
+ </footer>
156
+ </div>
157
+ </div>
158
+ );
159
+ }
160
+
161
+ export default Shell;