diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml new file mode 100644 index 0000000..76880f4 --- /dev/null +++ b/.github/workflows/lighthouse.yml @@ -0,0 +1,34 @@ +name: Lighthouse (website) + +on: + pull_request: + paths: + - 'website/**' + - '.github/workflows/lighthouse.yml' + push: + branches: [main] + paths: + - 'website/**' + workflow_dispatch: + +concurrency: + group: lighthouse-${{ github.ref }} + cancel-in-progress: true + +jobs: + lighthouse: + name: Lighthouse CI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node 22 + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Run Lighthouse CI + uses: treosh/lighthouse-ci-action@v12 + with: + configPath: ./website/lighthouserc.json + temporaryPublicStorage: true diff --git a/.gitignore b/.gitignore index fede0f7..a7ec5f8 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ node_modules/ # and agent worktrees. afca53e removed the loop file from tracking once already. .claude/ralph-loop.local.md .claude/worktrees/ + +# Lighthouse CI local reports +.lighthouseci/ diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..a74a0e3 --- /dev/null +++ b/website/README.md @@ -0,0 +1,26 @@ +# Mouse website + +Marketing landing page for Mouse — a dark, single-page site in the app's own +visual language (`DESIGN.md`): IBM Plex Mono, monochrome, black panels, hero +driven by the dither-boids widget. + +## Run locally + +```sh +cd website +python3 -m http.server 8765 +# open http://127.0.0.1:8765/ +``` + +## Layout + +| Path | Role | +|---|---| +| `index.html` | Landing page | +| `styles.css` | Dark theme, monochrome + IBM Plex Mono | +| `main.js` | Nav solidify, GitHub star count, reveal-on-scroll, dither pause | +| `dither.html` | Dither-boids widget (`?embed=1&imgs=…`) | +| `assets/` | Logo, app screens, interface screenshots | + +Embed mode hides the widget chrome, biases the flock to the right for hero copy, +auto-loads the listed images, and cycles them. diff --git a/website/assets/app-history.png b/website/assets/app-history.png new file mode 100644 index 0000000..104d854 Binary files /dev/null and b/website/assets/app-history.png differ diff --git a/website/assets/app-ruby.png b/website/assets/app-ruby.png new file mode 100644 index 0000000..6843809 Binary files /dev/null and b/website/assets/app-ruby.png differ diff --git a/website/assets/app-vite.png b/website/assets/app-vite.png new file mode 100644 index 0000000..b227c9d Binary files /dev/null and b/website/assets/app-vite.png differ diff --git a/website/assets/fonts/ibm-plex-mono-latin-400-normal.woff2 b/website/assets/fonts/ibm-plex-mono-latin-400-normal.woff2 new file mode 100644 index 0000000..0804aaf Binary files /dev/null and b/website/assets/fonts/ibm-plex-mono-latin-400-normal.woff2 differ diff --git a/website/assets/fonts/ibm-plex-mono-latin-500-normal.woff2 b/website/assets/fonts/ibm-plex-mono-latin-500-normal.woff2 new file mode 100644 index 0000000..090f82f Binary files /dev/null and b/website/assets/fonts/ibm-plex-mono-latin-500-normal.woff2 differ diff --git a/website/assets/fonts/ibm-plex-mono-latin-600-normal.woff2 b/website/assets/fonts/ibm-plex-mono-latin-600-normal.woff2 new file mode 100644 index 0000000..67aeeb0 Binary files /dev/null and b/website/assets/fonts/ibm-plex-mono-latin-600-normal.woff2 differ diff --git a/website/assets/fonts/ibm-plex-mono-latin-700-normal.woff2 b/website/assets/fonts/ibm-plex-mono-latin-700-normal.woff2 new file mode 100644 index 0000000..5d7ff0c Binary files /dev/null and b/website/assets/fonts/ibm-plex-mono-latin-700-normal.woff2 differ diff --git a/website/assets/interface-editor.png b/website/assets/interface-editor.png new file mode 100644 index 0000000..ec132dd Binary files /dev/null and b/website/assets/interface-editor.png differ diff --git a/website/assets/interface-git-flow.png b/website/assets/interface-git-flow.png new file mode 100644 index 0000000..a01bdda Binary files /dev/null and b/website/assets/interface-git-flow.png differ diff --git a/website/assets/interface-ring.png b/website/assets/interface-ring.png new file mode 100644 index 0000000..26d2af4 Binary files /dev/null and b/website/assets/interface-ring.png differ diff --git a/website/assets/logo.png b/website/assets/logo.png new file mode 100644 index 0000000..1b9d5bf Binary files /dev/null and b/website/assets/logo.png differ diff --git a/website/assets/ocr/app-history.json b/website/assets/ocr/app-history.json new file mode 100644 index 0000000..9810411 --- /dev/null +++ b/website/assets/ocr/app-history.json @@ -0,0 +1 @@ +{"words":[{"t":"11:16","x0":0.06787330316742081,"x1":0.1568627450980392,"y0":0.023214285714285715,"y1":0.03357142857142857,"a":0.017797888386123683},{"t":"local/coding-on-the-phone","x0":0.08069381598793364,"x1":0.5610859728506787,"y0":0.08035714285714286,"y1":0.09464285714285714,"a":0.019215686274509803},{"t":"history","x0":0.6063348416289592,"x1":0.7360482654600302,"y0":0.08071428571428571,"y1":0.09464285714285714,"a":0.018530489118724413},{"t":"commit","x0":0.08144796380090498,"x1":0.19984917043740574,"y0":0.11785714285714285,"y1":0.12964285714285714,"a":0.01973353443941679},{"t":"sync","x0":0.2420814479638009,"x1":0.31825037707390647,"y0":0.12142857142857143,"y1":0.1332142857142857,"a":0.019042232277526393},{"t":"branch","x0":0.36048265460030166,"x1":0.48114630467571645,"y0":0.1175,"y1":0.13,"a":0.020110608345902465},{"t":"merge","x0":0.521870286576169,"x1":0.6206636500754148,"y0":0.12035714285714286,"y1":0.1332142857142857,"a":0.01975867269984917},{"t":"refresh","x0":0.6628959276018099,"x1":0.801659125188537,"y0":0.1175,"y1":0.12964285714285714,"a":0.019823313940961},{"t":"main","x0":0.1515837104072398,"x1":0.21568627450980393,"y0":0.15964285714285714,"y1":0.1875,"a":0.016025641025641024},{"t":"|","x0":0.23001508295625941,"x1":0.24509803921568626,"y0":0.15964285714285714,"y1":0.1875,"a":0.015082956259426848},{"t":"initial","x0":0.27149321266968324,"x1":0.41704374057315236,"y0":0.16714285714285715,"y1":0.18,"a":0.020792932557638442},{"t":"commit","x0":0.44419306184012064,"x1":0.5686274509803921,"y0":0.16714285714285715,"y1":0.18035714285714285,"a":0.020739064856711915},{"t":"mouse","x0":0.31070889894419307,"x1":0.3974358974358974,"y0":0.2032142857142857,"y1":0.21,"a":0.017345399698340876},{"t":"(5","x0":0.09728506787330317,"x1":0.1334841628959276,"y0":0.29428571428571426,"y1":0.315,"a":0.01809954751131222},{"t":"(5","x0":0.09728506787330317,"x1":0.1334841628959276,"y0":0.3382142857142857,"y1":0.35928571428571426,"a":0.01809954751131222},{"t":"[5","x0":0.09728506787330317,"x1":0.1334841628959276,"y0":0.3825,"y1":0.4032142857142857,"a":0.01809954751131222},{"t":"(5","x0":0.09728506787330317,"x1":0.1334841628959276,"y0":0.42642857142857143,"y1":0.4471428571428571,"a":0.01809954751131222},{"t":"vite.config.ts","x0":0.1636500754147813,"x1":0.473604826546003,"y0":0.43,"y1":0.4475,"a":0.022139625080801553},{"t":"+","x0":0.10030165912518854,"x1":0.11613876319758673,"y0":0.47785714285714287,"y1":0.4857142857142857,"a":0.01583710407239819},{"t":"add","x0":0.14328808446455504,"x1":0.20211161387631976,"y0":0.4742857142857143,"y1":0.48678571428571427,"a":0.0196078431372549},{"t":"file","x0":0.23001508295625941,"x1":0.31070889894419307,"y0":0.4742857142857143,"y1":0.48678571428571427,"a":0.02017345399698341},{"t":"vite.config.ts","x0":0.08069381598793364,"x1":0.3431372549019608,"y0":0.5589285714285714,"y1":0.5728571428571428,"a":0.01874595992243051},{"t":"import","x0":0.08069381598793364,"x1":0.1907993966817496,"y0":0.5953571428571428,"y1":0.6096428571428572,"a":0.018350930115636},{"t":"tailwindcss","x0":0.21342383107088989,"x1":0.42081447963800905,"y0":0.5953571428571428,"y1":0.6067857142857143,"a":0.01885369532428356},{"t":"from","x0":0.44419306184012064,"x1":0.5173453996983409,"y0":0.5953571428571428,"y1":0.6067857142857143,"a":0.018288084464555054},{"t":"'@tailwindcss/vite’;","x0":0.5467571644042232,"x1":0.9140271493212669,"y0":0.595,"y1":0.6089285714285714,"a":0.018363499245852188},{"t":"import","x0":0.08069381598793364,"x1":0.1907993966817496,"y0":0.62,"y1":0.6342857142857142,"a":0.018350930115636},{"t":"{","x0":0.2149321266968326,"x1":0.2277526395173454,"y0":0.6196428571428572,"y1":0.6332142857142857,"a":0.01282051282051282},{"t":"defineConfig","x0":0.25188536953242835,"x1":0.47888386123680243,"y0":0.62,"y1":0.6342857142857142,"a":0.018916540975364503},{"t":"from","x0":0.5399698340874811,"x1":0.6131221719457014,"y0":0.62,"y1":0.6314285714285715,"a":0.018288084464555054},{"t":"import","x0":0.08069381598793364,"x1":0.1907993966817496,"y0":0.6689285714285714,"y1":0.6832142857142857,"a":0.018350930115636},{"t":"playwright","x0":0.2149321266968326,"x1":0.40120663650075417,"y0":0.6689285714285714,"y1":0.6835714285714286,"a":0.018627450980392157},{"t":"from","x0":0.42458521870286575,"x1":0.4984917043740573,"y0":0.6689285714285714,"y1":0.6803571428571429,"a":0.01847662141779789},{"t":"'@vitest/browser-","x0":0.5271493212669683,"x1":0.8393665158371041,"y0":0.6689285714285714,"y1":0.6821428571428572,"a":0.018365717327655042},{"t":"playwright';","x0":0.08144796380090498,"x1":0.3009049773755656,"y0":0.6935714285714286,"y1":0.7075,"a":0.018288084464555054},{"t":"import","x0":0.08069381598793364,"x1":0.1907993966817496,"y0":0.7182142857142857,"y1":0.7321428571428571,"a":0.018350930115636},{"t":"adapter","x0":0.21417797888386123,"x1":0.34464555052790347,"y0":0.7182142857142857,"y1":0.7321428571428571,"a":0.01863822452057746},{"t":"from","x0":0.36726998491704377,"x1":0.4411764705882353,"y0":0.7182142857142857,"y1":0.7296428571428571,"a":0.01847662141779789},{"t":"'@sveltejs/adapter-","x0":0.4698340874811463,"x1":0.8197586726998491,"y0":0.7178571428571429,"y1":0.7321428571428571,"a":0.01841708343256331},{"t":"auto';","x0":0.07993966817496229,"x1":0.18552036199095023,"y0":0.7425,"y1":0.7557142857142857,"a":0.017596782302664656},{"t":"import","x0":0.08069381598793364,"x1":0.1907993966817496,"y0":0.7671428571428571,"y1":0.7814285714285715,"a":0.018350930115636},{"t":"sveltekit","x0":0.2526395173453997,"x1":0.42081447963800905,"y0":0.7671428571428571,"y1":0.7785714285714286,"a":0.01868610692140104},{"t":"from","x0":0.48265460030165913,"x1":0.5558069381598794,"y0":0.7671428571428571,"y1":0.7785714285714286,"a":0.018288084464555054},{"t":"export","x0":0.07918552036199095,"x1":0.1907993966817496,"y0":0.8410714285714286,"y1":0.8546428571428571,"a":0.01860231271995978},{"t":"default","x0":0.21342383107088989,"x1":0.3438914027149321,"y0":0.8403571428571428,"y1":0.8517857142857143,"a":0.01863822452057746},{"t":"plugins:","x0":0.15761689291101055,"x1":0.3009049773755656,"y0":0.865,"y1":0.8796428571428572,"a":0.01791101055806938},{"t":"[","x0":0.33408748114630465,"x1":0.3431372549019608,"y0":0.865,"y1":0.8782142857142857,"a":0.00904977375565611},{"t":"tailwindcss(),","x0":0.23227752639517346,"x1":0.4924585218702866,"y0":0.8892857142857142,"y1":0.9025,"a":0.018584356819650937},{"t":"sveltekit({","x0":0.23378582202111614,"x1":0.43891402714932126,"y0":0.9139285714285714,"y1":0.9275,"a":0.018648018648018648},{"t":"compilerOptions:","x0":0.31070889894419307,"x1":0.6078431372549019,"y0":0.9385714285714286,"y1":0.9525,"a":0.018570889894419306},{"t":"//","x0":0.38838612368024134,"x1":0.4193061840120664,"y0":0.9628571428571429,"y1":0.9760714285714286,"a":0.01546003016591252},{"t":"Force","x0":0.4457013574660634,"x1":0.5354449472096531,"y0":0.9639285714285715,"y1":0.9742857142857143,"a":0.01794871794871795},{"t":"runes","x0":0.5603318250377074,"x1":0.6500754147812972,"y0":0.9667857142857142,"y1":0.9742857142857143,"a":0.01794871794871795},{"t":"mode","x0":0.6742081447963801,"x1":0.7458521870286576,"y0":0.9632142857142857,"y1":0.9742857142857143,"a":0.01791101055806938},{"t":"for","x0":0.770739064856712,"x1":0.8235294117647058,"y0":0.9632142857142857,"y1":0.9742857142857143,"a":0.017596782302664656},{"t":"the","x0":0.8461538461538461,"x1":0.8989441930618401,"y0":0.9632142857142857,"y1":0.9742857142857143,"a":0.017596782302664656},{"t":"runes:","x0":0.38687782805429866,"x1":0.4924585218702866,"y0":0.9907142857142858,"y1":0.9989285714285714,"a":0.017596782302664656},{"t":"(3","x0":0.5248868778280543,"x1":0.5535444947209653,"y0":0.9871428571428571,"y1":1,"a":0.014328808446455505},{"t":"filename","x0":0.5784313725490197,"x1":0.7277526395173454,"y0":0.9875,"y1":0.9989285714285714,"a":0.018665158371040724},{"t":"=>","x0":0.8076923076923077,"x1":0.8416289592760181,"y0":0.99,"y1":0.9982142857142857,"a":0.016968325791855202}],"advN":0.01847662141779789,"count":60} \ No newline at end of file diff --git a/website/assets/ocr/app-ruby.json b/website/assets/ocr/app-ruby.json new file mode 100644 index 0000000..f28a272 --- /dev/null +++ b/website/assets/ocr/app-ruby.json @@ -0,0 +1 @@ +{"words":[{"t":"~","x0":0.07033898305084746,"x1":0.08389830508474576,"y0":0.11463414634146342,"y1":0.12378048780487805,"a":0.013559322033898305},{"t":"$","x0":0.1038135593220339,"x1":0.11779661016949153,"y0":0.09817073170731708,"y1":0.13597560975609757,"a":0.013983050847457627},{"t":"pkg","x0":0.1385593220338983,"x1":0.18516949152542372,"y0":0.10182926829268292,"y1":0.1402439024390244,"a":0.015536723163841807},{"t":"install","x0":0.2059322033898305,"x1":0.31822033898305085,"y0":0.10121951219512196,"y1":0.1323170731707317,"a":0.016041162227602903},{"t":"ruby","x0":0.33940677966101696,"x1":0.4025423728813559,"y0":0.10182926829268292,"y1":0.13963414634146343,"a":0.015783898305084747},{"t":"fetching","x0":0.07076271186440677,"x1":0.2016949152542373,"y0":0.16158536585365854,"y1":0.200609756097561,"a":0.016366525423728812},{"t":"ruby","x0":0.22203389830508474,"x1":0.2851694915254237,"y0":0.16219512195121952,"y1":0.2,"a":0.015783898305084747},{"t":"3.4.1","x0":0.3050847457627119,"x1":0.38601694915254237,"y0":0.16341463414634147,"y1":0.1926829268292683,"a":0.016186440677966103},{"t":"(25","x0":0.4101694915254237,"x1":0.45254237288135596,"y0":0.16097560975609757,"y1":0.1975609756097561,"a":0.014124293785310736},{"t":"MB)","x0":0.4733050847457627,"x1":0.5161016949152543,"y0":0.16097560975609757,"y1":0.1975609756097561,"a":0.01426553672316384},{"t":"installed","x0":0.07161016949152542,"x1":0.21694915254237288,"y0":0.22195121951219512,"y1":0.2530487804878049,"a":0.016148775894538607},{"t":"ruby","x0":0.23855932203389832,"x1":0.3021186440677966,"y0":0.2225609756097561,"y1":0.2603658536585366,"a":0.015889830508474576},{"t":"3.4.1","x0":0.32161016949152543,"x1":0.40296610169491526,"y0":0.22378048780487805,"y1":0.2530487804878049,"a":0.016271186440677966},{"t":"~","x0":0.07033898305084746,"x1":0.08389830508474576,"y0":0.29573170731707316,"y1":0.3048780487804878,"a":0.013559322033898305},{"t":"$","x0":0.1038135593220339,"x1":0.11779661016949153,"y0":0.27926829268292686,"y1":0.3170731707317073,"a":0.013983050847457627},{"t":"echo","x0":0.13771186440677965,"x1":0.20127118644067796,"y0":0.28292682926829266,"y1":0.31341463414634146,"a":0.015889830508474576},{"t":"\"puts","x0":0.2245762711864407,"x1":0.30127118644067796,"y0":0.28292682926829266,"y1":0.3207317073170732,"a":0.015338983050847459},{"t":"'hello","x0":0.32754237288135596,"x1":0.4190677966101695,"y0":0.28292682926829266,"y1":0.31341463414634146,"a":0.015254237288135594},{"t":"from","x0":0.4398305084745763,"x1":0.5033898305084745,"y0":0.28292682926829266,"y1":0.31341463414634146,"a":0.015889830508474576},{"t":"ruby","x0":0.523728813559322,"x1":0.5872881355932204,"y0":0.28292682926829266,"y1":0.3207317073170732,"a":0.015889830508474576},{"t":"'","x0":0.6127118644067797,"x1":0.6152542372881356,"y0":0.28292682926829266,"y1":0.2939024390243902,"a":0.002542372881355932},{"t":"+","x0":0.6411016949152543,"x1":0.6542372881355932,"y0":0.29085365853658535,"y1":0.3103658536585366,"a":0.013135593220338982},{"t":"RUBY_VERSION;","x0":0.675,"x1":0.8847457627118644,"y0":0.2841463414634146,"y1":0.3195121951219512,"a":0.016134289439374186},{"t":"puts","x0":0.0711864406779661,"x1":0.13347457627118645,"y0":0.3445121951219512,"y1":0.38109756097560976,"a":0.015572033898305086},{"t":"(1..5).map","x0":0.15847457627118644,"x1":0.31864406779661014,"y0":0.34207317073170734,"y1":0.38109756097560976,"a":0.01601694915254237},{"t":"In|","x0":0.37796610169491524,"x1":0.4135593220338983,"y0":0.34207317073170734,"y1":0.37865853658536586,"a":0.011864406779661017},{"t":"n*n","x0":0.4406779661016949,"x1":0.48559322033898306,"y0":0.35,"y1":0.37317073170731707,"a":0.014971751412429379},{"t":"%.inspect\"","x0":0.5076271186440678,"x1":0.6677966101694915,"y0":0.3426829268292683,"y1":0.38109756097560976,"a":0.01601694915254237},{"t":">","x0":0.6919491525423729,"x1":0.7038135593220339,"y0":0.349390243902439,"y1":0.3725609756097561,"a":0.011864406779661017},{"t":"hello.rb;","x0":0.7258474576271187,"x1":0.8677966101694915,"y0":0.3432926829268293,"y1":0.37865853658536586,"a":0.01577212806026365},{"t":"ruby","x0":0.07076271186440677,"x1":0.13432203389830508,"y0":0.4036585365853659,"y1":0.44146341463414634,"a":0.015889830508474576},{"t":"hello","x0":0.0711864406779661,"x1":0.15084745762711865,"y0":0.46402439024390246,"y1":0.4945121951219512,"a":0.01593220338983051},{"t":"from","x0":0.1711864406779661,"x1":0.23516949152542374,"y0":0.46402439024390246,"y1":0.4945121951219512,"a":0.015995762711864408},{"t":"ruby","x0":0.2555084745762712,"x1":0.31864406779661014,"y0":0.46402439024390246,"y1":0.5018292682926829,"a":0.015783898305084747},{"t":"3.4.1","x0":0.3385593220338983,"x1":0.41991525423728815,"y0":0.4652439024390244,"y1":0.4945121951219512,"a":0.016271186440677966},{"t":"[1,","x0":0.075,"x1":0.11271186440677966,"y0":0.5237804878048781,"y1":0.5597560975609757,"a":0.012570621468926554},{"t":"4,","x0":0.13686440677966102,"x1":0.163135593220339,"y0":0.526219512195122,"y1":0.5597560975609757,"a":0.013135593220338982},{"t":"9,","x0":0.18771186440677967,"x1":0.2135593220338983,"y0":0.525,"y1":0.5597560975609757,"a":0.012923728813559321},{"t":"16,","x0":0.23813559322033898,"x1":0.2805084745762712,"y0":0.525609756097561,"y1":0.5597560975609757,"a":0.014124293785310736},{"t":"25]","x0":0.30550847457627117,"x1":0.3474576271186441,"y0":0.5237804878048781,"y1":0.5597560975609757,"a":0.013983050847457627},{"t":"~","x0":0.07033898305084746,"x1":0.08389830508474576,"y0":0.5975609756097561,"y1":0.6067073170731707,"a":0.013559322033898305},{"t":"$1","x0":0.1038135593220339,"x1":0.15338983050847457,"y0":0.5810975609756097,"y1":0.6219512195121951,"a":0.02478813559322034}],"advN":0.015889830508474576,"count":42} \ No newline at end of file diff --git a/website/assets/ocr/app-vite.json b/website/assets/ocr/app-vite.json new file mode 100644 index 0000000..690d06b --- /dev/null +++ b/website/assets/ocr/app-vite.json @@ -0,0 +1 @@ +{"words":[{"t":"~","x0":0.07033898305084746,"x1":0.08389830508474576,"y0":0.11951219512195121,"y1":0.12865853658536586,"a":0.013559322033898305},{"t":"$","x0":0.1038135593220339,"x1":0.11779661016949153,"y0":0.10304878048780487,"y1":0.14085365853658535,"a":0.013983050847457627},{"t":"npx","x0":0.1385593220338983,"x1":0.1843220338983051,"y0":0.1152439024390244,"y1":0.1445121951219512,"a":0.015254237288135594},{"t":"create-vite","x0":0.20508474576271185,"x1":0.3855932203389831,"y0":0.10609756097560975,"y1":0.13719512195121952,"a":0.01640986132511556},{"t":"|","x0":0.07627118644067797,"x1":0.07796610169491526,"y0":0.16646341463414635,"y1":0.20243902439024392,"a":0.001694915254237288},{"t":"Project","x0":0.12161016949152542,"x1":0.23432203389830508,"y0":0.22682926829268293,"y1":0.2658536585365854,"a":0.016101694915254237},{"t":"name:","x0":0.2559322033898305,"x1":0.3309322033898305,"y0":0.23597560975609755,"y1":0.2579268292682927,"a":0.015},{"t":"Project","x0":0.12161016949152542,"x1":0.23432203389830508,"y0":0.2871951219512195,"y1":0.32621951219512196,"a":0.016101694915254237},{"t":"name:","x0":0.2559322033898305,"x1":0.3309322033898305,"y0":0.29634146341463413,"y1":0.3182926829268293,"a":0.015},{"t":"|","x0":0.07627118644067797,"x1":0.07796610169491526,"y0":0.40792682926829266,"y1":0.44390243902439025,"a":0.001694915254237288},{"t":"m","x0":0.06906779661016949,"x1":0.08516949152542373,"y0":0.4768292682926829,"y1":0.5,"a":0.016101694915254237},{"t":"Select","x0":0.12033898305084746,"x1":0.21779661016949153,"y0":0.4689024390243902,"y1":0.499390243902439,"a":0.016242937853107344},{"t":"a","x0":0.23855932203389832,"x1":0.2521186440677966,"y0":0.4774390243902439,"y1":0.499390243902439,"a":0.013559322033898305},{"t":"framework:","x0":0.27203389830508473,"x1":0.43177966101694915,"y0":0.4689024390243902,"y1":0.499390243902439,"a":0.015974576271186442},{"t":"|","x0":0.07627118644067797,"x1":0.07796610169491526,"y0":0.5286585365853659,"y1":0.5646341463414634,"a":0.001694915254237288},{"t":"Vanilla","x0":0.11991525423728813,"x1":0.23516949152542374,"y0":0.5286585365853659,"y1":0.5597560975609757,"a":0.016464891041162225},{"t":"|","x0":0.07627118644067797,"x1":0.07796610169491526,"y0":0.5890243902439024,"y1":0.625,"a":0.001694915254237288},{"t":"L","x0":0.07627118644067797,"x1":0.08559322033898305,"y0":0.6420731707317073,"y1":0.6676829268292683,"a":0.009322033898305085},{"t":"cancelled","x0":0.28940677966101697,"x1":0.4351694915254237,"y0":0.65,"y1":0.6804878048780488,"a":0.016195856873822974}],"advN":0.016101694915254237,"count":19} \ No newline at end of file diff --git a/website/assets/og-cover.png b/website/assets/og-cover.png new file mode 100644 index 0000000..3c5428d Binary files /dev/null and b/website/assets/og-cover.png differ diff --git a/website/assets/screen-graph.png b/website/assets/screen-graph.png new file mode 100644 index 0000000..0de7b0a Binary files /dev/null and b/website/assets/screen-graph.png differ diff --git a/website/assets/screen-lanes.png b/website/assets/screen-lanes.png new file mode 100644 index 0000000..cf999be Binary files /dev/null and b/website/assets/screen-lanes.png differ diff --git a/website/assets/screen-shell.png b/website/assets/screen-shell.png new file mode 100644 index 0000000..26d2af4 Binary files /dev/null and b/website/assets/screen-shell.png differ diff --git a/website/assets/screen-spread.png b/website/assets/screen-spread.png new file mode 100644 index 0000000..22bb5b0 Binary files /dev/null and b/website/assets/screen-spread.png differ diff --git a/website/dither.html b/website/dither.html new file mode 100644 index 0000000..db65d48 --- /dev/null +++ b/website/dither.html @@ -0,0 +1,1210 @@ + + + + + +dither boids + + + + +
+
+ + + +
+
+
+ + + + +
+
+ + + + + + diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..b875ad9 --- /dev/null +++ b/website/index.html @@ -0,0 +1,378 @@ + + + + + + Mouse — Mobile Coding App & IDE for Builders + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+
+ + +
+ Native for iOS & Android · open source +

