feat: integrate Just Works app and native business setup
Preserve hosted website UX inside Archipelago, add isolated Business and owner-authorized native wallet setup, and include deployment handoff. Based on main with native signer import already accepted.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
// Connector for the native OS guide. No Just Works UI is replaced.
|
||||
const base = new URL('..', import.meta.url).pathname.replace(/\/?$/, '/');
|
||||
let parentOrigin;
|
||||
try { parentOrigin = new URL(document.referrer).origin; } catch { /* standalone */ }
|
||||
if (parentOrigin && new URL(parentOrigin).hostname === location.hostname && new URL(parentOrigin).protocol === location.protocol && window.parent !== window) {
|
||||
window.addEventListener('message', async event => {
|
||||
if (event.source !== parent || event.origin !== parentOrigin || typeof event.data.requestId !== 'string') return;
|
||||
const requestId = event.data.requestId;
|
||||
try {
|
||||
if (event.data.type === 'justworks:get-connection') {
|
||||
const siteId = localStorage.getItem('archipelago-justworks-site-v1') || '';
|
||||
let npub = '';
|
||||
if (siteId) {
|
||||
const response = await fetch(`${base}api/site?url=${encodeURIComponent(siteId)}`, {cache:'no-store', signal:AbortSignal.timeout(12000)});
|
||||
const site = await response.json();
|
||||
if (response.ok && site.id === siteId) npub = site.npub;
|
||||
}
|
||||
parent.postMessage({type:'justworks:connection', requestId, siteId, npub}, parentOrigin);
|
||||
return;
|
||||
}
|
||||
if (event.data.type === 'justworks:business-login') {
|
||||
const response = await fetch(`${base}business-demo/api/business-owner-login`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(event.data.payload), signal:AbortSignal.timeout(60000)});
|
||||
const result = await response.json();
|
||||
parent.postMessage({type:'justworks:business-result', requestId, ok:response.ok && result.ok === true, npub:result.npub, error:result.error}, parentOrigin);
|
||||
return;
|
||||
}
|
||||
if (event.data.type === 'justworks:connect-payment') {
|
||||
const response = await fetch(`${base}api/payment-connect`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(event.data.payload), signal:AbortSignal.timeout(170000)});
|
||||
const result = await response.json();
|
||||
parent.postMessage({type:'justworks:payment-result', requestId, ...result}, parentOrigin);
|
||||
return;
|
||||
}
|
||||
if (event.data.type !== 'justworks:check-site' || typeof event.data.url !== 'string') return;
|
||||
const response = await fetch(`${base}api/site?url=${encodeURIComponent(event.data.url)}`, {cache:'no-store', signal:AbortSignal.timeout(15000)});
|
||||
const site = await response.json();
|
||||
if (!response.ok || site.published !== true) throw new Error(site.error || 'Publish your website in Just Works, then try again.');
|
||||
let remembered = true;
|
||||
try { localStorage.setItem('archipelago-justworks-site-v1', site.id); } catch { remembered = false; }
|
||||
parent.postMessage({type:'justworks:site-connected', requestId, site:{id:site.id,published:true}, remembered}, parentOrigin);
|
||||
} catch (error) {
|
||||
parent.postMessage({type:'justworks:site-error', requestId, error:error.message}, parentOrigin);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/* Just Works design system: load this single file before page CSS. */
|
||||
@import url("./tokens.css");
|
||||
@import url("./ds/base.css");
|
||||
@import url("./ds.css");
|
||||
@import url("./ds/components.css");
|
||||
@import url("./ds/topbar.css");
|
||||
@import url("./ds/mobile.css");
|
||||
@import url("./ds/presets.css");
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Design-system chrome shared with cash-just-works: the three-way theme
|
||||
* toggle and the brand mark. Ported verbatim from public/css/components.css
|
||||
* in the cash-just-works repo — colors resolve through tokens.css, so this
|
||||
* follows the same light/dark rules as the source site.
|
||||
*/
|
||||
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--jw-line);
|
||||
border-radius: var(--jw-radius-pill);
|
||||
background: var(--jw-surface-sunken);
|
||||
}
|
||||
|
||||
.theme-toggle button {
|
||||
width: 32px;
|
||||
height: 28px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: var(--jw-radius-pill);
|
||||
background: transparent;
|
||||
color: var(--jw-text-muted);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--jw-duration-base) var(--jw-ease-out),
|
||||
color var(--jw-duration-base) var(--jw-ease-out);
|
||||
}
|
||||
|
||||
.theme-toggle button:hover { color: var(--jw-text-primary); }
|
||||
|
||||
.theme-toggle button[aria-pressed="true"] {
|
||||
background: var(--jw-surface);
|
||||
color: var(--jw-text-primary);
|
||||
box-shadow: var(--jw-shadow-xs);
|
||||
}
|
||||
|
||||
.theme-toggle button:focus-visible {
|
||||
outline: 2px solid var(--jw-field-border-focus);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.theme-toggle svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.8;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* Brand mark: one continuous line, path swapped by assets/ds/logo.js. */
|
||||
|
||||
.jw-mark {
|
||||
width: var(--jw-mark-width, 2.1em);
|
||||
height: auto;
|
||||
flex: none;
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.jw-mark-portal {
|
||||
width: auto;
|
||||
height: var(--jw-mark-height, 1.35em);
|
||||
}
|
||||
|
||||
.jw-mark-portal { filter: var(--jw-portal-depth); }
|
||||
|
||||
.jw-portal-frame { fill: var(--jw-text-primary); }
|
||||
.jw-portal-head { stop-color: var(--jw-text-subtle); }
|
||||
.jw-portal-floor { stop-color: transparent; }
|
||||
|
||||
.jw-mark-path {
|
||||
fill: none;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.jw-mark-stop-a { stop-color: var(--jw-accent); }
|
||||
.jw-mark-stop-b { stop-color: var(--jw-highlight); }
|
||||
|
||||
.brand .jw-mark { margin-right: -0.15em; }
|
||||
|
||||
/* App icon: the mark in a translucent rimmed container. Fixed dark ground in
|
||||
both themes on purpose (see tokens.css). */
|
||||
|
||||
.jw-app-icon {
|
||||
position: relative;
|
||||
width: var(--jw-app-icon-size);
|
||||
height: var(--jw-app-icon-size);
|
||||
flex: none;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: var(--jw-app-icon-radius);
|
||||
background: var(--jw-app-icon-bg);
|
||||
-webkit-backdrop-filter: blur(var(--jw-app-icon-blur));
|
||||
backdrop-filter: blur(var(--jw-app-icon-blur));
|
||||
}
|
||||
|
||||
.jw-app-icon::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: var(--jw-app-icon-rim-width);
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--jw-app-icon-rim-bright) 0%,
|
||||
var(--jw-app-icon-rim-fade) 32%,
|
||||
var(--jw-app-icon-rim-fade) 68%,
|
||||
var(--jw-app-icon-rim-bright) 100%
|
||||
);
|
||||
-webkit-mask:
|
||||
linear-gradient(#000 0 0) content-box,
|
||||
linear-gradient(#000 0 0);
|
||||
mask:
|
||||
linear-gradient(#000 0 0) content-box,
|
||||
linear-gradient(#000 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.jw-app-icon .jw-mark.jw-mark-portal,
|
||||
.jw-app-icon .jw-mark.jw-mark-line {
|
||||
width: auto;
|
||||
height: calc(var(--jw-app-icon-size) * 0.52);
|
||||
transform: translateY(calc(var(--jw-app-icon-size) * -0.025));
|
||||
}
|
||||
|
||||
.brand .jw-app-icon { margin-right: var(--jw-space-1); }
|
||||
|
||||
.jw-app-icon.lg { --jw-app-icon-size: 76px; --jw-app-icon-rim-width: 1.5px; }
|
||||
.jw-app-icon.sm { --jw-app-icon-size: 26px; }
|
||||
|
||||
.nav-actions { display: flex; align-items: center; gap: 14px; }
|
||||
@@ -0,0 +1,2 @@
|
||||
*,*::before,*::after{box-sizing:border-box}html{min-height:100%;background:var(--jw-bg);color-scheme:light}html[data-theme="dark"]{color-scheme:dark}body{min-height:100vh;margin:0;background:var(--jw-bg);color:var(--jw-text-primary);font-family:var(--jw-font-sans);font-size:1rem;line-height:1.5;text-rendering:optimizeLegibility}button,input,textarea,select{font:inherit}input,textarea,select{font-size:1rem}button,a{touch-action:manipulation}img,svg{max-width:100%}a{color:inherit}button{cursor:pointer}:is(a,button,input,textarea,select,[tabindex]):focus-visible{outline:2px solid var(--jw-field-border-focus);outline-offset:3px}body.creating{overflow:hidden}@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto!important}*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}
|
||||
#themeToggle{display:none!important}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Brand */
|
||||
.jw-brand,.jw-topbar .brand{display:inline-flex;align-items:center;gap:var(--jw-space-3);color:inherit;text-decoration:none;white-space:nowrap}.jw-brand>[data-logo],.jw-topbar .brand>[data-logo],.brand>[data-logo]{width:var(--jw-app-icon-size);height:var(--jw-app-icon-size);display:block;flex:none}.jw-brand b,.jw-topbar .brand b{font-size:1rem;font-weight:var(--jw-weight-black);line-height:1}.jw-brand small,.jw-topbar .brand small{display:inline!important;margin:0!important;color:inherit!important;font:inherit!important;letter-spacing:0!important;text-transform:none!important}.jw-brand small::before,.jw-topbar .brand small::before{content:"·";margin:0 var(--jw-space-2);color:var(--jw-text-muted)}
|
||||
/* Buttons */
|
||||
.jw-button{min-height:var(--jw-button-height);display:inline-flex;align-items:center;justify-content:center;gap:var(--jw-space-2);padding:var(--jw-button-padding);border:1px solid var(--jw-line-strong);border-radius:var(--jw-button-radius);background:var(--jw-surface);color:var(--jw-text-primary);font-weight:var(--jw-button-weight);text-decoration:none;transition:background var(--jw-duration-base) var(--jw-ease-out),color var(--jw-duration-base) var(--jw-ease-out),transform var(--jw-duration-fast) var(--jw-ease-out)}.jw-button:hover{background:var(--jw-surface-sunken)}.jw-button:active{transform:translateY(1px)}.jw-button--primary{border-color:var(--jw-button-primary-fill);background:var(--jw-button-primary-fill);color:var(--jw-button-primary-text)}.jw-button--primary:hover{background:var(--jw-button-primary-fill-hover)}.jw-button--pill{border-radius:var(--jw-radius-pill)}.jw-button--compact{min-height:42px;padding-inline:var(--jw-space-4);font-size:.875rem}.jw-icon-button{width:44px;height:44px;display:inline-grid;place-items:center;padding:0;border:1px solid var(--jw-line);border-radius:50%;background:var(--jw-surface);color:var(--jw-text-primary)}
|
||||
/* Cards and panels */
|
||||
.jw-card{padding:var(--jw-card-padding);border:1px solid var(--jw-card-border);border-radius:var(--jw-card-radius);background:var(--jw-card-bg);box-shadow:var(--jw-shadow-sm)}.jw-card--flat{box-shadow:none}.jw-card--sunken{background:var(--jw-surface-sunken)}.jw-card--glass{background:var(--jw-surface-glass);-webkit-backdrop-filter:blur(var(--jw-topbar-blur));backdrop-filter:blur(var(--jw-topbar-blur))}
|
||||
/* Forms */
|
||||
.jw-field{display:grid;gap:var(--jw-space-2);margin-top:var(--jw-space-5)}.jw-field>span,.jw-label{color:var(--jw-field-label);font-size:.8125rem;font-weight:var(--jw-weight-bold);letter-spacing:var(--jw-tracking-eyebrow);text-transform:uppercase}.jw-field :is(input,textarea,select),.jw-input{width:100%;min-height:var(--jw-field-height);padding:var(--jw-space-3) var(--jw-space-4);border:1px solid var(--jw-field-border);border-radius:var(--jw-field-radius);background:var(--jw-field-bg);color:var(--jw-field-text);font-size:1rem;outline:0}.jw-field textarea{min-height:112px;resize:vertical}.jw-field :is(input,textarea,select):focus,.jw-input:focus{border-color:var(--jw-field-border-focus);box-shadow:var(--jw-field-ring)}.jw-help{margin:var(--jw-space-2) 0 0;color:var(--jw-text-muted);font-size:.8125rem;line-height:1.5}
|
||||
/* Chips and status */
|
||||
.jw-chip{min-height:42px;display:inline-flex;align-items:center;justify-content:center;padding:0 var(--jw-space-4);border:1px solid var(--jw-chip-border);border-radius:var(--jw-radius-pill);background:var(--jw-chip-bg);color:var(--jw-chip-text);font-size:.8125rem;font-weight:var(--jw-weight-bold)}.jw-chip[aria-pressed="true"],.jw-chip.is-active{background:var(--jw-text-primary);color:var(--jw-bg)}.jw-badge{display:inline-flex;align-items:center;gap:var(--jw-space-2);padding:var(--jw-space-2) var(--jw-space-3);border:1px solid currentColor;border-radius:var(--jw-radius-pill);font-size:.8125rem;font-weight:var(--jw-weight-bold)}.jw-badge--success{color:var(--jw-success)}.jw-badge--warning{color:var(--jw-warning)}.jw-badge--danger{color:var(--jw-danger)}
|
||||
/* Async states */
|
||||
.jw-state{min-height:50vh;display:grid;place-content:center;justify-items:center;gap:var(--jw-space-3);padding:var(--jw-space-6);text-align:center}.jw-state__mark{font-size:clamp(3rem,8vw,5rem)}.jw-state h1{max-width:15ch;margin:0;font-family:var(--jw-font-display);font-size:clamp(2.5rem,7vw,5.5rem);line-height:.9;letter-spacing:-.055em}.jw-state p{max-width:42rem;margin:0;color:var(--jw-text-muted);font-size:1rem}.jw-state--page{min-height:100vh;background:var(--jw-black);color:var(--jw-white)}
|
||||
/* Toast */
|
||||
.jw-toast{position:fixed;right:var(--jw-space-5);bottom:var(--jw-space-5);z-index:var(--jw-z-toast);padding:var(--jw-space-3) var(--jw-space-4);border-radius:var(--jw-radius-md);background:var(--jw-text-primary);color:var(--jw-bg);box-shadow:var(--jw-shadow-lg);font-size:.875rem;font-weight:var(--jw-weight-bold)}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {mountLogo} from './logo.js';
|
||||
import {startNavScroll} from './header.js';
|
||||
|
||||
const prod=location.hostname.endsWith('justworks.cash');
|
||||
export const base=location.pathname.startsWith('/business-demo')?'/business-demo':'';
|
||||
const search=location.search;
|
||||
export const link=id=>prod?`${id==='business'?'https://business.justworks.cash/':`https://${id}.justworks.cash/`}${search}`:({business:`${base}/dashboard${search}`,pos:`${base}/pos${search}`,order:`${base}/order${search}`,book:`${base}/book${search}`,booking:`${base}/book${search}`,screen:`${base}/screen${search}`,gallery:`${base}/gallery${search}`})[id]||`${base}/portal?surface=${id}`;
|
||||
// Archipelago navigation adapter; visual components remain upstream.
|
||||
let links=[['order','Order'],['pay','Pay Bill'],['book','Book'],['gallery','Explore']];
|
||||
let brandHref;
|
||||
export function configureNavigation(options){links=options.items;brandHref=options.brandHref}
|
||||
const destination=id=>brandHref ? id : link(id);
|
||||
const menuSvg='<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 5h11M2.5 11h11"/></svg>';
|
||||
const closeSvg='<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
|
||||
const upSvg='<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 9.5 8 5l4.5 4.5"/></svg>';
|
||||
const topbar=(label,endHtml='')=>`<nav class="jw-topbar" data-topbar><a class="jw-brand brand" href="${brandHref || link('business')}"><span data-logo="icon"></span><b>Just Works<small>${label}</small></b></a><div class="jw-topbar-links links">${links.map(([id,name])=>`<a href="${destination(id)}">${name}</a>`).join('')}</div>${endHtml?`<div class="jw-topbar-end top">${endHtml}</div>`:''}</nav>`;
|
||||
|
||||
function mountChromeControls(app){
|
||||
const existing=app.querySelector('.mobile-menu');
|
||||
if(existing)return existing;
|
||||
app.insertAdjacentHTML('beforeend',`<button class="button icon ring mobile-menu-button" type="button" aria-label="Menu" aria-expanded="false" aria-controls="mobileMenu">${menuSvg}</button><button class="button icon ring back-to-top" type="button" aria-label="Back to top">${upSvg}</button><div class="mobile-menu" id="mobileMenu" aria-hidden="true"><button class="button icon ring mobile-menu-close" type="button" aria-label="Close menu">${closeSvg}</button><nav class="mobile-menu-items" aria-label="Menu">${links.map(([id,name])=>`<a href="${destination(id)}">${name}</a>`).join('')}</nav></div>`);
|
||||
const trigger=app.querySelector('.mobile-menu-button');
|
||||
const menu=app.querySelector('.mobile-menu');
|
||||
const close=app.querySelector('.mobile-menu-close');
|
||||
const back=app.querySelector('.back-to-top');
|
||||
let previousFocus=null;
|
||||
const focusable=()=>[...menu.querySelectorAll('a,button,[tabindex]:not([tabindex="-1"])')];
|
||||
const setMenu=open=>{menu.classList.toggle('open',open);menu.setAttribute('aria-hidden',String(!open));trigger.setAttribute('aria-expanded',String(open));document.body.classList.toggle('creating',open);if(open){previousFocus=document.activeElement;close.focus()}else if(previousFocus instanceof HTMLElement){previousFocus.focus()}};
|
||||
trigger.addEventListener('click',()=>setMenu(true));
|
||||
close.addEventListener('click',()=>setMenu(false));
|
||||
menu.addEventListener('click',event=>{if(event.target.closest('a'))setMenu(false)});
|
||||
menu.addEventListener('keydown',event=>{if(event.key==='Escape'){setMenu(false);return}if(event.key!=='Tab')return;const items=focusable();const first=items[0],last=items.at(-1);if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus()}else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus()}});
|
||||
back.addEventListener('click',()=>scrollTo({top:0,behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'auto':'smooth'}));
|
||||
let request=0;
|
||||
const update=()=>{request=0;back.classList.toggle('show',scrollY>innerHeight)};
|
||||
addEventListener('scroll',()=>{if(!request)request=requestAnimationFrame(update)},{passive:true});
|
||||
addEventListener('resize',()=>{if(innerWidth>980&&menu.classList.contains('open'))setMenu(false)},{passive:true});
|
||||
update();
|
||||
return menu;
|
||||
}
|
||||
|
||||
function initialize(app,grounded=false){
|
||||
app.querySelectorAll('[data-logo]').forEach(host=>mountLogo(host,{icon:true}));
|
||||
mountChromeControls(app);
|
||||
const bar=app.querySelector('[data-topbar]');
|
||||
if(grounded)bar?.classList.add('is-scrolled');else startNavScroll(bar);
|
||||
}
|
||||
|
||||
export function mountMobileChrome(app){return mountChromeControls(app)}
|
||||
export function mountAppChrome(app,{label='',endHtml='',grounded=false}={}){app.classList.toggle('chrome-grounded',grounded);app.insertAdjacentHTML('afterbegin',topbar(label,endHtml));initialize(app,grounded)}
|
||||
export function frame(app,{label='',surface='',html='',nav=true}){app.innerHTML=nav?`${topbar(label)}<main class="surface ${surface}">${html}</main>`:`<main class="surface ${surface}">${html}</main>`;if(nav)initialize(app);else app.querySelectorAll('[data-logo]').forEach(host=>mountLogo(host,{icon:true}))}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Top bar scroll state — shared with the main site's behaviour.
|
||||
* Transparent while parked; past the first 80px the bar earns the
|
||||
* translucent ground, hairline and blur. Toggles `is-scrolled`.
|
||||
*/
|
||||
const SCROLL_START = 80;
|
||||
|
||||
export function startNavScroll(bar = document.querySelector("[data-topbar]")) {
|
||||
if (!bar) return () => {};
|
||||
let frame = 0;
|
||||
const apply = () => {
|
||||
frame = 0;
|
||||
bar.classList.toggle("is-scrolled", window.scrollY > SCROLL_START);
|
||||
};
|
||||
const onScroll = () => {
|
||||
if (!frame) frame = requestAnimationFrame(apply);
|
||||
};
|
||||
apply();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
if (frame) cancelAnimationFrame(frame);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* The Just Works mark.
|
||||
*
|
||||
* One continuous line, drawn as a single closed path, with no beginning and no
|
||||
* end. As it travels it settles through three states: a plain loop, a section
|
||||
* squared into a card outline (payments), and a section drawn out into a
|
||||
* speech bubble (social). The resting state carries a pointer notch on the
|
||||
* left lobe (website), so the mark reads correctly even with motion disabled.
|
||||
*
|
||||
* The path is the ONLY thing that changes. Stroke, gradient, size and caps are
|
||||
* styled in components.css from tokens, so the mark follows the theme.
|
||||
*
|
||||
* Rendered from here rather than written into each page, so the topbar, the
|
||||
* footer and /design all draw the same mark from one source.
|
||||
*/
|
||||
|
||||
/*
|
||||
* All four states share the same anchor budget and the same left-lobe opening,
|
||||
* so the morph travels rather than jumps. Authored on a 56x28 field with the
|
||||
* crossing point at the centre.
|
||||
*/
|
||||
export const LOGO_STATES = {
|
||||
/* The loop at rest, with the pointer notch on the left lobe. */
|
||||
loop: "M13.5 14C13.5 7.5 20 7 27 14C34 21 41 20.5 41.5 14C42 7.5 34.5 7 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
/* Right lobe squared into a card. */
|
||||
card: "M13.5 14C13.5 7.5 20 7 27 14C29.5 16.5 30 19.5 33 19.5L41 19.5C43 19.5 44 18.5 44 16.5L44 11.5C44 9.5 43 8.5 41 8.5L33 8.5C30 8.5 29.5 11.5 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
/* Right lobe drawn out into a speech bubble, tail dropping below. */
|
||||
bubble: "M13.5 14C13.5 7.5 20 7 27 14C29 16 29 18.5 30.5 19.5C31.6 20.3 31 23.4 32.6 22.3C34 21.3 34.6 19.5 36.6 19.5L39 19.5C42 19.5 44 17 44 14C44 11 42 8.5 39 8.5L33 8.5C29.5 8.5 29 11.5 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
/* Lab 07: the right lobe opens into a round doorway. The resting mark for
|
||||
now, per the direction of 2026-08-18, until the lab work settles. */
|
||||
portal: "M13.5 14C13.5 7.5 20 7 27 14C30.5 17.5 34.5 19.5 37.5 18C35.5 15.5 35 13.5 36.5 12C38 10.5 41 10.5 42 12.5C43 14.5 42 16 40.5 17.5C43 17.5 44 14.5 43 11.5C41.5 7.5 34 6.5 30 10.5C28.5 12 28 13 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
/* Left lobe pulled into a cursor arrow. */
|
||||
cursor: "M13.5 14C10.6 8.2 9.8 4.6 11.6 4.2C13.4 3.8 20.6 9.6 27 14C34 21 41 20.5 41.5 14C42 7.5 34.5 7 27 14C23 16.6 18.6 21.6 17.1 22.6C15.6 23.6 14.6 19.8 13.5 14Z"
|
||||
};
|
||||
|
||||
/*
|
||||
* The lab: further single-line ideas, same method as LOGO_STATES — one
|
||||
* continuous closed path on the 56x28 field, each working a different motif
|
||||
* into the loop's travel. Rendered in /design for comparison; not part of the
|
||||
* live cycle. Keep adding here rather than replacing.
|
||||
*/
|
||||
export const LOGO_LAB = {
|
||||
/* Payments and Bitcoin. */
|
||||
"sat-symbol": "M13.5 14C13.5 7.5 20 7 27 14C30 17 30 19 34 19L40 19M34 9L40 9M31 14L43 14C43 14 43.5 10 41.5 8.5C38 6.5 31 9.5 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"coin-edge": "M13.5 14C13.5 7.5 20 7 27 14C30 17 32 18.5 35 18.5C39 18.5 42.5 16.5 42.5 13.5C42.5 10.5 39 8.5 35 8.5C32 8.5 30 11 27 14C24 17 21 19.5 17.5 19.5C14.5 19.5 13.5 17 13.5 14Z",
|
||||
"zap-bolt": "M13.5 14C13.5 7.5 20 7 27 14C30 17 33 18 35 15.5L32 15.5L38 8.5L36 13L39.5 13C41.5 13 42 10 40 8.5C36.5 6 30.5 10 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"lightning-path": "M13.5 14C13.5 7.5 20 7 27 14C34 21 41 20.5 41.5 14C42 7.5 34.5 7 27 14C25 16 23.5 14.5 22 16.5C20.5 18.5 19 16.5 17.5 18.5C15.5 21 13.5 17.5 13.5 14Z",
|
||||
"banknote": "M13.5 14C13.5 7.5 20 7 27 14C29.5 16.5 30 18.5 33 18.5L42 18.5C43.5 18.5 44 18 44 16.5L44 11.5C44 10 43.5 9.5 42 9.5L33 9.5C30 9.5 29.5 11.5 27 14C20.5 20.2 15 21 13.5 14ZM38.5 11.7A2.3 2.3 0 1 0 38.5 16.3A2.3 2.3 0 1 0 38.5 11.7Z",
|
||||
/* Identity and keys. */
|
||||
"key-bit": "M13.5 14C13.5 7.5 20 7 27 14C30 17 32 18 35 18L37 18L37 20L39 20L39 18L41 18L41 20.5L43.5 20.5L43.5 16C43.5 16 42 15.5 40 13C37.5 10 34 7.5 31 9.5C28.5 11 28.5 12.5 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"portal": "M13.5 14C13.5 7.5 20 7 27 14C30.5 17.5 34.5 19.5 37.5 18C35.5 15.5 35 13.5 36.5 12C38 10.5 41 10.5 42 12.5C43 14.5 42 16 40.5 17.5C43 17.5 44 14.5 43 11.5C41.5 7.5 34 6.5 30 10.5C28.5 12 28 13 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"signature": "M13.5 14C13.5 7.5 20 7 27 14C29 16 30.5 17 32 15.5C33.5 14 32 12 33.5 10.5C35 9 36.5 11 38 9.5C39.5 8 41 9.5 42.5 8C43.5 7 44 8.5 43 10C41 13 37 19 33 19C30 19 29 16 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
/* Web and social. */
|
||||
"at-sign": "M13.5 14C13.5 7.5 20 7 27 14C29.5 16.5 31.5 19 35.5 19C39.5 19 43 16.5 43 13C43 9.5 40.5 8 38 8C35.5 8 33.5 9.5 33.5 12C33.5 14.5 35.5 16 37.5 15.5C39.5 15 40 13 39.5 11.5C39 13.5 38 14.5 36.5 14C35 13.5 35.5 10.5 38 10.5C40 10.5 41 12 41 13.5C41 15.5 39 17 36.5 17C33.5 17 31 15 29.5 12.5C28.5 11 28 12.8 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"pin-drop": "M13.5 14C13.5 7.5 20 7 27 14C30 17 33.5 20.5 35.5 22C37.5 20.5 41 16.5 41 12.5C41 9 38.5 7 35.5 7C32.5 7 31 9 29.5 11.5C28.7 12.8 28 13 27 14C20.5 20.2 15 21 13.5 14ZM35.5 10.5A2 2 0 1 0 35.5 14.5A2 2 0 1 0 35.5 10.5Z",
|
||||
"heart-zap": "M13.5 14C13.5 7.5 20 7 27 14C29 16 31 18.5 34.5 21C38 18.5 43 14.5 43 11C43 8.5 41 7.5 39.5 7.5C37.5 7.5 36.5 9 34.5 11C32.5 9 31.5 7.5 29.5 7.5C28.5 7.5 27.5 8 27 9.5C26 12 24 16 20 18.5C16.5 20.5 13.5 17.5 13.5 14Z",
|
||||
"follow-plus": "M13.5 14C13.5 7.5 20 7 27 14C30 17 32 18 34.5 18L34.5 21L37.5 21L37.5 18L40.5 18L40.5 15L37.5 15L37.5 12L34.5 12L34.5 15C32 15 30 12 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
/* Continuity and craft. */
|
||||
"mobius": "M13.5 14C13.5 7.5 21 6 27 14C33 22 40.5 20.5 41.5 14C42.5 7 35 6.5 29 12C24 16.5 18 22 14.5 18C12.5 15.8 13 15 13.5 14Z",
|
||||
"knot": "M13.5 14C13.5 8 19 6.5 24 10.5C27 13 25 17.5 28.5 19C32 20.5 35 18 34.5 14.5C34 11 30 10 27 14C24.5 17.5 21 21.5 16.5 20C13.5 19 13.5 16 13.5 14Z",
|
||||
"orbit-dot": "M13.5 14C13.5 7.5 20 7 27 14C34 21 41 20.5 41.5 14C42 7.5 34.5 7 27 14C20.5 20.2 15 21 13.5 14ZM45 8A1.8 1.8 0 1 0 45 11.6A1.8 1.8 0 1 0 45 8Z",
|
||||
"spark-tail": "M13.5 14C13.5 7.5 20 7 27 14C33 20 39 21 41 17C42 15 41 13.5 43 12.5C44.5 11.8 45 10 44 9C42.5 7.5 40 8 38.5 9.5C34.5 6.5 30 10.5 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"wave-crest": "M13.5 14C13.5 7.5 20 7 27 14C30 17 32 18.5 34 16.5C36 14.5 34.5 12 36.5 10C38.5 8 41.5 8.5 42.5 11C43.5 13.5 42 16 39.5 17.5C42.5 17.5 44.5 14.5 43.5 11C42 6.5 36 6.5 33 10C31 12.3 28.7 12.3 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"double-cross": "M13.5 14C13.5 8 19 6.5 24.5 11C27 13 29 15 31.5 17C34.5 19.5 38.5 20 41 17.5C43.5 15 42.5 11 39.5 9C36.5 7 33 8.5 30.5 11C28 13.5 25 16.5 21 18.5C16.5 20.5 13.5 17.5 13.5 14Z",
|
||||
"qr-corner": "M13.5 14C13.5 7.5 20 7 27 14C29.5 16.5 30.5 18.5 33.5 18.5L43.5 18.5L43.5 8.5L33.5 8.5C30.5 8.5 29.5 11.5 27 14C20.5 20.2 15 21 13.5 14ZM36 11L41 11L41 16L36 16Z",
|
||||
"storefront": "M13.5 14C13.5 7.5 20 7 27 14C29 16 29.5 18.5 32 18.5L42 18.5L42 12.5C43.5 12 44 10.5 43 9.5C42 8.3 40 8.5 36.5 8.5C33 8.5 31.5 8.5 30.5 9.5C29.5 10.5 30 12 31.5 12.5L31.5 15C31 13 29 12 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
/* Batch two — 2026-08-18. */
|
||||
"relay-waves": "M13.5 14C13.5 7.5 20 7 27 14C30 17 32 18.5 34 17C32.5 15.5 32.5 12.5 34 11C35 13 37 13.5 38 12C36.5 10.5 37 8.5 39 8C41.5 7.4 43.5 9.5 43.5 12C43.5 16.5 38 20 33 17.5C30.5 16.2 29 15.8 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"antenna-mast": "M13.5 14C13.5 7.5 20 7 27 14C29.5 16.5 32 18 35 18L36.5 18L36.5 9.5C35 9 34.5 7 36 6C37.5 5 39.5 6 39.5 7.5C39.5 8.5 39 9.2 38 9.5L38 18L40 18C42.5 18 43.5 15.5 42.5 13C44.5 14 44.5 17.5 42 19C38.5 21 32 19 29 15.5C28.2 14.6 27.7 14.6 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"chain-link": "M13.5 14C13.5 7.5 20 7 27 14C29 16 30 17.5 32.5 17.5C34.5 17.5 36 16 36 14C36 12 34.5 10.5 32.5 10.5C31 10.5 30 11.5 29 13C30 10 32.5 8 35.5 9C38.5 10 39.5 13.5 38 16C39.5 17 42 16.5 43 14.5C43.5 17.5 40 19.5 37 18.5C33.5 17.4 31 14 29.5 12.5C28.6 13.1 27.8 13.4 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"menu-book": "M13.5 14C13.5 7.5 20 7 27 14C29 16 29.5 18.5 32 18.5L36.5 18.5L36.5 9.5C34.5 8.5 31.5 9 30 10.5C29.2 11.5 28.2 12.6 27 14C20.5 20.2 15 21 13.5 14ZM38 18.5L42.5 18.5C43.5 18.5 44 18 44 17L44 10.5C44 9.5 43.5 9 42.5 9L38 9Z",
|
||||
"cup-steam": "M13.5 14C13.5 7.5 20 7 27 14C29 16 29.5 18 32 18.5C34.5 19 37.5 19 39.5 18C41.5 17 42 15 42 13L33 13C33 11.5 33.5 10 35 9C34 7.5 34.5 6 36 5.5C35.5 7 36.5 7.5 36.5 9L42 9C43.5 9 44 10 43.5 11.5L42.9 13.4C44.5 13.6 45 15 44 16C43.4 16.6 42.5 16.6 41.8 16.2C40.5 18.8 36.5 20.5 32.5 19.5C29.5 18.7 28.5 15.6 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"receipt": "M13.5 14C13.5 7.5 20 7 27 14C29 16 29.5 18.5 32 19L32 20.5L33.8 19.3L35.6 20.5L37.4 19.3L39.2 20.5L41 19.3L42.5 20.3L42.5 9.5C42.5 8.5 42 8 41 8L34 8C31 8 29.5 11.5 27 14C20.5 20.2 15 21 13.5 14ZM35 12L40 12M35 15L40 15",
|
||||
"price-tag": "M13.5 14C13.5 7.5 20 7 27 14C29.5 16.5 32 19.5 34.5 20.5C35.5 21 36.5 20.8 37.5 19.8L42.8 14.5C43.8 13.5 43.8 12.2 42.8 11.2L38.5 7.5C36 7 32 7.5 30 10C28.8 11.5 28 13 27 14C20.5 20.2 15 21 13.5 14ZM39 11A1.5 1.5 0 1 0 39 14A1.5 1.5 0 1 0 39 11Z",
|
||||
"calendar-day": "M13.5 14C13.5 7.5 20 7 27 14C29 16 29.5 18.5 32 18.5L42 18.5C43.5 18.5 44 18 44 16.5L44 10.5C44 9 43.5 8.5 42 8.5L40.5 8.5L40.5 7L38.5 7L38.5 8.5L35 8.5L35 7L33 7L33 8.7C30.8 9.6 29.2 11.6 27 14C20.5 20.2 15 21 13.5 14ZM32 12L44 12",
|
||||
"clock-open": "M13.5 14C13.5 7.5 20 7 27 14C29.5 16.5 31.5 18.5 35 18.5C39 18.5 42 16.5 42 13.5C42 10.5 39.5 8.5 36.5 8.5C33.5 8.5 31.5 10.5 29.5 12.5C28.6 13.3 27.9 13.2 27 14C20.5 20.2 15 21 13.5 14ZM36.5 10.5L36.5 13.5L39 15",
|
||||
"compass-needle": "M13.5 14C13.5 7.5 20 7 27 14C34 21 41 20.5 41.5 14C41.9 9 37.5 7.2 33 10L36.5 12.5L31 17L29.5 12.2C28.6 12.9 27.8 13.4 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"north-arrow": "M13.5 14C13.5 7.5 20 7 27 14C30 17 32.5 19.5 35.5 21.5L37.5 12.5L42 14.5C41 10 36.5 6.5 32 8.5C29.8 9.6 28.5 12 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"camera-eye": "M13.5 14C13.5 7.5 20 7 27 14C29 16 29.5 18.5 32.5 18.5L41 18.5C42.5 18.5 43.5 17.5 43.5 16L43.5 11C43.5 9.5 42.5 8.5 41 8.5L39.5 8.5L38.5 7L35.5 7L34.5 8.5C31.5 8.5 29.5 11.5 27 14C20.5 20.2 15 21 13.5 14ZM37 10.5A3 3 0 1 0 37 16.5A3 3 0 1 0 37 10.5Z",
|
||||
"mic-live": "M13.5 14C13.5 7.5 20 7 27 14C29 16 30 18 32.5 18.5L34.5 18.5L34.5 16.8C32.5 16.3 31.5 14.8 31.5 13L33 13C33 14.5 34.5 15.5 36 15.5C37.5 15.5 39 14.5 39 13L40.5 13C40.5 14.8 39.5 16.3 37.5 16.8L37.5 18.5C40 18.5 42 17 42.5 14.5C44 15.5 43.5 18 41 19.3C37.5 21 31 19.5 29 15.8C28.3 14.9 27.7 14.6 27 14C20.5 20.2 15 21 13.5 14ZM34 9.5C34 8 35 7 36 7C37 7 38 8 38 9.5L38 12C38 13.5 37 14 36 14C35 14 34 13.5 34 12Z",
|
||||
"note-beam": "M13.5 14C13.5 7.5 20 7 27 14C29.5 16.5 31 18 33 17.5L33 10L41 8L41 15.5C42.5 15.8 43.5 17 43 18.3C42.3 19.8 40 20 39.3 18.5C38.9 17.6 39.3 16.6 40.3 16.2L40.3 10.5L33.7 12.2L33.7 18C34.2 19.8 32 21 30.3 20C28.8 19 28.8 17 30.3 16.2C29 15.5 28 14.8 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"ticket-stub": "M13.5 14C13.5 7.5 20 7 27 14C29 16 29.5 18.5 32 18.5L42 18.5C43 18.5 43.5 18 43.5 17C42.5 17 41.8 16.2 41.8 15.2C41.8 14.2 42.5 13.5 43.5 13.5L43.5 10C43.5 9 43 8.5 42 8.5L33.5 8.5C30.5 8.5 29.5 11.5 27 14C20.5 20.2 15 21 13.5 14ZM37.5 8.5L37.5 18.5",
|
||||
"bell-ring": "M13.5 14C13.5 7.5 20 7 27 14C29 16 30 17.5 32 18L32 19C32 20 32.8 20.8 33.8 20.8C34.8 20.8 35.6 20 35.6 19L41.5 19C41.5 17.5 40 17 40 14.5C40 11 38 8.5 35 8.5C32 8.5 30.8 11 29.3 12.7C28.5 13.4 27.7 13.4 27 14C20.5 20.2 15 21 13.5 14ZM30 19L41.5 19",
|
||||
"star-review": "M13.5 14C13.5 7.5 20 7 27 14C29.5 16.5 31.5 18.5 33.5 20L35.5 15.5L31.5 12.5L36.5 12.5L38 8L39.5 12.5L44.5 12.5L40.5 15.5L42 20.2L38 17.4C36 19 33 20.7 30 18C28.9 17 28 15 27 14C20.5 20.2 15 21 13.5 14Z",
|
||||
"check-badge": "M13.5 14C13.5 7.5 20 7 27 14C29 16 30 17.8 32.3 18.4C32.1 19.4 32.8 20.3 33.9 20.3C34.9 20.3 35.6 19.5 35.5 18.5C37 18.2 38.4 17.3 39.2 16C40.2 16.4 41.4 16 41.9 15C42.4 14 42 12.9 41.1 12.4C41.2 10.8 40.5 9.3 39.2 8.4C36.4 6.5 32.5 7.8 30.5 10.7C29.4 12.2 28.2 13 27 14C20.5 20.2 15 21 13.5 14ZM33.5 13.5L35.5 15.5L39.5 11.5",
|
||||
"handshake-loop": "M13.5 14C13.5 8 18.5 6.5 23.5 10C26 11.7 28.5 13.8 31 15.8C33 17.5 36 18 38.5 16.5C41.5 14.7 42 11 39.5 9C37 7 33.5 8 31.5 10.5C30 12.4 28 15.5 24.5 17.8C20 20.8 13.5 19 13.5 14Z",
|
||||
"ribbon-award": "M13.5 14C13.5 7.5 20 7 27 14C29.5 16.5 31 18 33.5 18.5L32.5 21.5L35 20L37.5 21.5L36.5 18.5C39.5 18.5 42 16.5 42 13.5C42 10 39.5 8 36.5 8C33 8 31 10.5 29.2 12.7C28.4 13.4 27.7 13.4 27 14C20.5 20.2 15 21 13.5 14ZM35 11A2.6 2.6 0 1 0 35 16.2A2.6 2.6 0 1 0 35 11Z"
|
||||
};
|
||||
|
||||
/* Parked while the mark rests on `portal`; restore the startLogoMotion
|
||||
calls in application.js and design.js to revive the cycle. */
|
||||
/*
|
||||
* Portal at small sizes: the full portal's inner spiral cannot resolve under
|
||||
* ~40px, so icon-scale marks use this variant — the same continuous line,
|
||||
* with the doorway drawn as a literal opening in the right lobe. One line,
|
||||
* one gap: the portal.
|
||||
*/
|
||||
export const LOGO_PORTAL_SMALL = "M41.5 16.5C40 20.8 33.5 20.5 27 14C20 7 13.5 7.5 13.5 14C13.5 21 20.5 20.2 27 14C34.5 7 42 7.5 41.7 11.5";
|
||||
|
||||
const ORDER = ["portal", "loop", "card", "bubble", "cursor"];
|
||||
|
||||
/*
|
||||
* THE MARK — "07 Portal", from the logo exploration deck: a doorway whose far
|
||||
* side is lit. The frame is a heavy silhouette at maximum contrast so it
|
||||
* survives any ground; all of the depth lives in the interior gradient,
|
||||
* running from the ramp's mid tone at the head to its faintest at the floor.
|
||||
* The frame's legs run to the floor with no bottom bar: the door does not
|
||||
* close behind you.
|
||||
*
|
||||
* Colours come from CSS (.jw-portal-*), so the mark follows the theme.
|
||||
*/
|
||||
export const LOGO_MARKUP = `
|
||||
<svg class="jw-mark jw-mark-portal" viewBox="0 0 32 32" role="img" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<linearGradient class="jw-portal-glow" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop class="jw-portal-head" offset="0%"></stop>
|
||||
<stop class="jw-portal-floor" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path class="jw-portal-light" d="M9 32V16a7 7 0 0 1 14 0v16Z"></path>
|
||||
<path class="jw-portal-frame" fill-rule="evenodd"
|
||||
d="M2 32V16C2 8.3 8.3 2 16 2s14 6.3 14 14v16h-7V16a7 7 0 0 0-14 0v16Z"></path>
|
||||
</svg>`.trim();
|
||||
|
||||
/* The one-line loop markup, kept for the /design specimens and the lab. */
|
||||
export const LOGO_LINE_MARKUP = `
|
||||
<svg class="jw-mark jw-mark-line" viewBox="0 0 56 28" role="img" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<linearGradient class="jw-mark-gradient" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop class="jw-mark-stop-a" offset="0%"></stop>
|
||||
<stop class="jw-mark-stop-b" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path class="jw-mark-path" d="${LOGO_STATES.portal}"></path>
|
||||
</svg>`.trim();
|
||||
|
||||
/* Each mounted mark needs its own gradient id, or the second one on the page
|
||||
would reference the first one's definition. */
|
||||
let gradientSeed = 0;
|
||||
|
||||
/**
|
||||
* Renders the mark into `host` and returns the created <svg>.
|
||||
* Pass `{ icon: true }` to wrap it in the app-icon container.
|
||||
* Motion is opt-in via startLogoMotion so a static mark costs nothing.
|
||||
*/
|
||||
export function mountLogo(host, { icon = false, variant = "portal" } = {}) {
|
||||
if (!host) return null;
|
||||
|
||||
/* "portal" is the brand mark (the lit doorway). "line" is the one-line loop
|
||||
used by the /design specimens and the lab. `icon` wraps either in the
|
||||
app-icon container from components.css. */
|
||||
const markup = variant === "line" ? LOGO_LINE_MARKUP : LOGO_MARKUP;
|
||||
host.innerHTML = icon ? `<span class="jw-app-icon">${markup}</span>` : markup;
|
||||
|
||||
const svg = host.querySelector(".jw-mark");
|
||||
gradientSeed += 1;
|
||||
|
||||
if (variant === "line") {
|
||||
const gradient = svg.querySelector(".jw-mark-gradient");
|
||||
const path = svg.querySelector(".jw-mark-path");
|
||||
const id = `jw-mark-gradient-${gradientSeed}`;
|
||||
gradient.id = id;
|
||||
path.setAttribute("stroke", `url(#${id})`);
|
||||
} else {
|
||||
const glow = svg.querySelector(".jw-portal-glow");
|
||||
const id = `jw-portal-glow-${gradientSeed}`;
|
||||
glow.id = id;
|
||||
svg.querySelector(".jw-portal-light").setAttribute("fill", `url(#${id})`);
|
||||
}
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycles the mark through its states. Returns a teardown.
|
||||
*
|
||||
* `gsap` and `MorphSVGPlugin` are passed in rather than imported so this stays
|
||||
* usable without motion: if either is missing the mark simply rests on `loop`.
|
||||
*/
|
||||
export function startLogoMotion(svg, { gsap, MorphSVGPlugin, hold = 2.4, morph = 1.1 } = {}) {
|
||||
if (!svg || !gsap || !MorphSVGPlugin) return () => {};
|
||||
|
||||
const path = svg.querySelector(".jw-mark-path");
|
||||
if (!path) return () => {};
|
||||
|
||||
const timeline = gsap.timeline({ repeat: -1, defaults: { ease: "power2.inOut" } });
|
||||
|
||||
for (let index = 1; index <= ORDER.length; index += 1) {
|
||||
/* Wraps back to `loop` on the last step, so the cycle closes seamlessly. */
|
||||
const next = ORDER[index % ORDER.length];
|
||||
timeline.to(path, {
|
||||
duration: morph,
|
||||
morphSVG: { shape: LOGO_STATES[next], shapeIndex: 0 }
|
||||
}, `+=${hold}`);
|
||||
}
|
||||
|
||||
return () => timeline.kill();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.button{position:relative;isolation:isolate;min-height:var(--jw-button-height);display:inline-flex;align-items:center;justify-content:center;gap:var(--jw-space-2);padding:var(--jw-button-padding);border:0;border-radius:var(--jw-button-radius);background:transparent;color:var(--jw-button-secondary-text);font-weight:var(--jw-button-weight);cursor:pointer}.button::before{content:"";position:absolute;inset:0;z-index:-1;padding:var(--jw-button-rim-width);border-radius:inherit;background:linear-gradient(135deg,var(--jw-button-rim-bright) 0%,transparent var(--jw-button-rim-gap-start),transparent var(--jw-button-rim-gap-end),var(--jw-button-rim-bright) 100%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;pointer-events:none}.button::after{content:"";position:absolute;inset:var(--jw-button-rim-width);z-index:-2;border-radius:calc(var(--jw-button-radius) - var(--jw-button-rim-width));background:var(--jw-button-secondary-fill)}.button:hover:not(:disabled)::after{background:var(--jw-button-secondary-fill-hover)}.button.icon.ring{width:var(--jw-ring-button-size);min-width:var(--jw-ring-button-size);height:var(--jw-ring-button-size);min-height:var(--jw-ring-button-size);padding:0;border-radius:var(--jw-radius-pill);color:var(--jw-text-primary)}.button.icon.ring::before{padding:var(--jw-app-icon-rim-width);background:linear-gradient(135deg,var(--jw-app-icon-rim-bright) 0%,var(--jw-app-icon-rim-fade) 32%,var(--jw-app-icon-rim-fade) 68%,var(--jw-app-icon-rim-bright) 100%)}.button.icon.ring::after{inset:var(--jw-app-icon-rim-width);border-radius:var(--jw-radius-pill);background:var(--jw-bg)}.mobile-menu-button{display:none;position:fixed;right:max(var(--jw-space-5),env(safe-area-inset-right));bottom:max(var(--jw-space-5),env(safe-area-inset-bottom));z-index:var(--jw-z-overlay);--jw-ring-button-size:54px}.mobile-menu{position:fixed;inset:0;z-index:var(--jw-z-modal);display:none;flex-direction:column;overflow:auto;padding:max(var(--jw-space-6),env(safe-area-inset-top)) max(var(--jw-space-6),env(safe-area-inset-right)) max(var(--jw-space-6),env(safe-area-inset-bottom)) max(var(--jw-space-6),env(safe-area-inset-left));background:var(--jw-modal-bg);-webkit-backdrop-filter:blur(26px);backdrop-filter:blur(26px)}.mobile-menu.open{display:flex}.mobile-menu-close{position:fixed;right:max(var(--jw-space-5),env(safe-area-inset-right));bottom:max(var(--jw-space-5),env(safe-area-inset-bottom));--jw-ring-button-size:54px}.mobile-menu-items{position:static!important;inset:auto!important;z-index:auto!important;width:auto!important;height:auto!important;flex:1;display:grid!important;align-content:center;justify-items:start;gap:var(--jw-space-6);padding:0!important;border:0!important;background:transparent!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important;pointer-events:auto!important}.mobile-menu-items a{color:var(--jw-text-primary);font-family:var(--jw-font-display);font-size:clamp(2.2rem,10vw,3.4rem);font-weight:700;line-height:1.3;text-decoration:none}.mobile-menu-foot{display:flex;align-items:center;justify-content:space-between;gap:var(--jw-space-4);padding-top:var(--jw-space-5);margin-bottom:calc(54px + var(--jw-space-6));border-top:1px solid var(--jw-line)}.mobile-menu-foot>span{color:var(--jw-text-muted);font-size:.8125rem;font-weight:var(--jw-weight-bold);letter-spacing:var(--jw-tracking-eyebrow);text-transform:uppercase}.mobile-menu-button svg,.mobile-menu-close svg,.back-to-top svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.back-to-top{display:none;position:fixed;right:max(var(--jw-space-5),env(safe-area-inset-right));bottom:calc(max(var(--jw-space-5),env(safe-area-inset-bottom)) + 66px);z-index:var(--jw-z-overlay);--jw-ring-button-size:44px;opacity:0;visibility:hidden;transform:translateY(var(--jw-space-2));transition:opacity var(--jw-duration-base) var(--jw-ease-out),transform var(--jw-duration-base) var(--jw-ease-out),visibility var(--jw-duration-base)}.back-to-top.show{opacity:1;visibility:visible;transform:none}@media(max-width:980px){.mobile-menu-button,.back-to-top{display:inline-flex}}@media(max-height:520px) and (orientation:landscape){.mobile-menu-items{align-content:start;padding-top:var(--jw-space-10)!important}.mobile-menu-items a{font-size:2rem}.mobile-menu-foot{margin-top:var(--jw-space-6)}}
|
||||
@@ -0,0 +1,2 @@
|
||||
/* Layout presets for page composition. */
|
||||
.jw-container{width:min(var(--jw-container),calc(100% - 2.5rem));margin-inline:auto}.jw-container--narrow{width:min(var(--jw-container-narrow),calc(100% - 2.5rem));margin-inline:auto}.jw-page{padding-block:clamp(3rem,7vw,7rem)}.jw-stack{display:grid;gap:var(--jw-stack-gap,var(--jw-space-5))}.jw-cluster{display:flex;align-items:center;flex-wrap:wrap;gap:var(--jw-cluster-gap,var(--jw-space-3))}.jw-split{display:grid;grid-template-columns:minmax(0,.9fr) minmax(22rem,1.1fr);align-items:start;gap:clamp(2rem,8vw,7rem)}.jw-split__copy{position:sticky;top:calc(var(--jw-topbar-height) + var(--jw-space-10))}.jw-split__copy h1{max-width:10ch;margin:var(--jw-space-3) 0 var(--jw-space-5);font-family:var(--jw-font-display);font-size:clamp(3rem,7vw,6rem);font-weight:650;line-height:.9;letter-spacing:-.06em}.jw-split__copy p{max-width:38rem;color:var(--jw-text-body);font-size:1.125rem;line-height:1.6}.jw-grid{display:grid;grid-template-columns:repeat(var(--jw-grid-columns,3),minmax(0,1fr));gap:var(--jw-grid-gap,var(--jw-space-4))}.jw-hero{position:relative;min-height:clamp(22rem,52vw,42rem);display:flex;align-items:flex-end;padding:clamp(2rem,7vw,6rem);overflow:hidden}.jw-hero__content{position:relative;z-index:1;max-width:60rem}.jw-hero h1{margin:var(--jw-space-4) 0;font-family:var(--jw-font-display);font-size:clamp(3.5rem,10vw,9rem);font-weight:650;line-height:.8;letter-spacing:-.07em}.jw-eyebrow{margin:0;color:var(--jw-accent);font-size:.8125rem;font-weight:var(--jw-weight-black);letter-spacing:var(--jw-tracking-eyebrow);text-transform:uppercase}@media(max-width:980px){.jw-split{grid-template-columns:1fr}.jw-split__copy{position:static}.jw-grid{--jw-grid-columns:2}}@media(max-width:640px){.jw-container,.jw-container--narrow{width:min(100% - 2rem,var(--jw-container))}.jw-grid{--jw-grid-columns:1}.jw-hero{padding-inline:var(--jw-space-5)}}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Theme controller.
|
||||
*
|
||||
* Three preferences: "system" (default), "light", "dark". The choice is
|
||||
* stored in localStorage and applied as `data-theme` on <html>; "system"
|
||||
* removes the attribute so the `prefers-color-scheme` block in tokens.css
|
||||
* takes over. A small inline snippet in each page's <head> applies the stored
|
||||
* value before first paint, so this module only has to own the control.
|
||||
*/
|
||||
|
||||
export const THEME_STORAGE_KEY = "jw-theme";
|
||||
const THEMES = ["system", "light", "dark"];
|
||||
|
||||
/* Must match --jw-bg in tokens.css for each theme. */
|
||||
const BROWSER_CHROME = { light: "#fafafa", dark: "#0b100f" };
|
||||
|
||||
const darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const listeners = new Set();
|
||||
|
||||
function readStored() {
|
||||
try {
|
||||
const stored = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
return THEMES.includes(stored) ? stored : "system";
|
||||
} catch {
|
||||
/* Private mode or blocked storage: fall back to the system preference. */
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
|
||||
/** The preference the user chose ("system" | "light" | "dark"). */
|
||||
export function getPreference() {
|
||||
return readStored();
|
||||
}
|
||||
|
||||
/** The theme actually on screen ("light" | "dark"), resolving "system". */
|
||||
export function getResolvedTheme(preference = readStored()) {
|
||||
if (preference !== "system") return preference;
|
||||
return darkQuery.matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
export function applyTheme(preference) {
|
||||
const next = THEMES.includes(preference) ? preference : "system";
|
||||
const root = document.documentElement;
|
||||
|
||||
if (next === "system") root.removeAttribute("data-theme");
|
||||
else root.setAttribute("data-theme", next);
|
||||
|
||||
const meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) meta.setAttribute("content", BROWSER_CHROME[getResolvedTheme(next)]);
|
||||
|
||||
try {
|
||||
localStorage.setItem(THEME_STORAGE_KEY, next);
|
||||
} catch {
|
||||
/* Not fatal: the theme still applies for this page view. */
|
||||
}
|
||||
|
||||
for (const listener of listeners) listener(next, getResolvedTheme(next));
|
||||
return next;
|
||||
}
|
||||
|
||||
/** Subscribe to preference changes. Returns an unsubscribe function. */
|
||||
export function onThemeChange(listener) {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
|
||||
/* Keep the browser chrome correct when the OS flips while set to "system". */
|
||||
darkQuery.addEventListener("change", () => {
|
||||
if (readStored() === "system") applyTheme("system");
|
||||
});
|
||||
|
||||
const ICONS = {
|
||||
system: '<path d="M3 4.5h12v8H3z"/><path d="M6.5 15.5h5"/>',
|
||||
light: '<circle cx="9" cy="9" r="3.4"/><path d="M9 1.6v1.8M9 14.6v1.8M1.6 9h1.8M14.6 9h1.8M3.8 3.8l1.3 1.3M12.9 12.9l1.3 1.3M14.2 3.8l-1.3 1.3M5.1 12.9l-1.3 1.3"/>',
|
||||
dark: '<path d="M14.5 10.4A6 6 0 0 1 7.6 3.5a6 6 0 1 0 6.9 6.9Z"/>'
|
||||
};
|
||||
|
||||
const LABELS = { system: "Match system theme", light: "Light theme", dark: "Dark theme" };
|
||||
|
||||
/**
|
||||
* Renders the segmented theme control into `container` and keeps its pressed
|
||||
* state in sync with the current preference.
|
||||
*/
|
||||
export function mountThemeToggle(container) {
|
||||
if (!container) return null;
|
||||
|
||||
container.classList.add("theme-toggle");
|
||||
container.setAttribute("role", "group");
|
||||
container.setAttribute("aria-label", "Color theme");
|
||||
container.innerHTML = THEMES.map((theme) => `
|
||||
<button type="button" data-theme-option="${theme}" title="${LABELS[theme]}" aria-label="${LABELS[theme]}">
|
||||
<svg viewBox="0 0 18 18" aria-hidden="true">${ICONS[theme]}</svg>
|
||||
</button>
|
||||
`).join("");
|
||||
|
||||
const buttons = [...container.querySelectorAll("[data-theme-option]")];
|
||||
|
||||
function sync(preference = readStored()) {
|
||||
for (const button of buttons) {
|
||||
button.setAttribute("aria-pressed", String(button.dataset.themeOption === preference));
|
||||
}
|
||||
}
|
||||
|
||||
container.addEventListener("click", (event) => {
|
||||
const button = event.target.closest("[data-theme-option]");
|
||||
if (button) applyTheme(button.dataset.themeOption);
|
||||
});
|
||||
|
||||
onThemeChange(sync);
|
||||
sync();
|
||||
return { sync };
|
||||
}
|
||||
|
||||
/**
|
||||
* The snippet that must run before first paint. Kept here so the inline copy
|
||||
* in each page's <head> has a single documented source; if you change it,
|
||||
* update index.html and design/index.html to match.
|
||||
*/
|
||||
export const PREPAINT_SNIPPET = `(function(){try{var t=localStorage.getItem("${THEME_STORAGE_KEY}");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t)}catch(e){}})()`;
|
||||
@@ -0,0 +1 @@
|
||||
[data-topbar]{position:fixed;inset:0 0 auto;z-index:var(--jw-z-overlay);height:76px;display:flex;align-items:center;gap:var(--jw-space-6);padding:0 clamp(1.125rem,5vw,4.375rem);border-bottom:1px solid transparent;background:transparent;color:var(--jw-text-primary);pointer-events:none;transition:background var(--jw-duration-base) var(--jw-ease-out),border-color var(--jw-duration-base) var(--jw-ease-out),box-shadow var(--jw-duration-base) var(--jw-ease-out)}[data-topbar]>*{pointer-events:auto}[data-topbar].is-scrolled{border-bottom-color:var(--jw-topbar-scrolled-border);background:var(--jw-topbar-scrolled-bg);box-shadow:var(--jw-topbar-scrolled-shadow);-webkit-backdrop-filter:blur(var(--jw-topbar-blur));backdrop-filter:blur(var(--jw-topbar-blur))}[data-theme="dark"] [data-topbar].is-scrolled{border-bottom-color:rgba(255,255,255,.06);background:linear-gradient(180deg,rgba(10,10,10,.72),rgba(10,10,10,.42))}.jw-topbar-links,.jw-topbar .links{display:flex;align-items:center;gap:var(--jw-space-2);margin-left:auto}.jw-topbar-links a,.jw-topbar .links a{min-height:42px;display:inline-flex;align-items:center;padding:0 var(--jw-space-3);border-radius:var(--jw-radius-sm);color:var(--jw-text-body);text-decoration:none;font-size:.875rem;font-weight:var(--jw-weight-bold)}.jw-topbar-links a:hover,.jw-topbar .links a:hover{background:var(--jw-surface-sunken);color:var(--jw-text-primary)}.jw-topbar-end{display:flex;align-items:center;gap:var(--jw-space-3)}.chrome-grounded{padding-top:76px}.jw-chrome--inverse{color:var(--jw-white)}.jw-chrome--inverse .jw-brand small::before{color:rgba(255,255,255,.62)}@media(max-width:980px){[data-topbar]{height:66px;padding-inline:var(--jw-space-5)}.chrome-grounded{padding-top:66px}.jw-topbar-links,.jw-topbar .links,.jw-topbar-end{display:none}}@media(max-width:420px){.jw-brand b,.jw-topbar .brand b{font-size:.875rem}.jw-brand small,.jw-topbar .brand small{font-size:.875rem!important}.jw-brand small::before,.jw-topbar .brand small::before{margin-inline:var(--jw-space-1)}}
|
||||
@@ -0,0 +1,4 @@
|
||||
try {
|
||||
const theme = localStorage.getItem('jw-theme');
|
||||
if (theme === 'light' || theme === 'dark') document.documentElement.dataset.theme = theme;
|
||||
} catch { /* System theme also works without storage. */ }
|
||||
@@ -0,0 +1,49 @@
|
||||
[hidden]{display:none!important}
|
||||
.launcher{padding-top:calc(var(--jw-topbar-height) + var(--jw-space-16));padding-bottom:var(--jw-space-8)}
|
||||
/* The desktop chrome uses 5vw gutters; keep both launch cards within them.
|
||||
Below the chrome's mobile breakpoint the existing spacing stays unchanged. */
|
||||
@media(min-width:981px){.launcher{width:min(var(--jw-container),calc(100% - 2 * clamp(1.125rem,5vw,4.375rem)))}}
|
||||
.intro{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--jw-space-8);margin-bottom:var(--jw-space-10)}
|
||||
.intro h1,.setup-intro h1{font-family:var(--jw-font-display);font-size:clamp(3rem,6vw,5.8rem);line-height:.98;letter-spacing:-.065em;margin:var(--jw-space-4) 0 0;font-weight:650}
|
||||
.intro-note{color:var(--jw-text-body);line-height:1.7;margin:0 0 var(--jw-space-2);max-width:23rem}
|
||||
.launch-options{--jw-grid-columns:2;--jw-grid-gap:var(--jw-space-5)}
|
||||
.launch-card{padding:var(--jw-space-6);display:flex;flex-direction:column;overflow:hidden;box-shadow:var(--jw-shadow-sm)}
|
||||
.card-heading{display:flex;align-items:center;justify-content:space-between;gap:var(--jw-space-4)}
|
||||
.card-arrow{font-size:1.5rem;color:var(--jw-text-muted)}
|
||||
.website-art,.business-art{height:235px;margin:var(--jw-space-6) 0;display:grid;place-items:center;background:var(--jw-surface-sunken);border-radius:var(--jw-radius-md);overflow:hidden;padding:var(--jw-space-6)}
|
||||
.mini-browser{width:86%;max-width:320px;background:var(--jw-bg);border:1px solid var(--jw-line-strong);border-radius:var(--jw-radius-md);transform:rotate(-4deg);box-shadow:var(--jw-shadow-lg);overflow:hidden}
|
||||
.mini-toolbar{height:30px;display:flex;align-items:center;gap:var(--jw-space-1);padding-inline:var(--jw-space-3);border-bottom:1px solid var(--jw-line)}
|
||||
.mini-toolbar>span{width:5px;height:5px;border-radius:var(--jw-radius-pill);background:var(--jw-line-strong)}
|
||||
.mini-toolbar>b{font-size:13px;font-weight:400;margin-left:var(--jw-space-2);color:var(--jw-text-muted)}
|
||||
.mini-page{position:relative;padding:var(--jw-space-5)}
|
||||
.mini-page-copy{position:relative;font-family:var(--jw-font-display);font-size:clamp(1.35rem,2vw,1.8rem);line-height:1.05;letter-spacing:-.04em;font-weight:650}
|
||||
.mini-sun{position:absolute;right:var(--jw-space-5);top:var(--jw-space-5);width:65px;height:65px;border-radius:50%;background:var(--jw-accent-soft)}
|
||||
.mini-lines{width:55%;height:14px;border-block:3px solid var(--jw-line);margin-block:var(--jw-space-3)}
|
||||
.mini-cta{font-size:13px;font-weight:650;color:var(--jw-accent-strong)}
|
||||
.mini-operations{width:90%;max-width:330px;display:grid;gap:var(--jw-space-2);transform:rotate(3deg)}
|
||||
.mini-operation{display:grid;grid-template-columns:38px 1fr;column-gap:var(--jw-space-3);align-items:center;background:var(--jw-bg);border:1px solid var(--jw-line);border-radius:var(--jw-radius-md);padding:var(--jw-space-3);box-shadow:var(--jw-shadow-sm)}
|
||||
.mini-operation:nth-child(2){transform:translateX(12px)}
|
||||
.mini-operation>b{font-size:14px}.mini-operation>span:last-child{font-size:13px;color:var(--jw-text-muted)}
|
||||
.operation-mark{grid-row:span 2;display:grid;place-items:center;width:38px;height:38px;background:var(--jw-accent-soft);border-radius:var(--jw-radius-sm);color:var(--jw-accent-strong);font-size:1.25rem}
|
||||
.card-copy h2{font-family:var(--jw-font-display);font-size:2.4rem;line-height:1.05;letter-spacing:-.04em;margin:0 0 var(--jw-space-3)}
|
||||
.card-copy p{color:var(--jw-text-body);margin:0 0 var(--jw-space-6);max-width:32rem;line-height:1.65}
|
||||
.launch-card>.jw-button{margin-top:auto;justify-content:space-between}.launch-card>.jw-help{margin-top:var(--jw-space-3)}
|
||||
.connected-strip{display:flex;align-items:center;justify-content:space-between;gap:var(--jw-space-5);padding-block:var(--jw-space-8);border-bottom:1px solid var(--jw-line)}
|
||||
.connected-strip p{margin:var(--jw-space-2) 0 0;color:var(--jw-text-muted);font-size:.875rem}.connected-strip>div{min-width:0}
|
||||
.site-address{overflow-wrap:anywhere}.status-dot{display:inline-block;width:8px;height:8px;background:var(--jw-accent);border-radius:50%;margin-right:var(--jw-space-3)}
|
||||
.launcher-footer{display:flex;flex-wrap:wrap;justify-content:space-between;gap:var(--jw-space-3);color:var(--jw-text-muted);font-size:.8125rem;padding-top:var(--jw-space-6)}
|
||||
@media(max-width:980px){.intro{display:block}.intro-note{margin-top:var(--jw-space-5)}.connected-strip{align-items:flex-start;flex-direction:column}.launcher-footer{padding-bottom:var(--jw-space-16)}}
|
||||
.launch-options:not(:has(#business-card:not([hidden]))){--jw-grid-columns:1;max-width:42rem}
|
||||
|
||||
#hosted-surface{position:fixed;inset:0;display:flex;flex-direction:column;background:var(--jw-bg)}
|
||||
#hosted-site{width:100%;flex:1;min-height:0;border:0}
|
||||
#back-to-launcher{font:inherit;text-align:left;padding:12px 20px;border:0;border-bottom:1px solid var(--jw-line);background:var(--jw-bg);color:var(--jw-text-primary);cursor:pointer}
|
||||
|
||||
.hosted-navigation{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:0 12px;border-bottom:1px solid var(--jw-line)}
|
||||
.hosted-navigation #back-to-launcher{margin-right:auto;padding:10px 0;border:0;font-size:13px}
|
||||
#hosted-switch{position:absolute;left:max(var(--jw-space-5),env(safe-area-inset-left));bottom:max(var(--jw-space-5),env(safe-area-inset-bottom));z-index:2;--jw-ring-button-size:54px;width:auto;min-width:54px;gap:9px;padding:0 18px;text-decoration:none;font-size:13px;font-weight:600;box-shadow:0 4px 20px rgba(0,0,0,.08);-webkit-backdrop-filter:blur(var(--jw-app-icon-blur));backdrop-filter:blur(var(--jw-app-icon-blur))}
|
||||
#hosted-switch svg{width:20px;height:20px;flex:none;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
||||
#hosted-switch:focus-visible{outline:2px solid var(--jw-accent);outline-offset:4px}
|
||||
.hosted-navigation:not(:has(#back-to-launcher:not([hidden]))){display:none}
|
||||
@media(max-width:600px){#hosted-switch{width:54px;padding:0}#switch-label{display:none}}
|
||||
@media(prefers-reduced-motion:no-preference){#hosted-switch{transition:transform .18s ease,box-shadow .18s ease}#hosted-switch:hover{transform:translateY(-2px);box-shadow:0 6px 24px rgba(0,0,0,.12)}#hosted-switch:active{transform:translateY(0)}}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { configureNavigation, mountAppChrome } from './ds/frame.js';
|
||||
const $ = id => document.getElementById(id);
|
||||
const base = new URL('..', import.meta.url).pathname.replace(/\/?$/, '/');
|
||||
const key = 'archipelago-justworks-site-v1';
|
||||
let connectedSite;
|
||||
let businessOpen = false;
|
||||
configureNavigation({brandHref: base, items: [[base, 'Your launcher']]});
|
||||
mountAppChrome($('app'), {label: 'Archipelago'});
|
||||
function showLauncher() {
|
||||
if (!connectedSite) return;
|
||||
$('hosted-surface').hidden = true;
|
||||
$('app').hidden = false;
|
||||
}
|
||||
function openHosted(url, title) {
|
||||
const target = new URL(url, location.href);
|
||||
businessOpen = target.origin === location.origin && target.pathname.startsWith('/business-demo/');
|
||||
$('switch-label').textContent = businessOpen ? 'Website' : 'Business';
|
||||
$('switch-building').toggleAttribute('hidden', businessOpen);
|
||||
$('switch-person').toggleAttribute('hidden', !businessOpen);
|
||||
const switchLabel = businessOpen ? 'Switch to customer website' : 'Switch to Business tools';
|
||||
$('hosted-switch').setAttribute('aria-label', switchLabel);
|
||||
$('hosted-switch').title = switchLabel;
|
||||
$('hosted-switch').href = businessOpen ? (connectedSite?.editorUrl || 'https://justworks.cash/') : (connectedSite?.businessUrl || '/business-demo/');
|
||||
$('app').hidden = true;
|
||||
$('hosted-site').src = url;
|
||||
$('hosted-site').title = title;
|
||||
$('back-to-launcher').hidden = !connectedSite;
|
||||
$('hosted-surface').hidden = false;
|
||||
}
|
||||
$('hosted-switch').addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
openHosted($('hosted-switch').href, businessOpen ? 'Just Works website' : 'Just Works business tools');
|
||||
});
|
||||
$('back-to-launcher').addEventListener('click', showLauncher);
|
||||
$('website-launch').addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
openHosted(connectedSite?.editorUrl || 'https://justworks.cash/', 'Just Works website');
|
||||
});
|
||||
$('business-launch').addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
if (connectedSite) openHosted(connectedSite.businessUrl, 'Just Works business tools');
|
||||
});
|
||||
async function refresh() {
|
||||
$('business-card').hidden = true;
|
||||
try {
|
||||
const saved = localStorage.getItem(key);
|
||||
if (!saved) return;
|
||||
const response = await fetch(`${base}api/site?url=${encodeURIComponent(saved)}`, {cache:'no-store', signal:AbortSignal.timeout(15000)});
|
||||
const site = await response.json();
|
||||
if (!response.ok || site.published !== true) return;
|
||||
connectedSite = site;
|
||||
$('website-launch').href = site.editorUrl;
|
||||
$('website-launch').textContent = 'Open your website';
|
||||
$('business-card').hidden = false;
|
||||
$('business-launch').href = site.businessUrl;
|
||||
$('business-launch').textContent = 'Open business tools';
|
||||
$('business-help').textContent = site.businessAvailable === true ? 'Your merchant dashboard, POS and signage.' : 'Your website is ready. Business tools depend on availability in Just Works.';
|
||||
$('connection-title').textContent = site.name;
|
||||
$('connection-description').textContent = site.url;
|
||||
$('forget').hidden = false;
|
||||
showLauncher();
|
||||
} catch { /* Keep the original Just Works first screen available. */ }
|
||||
}
|
||||
$('forget').addEventListener('click', () => { localStorage.removeItem(key); location.reload(); });
|
||||
window.addEventListener('storage', () => { if (!connectedSite) void refresh(); });
|
||||
void refresh();
|
||||
@@ -0,0 +1,553 @@
|
||||
/*
|
||||
* Just Works design tokens — the single source of truth for color, type,
|
||||
* space, radius, shadow, and motion. Everything else in the site (base.css,
|
||||
* components.css, home.css, cms.css, public-page.css) and the /design viewer
|
||||
* read from here. Change a value in this file and both the live site and the
|
||||
* design system move together.
|
||||
*
|
||||
* Three tiers, in order:
|
||||
* 1. Primitives --jw-<hue>-<step> raw palette, identical in every theme
|
||||
* 2. Semantic --jw-bg, --jw-text-*, --jw-accent... theme-dependent roles
|
||||
* 3. Component --jw-button-*, --jw-field-*... what components actually use
|
||||
*
|
||||
* Components must consume tier 3 (or tier 2), never tier 1 directly, so a
|
||||
* theme swap reaches every surface.
|
||||
*
|
||||
* THEMING: light is the default; dark is declared twice on purpose, once for
|
||||
* `prefers-color-scheme: dark` (system setting, works with JS disabled) and
|
||||
* once for `[data-theme="dark"]` (explicit toggle). The two blocks must stay
|
||||
* in sync — `npm run check` fails the build if they drift apart.
|
||||
*/
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 1. PRIMITIVES — the raw brand palette. Theme-independent.
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
:root {
|
||||
/* Mint — the primary brand hue. */
|
||||
--jw-mint-50: #e8faf6;
|
||||
--jw-mint-300: #55d6bd;
|
||||
--jw-mint-400: #20c5a8;
|
||||
--jw-mint-500: #12a98e;
|
||||
--jw-mint-600: #08806b;
|
||||
|
||||
/* Sun — warmth, highlights, the "create" energy. */
|
||||
--jw-sun-50: #fff8e3;
|
||||
--jw-sun-300: #ffd463;
|
||||
--jw-sun-400: #ffcc4d;
|
||||
--jw-sun-600: #cd8f08;
|
||||
|
||||
/* Coral — alerts, destructive states, accent contrast. */
|
||||
--jw-coral-50: #ffecec;
|
||||
--jw-coral-300: #ff8585;
|
||||
--jw-coral-500: #ed3c3c;
|
||||
|
||||
/* Blue — links, information, the Nostr/identity surfaces. */
|
||||
--jw-blue-50: #eaf0ff;
|
||||
--jw-blue-100: #cfdcff;
|
||||
--jw-blue-300: #6f95ff;
|
||||
--jw-blue-400: #2764ff;
|
||||
|
||||
/* Canonical white and black. Every white or black surface, fill, and piece
|
||||
of text in the system resolves to one of these two — never to #fff or
|
||||
#000 — so the whole site moves if they change. */
|
||||
--jw-white: #fafafa;
|
||||
--jw-black: #0a0a0a;
|
||||
|
||||
/* Neutral ramp between them. */
|
||||
--jw-neutral-0: var(--jw-white);
|
||||
--jw-neutral-100: #f2f4f3;
|
||||
--jw-neutral-1000: var(--jw-black);
|
||||
|
||||
/* Ember — the accent. A vermilion red one step from Bitcoin orange:
|
||||
provenance without impersonating the coin itself. */
|
||||
--jw-ember-100: #ffe1d6;
|
||||
--jw-ember-200: #ffb49a;
|
||||
--jw-ember-300: #ff7a52;
|
||||
--jw-ember-400: #f4511e;
|
||||
--jw-ember-600: #b0300a;
|
||||
|
||||
/* Bitcoin orange. Payment rails only — never decorative. */
|
||||
--jw-bitcoin: #f7931a;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 2. SEMANTIC — light theme (the default).
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
/* Ground and surfaces, back to front. */
|
||||
--jw-bg: var(--jw-white);
|
||||
--jw-bg-gradient: linear-gradient(180deg, var(--jw-white) 0%, #f4f5f4 46%, #eef0ef 78%, #e9ebea 100%);
|
||||
--jw-surface: var(--jw-white);
|
||||
--jw-surface-raised: var(--jw-white);
|
||||
--jw-surface-sunken: var(--jw-neutral-100);
|
||||
--jw-surface-veil: rgba(250, 250, 250, 0.78);
|
||||
--jw-surface-glass: rgba(250, 250, 250, 0.86);
|
||||
--jw-scrim: rgba(10, 10, 10, 0.42);
|
||||
|
||||
/* Text, strongest to faintest. */
|
||||
--jw-text-primary: var(--jw-black);
|
||||
--jw-text-strong: #1f2422;
|
||||
--jw-text-body: #454545;
|
||||
--jw-text-muted: #737373;
|
||||
--jw-text-subtle: #8f9995;
|
||||
--jw-text-inverse: var(--jw-white);
|
||||
--jw-text-on-accent: var(--jw-black);
|
||||
|
||||
/* Lines and dividers. */
|
||||
--jw-line: rgba(10, 10, 10, 0.1);
|
||||
--jw-line-strong: rgba(10, 10, 10, 0.2);
|
||||
--jw-line-faint: rgba(10, 10, 10, 0.06);
|
||||
|
||||
/* Accents. `accent` is the one the eye should follow. */
|
||||
--jw-accent: var(--jw-ember-400);
|
||||
--jw-accent-strong: var(--jw-ember-600);
|
||||
--jw-accent-soft: var(--jw-ember-100);
|
||||
--jw-accent-contrast: var(--jw-white);
|
||||
--jw-accent-ring: rgba(244, 81, 30, 0.22);
|
||||
--jw-highlight: var(--jw-sun-400);
|
||||
--jw-highlight-soft: var(--jw-sun-50);
|
||||
--jw-info: var(--jw-blue-400);
|
||||
--jw-info-soft: var(--jw-blue-50);
|
||||
|
||||
/* Status. */
|
||||
--jw-success: var(--jw-mint-600);
|
||||
--jw-success-soft: var(--jw-mint-50);
|
||||
--jw-warning: var(--jw-sun-600);
|
||||
--jw-warning-soft: var(--jw-sun-50);
|
||||
--jw-danger: var(--jw-coral-500);
|
||||
--jw-danger-soft: var(--jw-coral-50);
|
||||
|
||||
/* Shadows carry the light theme's depth. */
|
||||
--jw-shadow-color: rgba(0, 0, 0, 0.12);
|
||||
--jw-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
--jw-shadow-sm: 0 8px 22px rgba(0, 0, 0, 0.045);
|
||||
--jw-shadow-md: 0 14px 34px rgba(0, 0, 0, 0.055);
|
||||
--jw-shadow-lg: 0 18px 54px rgba(0, 0, 0, 0.08);
|
||||
--jw-shadow-xl: 0 28px 80px rgba(0, 0, 0, 0.16);
|
||||
--jw-shadow-accent: 0 14px 34px rgba(244, 81, 30, 0.16);
|
||||
|
||||
/* Ambient shadow behind the hero device: enough depth to lift a dark
|
||||
object off either ground without reading as a glow. */
|
||||
--jw-device-shadow:
|
||||
0 4px 10px rgba(0, 0, 0, 0.18),
|
||||
0 30px 60px rgba(0, 0, 0, 0.26);
|
||||
|
||||
/* The brand mark's conic sweep, so the logo follows the theme. */
|
||||
--jw-mark-sweep: conic-gradient(from 200deg, #ffcc4d, #ff5d5d, #2764ff, #20c5a8, #ffcc4d);
|
||||
--jw-mark-inset: var(--jw-white);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 2b. SEMANTIC — dark theme, applied by system preference.
|
||||
*
|
||||
* Keep every declaration below identical to the [data-theme="dark"] block.
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
color-scheme: dark;
|
||||
|
||||
--jw-bg: var(--jw-black);
|
||||
--jw-bg-gradient: linear-gradient(180deg, var(--jw-black), var(--jw-black));
|
||||
--jw-surface: rgba(255, 255, 255, 0.04);
|
||||
--jw-surface-raised: rgba(255, 255, 255, 0.07);
|
||||
--jw-surface-sunken: rgba(0, 0, 0, 0.28);
|
||||
--jw-surface-veil: rgba(18, 18, 18, 0.72);
|
||||
--jw-surface-glass: rgba(10, 10, 10, 0.82);
|
||||
--jw-scrim: rgba(0, 0, 0, 0.66);
|
||||
|
||||
--jw-text-primary: var(--jw-white);
|
||||
--jw-text-strong: rgba(250, 250, 250, 0.92);
|
||||
--jw-text-body: rgba(250, 250, 250, 0.72);
|
||||
--jw-text-muted: rgba(250, 250, 250, 0.56);
|
||||
--jw-text-subtle: rgba(250, 250, 250, 0.44);
|
||||
--jw-text-inverse: var(--jw-black);
|
||||
--jw-text-on-accent: #052e27;
|
||||
|
||||
--jw-line: rgba(255, 255, 255, 0.12);
|
||||
--jw-line-strong: rgba(255, 255, 255, 0.24);
|
||||
--jw-line-faint: rgba(255, 255, 255, 0.07);
|
||||
|
||||
--jw-accent: var(--jw-ember-300);
|
||||
--jw-accent-strong: var(--jw-ember-200);
|
||||
--jw-accent-soft: rgba(244, 81, 30, 0.16);
|
||||
--jw-accent-contrast: var(--jw-black);
|
||||
--jw-accent-ring: rgba(255, 122, 82, 0.28);
|
||||
--jw-highlight: var(--jw-sun-300);
|
||||
--jw-highlight-soft: rgba(255, 204, 77, 0.14);
|
||||
--jw-info: var(--jw-blue-300);
|
||||
--jw-info-soft: rgba(39, 100, 255, 0.16);
|
||||
|
||||
--jw-success: var(--jw-mint-300);
|
||||
--jw-success-soft: rgba(32, 197, 168, 0.14);
|
||||
--jw-warning: var(--jw-sun-300);
|
||||
--jw-warning-soft: rgba(255, 204, 77, 0.14);
|
||||
--jw-danger: var(--jw-coral-300);
|
||||
--jw-danger-soft: rgba(255, 93, 93, 0.16);
|
||||
|
||||
--jw-shadow-color: rgba(0, 0, 0, 0.6);
|
||||
--jw-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
--jw-shadow-sm: 0 8px 22px rgba(0, 0, 0, 0.34);
|
||||
--jw-shadow-md: 0 14px 34px rgba(0, 0, 0, 0.42);
|
||||
--jw-shadow-lg: 0 18px 54px rgba(0, 0, 0, 0.5);
|
||||
--jw-shadow-xl: 0 28px 80px rgba(0, 0, 0, 0.62);
|
||||
--jw-shadow-accent: 0 14px 34px rgba(244, 81, 30, 0.22);
|
||||
|
||||
--jw-topbar-scrolled-bg: linear-gradient(180deg, rgba(10, 10, 10, 0.6), rgba(10, 10, 10, 0.3));
|
||||
--jw-topbar-scrolled-border: rgba(255, 255, 255, 0.06);
|
||||
--jw-topbar-scrolled-shadow: none;
|
||||
--jw-button-primary-fill: var(--jw-white);
|
||||
--jw-button-primary-text: var(--jw-black);
|
||||
--jw-button-primary-fill-hover: #ececec;
|
||||
--jw-button-secondary-fill: var(--jw-black);
|
||||
--jw-button-secondary-fill-hover: #171717;
|
||||
--jw-app-icon-bg: rgba(10, 10, 10, 0.72);
|
||||
--jw-app-icon-rim-bright: rgba(255, 255, 255, 0.85);
|
||||
--jw-app-icon-rim-fade: rgba(10, 10, 10, 0.04);
|
||||
--jw-modal-bg: rgba(17, 17, 17, 0.78);
|
||||
--jw-portal-depth: drop-shadow(0 0 5px rgba(250, 250, 250, 0.22));
|
||||
--jw-device-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.07),
|
||||
0 6px 14px rgba(0, 0, 0, 0.5),
|
||||
0 34px 70px rgba(0, 0, 0, 0.6);
|
||||
--jw-mark-sweep: conic-gradient(from 200deg, #ffd463, #ff8585, #6f95ff, #55d6bd, #ffd463);
|
||||
--jw-mark-inset: var(--jw-black);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 2c. SEMANTIC — dark theme, applied by explicit toggle.
|
||||
*
|
||||
* Keep every declaration below identical to the prefers-color-scheme block.
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
|
||||
--jw-bg: var(--jw-black);
|
||||
--jw-bg-gradient: linear-gradient(180deg, var(--jw-black), var(--jw-black));
|
||||
--jw-surface: rgba(255, 255, 255, 0.04);
|
||||
--jw-surface-raised: rgba(255, 255, 255, 0.07);
|
||||
--jw-surface-sunken: rgba(0, 0, 0, 0.28);
|
||||
--jw-surface-veil: rgba(18, 18, 18, 0.72);
|
||||
--jw-surface-glass: rgba(10, 10, 10, 0.82);
|
||||
--jw-scrim: rgba(0, 0, 0, 0.66);
|
||||
|
||||
--jw-text-primary: var(--jw-white);
|
||||
--jw-text-strong: rgba(250, 250, 250, 0.92);
|
||||
--jw-text-body: rgba(250, 250, 250, 0.72);
|
||||
--jw-text-muted: rgba(250, 250, 250, 0.56);
|
||||
--jw-text-subtle: rgba(250, 250, 250, 0.44);
|
||||
--jw-text-inverse: var(--jw-black);
|
||||
--jw-text-on-accent: #052e27;
|
||||
|
||||
--jw-line: rgba(255, 255, 255, 0.12);
|
||||
--jw-line-strong: rgba(255, 255, 255, 0.24);
|
||||
--jw-line-faint: rgba(255, 255, 255, 0.07);
|
||||
|
||||
--jw-accent: var(--jw-ember-300);
|
||||
--jw-accent-strong: var(--jw-ember-200);
|
||||
--jw-accent-soft: rgba(244, 81, 30, 0.16);
|
||||
--jw-accent-contrast: var(--jw-black);
|
||||
--jw-accent-ring: rgba(255, 122, 82, 0.28);
|
||||
--jw-highlight: var(--jw-sun-300);
|
||||
--jw-highlight-soft: rgba(255, 204, 77, 0.14);
|
||||
--jw-info: var(--jw-blue-300);
|
||||
--jw-info-soft: rgba(39, 100, 255, 0.16);
|
||||
|
||||
--jw-success: var(--jw-mint-300);
|
||||
--jw-success-soft: rgba(32, 197, 168, 0.14);
|
||||
--jw-warning: var(--jw-sun-300);
|
||||
--jw-warning-soft: rgba(255, 204, 77, 0.14);
|
||||
--jw-danger: var(--jw-coral-300);
|
||||
--jw-danger-soft: rgba(255, 93, 93, 0.16);
|
||||
|
||||
--jw-shadow-color: rgba(0, 0, 0, 0.6);
|
||||
--jw-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
--jw-shadow-sm: 0 8px 22px rgba(0, 0, 0, 0.34);
|
||||
--jw-shadow-md: 0 14px 34px rgba(0, 0, 0, 0.42);
|
||||
--jw-shadow-lg: 0 18px 54px rgba(0, 0, 0, 0.5);
|
||||
--jw-shadow-xl: 0 28px 80px rgba(0, 0, 0, 0.62);
|
||||
--jw-shadow-accent: 0 14px 34px rgba(244, 81, 30, 0.22);
|
||||
|
||||
--jw-topbar-scrolled-bg: linear-gradient(180deg, rgba(10, 10, 10, 0.6), rgba(10, 10, 10, 0.3));
|
||||
--jw-topbar-scrolled-border: rgba(255, 255, 255, 0.06);
|
||||
--jw-topbar-scrolled-shadow: none;
|
||||
--jw-button-primary-fill: var(--jw-white);
|
||||
--jw-button-primary-text: var(--jw-black);
|
||||
--jw-button-primary-fill-hover: #ececec;
|
||||
--jw-button-secondary-fill: var(--jw-black);
|
||||
--jw-button-secondary-fill-hover: #171717;
|
||||
--jw-app-icon-bg: rgba(10, 10, 10, 0.72);
|
||||
--jw-app-icon-rim-bright: rgba(255, 255, 255, 0.85);
|
||||
--jw-app-icon-rim-fade: rgba(10, 10, 10, 0.04);
|
||||
--jw-modal-bg: rgba(17, 17, 17, 0.78);
|
||||
--jw-portal-depth: drop-shadow(0 0 5px rgba(250, 250, 250, 0.22));
|
||||
--jw-device-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.07),
|
||||
0 6px 14px rgba(0, 0, 0, 0.5),
|
||||
0 34px 70px rgba(0, 0, 0, 0.6);
|
||||
--jw-mark-sweep: conic-gradient(from 200deg, #ffd463, #ff8585, #6f95ff, #55d6bd, #ffd463);
|
||||
--jw-mark-inset: var(--jw-black);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 3. TYPE, SPACE, SHAPE, MOTION — theme-independent.
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
:root {
|
||||
/* Geist carries the headings; Inter carries everything else. Swap this one
|
||||
token to change every heading on the site and in /design. */
|
||||
--jw-font-display: "Syne Variable", "Geist Variable", Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
--jw-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--jw-font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
|
||||
/* Static steps for UI. */
|
||||
--jw-text-2xs: 0.72rem;
|
||||
--jw-text-xs: 0.78rem;
|
||||
--jw-text-sm: 0.88rem;
|
||||
--jw-text-base: 1rem;
|
||||
--jw-text-lg: 1.125rem;
|
||||
--jw-text-xl: 1.25rem;
|
||||
--jw-text-2xl: 1.5rem;
|
||||
--jw-text-3xl: 1.875rem;
|
||||
|
||||
/* Fluid steps for editorial headlines. The vw term dominates from phone to
|
||||
desktop so poster type scales with the viewport rather than stepping. */
|
||||
--jw-text-title: clamp(1.7rem, 3vw, 2.6rem);
|
||||
--jw-text-headline: clamp(2.1rem, 4.6vw, 3.6rem);
|
||||
--jw-text-display: clamp(2.45rem, 6.5vw, 5rem);
|
||||
|
||||
/* Syne hangs deep descenders; 0.96 clipped them. */
|
||||
--jw-leading-tight: 1.06;
|
||||
--jw-leading-snug: 1.12;
|
||||
--jw-leading-normal: 1.45;
|
||||
--jw-leading-relaxed: 1.6;
|
||||
|
||||
--jw-weight-normal: 450;
|
||||
--jw-weight-medium: 560;
|
||||
--jw-weight-semibold: 700;
|
||||
--jw-weight-bold: 820;
|
||||
--jw-weight-black: 900;
|
||||
|
||||
--jw-tracking-eyebrow: 0.09em;
|
||||
--jw-tracking-tight: -0.01em;
|
||||
|
||||
/* Space scale — 4px base. */
|
||||
--jw-space-1: 4px;
|
||||
--jw-space-2: 8px;
|
||||
--jw-space-3: 12px;
|
||||
--jw-space-4: 16px;
|
||||
--jw-space-5: 20px;
|
||||
--jw-space-6: 24px;
|
||||
--jw-space-8: 32px;
|
||||
--jw-space-10: 40px;
|
||||
--jw-space-12: 48px;
|
||||
--jw-space-16: 64px;
|
||||
--jw-space-20: 80px;
|
||||
--jw-space-24: 96px;
|
||||
--jw-section-gap: clamp(64px, 9vw, 128px);
|
||||
|
||||
--jw-radius-xs: 4px;
|
||||
--jw-radius-sm: 6px;
|
||||
--jw-radius-md: 8px;
|
||||
--jw-radius-lg: 14px;
|
||||
--jw-radius-xl: 22px;
|
||||
--jw-radius-pill: 999px;
|
||||
|
||||
/* Fixed at 1220px through laptop widths, then grows with the window to a
|
||||
1560px ceiling so large displays do not strand the content in gutters.
|
||||
The outer min() keeps the side gutter on small screens. */
|
||||
--jw-container: min(calc(100% - 32px), clamp(1220px, 82vw, 1560px));
|
||||
--jw-container-narrow: min(calc(100% - 32px), 880px);
|
||||
--jw-hitbox-min: 42px;
|
||||
|
||||
/* Motion. GSAP reads the durations/eases through motion.js so CSS
|
||||
transitions and scripted tweens stay on the same curve. */
|
||||
--jw-duration-fast: 120ms;
|
||||
--jw-duration-base: 180ms;
|
||||
--jw-duration-slow: 340ms;
|
||||
--jw-duration-slower: 620ms;
|
||||
--jw-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--jw-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
--jw-ease-spring: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
|
||||
--jw-z-base: 1;
|
||||
--jw-z-sticky: 10;
|
||||
--jw-z-overlay: 60;
|
||||
--jw-z-modal: 70;
|
||||
--jw-z-toast: 80;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 4. COMPONENT TOKENS
|
||||
*
|
||||
* What components.css actually reads. Every value resolves to a semantic
|
||||
* token, so these follow the theme automatically and can be retargeted
|
||||
* (e.g. point --jw-button-primary-bg at --jw-info) without touching CSS
|
||||
* rules. This is the layer to tweak when a component needs to change.
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
:root {
|
||||
/* Buttons.
|
||||
There are two: primary and secondary. Both are rimmed rather than
|
||||
filled — a two-colour gradient painted onto the border only, with the
|
||||
middle left transparent so whatever is behind shows through. Hierarchy
|
||||
comes from which two colours the rim uses, not from a fill. */
|
||||
--jw-button-height: var(--jw-hitbox-min);
|
||||
--jw-button-radius: var(--jw-radius-md);
|
||||
--jw-button-padding: 0 var(--jw-space-6);
|
||||
--jw-button-weight: var(--jw-weight-bold);
|
||||
/* Matched to the ring/app-icon rim the menu button wears. */
|
||||
--jw-button-rim-width: 1px;
|
||||
/* The rim is bright at the two diagonal corners and drops out entirely
|
||||
across the middle, so the page shows through the gap. Same very light
|
||||
grey in both themes. */
|
||||
--jw-button-rim-bright: #e6e6e6;
|
||||
--jw-button-rim-gap-start: 27%;
|
||||
--jw-button-rim-gap-end: 73%;
|
||||
|
||||
/* Primary inverts the page: black on light, white on dark. */
|
||||
--jw-button-primary-fill: var(--jw-black);
|
||||
--jw-button-primary-text: var(--jw-white);
|
||||
--jw-button-primary-fill-hover: #1c1c1c;
|
||||
|
||||
--jw-button-secondary-fill: var(--jw-white);
|
||||
--jw-button-secondary-text: var(--jw-text-body);
|
||||
--jw-button-secondary-fill-hover: var(--jw-neutral-100);
|
||||
|
||||
/* Ghost — a quiet inline action: no rim, no fill until hovered. */
|
||||
--jw-button-ghost-text: var(--jw-text-muted);
|
||||
--jw-button-ghost-text-hover: var(--jw-text-primary);
|
||||
--jw-button-ghost-fill-hover: var(--jw-surface-veil);
|
||||
|
||||
/* Fields. */
|
||||
--jw-field-height: 50px;
|
||||
--jw-field-radius: var(--jw-radius-md);
|
||||
--jw-field-bg: var(--jw-surface);
|
||||
--jw-field-text: var(--jw-text-primary);
|
||||
--jw-field-placeholder: var(--jw-text-subtle);
|
||||
--jw-field-border: var(--jw-line);
|
||||
--jw-field-border-focus: var(--jw-info);
|
||||
--jw-field-ring: 0 0 0 4px var(--jw-accent-ring);
|
||||
--jw-field-shadow: var(--jw-shadow-sm);
|
||||
--jw-field-label: var(--jw-text-body);
|
||||
--jw-field-sheen: var(--jw-line);
|
||||
|
||||
/* Cards and panels. */
|
||||
--jw-card-bg: var(--jw-surface);
|
||||
--jw-card-border: var(--jw-line);
|
||||
--jw-card-radius: var(--jw-radius-md);
|
||||
--jw-card-shadow: var(--jw-shadow-lg);
|
||||
--jw-card-padding: var(--jw-space-5);
|
||||
--jw-modal-padding: clamp(20px, 3vw, 30px);
|
||||
/* Dialog glass: translucent over the scrim's blur, opaque enough to read. */
|
||||
--jw-modal-bg: rgba(252, 252, 252, 0.94);
|
||||
/* Depth under the brand mark: a soft contact shadow on light. */
|
||||
--jw-portal-depth: drop-shadow(0 1px 1.5px rgba(10, 10, 10, 0.28));
|
||||
|
||||
/* Chips. */
|
||||
--jw-chip-bg: var(--jw-surface-veil);
|
||||
--jw-chip-text: var(--jw-text-strong);
|
||||
--jw-chip-border: var(--jw-line);
|
||||
--jw-chip-dot: var(--jw-accent);
|
||||
|
||||
/* Navigation chrome. */
|
||||
--jw-topbar-height: 64px;
|
||||
--jw-topbar-bg: var(--jw-surface-glass);
|
||||
--jw-topbar-border: var(--jw-line);
|
||||
/* Applied only once the page has scrolled past --jw-topbar-scroll-start. */
|
||||
--jw-topbar-scrolled-bg: linear-gradient(180deg, rgba(250, 250, 250, 0.6), rgba(250, 250, 250, 0.3));
|
||||
--jw-topbar-scrolled-border: rgba(10, 10, 10, 0.06);
|
||||
--jw-topbar-scrolled-shadow: none;
|
||||
--jw-topbar-blur: 8px;
|
||||
|
||||
/* Brand mark. */
|
||||
--jw-mark-size: 30px;
|
||||
--jw-mark-radius: var(--jw-radius-md);
|
||||
--jw-mark-ring: 6px;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 5. LEGACY BRIDGE
|
||||
*
|
||||
* base.css, cms.css and public-page.css were written against these short
|
||||
* names before the token layer existed. Mapping them here means the existing
|
||||
* site inherits theming for free, and new work should reach for --jw-* names.
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
:root {
|
||||
--ink: var(--jw-text-primary);
|
||||
--paper: var(--jw-bg);
|
||||
--card: var(--jw-surface);
|
||||
--muted: var(--jw-text-muted);
|
||||
--line: var(--jw-line);
|
||||
--mint: var(--jw-accent);
|
||||
--green: var(--jw-accent-strong);
|
||||
--sun: var(--jw-highlight);
|
||||
--coral: var(--jw-danger);
|
||||
--blue: var(--jw-info);
|
||||
--shadow: var(--jw-shadow-color);
|
||||
--soft-shadow: var(--jw-shadow-lg);
|
||||
--radius: var(--jw-radius-md);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 6. APP ICON
|
||||
*
|
||||
* The mark in its container. Light theme: light ground, dark rim. Dark theme
|
||||
* overrides these to the inverse in the theme blocks above.
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
:root {
|
||||
--jw-app-icon-size: 38px;
|
||||
/* Roughly the iOS squircle ratio. */
|
||||
--jw-app-icon-radius: 24%;
|
||||
--jw-app-icon-bg: rgba(250, 250, 250, 0.72);
|
||||
--jw-app-icon-blur: 10px;
|
||||
--jw-app-icon-rim-width: 1px;
|
||||
--jw-app-icon-rim-bright: rgba(10, 10, 10, 0.6);
|
||||
--jw-app-icon-rim-fade: rgba(10, 10, 10, 0.04);
|
||||
|
||||
/* Circular icon actions (dialog close, help). */
|
||||
--jw-ring-button-size: 44px;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 7. SELF-HOSTED FONTS
|
||||
*
|
||||
* The same files justworks.cash serves, so every surface here — dashboard,
|
||||
* module pages, and the locally generated merchant site — renders the real
|
||||
* Syne headings and Geist body instead of falling back to system fonts.
|
||||
* Paths are relative to this stylesheet, so they work under any base path.
|
||||
* ------------------------------------------------------------------------ */
|
||||
|
||||
@font-face {
|
||||
font-family: "Syne Variable";
|
||||
src: url("../fonts/syne-latin-wght-normal.woff2") format("woff2-variations");
|
||||
font-weight: 400 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Geist Variable";
|
||||
src: url("../fonts/geist-latin-wght-normal.woff2") format("woff2-variations");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
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;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Geist Variable";
|
||||
src: url("../fonts/geist-latin-ext-wght-normal.woff2") format("woff2-variations");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7, U+02CC, U+02CE-02D7, U+02DD, U+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;
|
||||
}
|
||||
Reference in New Issue
Block a user