A real IDE,
built for a phone.

+

+ Mouse is a mobile coding app. A gesture shell where the whole + development environment lives: swipe between editors, pinch workspaces + open, and run real node and git against your GitHub repos. +

+ +
+ A full dev stack, on the device + msh shell + node on JavaScriptCore + git from scratch + zero dependencies +
+

Drag the flock · the app screens cycle on their own

+
+
+ +
+
+
+ Screens +

The whole workflow, in your thumb’s reach.

+

Files, editor, terminal, and the commit graph are containers on a ring. Everything a builder needs, one swipe away.

+
+
+
+
Mouse showing the commit graph, file list, and the vite.config.ts editor
+
+ Edit · commit +

Files to graph

+

Browse the tree, edit in place, and read the commit history with colored rails, all on one ring.

+
+
+
+
A terminal running npx create-vite inside Mouse
+
+ Terminal +

Real node & msh

+

Scaffold with npx, pipe commands, run scripts. The shell is verified against real /bin/sh.

+
+
+
+
Installing and running Ruby from the Mouse shell
+
+ On device +

Install & run

+

Pull toolchains and packages, then run them where you are. No server in the middle.

+
+
+
+
+
+ +
+
+
+ Rings of containers +

The screen is a window onto a ring.

+

Lanes show what is open; the rest wait in reserve. A commit moves the same view to a new offset, no reload flash.

+
+ +
+
+ +
+
+
+ Gestures +

One law. Every surface.

+

The shell owns navigation. Content owns reading and typing. Conflict is settled once, everywhere.

+
+
+
+ Lane +

Horizontal drag

+

Swipe a lane to the next container on the ring — files, editor, graph, terminal.

+
+
+ Ring +

Edge swipe

+

Drag from the screen edge to travel between rings, or mint a fresh one.

+
+
+ Layout +

Spread / pinch

+

Two fingers open a lane or close one. Pinch the last lane and the ring folds.

+
+
+ Split +

Divider drag

+

Pull the capsule between lanes to resize. Springs settle with mass.

+
+
+ Content +

Tap · scroll · type

+

Trees scroll, editors edit, terminals take keystrokes. Vertical stays with content.

+
+
+ Focus +

Editor stand-down

+

While the editor is focused, every drag on it belongs to the text — selection and caret first.

+
+
+
+ The gesture law. + One-finger horizontal drags and all two-finger gestures belong to the shell, everywhere. + Content gets taps, vertical scrolling, and the keyboard. +
+
+
+ +
+
+
+ The stack +

A full stack on the device.

+

No server in the middle. GitHub talks to the phone. The engines are native.

+
+
+
+ 01 · Shell +

msh

+

A from-scratch POSIX-flavored shell: pipes, redirects, functions, $(…), globs, scripts — verified against real /bin/sh.

+
+
+ 02 · Runtime +

node

+

JavaScriptCore Node layer: npm install, CommonJS + ESM, TypeScript, sockets, HTTP, workers, and full-screen TUIs.

+
+
+ 03 · VCS +

git

+

Native object store, packfiles, smart-HTTP fetch/push, and three-way merge — interoperable with the real CLI.

+
+
+ +
+ From clone to push +

Sign in once. The workspace is the project.

+
+
+
+
+

Sign in with GitHub

+

OAuth Device Flow. Tokens in the Keychain. The GitHub container is the same session everywhere.

+
+
+
+
+

Open a workspace

+

Clone via tarball or start a local project. One workspace per repo, shared across rings.

+
+
+
+
+

Edit in place

+

Tap the file. Type. Autosave. Shared buffers mean two rings on one file stay live.

+
+
+
+
+

Commit · sync · merge

+

The graph toolbar drives the native engine. Sync pushes; pull is an explicit terminal command.

+
+
+
+
+
+ +
+
+
+ FAQ +

What Mouse is, and what it is not.

+
+
+
+ What is Mouse? +

A native mobile IDE for iOS and Android. The development environment lives in a gesture-driven shell: rings of containers for files, editor, commit graph, and terminal.

+
+
+ Is this a web app or Electron wrapper? +

No. Two native apps — SwiftUI on iOS/iPadOS, Jetpack Compose on Android. No Capacitor, React Native, or Flutter bridge.

+
+
+ Does node really run on the phone? +

Yes. A Node-compatible runtime on JavaScriptCore, with npm from the real registry, sockets, HTTP, and packages like vite, webpack, jest, and express verified against real node.

+
+
+ How does git work without a git binary? +

Mouse ships a from-scratch engine: loose objects, packfiles, refs, merge, and GitHub smart-HTTP. Repositories it writes pass git fsck.

+
+
+ Where do I get it? +

The source is on GitHub today. Build the iOS app with Xcode + xcodegen, or the Android app with Gradle. Release packaging is tracked in the repo.

+
+
+ What about AI / agents? +

Product branches on the roadmap cover AI pair editing and background agents as containers. The shell’s multi-view model is the substrate; those slices land when feel-tested.

+
+
+
+
+ +
+
+

Build on a phone that can build.

+

Clone the repo. Generate the project. Run.

+ +
+
+
+ + + + + + diff --git a/website/lighthouserc.json b/website/lighthouserc.json new file mode 100644 index 0000000..edee715 --- /dev/null +++ b/website/lighthouserc.json @@ -0,0 +1,24 @@ +{ + "ci": { + "collect": { + "startServerCommand": "npx --yes http-server ./website -p 8099 -c-1", + "startServerReadyPattern": "Available on", + "url": ["http://localhost:8099/index.html"], + "numberOfRuns": 3, + "settings": { + "chromeFlags": "--no-sandbox --disable-dev-shm-usage" + } + }, + "assert": { + "assertions": { + "categories:performance": ["error", { "minScore": 0.85 }], + "categories:accessibility": ["error", { "minScore": 1 }], + "categories:best-practices": ["error", { "minScore": 1 }], + "categories:seo": ["error", { "minScore": 1 }] + } + }, + "upload": { + "target": "temporary-public-storage" + } + } +} diff --git a/website/main.js b/website/main.js new file mode 100644 index 0000000..3a497ae --- /dev/null +++ b/website/main.js @@ -0,0 +1,84 @@ +(() => { + document.documentElement.classList.add('js'); + + const nav = document.getElementById('nav'); + const hero = document.querySelector('.hero'); + + const onScroll = () => { + const past = hero ? window.scrollY > hero.offsetHeight - 72 : window.scrollY > 40; + nav.classList.toggle('is-solid', past); + }; + onScroll(); + window.addEventListener('scroll', onScroll, { passive: true }); + + const reveals = document.querySelectorAll('.reveal'); + const show = (el) => el.classList.add('is-in'); + if ('IntersectionObserver' in window) { + const io = new IntersectionObserver((entries) => { + for (const e of entries) { + if (e.isIntersecting) { + show(e.target); + io.unobserve(e.target); + } + } + }, { rootMargin: '0px 0px -6% 0px', threshold: 0.08 }); + reveals.forEach((el) => io.observe(el)); + // Programmatic scrolls / late layout: catch anything already on screen + requestAnimationFrame(() => { + reveals.forEach((el) => { + const r = el.getBoundingClientRect(); + if (r.top < window.innerHeight * 0.92 && r.bottom > 64) show(el); + }); + }); + } else { + reveals.forEach(show); + } + + // Defer the heavy dither iframe until the page has loaded and gone idle, so + // its canvas simulation never blocks first paint or inflates blocking time. + const frame = document.querySelector('.hero-viz'); + if (frame && frame.dataset.src) { + const load = () => { if (!frame.src) frame.src = frame.dataset.src; }; + const idle = () => ('requestIdleCallback' in window) + ? requestIdleCallback(load, { timeout: 1500 }) + : setTimeout(load, 200); + if (document.readyState === 'complete') idle(); + else window.addEventListener('load', idle, { once: true }); + + // Fade the hero up from black once the flock has formed. The dither posts + // 'mouse-dither-ready' when its first screen is applied; the load handler is + // a fallback so the hero never stays black if that signal is missed. + const reveal = () => frame.classList.add('is-loaded'); + window.addEventListener('message', (e) => { + if (e.data && e.data.type === 'mouse-dither-ready') reveal(); + }); + frame.addEventListener('load', () => setTimeout(reveal, 1600), { once: true }); + } + + // Pause dither iframe work when the hero leaves the viewport. + if (frame && 'IntersectionObserver' in window) { + const freeze = new IntersectionObserver(([e]) => { + try { + frame.contentWindow?.postMessage({ type: 'mouse-dither', visible: e.isIntersecting }, '*'); + } catch (_) { /* cross-origin or not ready */ } + }, { threshold: 0.05 }); + freeze.observe(hero); + } + + // Live GitHub star count in the nav pill; falls back to the plain label. + const stars = document.getElementById('gh-stars'); + const starsFallback = document.getElementById('gh-fallback'); + if (stars) { + fetch('https://api.github.com/repos/reagent-systems/mouse', { + headers: { Accept: 'application/vnd.github+json' } + }) + .then((r) => (r.ok ? r.json() : null)) + .then((d) => { + const n = d && d.stargazers_count; + if (typeof n !== 'number') return; + stars.textContent = n >= 1000 ? (n / 1000).toFixed(n >= 10000 ? 0 : 1).replace(/\.0$/, '') + 'k' : String(n); + if (starsFallback) starsFallback.style.display = 'none'; + }) + .catch(() => { /* offline or rate-limited: keep the label */ }); + } +})(); diff --git a/website/robots.txt b/website/robots.txt new file mode 100644 index 0000000..f5f5f0e --- /dev/null +++ b/website/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://reagent-systems.github.io/mouse/sitemap.xml diff --git a/website/sitemap.xml b/website/sitemap.xml new file mode 100644 index 0000000..d967e93 --- /dev/null +++ b/website/sitemap.xml @@ -0,0 +1,8 @@ + + + + https://reagent-systems.github.io/mouse/ + weekly + 1.0 + + diff --git a/website/styles.css b/website/styles.css new file mode 100644 index 0000000..0f0427f --- /dev/null +++ b/website/styles.css @@ -0,0 +1,575 @@ +/* Mouse marketing site — dark, terminal honesty, monochrome, IBM Plex Mono */ + +/* Self-hosted so the font never render-blocks on a third-party request. */ +@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; font-display: swap; src: url("assets/fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2"); } +@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 500; font-display: swap; src: url("assets/fonts/ibm-plex-mono-latin-500-normal.woff2") format("woff2"); } +@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 600; font-display: swap; src: url("assets/fonts/ibm-plex-mono-latin-600-normal.woff2") format("woff2"); } +@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 700; font-display: swap; src: url("assets/fonts/ibm-plex-mono-latin-700-normal.woff2") format("woff2"); } + +:root { + --bg: #060608; + --bg-2: #0b0b0f; + --card: #101015; + --card-2: #16161d; + --ink: #f4f2ec; + --muted: rgba(244, 242, 236, 0.60); + --faint: rgba(244, 242, 236, 0.40); + --line: rgba(244, 242, 236, 0.10); + --line-2: rgba(244, 242, 236, 0.20); + --radius: 20px; + --radius-sm: 14px; + --gutter: clamp(1.1rem, 4vw, 2.5rem); + --max: 1180px; + --font: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + --ease: cubic-bezier(0.22, 1, 0.36, 1); +} + +*, +*::before, +*::after { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +body { + margin: 0; + font-family: var(--font); + color: var(--ink); + background: var(--bg); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + overflow-x: hidden; +} + +img { max-width: 100%; display: block; } +a { color: inherit; text-decoration: none; } +button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; } + +.sr-only { + position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; + overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; +} + +.wrap { + width: min(100% - 2 * var(--gutter), var(--max)); + margin-inline: auto; +} + +/* ---------- nav ---------- */ + +.nav { + position: fixed; + inset: 0 0 auto 0; + z-index: 40; + height: 66px; + display: flex; + align-items: center; + transition: background 0.3s var(--ease), border-color 0.3s var(--ease), backdrop-filter 0.3s var(--ease); + border-bottom: 1px solid transparent; +} + +.nav.is-solid { + background: rgba(8, 8, 11, 0.72); + border-bottom-color: var(--line); + backdrop-filter: blur(14px) saturate(1.2); + -webkit-backdrop-filter: blur(14px) saturate(1.2); +} + +.nav-inner { + width: min(100% - 2 * var(--gutter), var(--max)); + margin-inline: auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.nav-brand { + display: inline-flex; + align-items: center; + gap: 0.6rem; + color: var(--ink); + font-weight: 700; + letter-spacing: -0.02em; + font-size: 1.05rem; +} + +.nav-brand img { width: 26px; height: 26px; object-fit: contain; filter: invert(1); } + +.nav-links { + display: none; + gap: 1.6rem; + position: absolute; + left: 50%; + transform: translateX(-50%); +} + +@media (min-width: 860px) { .nav-links { display: flex; } } + +.nav-link { + color: var(--muted); + font-size: 0.85rem; + transition: color 0.2s ease; +} +.nav-link:hover { color: var(--ink); } + +.nav-right { display: inline-flex; align-items: center; gap: 0.6rem; } + +.gh-pill { + display: inline-flex; + align-items: center; + gap: 0.5rem; + height: 36px; + padding: 0 0.85rem; + border-radius: 999px; + border: 1px solid var(--line-2); + color: var(--ink); + font-size: 0.8rem; + font-weight: 500; + transition: border-color 0.2s ease, background 0.2s ease; +} +.gh-pill:hover { border-color: var(--ink); background: rgba(244, 242, 236, 0.05); } +.gh-pill svg { opacity: 0.9; } +.gh-stars { font-variant-numeric: tabular-nums; color: var(--muted); } +.gh-stars:empty { display: none; } +.gh-stars::before { content: "★ "; color: var(--ink); } + +/* ---------- buttons ---------- */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + height: 46px; + padding: 0 1.35rem; + border-radius: 999px; + font-size: 0.9rem; + font-weight: 600; + letter-spacing: -0.01em; + transition: transform 0.18s var(--ease), background 0.18s ease, color 0.18s ease, border-color 0.18s ease, filter 0.18s ease; +} +.btn:hover { transform: translateY(-1px); } +.btn svg { transition: transform 0.2s var(--ease); } +.btn:hover svg { transform: translateX(2px); } + +.btn-primary { background: var(--ink); color: #0a0a0c; } +.btn-primary:hover { filter: brightness(1.06); } + +.btn-ghost { + border: 1px solid var(--line-2); + color: var(--ink); + background: rgba(244, 242, 236, 0.02); +} +.btn-ghost:hover { border-color: var(--ink); background: rgba(244, 242, 236, 0.06); } + +.btn-sm { height: 40px; padding: 0 1rem; font-size: 0.82rem; } + +/* ---------- hero ---------- */ + +.hero { + position: relative; + min-height: 100svh; + background: var(--bg); + color: var(--ink); + overflow: hidden; + display: grid; + align-items: center; +} + +.hero::before { + content: ""; + position: absolute; + left: 50%; top: -30%; + width: 90vw; height: 70vw; + max-width: 1100px; max-height: 800px; + transform: translateX(-60%); + background: radial-gradient(ellipse at center, rgba(244, 242, 236, 0.08), transparent 60%); + pointer-events: none; + z-index: 0; +} + +.hero-viz { + position: absolute; + inset: 0; + border: 0; + width: 100%; + height: 100%; + pointer-events: auto; + z-index: 1; + opacity: 0; + transition: opacity 0.9s var(--ease); +} + +/* Fades up from the black hero once the flock has formed (see main.js). */ +.hero-viz.is-loaded { opacity: 1; } + +.hero-fade { + position: absolute; + inset: 0; + z-index: 2; + pointer-events: none; + background: + linear-gradient(90deg, var(--bg) 0%, rgba(6, 6, 8, 0.82) 34%, rgba(6, 6, 8, 0.28) 62%, transparent 82%), + linear-gradient(180deg, rgba(6, 6, 8, 0.7) 0%, transparent 24%, transparent 60%, var(--bg) 100%); +} + +.hero-content { + position: relative; + z-index: 3; + width: min(100% - 2 * var(--gutter), var(--max)); + margin: 0 auto; + padding: 8rem 0 3rem; + pointer-events: none; +} +.hero-content > * { pointer-events: auto; } + +.badge { + display: inline-flex; + align-items: center; + gap: 0.55rem; + margin: 0 0 1.6rem; + padding: 0.4rem 0.85rem 0.4rem 0.7rem; + border-radius: 999px; + border: 1px solid var(--line-2); + background: rgba(244, 242, 236, 0.03); + font-size: 0.76rem; + color: var(--muted); + letter-spacing: 0.01em; +} +.badge b { color: var(--ink); font-weight: 600; } +.badge .dot { + width: 7px; height: 7px; border-radius: 50%; + background: var(--ink); + box-shadow: 0 0 0 4px rgba(244, 242, 236, 0.12); + animation: pulse-dot 2.2s ease-in-out infinite; +} +@keyframes pulse-dot { + 0%, 100% { opacity: 0.5; transform: scale(0.9); } + 50% { opacity: 1; transform: scale(1); } +} + +.hero-title { + margin: 0 0 1.35rem; + font-size: clamp(2.6rem, 7vw, 4.6rem); + line-height: 1.02; + letter-spacing: -0.045em; + font-weight: 700; + max-width: 15ch; +} +.hero-title .soft { color: var(--faint); } + +.hero-lede { + margin: 0 0 2rem; + font-size: clamp(1rem, 1.6vw, 1.15rem); + line-height: 1.55; + color: var(--muted); + max-width: 34rem; +} +.hero-lede code { font-family: inherit; color: var(--ink); } + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 3rem; +} + +/* spec strip in place of "trusted by" logos */ +.specs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.6rem 1.5rem; +} +.specs .caps { + width: 100%; + font-size: 0.68rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--faint); + margin-bottom: 0.35rem; +} +.spec { + display: inline-flex; + align-items: baseline; + gap: 0.5rem; + font-size: 0.85rem; + color: var(--muted); +} +.spec b { color: var(--ink); font-weight: 600; letter-spacing: -0.01em; } + +.hero-hint { + margin-top: 2rem; + font-size: 0.72rem; + color: var(--faint); +} + +/* ---------- sections ---------- */ + +.section { padding: clamp(4.5rem, 9vw, 7.5rem) 0; position: relative; } +.section-tint { background: var(--bg-2); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } + +.section-head { max-width: 40rem; margin-bottom: 2.75rem; } +.eyebrow { + display: inline-block; + margin-bottom: 0.9rem; + font-size: 0.7rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--faint); +} +.section-head h2 { + margin: 0 0 0.75rem; + font-size: clamp(1.8rem, 4vw, 2.6rem); + letter-spacing: -0.04em; + line-height: 1.1; + font-weight: 700; +} +.section-head p { margin: 0; color: var(--muted); font-size: 1rem; line-height: 1.55; } + +/* ---------- feature cards (screens) ---------- */ + +.feature-cards { + display: grid; + gap: 1.1rem; + grid-template-columns: 1fr; +} +@media (min-width: 720px) { .feature-cards { grid-template-columns: repeat(3, 1fr); } } + +.fcard { + display: flex; + flex-direction: column; + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--card); + overflow: hidden; + transition: transform 0.35s var(--ease), border-color 0.25s ease, background 0.25s ease; +} +.fcard:hover { transform: translateY(-4px); border-color: var(--line-2); background: var(--card-2); } + +.fcard-shot { + position: relative; + aspect-ratio: 4 / 3; + overflow: hidden; + background: #000; + border-bottom: 1px solid var(--line); +} +.fcard-shot img { + position: absolute; + inset: 0; + width: 100%; height: 100%; + object-fit: cover; + object-position: top center; + transition: transform 0.6s var(--ease); +} +.fcard:hover .fcard-shot img { transform: scale(1.04); } + +.fcard-body { padding: 1.25rem 1.3rem 1.4rem; } +.fcard .key { + display: inline-block; + margin-bottom: 0.7rem; + font-size: 0.66rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--faint); +} +.fcard h3 { margin: 0 0 0.45rem; font-size: 1.05rem; letter-spacing: -0.02em; } +.fcard p { margin: 0; font-size: 0.88rem; color: var(--muted); line-height: 1.5; } + +/* ---------- showcase (big screen + rail) ---------- */ + +.showcase { display: grid; gap: 1.25rem; } +@media (min-width: 900px) { .showcase { grid-template-columns: 1.1fr 0.9fr; align-items: stretch; } } + +.phone-frame { + position: relative; + border-radius: calc(var(--radius) + 6px); + background: #000; + padding: 0.8rem; + border: 1px solid var(--line); + overflow: hidden; + min-height: 420px; +} +.phone-frame img { width: 100%; height: 100%; object-fit: cover; object-position: top center; border-radius: var(--radius); background: #000; } +.phone-caption { + position: absolute; + left: 1.3rem; right: 1.3rem; bottom: 1.3rem; + padding: 0.85rem 1rem; + border-radius: 12px; + background: rgba(6, 6, 8, 0.82); + border: 1px solid var(--line-2); + backdrop-filter: blur(10px); +} +.phone-caption .label { display: block; font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); margin-bottom: 0.3rem; } +.phone-caption strong { font-size: 0.92rem; font-weight: 600; } + +.stack-cards { display: grid; gap: 1rem; align-content: start; } +.stack-card { + display: grid; + grid-template-columns: 76px 1fr; + gap: 1rem; + align-items: center; + padding: 0.85rem; + border-radius: var(--radius-sm); + border: 1px solid var(--line); + background: var(--card); + transition: transform 0.3s var(--ease), background 0.25s ease, border-color 0.2s ease; +} +.stack-card:hover { transform: translateX(4px); background: var(--card-2); border-color: var(--line-2); } +.stack-card img { width: 76px; height: 76px; object-fit: cover; border-radius: 10px; background: #000; } +.stack-card h3 { margin: 0 0 0.3rem; font-size: 0.98rem; } +.stack-card p { margin: 0; font-size: 0.82rem; color: var(--muted); line-height: 1.45; } + +/* ---------- gesture grid ---------- */ + +.gesture-grid { display: grid; gap: 1rem; grid-template-columns: 1fr; } +@media (min-width: 720px) { .gesture-grid { grid-template-columns: repeat(2, 1fr); } } +@media (min-width: 980px) { .gesture-grid { grid-template-columns: repeat(3, 1fr); } } + +.gesture-card { + padding: 1.4rem 1.35rem 1.5rem; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--card); + transition: transform 0.35s var(--ease), border-color 0.25s ease, background 0.25s ease; +} +.gesture-card:hover { transform: translateY(-3px); border-color: var(--line-2); background: var(--card-2); } +.gesture-card .key { display: inline-block; margin-bottom: 0.85rem; font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); } +.gesture-card h3 { margin: 0 0 0.5rem; font-size: 1.05rem; letter-spacing: -0.02em; } +.gesture-card p { margin: 0; font-size: 0.88rem; color: var(--muted); line-height: 1.5; } + +.law { + margin-top: 2rem; + padding: 1.3rem 1.4rem; + border: 1px solid var(--line); + border-left: 2px solid var(--ink); + border-radius: var(--radius-sm); + background: var(--card); + font-size: 0.95rem; + line-height: 1.55; + color: var(--muted); +} +.law strong { font-weight: 700; color: var(--ink); } + +/* ---------- stack (capabilities) ---------- */ + +.caps { display: grid; gap: 1rem; } +@media (min-width: 800px) { .caps { grid-template-columns: repeat(3, 1fr); } } +.cap { + padding: 1.6rem 1.4rem; + border-radius: var(--radius-sm); + background: var(--card); + border: 1px solid var(--line); + transition: border-color 0.2s ease, background 0.25s ease; +} +.cap:hover { border-color: var(--line-2); background: var(--card-2); } +.cap .num { display: block; margin-bottom: 1.1rem; font-size: 0.7rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--faint); } +.cap h3 { margin: 0 0 0.55rem; font-size: 1.25rem; letter-spacing: -0.03em; } +.cap p { margin: 0; color: var(--muted); font-size: 0.9rem; line-height: 1.55; } +.cap code { font-family: inherit; font-size: 0.85em; padding: 0.05em 0.3em; border-radius: 4px; background: rgba(244, 242, 236, 0.08); color: var(--ink); } + +/* ---------- steps ---------- */ + +.steps { display: grid; gap: 0; counter-reset: step; border-top: 1px solid var(--line); } +.step { + display: grid; gap: 0.75rem; + padding: 1.7rem 0; + border-bottom: 1px solid var(--line); + grid-template-columns: auto 1fr; + align-items: start; +} +.step::before { + counter-increment: step; + content: counter(step, decimal-leading-zero); + font-size: 0.78rem; + letter-spacing: 0.08em; + color: var(--faint); + padding-top: 0.2rem; + min-width: 2.4rem; +} +.step h3 { margin: 0 0 0.4rem; font-size: 1.1rem; letter-spacing: -0.02em; } +.step p { margin: 0; color: var(--muted); font-size: 0.92rem; max-width: 42rem; line-height: 1.55; } + +/* ---------- FAQ ---------- */ + +.faq { display: grid; gap: 0.65rem; max-width: 48rem; } +.faq details { + border: 1px solid var(--line); + border-radius: 12px; + background: var(--card); + padding: 0 1.15rem; + transition: border-color 0.2s ease, background 0.2s ease; +} +.faq details[open] { background: var(--card-2); border-color: var(--line-2); } +.faq summary { + list-style: none; cursor: pointer; + padding: 1.1rem 0; + font-weight: 600; font-size: 0.95rem; + display: flex; justify-content: space-between; gap: 1rem; align-items: center; +} +.faq summary::-webkit-details-marker { display: none; } +.faq summary::after { content: "+"; font-weight: 400; color: var(--muted); font-size: 1.2rem; line-height: 1; } +.faq details[open] summary::after { content: "−"; } +.faq details p { margin: 0 0 1.15rem; color: var(--muted); font-size: 0.9rem; line-height: 1.6; } + +/* ---------- CTA band ---------- */ + +.cta-band { + position: relative; + overflow: hidden; + background: var(--bg-2); + padding: clamp(4.5rem, 10vw, 7rem) 0; + text-align: center; + border-top: 1px solid var(--line); +} +.cta-band::before { + content: ""; + position: absolute; + left: 50%; top: -50%; + width: 80vw; height: 80vw; + max-width: 900px; max-height: 900px; + transform: translateX(-50%); + background: radial-gradient(circle, rgba(244, 242, 236, 0.10), transparent 62%); + pointer-events: none; +} +.cta-inner { position: relative; z-index: 1; width: min(100% - 2 * var(--gutter), 42rem); margin-inline: auto; } +.cta-band h2 { margin: 0 0 0.9rem; font-size: clamp(2rem, 5vw, 3rem); letter-spacing: -0.045em; line-height: 1.08; } +.cta-band p { margin: 0 0 2rem; color: var(--muted); font-size: 1.02rem; } +.cta-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.75rem; } + +/* ---------- footer ---------- */ + +.footer { padding: 2.5rem 0 3rem; border-top: 1px solid var(--line); background: var(--bg); } +.footer-inner { display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; align-items: center; font-size: 0.8rem; color: var(--muted); } +.footer-brand { display: inline-flex; align-items: center; gap: 0.55rem; color: var(--ink); font-weight: 700; } +.footer-brand img { width: 20px; height: 20px; filter: invert(1); } +.footer-links { display: flex; gap: 1.4rem; } +.footer-links a:hover { color: var(--ink); } + +/* ---------- motion reveals ---------- */ + +html.js .reveal { opacity: 0; transform: translateY(20px); transition: opacity 0.7s var(--ease), transform 0.7s var(--ease); } +html.js .reveal.is-in { opacity: 1; transform: none; } +html.js .reveal-delay-1 { transition-delay: 0.08s; } +html.js .reveal-delay-2 { transition-delay: 0.16s; } +html.js .reveal-delay-3 { transition-delay: 0.24s; } + +@media (max-width: 720px) { + .hero-fade { + background: + linear-gradient(180deg, rgba(6, 6, 8, 0.55) 0%, rgba(6, 6, 8, 0.3) 26%, rgba(6, 6, 8, 0.78) 60%, var(--bg) 100%), + linear-gradient(90deg, var(--bg) 0%, rgba(6, 6, 8, 0.5) 60%, rgba(6, 6, 8, 0.15) 100%); + } + .hero-content { padding: 7rem 0 2.5rem; } + .hero-viz { opacity: 0.65; } +}