From bc784926e4a059ddcc740aa5434728cc8e804ea4 Mon Sep 17 00:00:00 2001
From: Lu <42706009+luinbytes@users.noreply.github.com>
Date: Thu, 5 Nov 2026 00:32:12 +0000
Subject: [PATCH 1/3] Remove Pip page and homepage feature
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
---
.github/workflows/checks.yml | 1 -
README.md | 41 +-
app/entry-pip.tsx | 4 -
app/pip/page.tsx | 148 ----
app/pip/pip-contact.tsx | 125 ---
app/pip/pip.module.css | 811 ------------------
app/routes.tsx | 21 +-
.../signal-desk/signal-desk.module.css | 22 -
.../concepts/signal-desk/signal-desk.tsx | 4 -
components/pip/pip-feature.module.css | 245 ------
components/pip/pip-feature.tsx | 28 -
components/pip/pip-logo.module.css | 21 -
components/pip/pip-logo.tsx | 19 -
components/pip/pip-web-search-demo.module.css | 319 -------
components/pip/pip-web-search-demo.tsx | 428 ---------
components/pip/pip-web-search-renderer.ts | 195 -----
components/ui/skullet-image.tsx | 2 +-
lib/pip.ts | 16 -
package-lock.json | 355 ++------
package.json | 5 +-
pip.html | 15 -
public/images/portfolio/pip-everyday.webp | Bin 280228 -> 0 bytes
public/share-cards/luinbytes-dev-pip.png | Bin 202297 -> 0 bytes
public/sitemap.xml | 10 +-
scripts/generate-pip-qr.mjs | 16 -
scripts/generate-pip-share-card.py | 91 --
scripts/generate-sitemap.js | 2 +-
scripts/pip-share-card.html | 163 ----
tests/browser/browser_test.py | 751 ++--------------
tests/pip-contact.test.mts | 43 -
vite.config.ts | 2 +-
31 files changed, 140 insertions(+), 3763 deletions(-)
delete mode 100644 app/entry-pip.tsx
delete mode 100644 app/pip/page.tsx
delete mode 100644 app/pip/pip-contact.tsx
delete mode 100644 app/pip/pip.module.css
delete mode 100644 components/pip/pip-feature.module.css
delete mode 100644 components/pip/pip-feature.tsx
delete mode 100644 components/pip/pip-logo.module.css
delete mode 100644 components/pip/pip-logo.tsx
delete mode 100644 components/pip/pip-web-search-demo.module.css
delete mode 100644 components/pip/pip-web-search-demo.tsx
delete mode 100644 components/pip/pip-web-search-renderer.ts
delete mode 100644 lib/pip.ts
delete mode 100644 pip.html
delete mode 100644 public/images/portfolio/pip-everyday.webp
delete mode 100644 public/share-cards/luinbytes-dev-pip.png
delete mode 100644 scripts/generate-pip-qr.mjs
delete mode 100644 scripts/generate-pip-share-card.py
delete mode 100644 scripts/pip-share-card.html
delete mode 100644 tests/pip-contact.test.mts
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index c64c929..4d41693 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -26,7 +26,6 @@ jobs:
- run: npm ci
- run: npm audit --audit-level=high
- run: npm run lint
- - run: npm run test:pip
- run: npm run test:pond
- name: Install browser test dependencies
run: |
diff --git a/README.md b/README.md
index b6cc8cf..845a7c2 100644
--- a/README.md
+++ b/README.md
@@ -4,19 +4,13 @@ Lu's interactive personal portfolio, built around the living pixel pond version
The homepage focuses on Lu's current work at [Orchid.ai](https://orchid.ai), selected public projects, and an interactive procedural pond rather than a catalogue of product pages.
-The site has three public routes: `/`, the interactive portfolio; `/pip`, a static product page for Pip, Lu's warm, easygoing Telegram agent; and `/privacy`, a short personal-use privacy policy. The Pip page is a deliberate standalone exception to the portfolio's homepage-first structure. Retired concept and product URLs resolve to the pond-themed 404 instead of preserving stale copies of older portfolios.
+The site has two public routes: `/`, the interactive portfolio, and `/privacy`, a short personal-use privacy policy. Retired concept and product URLs resolve to the pond-themed 404.
The portfolio features [HomeBot](https://github.com/luinbytes/HomeBot), [rakazo-android](https://github.com/luinbytes/rakazo-android), [Linux Sonar](https://github.com/luinbytes/linux-sonar), [bongocat](https://github.com/luinbytes/bongocat), and [cursor-barrier](https://github.com/luinbytes/cursor-barrier).
-Pip has a dedicated introduction below the homepage hero, a **Meet Pip** link to
-`/pip`, and a navigation link at every screen size. Both pages share the Pip speech-bubble
-logo and Pixelify wordmark from `components/pip/pip-logo.tsx`.
-
The interface follows the system light/dark preference. The pond, fish, cat,
feeding controls, and portrait foil retain their existing behavior in both modes.
-Navigation is available at every screen size, including without JavaScript. Pip
-uses a dedicated pixel illustration with a readable example conversation and
-keeps its Telegram, copy-handle, and on-demand QR contact paths.
+Navigation is available at every screen size, including without JavaScript.
## Stack
@@ -41,7 +35,6 @@ Run the production checks:
```bash
npm run lint
npm run build
-npm run test:pip
npm run test:pond
```
@@ -57,45 +50,31 @@ npm run test:e2e
`npm run preview` serves the built `out` directory at http://127.0.0.1:3004 with real 404 responses. The browser suite uses this production export.
-The `/pip` page is exported with the rest of the site and keeps its contact action
-available as static, readable HTML. Its Telegram target is defined in
-`PIP_TELEGRAM_HANDLE` in `lib/pip.ts`; rebuild after changing it so the
-configured Telegram handle and contact controls are rendered into the exported
-page. Message Pip and copy-handle fallback are available directly, while the
-QR image is generated locally during development setup and production builds.
-It is requested from this site only after Show QR, without a third-party image service. The
-page's canonical URL is
-`https://luinbytes.dev/pip`, and it uses a Pip-specific share card so embeds keep
-the product page's identity.
-
## Deployment
Pushes to `master` build and deploy the static export to [luinbytes.dev](https://luinbytes.dev/) through GitHub Pages.
## Social preview
-Open Graph and Twitter large-image cards use checked-in 1200 x 630 PNGs:
-`public/share-cards/luinbytes-dev-pond.png` for the homepage and
-`public/share-cards/luinbytes-dev-pip.png` for `/pip`. The versioned filenames
-avoid reusing the old pink card's cached image URL. The old PNG remains available
+Open Graph and Twitter large-image cards use the checked-in 1200 x 630 PNG
+`public/share-cards/luinbytes-dev-pond.png` for the homepage. The versioned filename
+avoids reusing the old pink card's cached image URL. The old PNG remains available
for existing links, but no current metadata references it.
-The editable layouts are `scripts/share-card.html` for the homepage and
-`scripts/pip-share-card.html` for Pip. They use local assets/fonts and render
+The editable layout is `scripts/share-card.html` for the homepage. It uses local assets/fonts and renders
offline. To regenerate after an intentional design edit, use the browser-test
setup above, then:
```bash
npm run build
python3 scripts/generate-share-card.py
-python3 scripts/generate-pip-share-card.py
python3 -m unittest discover -s tests/browser -p '*_test.py' -k ShareCardStaticExportTests -v
```
-The generators embed the original card fonts from installed Fontsource packages, render offline, and check loaded
-fonts plus a 32px text-safe inset. Review both PNGs at full size and at typical
+The generator embeds the original card fonts from installed Fontsource packages, renders offline, and checks loaded
+fonts plus a 32px text-safe inset. Review the PNG at full size and at typical
embed size before updating the expected SHA-256 values in
-`ShareCardStaticExportTests` to the generators' printed values. The browser suite builds once and checks the metadata, asset dimensions/hash and absence of old
+`ShareCardStaticExportTests` to the generator's printed value. The browser suite builds once and checks the metadata, asset dimensions/hash and absence of old
image references in exported HTML. Normal Pages builds need no Python, browser,
image service or runtime route: Vite copies the checked-in PNGs into the static export.
@@ -106,7 +85,7 @@ image service or runtime route: Vite copies the checked-in PNGs into the static
## Security and page loading
-Home, Pip, and the 404 page have separate HTML and client entries. Vite generates
+Home, privacy, and the 404 page have separate HTML and client entries. Vite generates
their stylesheet and preload links. Run `node scripts/measure-page-assets.mjs`
after a build to inspect each page's initial uncompressed JavaScript and CSS.
diff --git a/app/entry-pip.tsx b/app/entry-pip.tsx
deleted file mode 100644
index 5b3faf8..0000000
--- a/app/entry-pip.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import PipPage from "./pip/page";
-import { hydratePage } from "./hydrate-page";
-
-hydratePage(PipPage);
diff --git a/app/pip/page.tsx b/app/pip/page.tsx
deleted file mode 100644
index 555e14e..0000000
--- a/app/pip/page.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import { ArrowUpRight, Bell, Compass, Heart, Search, Send, Smile, Sparkles } from "lucide-react";
-import { getPipContact } from "@/lib/pip";
-import { PipFace, PipLogo } from "@/components/pip/pip-logo";
-import { PipWebSearchDemo } from "@/components/pip/pip-web-search-demo";
-import { Button } from "@/components/ui/button";
-import { SkulletImage } from "@/components/ui/skullet-image";
-import { PipContact } from "./pip-contact";
-import styles from "./pip.module.css";
-
-const features = [
- {
- icon: Bell,
- title: "Reminders",
- text: "Set a reminder in chat for the time you choose.",
- },
- {
- icon: Smile,
- title: "Stickers and images",
- text: "Share images and stickers when words are not quite enough.",
- },
- {
- icon: Compass,
- title: "Maps and weather",
- text: "Find places on a map and check the weather before you head out.",
- },
- {
- icon: Heart,
- title: "Conversation memory",
- text: "Pip can remember details about each person so later chats have more context.",
- },
- {
- icon: Sparkles,
- title: "Everyday advice",
- text: "Talk through meal ideas, second opinions, and small decisions.",
- },
-];
-
-export default function PipPage() {
- const contact = getPipContact();
-
- return (
-
-
-
-
-
-
-
A little help. A little banter.
-
Pip is your Telegram mate for quick answers, useful reminders, and the everyday things on your mind.
-
- {contact ? (
-
Message Pip
- ) : (
-
Message Pip
- )}
-
- {!contact &&
Pip is not available to message right now.
}
-
-
-
-
-
-
-
-
-
-
A little help, in context.
-
A dinner idea, then a reminder. Both fit in the same conversation.
-
-
- Example conversation
-
-
You
-
Got eggs, rice and zero motivation.
-
-
-
Pip
-
Egg fried rice. Ten minutes, one pan. Future you will thank you.
-
-
-
You
-
Remind me to get soy sauce tomorrow at 6pm.
-
-
-
Pip
-
You got it. Tomorrow at 6pm.
-
-
-
-
-
-
-
Small asks. Sorted.
-
Search, remember a detail, or make a quick plan. Pip can help with the small stuff.
-
-
-
-
- Web search
- Ask a quick question or follow a curiosity. Pip searches the web and brings back useful links.
-
-
-
- {features.map(({ icon: Icon, title: featureTitle, text }) => (
-
-
-
-
{featureTitle}
-
{text}
-
-
- ))}
-
-
-
-
-
-
-
-
Easy company. Clear boundaries.
-
Warm, short, and easygoing. Pip keeps things helpful with a gentle redirect when needed, and a firm no for clearly illegal requests.
-
-
-
-
-
-
-
-
- );
-}
diff --git a/app/pip/pip-contact.tsx b/app/pip/pip-contact.tsx
deleted file mode 100644
index 1ff44eb..0000000
--- a/app/pip/pip-contact.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import { ArrowUpRight, Check, Copy, Send, QrCode } from "lucide-react";
-import type { getPipContact } from "@/lib/pip";
-import { Button } from "@/components/ui/button";
-import { loadSkulletOverlay, SKELETON_HOLD_MS, SkulletImage } from "@/components/ui/skullet-image";
-import styles from "./pip.module.css";
-
-function loadQrOverlay() {
- let timer = 0;
- const timeout = new Promise((resolve) => {
- timer = window.setTimeout(() => resolve(false), SKELETON_HOLD_MS);
- });
- return Promise.race([loadSkulletOverlay(), timeout]).finally(() => {
- window.clearTimeout(timer);
- });
-}
-
-export function PipContact({ contact }: { contact: ReturnType }) {
- const [copyStatus, setCopyStatus] = useState<"idle" | "copied" | "failed">("idle");
- const [showQr, setShowQr] = useState(false);
- const [qrImmediate, setQrImmediate] = useState(false);
- const [qrFailed, setQrFailed] = useState(false);
- const qrOpenRequest = useRef(0);
- useEffect(() => () => {
- qrOpenRequest.current++;
- }, []);
-
- async function copyHandle() {
- if (!contact) return;
- try {
- await navigator.clipboard.writeText(contact.handle);
- setCopyStatus("copied");
- } catch {
- setCopyStatus("failed");
- }
- }
-
- return (
-
-
-
-
A question, a half-formed thought, or just a hello. Start wherever you like.
-
-
-
- {contact ? (
- <>
-
PIP’S TELEGRAM HANDLE
-
{contact.handle}
-
- Message Pip
-
-
Opens Pip in Telegram.
- >
- ) : (
- <>
-
A LITTLE HELLO, SOON
-
Chat opens soon.
-
Pip’s public handle is on its way. Check back here to start a chat.
-
- Message Pip
-
- >
- )}
-
-
- {copyStatus === "copied" ? : } Copy handle
-
- {
- const request = ++qrOpenRequest.current;
- if (showQr) {
- setShowQr(false);
- return;
- }
- void loadQrOverlay().then((ready) => {
- if (request !== qrOpenRequest.current) return;
- setQrImmediate(ready);
- setShowQr(true);
- });
- }}
- >
- {showQr ? "Hide QR code" : "Show QR code"}
-
-
-
- {copyStatus === "copied" && "Handle copied."}
- {copyStatus === "failed" && "Couldn’t copy. Select the handle above and copy it manually."}
-
-
- {showQr && contact && (
-
- {qrFailed ? (
-
QR code couldn’t load. Use Message Pip or copy the handle instead.
- ) : (
- <>
-
setQrFailed(true)}
- />
- Point your phone’s camera here to say hello.
- >
- )}
-
- )}
-
-
Without JavaScript, use the handle above or Message Pip. Copy and QR controls need JavaScript.
-
-
- );
-}
diff --git a/app/pip/pip.module.css b/app/pip/pip.module.css
deleted file mode 100644
index 8d3cb6e..0000000
--- a/app/pip/pip.module.css
+++ /dev/null
@@ -1,811 +0,0 @@
-.page {
- --pip-accent: var(--site-accent);
- --pip-accent-soft: var(--glass-selected);
- --pip-action-bg: var(--site-accent);
- --pip-action-text: var(--site-contrast);
- min-height: 100svh;
- color: var(--site-text);
- background: var(--site-bg);
- font-family: var(--font-body, var(--font-manrope, sans-serif));
-}
-
-.page :where(a, button):focus-visible {
- outline: 3px solid var(--site-accent);
- outline-offset: 4px;
-}
-
-.page ::selection {
- color: var(--pip-action-text);
- background: var(--pip-action-bg);
-}
-
-.shell {
- width: min(100%, 1232px);
- margin-inline: auto;
- padding: 0 clamp(16px, 3.6vw, 40px) max(20px, env(safe-area-inset-bottom));
-}
-
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- min-height: 68px;
- margin-top: max(16px, env(safe-area-inset-top));
- padding: 7px 10px 7px 16px;
- border: 1px solid var(--glass-edge);
- border-radius: var(--site-control-radius);
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-shadow);
- backdrop-filter: blur(14px) saturate(1.16);
-}
-
-.wordmark {
- display: inline-flex;
- align-items: center;
- min-width: 44px;
- min-height: 48px;
- color: var(--pip-accent);
- text-decoration: none;
-}
-
-.wordmark > span {
- color: var(--pip-accent);
-}
-
-.homeLink {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- min-height: 48px;
- padding-inline: 16px;
- border: 1px solid transparent;
- border-radius: var(--site-control-radius);
- color: var(--site-muted);
- font-size: 13px;
- font-weight: 600;
- text-decoration: none;
- white-space: nowrap;
- transition: color var(--ui-duration) var(--ui-ease), border-color var(--ui-duration) var(--ui-ease), transform var(--ui-duration) var(--ui-ease);
-}
-
-.homeLink svg,
-.footer > a svg {
- width: 16px;
- height: 16px;
- flex: 0 0 auto;
-}
-
-.homeLink:hover,
-.footer > a:hover {
- color: var(--site-text);
-}
-
-.homeLink:hover {
- border-color: var(--glass-edge);
- background: var(--glass-selected);
-}
-
-.hero {
- display: grid;
- gap: 20px;
- align-items: center;
- margin-top: clamp(24px, 5vw, 56px);
- padding: clamp(14px, 2vw, 20px);
- border: 1px solid var(--glass-edge);
- border-radius: 32px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-panel-shadow);
- backdrop-filter: blur(16px) saturate(1.14);
-}
-
-.heroCopy {
- max-width: 600px;
- padding: clamp(8px, 2vw, 22px);
-}
-
-.hero h1 {
- margin: 0;
- font-size: clamp(2.55rem, 8.8vw, 4.85rem);
- font-weight: 600;
- line-height: 1;
- letter-spacing: -.065em;
- text-wrap: balance;
-}
-
-.hero h1 > span,
-.sectionHeading h2 > span,
-.contact h2 > span {
- color: var(--pip-accent);
-}
-
-.intro {
- max-width: 38ch;
- margin: 20px 0 24px;
- color: var(--site-muted);
- font-size: 16px;
- line-height: 1.7;
- text-wrap: pretty;
-}
-
-.heroActions {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.primaryAction {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- min-width: 44px;
- min-height: 52px;
- padding: 13px 20px;
- border: 1px solid color-mix(in srgb, var(--pip-action-bg) 76%, var(--glass-rim));
- border-radius: var(--site-control-radius);
- background: linear-gradient(145deg, rgb(255 255 255 / .06), transparent), var(--pip-action-bg);
- color: var(--pip-action-text);
- font-size: 14px;
- font-weight: 700;
- line-height: 1.2;
- text-decoration: none;
- text-align: center;
- cursor: pointer;
- box-shadow: var(--glass-shadow);
- transition: border-color var(--ui-duration) var(--ui-ease), box-shadow var(--ui-duration) var(--ui-ease), transform var(--ui-duration) var(--ui-ease);
-}
-
-.primaryAction svg {
- width: 18px;
- height: 18px;
- flex: 0 0 auto;
-}
-
-.primaryAction:not(:disabled):hover {
- border-color: var(--glass-rim);
- box-shadow: var(--glass-panel-shadow);
- transform: translateY(-2px);
-}
-
-.primaryAction:not(:disabled):active {
- transform: translateY(1px);
-}
-
-.primaryAction:disabled {
- border-color: var(--glass-edge);
- background: var(--site-surface-soft);
- color: var(--site-muted);
- cursor: not-allowed;
- box-shadow: none;
-}
-
-.heroVisual {
- position: relative;
- width: 100%;
- aspect-ratio: 1.35 / 1;
- overflow: hidden;
- border: 1px solid var(--glass-rim);
- border-radius: 24px;
- background: var(--site-surface-soft);
- box-shadow: var(--glass-shadow);
-}
-
-.heroImage {
- object-fit: cover;
-}
-
-.example {
- display: grid;
- gap: 24px;
- align-items: start;
- margin-top: clamp(48px, 7vw, 84px);
-}
-
-.exampleHeading,
-.sectionHeading {
- max-width: 58ch;
-}
-
-.example h2,
-.sectionHeading h2,
-.personality h2,
-.contact h2 {
- margin: 0;
- font-size: clamp(2rem, 5.4vw, 3.6rem);
- font-weight: 600;
- line-height: 1.05;
- letter-spacing: -.055em;
- text-wrap: balance;
-}
-
-.exampleHeading > p,
-.sectionHeading > p {
- max-width: 48ch;
- margin: 13px 0 0;
- color: var(--site-muted);
- font-size: 15px;
- line-height: 1.7;
- text-wrap: pretty;
-}
-
-.conversation {
- min-width: 0;
- margin: 0;
- padding: clamp(18px, 3.5vw, 30px);
- border: 1px solid var(--glass-edge);
- border-radius: 28px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-panel-shadow);
- backdrop-filter: blur(12px) saturate(1.12);
-}
-
-.conversationCaption {
- display: inline-flex;
- align-items: center;
- min-height: 30px;
- margin-bottom: 10px;
- padding: 5px 11px;
- border: 1px solid var(--glass-edge);
- border-radius: var(--site-control-radius);
- color: var(--site-muted);
- background: var(--glass-fill);
- font-size: 11px;
- font-weight: 600;
- line-height: 1.4;
-}
-
-.exchange {
- display: grid;
- grid-template-columns: 38px minmax(0, 1fr);
- gap: 10px;
- align-items: start;
- padding: 8px 0;
-}
-
-.speaker {
- padding-top: 13px;
- color: var(--site-muted);
- font-family: var(--font-mono, var(--font-space-mono, monospace));
- font-size: 10px;
- line-height: 1.5;
-}
-
-.exchange:nth-of-type(even) {
- grid-template-columns: minmax(0, 1fr) 38px;
-}
-
-.exchange:nth-of-type(even) .speaker {
- order: 2;
- color: var(--pip-accent);
- text-align: right;
-}
-
-.exchange blockquote {
- width: fit-content;
- max-width: 100%;
- margin: 0;
- padding: 12px 15px;
- border: 1px solid var(--glass-edge);
- border-radius: 18px 18px 18px 5px;
- color: var(--site-text);
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-surface-soft);
- font-size: 14px;
- line-height: 1.65;
- overflow-wrap: anywhere;
-}
-
-.exchange:nth-of-type(even) blockquote {
- justify-self: end;
- border-color: color-mix(in srgb, var(--site-accent) 36%, var(--glass-edge));
- border-radius: 18px 18px 5px 18px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--glass-selected);
-}
-
-.everyday {
- margin-top: clamp(56px, 8vw, 96px);
- scroll-margin-top: 24px;
-}
-
-.features {
- display: grid;
- gap: 18px;
- margin-top: 28px;
-}
-
-.featureLead,
-.featureList {
- min-width: 0;
- border: 1px solid var(--glass-edge);
- border-radius: 28px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-panel-shadow);
- backdrop-filter: blur(12px) saturate(1.12);
-}
-
-.featureLead {
- padding: clamp(24px, 4vw, 40px);
-}
-
-.featureIcon {
- width: 21px;
- height: 21px;
- color: var(--pip-accent);
- flex: 0 0 auto;
-}
-
-.featureLead .featureIcon {
- width: 30px;
- height: 30px;
- margin-bottom: 24px;
-}
-
-.featureLead h3,
-.featureRow h3 {
- margin: 0;
- font-size: 17px;
- font-weight: 700;
- line-height: 1.4;
- letter-spacing: -.025em;
-}
-
-.featureLead h3 {
- font-size: 25px;
-}
-
-.featureLead > p,
-.featureRow p {
- max-width: 48ch;
- margin: 9px 0 0;
- color: var(--site-muted);
- font-size: 14px;
- line-height: 1.7;
- text-wrap: pretty;
-}
-
-.featureList {
- margin: 0;
- padding: 6px 20px;
- list-style: none;
-}
-
-.featureRow {
- display: grid;
- grid-template-columns: 24px minmax(0, 1fr);
- gap: 13px;
- align-items: start;
- padding: 17px 0;
-}
-
-.featureRow + .featureRow {
- border-top: 1px solid var(--glass-edge);
-}
-
-.featureRow .featureIcon {
- margin-top: 2px;
-}
-
-.personality {
- display: grid;
- gap: 22px;
- align-items: center;
- margin-top: clamp(52px, 7vw, 80px);
- padding: clamp(18px, 3.5vw, 36px);
- border: 1px solid var(--glass-edge);
- border-radius: 30px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-panel-shadow);
- backdrop-filter: blur(14px) saturate(1.12);
-}
-
-.personalityArt {
- display: grid;
- min-height: 190px;
- place-items: center;
- border: 1px solid var(--glass-rim);
- border-radius: 24px;
- background: radial-gradient(circle at 50% 38%, var(--glass-selected), transparent 70%), var(--glass-fill);
- box-shadow: var(--glass-shadow);
-}
-
-.personalityArt svg {
- width: 88px;
- height: 88px;
- color: var(--pip-accent);
- filter: drop-shadow(0 9px 18px color-mix(in srgb, var(--site-accent) 26%, transparent));
-}
-
-.personality h2 {
- font-size: clamp(2rem, 4vw, 3.1rem);
-}
-
-.personality p {
- max-width: 49ch;
- margin: 14px 0 0;
- color: var(--site-muted);
- font-size: 15px;
- line-height: 1.75;
- text-wrap: pretty;
-}
-
-.contact {
- display: grid;
- gap: 24px;
- margin-top: clamp(52px, 7vw, 84px);
- padding: clamp(20px, 4vw, 48px);
- border: 1px solid var(--glass-edge);
- border-radius: 30px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-panel-shadow);
- backdrop-filter: blur(16px) saturate(1.14);
- scroll-margin-top: 24px;
-}
-
-.contactIntro {
- min-width: 0;
-}
-
-.contactIntro > p {
- max-width: 44ch;
- margin: 14px 0 0;
- color: var(--site-muted);
- font-size: 15px;
- line-height: 1.75;
-}
-
-.contactActions {
- min-width: 0;
- padding: clamp(16px, 2.8vw, 26px);
- border: 1px solid var(--glass-edge);
- border-radius: 24px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--glass-fill);
- box-shadow: var(--glass-shadow);
-}
-
-.label {
- display: block;
- color: var(--pip-accent);
- font-family: var(--font-mono, var(--font-space-mono, monospace));
- font-size: 10px;
- font-weight: 600;
- line-height: 1.6;
- letter-spacing: .07em;
-}
-
-.handle {
- margin: 8px 0 14px;
- color: var(--site-text);
- font-family: var(--font-mono, var(--font-space-mono, monospace));
- font-size: 14px;
- line-height: 1.7;
- overflow-wrap: anywhere;
- user-select: all;
-}
-
-.contactActions > .primaryAction {
- width: fit-content;
- max-width: 100%;
- margin-top: 14px;
-}
-
-.comingSoon {
- margin: 10px 0;
- font-size: 20px;
- font-weight: 600;
- letter-spacing: -.03em;
-}
-
-.actionHint {
- margin: 10px 0 0;
- color: var(--site-muted);
- font-size: 12px;
- line-height: 1.7;
-}
-
-.secondaryActions {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-top: 14px;
-}
-
-.secondaryAction {
- display: inline-flex;
- flex: 1 1 108px;
- align-items: center;
- justify-content: center;
- gap: 8px;
- min-width: 0;
- min-height: 48px;
- padding: 10px 12px;
- border: 1px solid var(--glass-edge);
- border-radius: var(--site-control-radius);
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--glass-fill);
- color: var(--site-text);
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- box-shadow: var(--glass-shadow);
- transition: border-color var(--ui-duration) var(--ui-ease), box-shadow var(--ui-duration) var(--ui-ease), transform var(--ui-duration) var(--ui-ease);
-}
-
-.secondaryAction svg {
- width: 15px;
- height: 15px;
- flex: 0 0 auto;
-}
-
-.secondaryAction:not(:disabled):hover {
- border-color: var(--glass-rim);
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--glass-selected);
- box-shadow: var(--glass-panel-shadow);
-}
-
-.secondaryAction:not(:disabled):active {
- transform: translateY(1px);
-}
-
-.secondaryAction:disabled {
- color: var(--site-muted);
- cursor: not-allowed;
-}
-
-.copyStatus {
- min-height: 1.7em;
- margin: 8px 0 0;
- color: var(--pip-accent);
- font-size: 12px;
- line-height: 1.7;
-}
-
-.copyStatus:empty {
- min-height: 0;
- margin: 0;
-}
-
-.qrPanel {
- display: grid;
- justify-items: center;
- gap: 14px;
- margin-top: 18px;
- padding: 18px;
- border: 1px solid var(--glass-edge);
- border-radius: 20px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-shadow);
-}
-
-.qrPanel img {
- display: block;
- width: min(100%, 240px);
- height: auto;
- border: 1px solid var(--glass-edge);
- border-radius: 12px;
- background: white;
-}
-
-.qrPanel img[data-pending-media] {
- background: transparent;
- border-color: transparent;
-}
-
-.qrPanel p {
- margin: 0;
- color: var(--site-muted);
- font-size: 12px;
- line-height: 1.6;
- text-align: center;
-}
-
-.footer {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 12px 20px;
- margin-top: 28px;
- padding: 16px 4px max(20px, env(safe-area-inset-bottom));
- border-top: 1px solid var(--glass-edge);
-}
-
-.footer > a {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- min-height: 44px;
- color: var(--site-muted);
- font-family: var(--font-mono, var(--font-space-mono, monospace));
- font-size: 10px;
- letter-spacing: .07em;
- text-decoration: none;
- transition: color var(--ui-duration) var(--ui-ease);
-}
-
-.footer p {
- flex: 1;
- min-width: min(100%, 170px);
- margin: 0;
- color: var(--site-muted);
- font-size: 12px;
- line-height: 1.65;
-}
-
-.footerPip {
- color: var(--pip-accent);
- font-family: var(--font-pixelify), sans-serif;
- font-size: 32px;
-}
-
-@media (min-width: 640px) {
- .shell {
- padding-inline: clamp(24px, 4vw, 40px);
- }
-
- .hero {
- gap: clamp(22px, 3vw, 40px);
- padding: clamp(16px, 2vw, 24px);
- }
-
- .heroCopy {
- padding: clamp(12px, 2vw, 24px);
- }
-
- .heroVisual {
- aspect-ratio: 1.2 / 1;
- }
-
- .conversation {
- padding: 28px;
- }
-
- .featureLead {
- padding: 34px;
- }
-
- .contact {
- padding: 32px;
- }
-
- .footer p {
- text-align: center;
- }
-}
-
-@media (min-width: 960px) {
- .hero {
- grid-template-columns: minmax(0, .95fr) minmax(0, 1.05fr);
- gap: clamp(28px, 4vw, 56px);
- padding: clamp(18px, 2.2vw, 28px);
- }
-
- .heroCopy {
- padding: clamp(16px, 2vw, 28px);
- }
-
- .heroVisual {
- aspect-ratio: 1.16 / 1;
- }
-
- .example {
- grid-template-columns: minmax(240px, .68fr) minmax(0, 1.32fr);
- gap: clamp(28px, 5vw, 64px);
- align-items: start;
- }
-
- .conversation {
- padding: 30px;
- }
-
- .features {
- grid-template-columns: minmax(0, .88fr) minmax(0, 1.12fr);
- gap: 24px;
- align-items: stretch;
- }
-
- .featureLead {
- padding: 40px;
- }
-
- .personality {
- grid-template-columns: minmax(220px, .7fr) minmax(0, 1.3fr);
- gap: clamp(32px, 5vw, 64px);
- padding: 36px;
- }
-
- .personalityArt {
- min-height: 240px;
- }
-
- .personalityArt svg {
- width: 112px;
- height: 112px;
- }
-
- .contact {
- grid-template-columns: minmax(0, 1fr) minmax(280px, .8fr);
- gap: clamp(32px, 5vw, 56px);
- align-items: center;
- padding: 42px;
- }
-}
-
-@media (max-width: 420px) {
- .header {
- min-height: 62px;
- }
-
- .homeLink {
- padding-inline: 12px;
- font-size: 12px;
- }
-
- .heroCopy {
- padding: 5px 4px 0;
- }
-
- .hero h1 {
- font-size: clamp(2.4rem, 11.4vw, 3.1rem);
- }
-
- .intro {
- margin-block: 16px 20px;
- font-size: 15px;
- }
-
- .heroVisual {
- aspect-ratio: 1.18 / 1;
- }
-
- .example h2,
- .sectionHeading h2,
- .contact h2 {
- font-size: clamp(1.9rem, 9.4vw, 2.5rem);
- }
-
- .conversation {
- padding: 16px 12px;
- }
-
- .exchange {
- grid-template-columns: 32px minmax(0, 1fr);
- gap: 7px;
- }
-
- .exchange:nth-of-type(even) {
- grid-template-columns: minmax(0, 1fr) 32px;
- }
-
- .exchange blockquote {
- padding: 10px 12px;
- font-size: 13px;
- }
-
- .featureList {
- padding-inline: 14px;
- }
-
- .contactActions > .primaryAction {
- width: 100%;
- }
-
- .secondaryActions {
- display: grid;
- grid-template-columns: minmax(0, 1fr);
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .homeLink,
- .primaryAction,
- .secondaryAction,
- .footer > a {
- transition: none;
- }
-
- .homeLink:hover,
- .primaryAction:not(:disabled):hover,
- .primaryAction:not(:disabled):active,
- .secondaryAction:not(:disabled):active {
- transform: none;
- }
-}
diff --git a/app/routes.tsx b/app/routes.tsx
index 4f88ad5..1bc63a3 100644
--- a/app/routes.tsx
+++ b/app/routes.tsx
@@ -1,7 +1,6 @@
import type { ComponentType } from "react";
import siteConfig from "@/site.config.json";
import Home from "./page";
-import PipPage from "./pip/page";
import PrivacyPage from "./privacy/page";
import NotFound from "./not-found";
@@ -17,8 +16,8 @@ type RouteMetadata = {
};
export type SiteRoute = {
- path: "/" | "/pip" | "/privacy" | "/404";
- output: "index.html" | "pip.html" | "privacy.html" | "404.html";
+ path: "/" | "/privacy" | "/404";
+ output: "index.html" | "privacy.html" | "404.html";
Component: ComponentType;
metadata: RouteMetadata;
};
@@ -33,8 +32,6 @@ const homeMetadata: RouteMetadata = {
imageAlt: "Lu | Software Engineer",
};
-const pipDescription = "A little help, one message away. Pip is your warm, easygoing Telegram agent for questions, web searches, reminders, and everyday life. Built on Keiki.";
-
const notFoundRoute: SiteRoute = {
path: "/404",
output: "404.html",
@@ -44,20 +41,6 @@ const notFoundRoute: SiteRoute = {
export const siteRoutes: readonly SiteRoute[] = [
{ path: "/", output: "index.html", Component: Home, metadata: homeMetadata },
- {
- path: "/pip",
- output: "pip.html",
- Component: PipPage,
- metadata: {
- title: "Pip - Your Telegram mate | Lu",
- socialTitle: "Pip - Your Telegram mate",
- description: pipDescription,
- socialDescription: pipDescription,
- canonical: `${siteConfig.siteUrl}/pip`,
- image: `${siteConfig.siteUrl}/share-cards/luinbytes-dev-pip.png`,
- imageAlt: "Pip - Your Telegram mate",
- },
- },
{
path: "/privacy",
output: "privacy.html",
diff --git a/components/concepts/signal-desk/signal-desk.module.css b/components/concepts/signal-desk/signal-desk.module.css
index 1a657ec..a22ed18 100644
--- a/components/concepts/signal-desk/signal-desk.module.css
+++ b/components/concepts/signal-desk/signal-desk.module.css
@@ -112,15 +112,7 @@
transition: color var(--ui-duration, 280ms) ease, background-color var(--ui-duration, 280ms) ease, border-color var(--ui-duration, 280ms) ease, transform var(--ui-duration, 280ms) var(--ease-out);
}
-.header nav .pipNavLink {
- gap: 5px;
-}
-.pipNavLink svg {
- width: 18px;
- height: 18px;
- color: #e9a982;
-}
.hero {
position: relative;
@@ -791,14 +783,7 @@
font-size: 12px;
}
- .header nav .pipNavLink {
- gap: 3px;
- }
- .pipNavLink svg {
- width: 16px;
- height: 16px;
- }
.hero {
width: min(calc(100% - 24px - env(safe-area-inset-left) - env(safe-area-inset-right)), 540px);
@@ -896,14 +881,7 @@
font-size: 11px;
}
- .header nav .pipNavLink {
- gap: 2px;
- }
- .pipNavLink svg {
- width: 15px;
- height: 15px;
- }
.footer > div {
gap: 8px;
diff --git a/components/concepts/signal-desk/signal-desk.tsx b/components/concepts/signal-desk/signal-desk.tsx
index e247f7c..eaa415c 100644
--- a/components/concepts/signal-desk/signal-desk.tsx
+++ b/components/concepts/signal-desk/signal-desk.tsx
@@ -10,8 +10,6 @@ import {
} from "@/lib/portfolio-content";
import { PondEnvironment } from "./pond-environment";
import { ProfileCard } from "./profile-card";
-import { PipFace } from "@/components/pip/pip-logo";
-import { PipFeature } from "@/components/pip/pip-feature";
import { Button } from "@/components/ui/button";
import { SkulletImage } from "@/components/ui/skullet-image";
import projectStyles from "./project-explorer.module.css";
@@ -253,7 +251,6 @@ export function SignalDesk() {
Work
- Pip
About
Contact
@@ -272,7 +269,6 @@ export function SignalDesk() {
-
diff --git a/components/pip/pip-feature.module.css b/components/pip/pip-feature.module.css
deleted file mode 100644
index d1b0e49..0000000
--- a/components/pip/pip-feature.module.css
+++ /dev/null
@@ -1,245 +0,0 @@
-.section {
- position: relative;
- z-index: var(--layer-content, 1);
- padding: 0 max(20px, env(safe-area-inset-right)) 0 max(20px, env(safe-area-inset-left));
- pointer-events: none;
- scroll-margin-top: 96px;
-}
-
-.card {
- --pip-action-bg: var(--site-accent, #9984cf);
- --pip-action-text: var(--site-contrast, #1b1227);
- display: grid;
- grid-template-columns: minmax(180px, .55fr) minmax(0, 1.3fr) auto;
- gap: 30px;
- align-items: center;
- width: min(100%, 1316px);
- margin-inline: auto;
- padding: 32px 36px;
- border: 1px solid var(--glass-edge, var(--site-border, #47675b));
- border-radius: var(--site-radius, 24px);
- color: var(--site-text, #eff7f1);
- background: linear-gradient(145deg, var(--glass-top, rgb(255 255 255 / .16)), var(--glass-bottom, transparent)), var(--site-panel-bg, var(--site-surface, #0c2a27));
- box-shadow: var(--glass-panel-shadow, var(--site-shadow));
- backdrop-filter: blur(16px) saturate(1.14);
- pointer-events: auto;
-}
-
-.card > * {
- min-width: 0;
-}
-
-.identity {
- display: grid;
- justify-items: start;
- gap: 12px;
-}
-
-.logo {
- --pip-face-size: 48px;
- --pip-wordmark-size: 68px;
- --pip-logo-gap: 10px;
- /* Pixelify's low lowercase ink needs a lift against the face at this scale. */
- --pip-wordmark-offset: -.09em;
-}
-
-.identity > span:last-child {
- color: var(--site-muted, #b2c8bd);
- font-family: var(--font-mono, var(--font-space-mono, monospace));
- font-size: 11px;
- font-weight: 600;
-}
-
-.copy h2 {
- margin: 0;
- color: var(--site-text, #eff7f1);
- font-size: clamp(2rem, 4vw, 3.4rem);
- line-height: 1.02;
- letter-spacing: -.055em;
- font-weight: 600;
- text-wrap: balance;
-}
-
-.copy h2 > span {
- color: var(--site-accent, #9984cf);
-}
-
-.copy p {
- max-width: 44ch;
- margin: 13px 0 0;
- color: var(--site-muted, #b2c8bd);
- font-size: 14px;
- line-height: 1.68;
-}
-
-.actions {
- display: grid;
- align-self: stretch;
- grid-template-rows: minmax(0, 1fr) auto minmax(0, 1fr);
- justify-items: start;
- gap: 9px;
- min-width: 132px;
-}
-
-.actions::before {
- content: "";
-}
-
-.actions > span {
- grid-row: 3;
- align-self: start;
- color: var(--site-muted, #b2c8bd);
- font-family: var(--font-mono, var(--font-space-mono, monospace));
- font-size: 10px;
- line-height: 1.55;
-}
-
-.actions :global(a) {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 9px;
- min-width: 44px;
- min-height: 48px;
- padding: 12px 19px;
- border: 1px solid color-mix(in srgb, var(--pip-action-bg) 76%, var(--glass-rim, white));
- border-radius: var(--site-control-radius, 999px);
- color: var(--pip-action-text);
- background: linear-gradient(145deg, rgb(255 255 255 / .06), transparent), var(--pip-action-bg);
- box-shadow: var(--glass-shadow, var(--site-shadow));
- font-size: 14px;
- font-weight: 700;
- text-decoration: none;
- transition: border-color var(--ui-duration, 280ms) var(--ui-ease, ease), box-shadow var(--ui-duration, 280ms) var(--ui-ease, ease), transform var(--ui-duration, 280ms) var(--ui-ease, ease);
-}
-
-.actions :global(a):hover {
- border-color: var(--glass-rim, white);
- box-shadow: var(--glass-panel-shadow, var(--site-shadow));
- transform: translateY(-2px);
-}
-
-.actions :global(a):active {
- transform: translateY(1px);
-}
-
-.actionContent {
- display: inline-flex;
- align-items: center;
- gap: 9px;
-}
-
-.actionContent svg {
- width: 16px;
- height: 16px;
- flex: 0 0 auto;
- transition: transform var(--ui-duration, 280ms) var(--ui-ease, ease);
-}
-
-.actions :global(a):hover .actionContent svg {
- transform: translate3d(2px, -2px, 0);
-}
-
-@media (min-width: 821px) and (max-width: 1120px) {
- .card {
- grid-template-columns: minmax(148px, .55fr) minmax(0, 1.25fr) auto;
- gap: 22px;
- padding: 28px;
- }
-
- .logo {
- --pip-face-size: 42px;
- --pip-wordmark-size: 59px;
- --pip-logo-gap: 8px;
- }
-
- .copy h2 {
- font-size: clamp(1.9rem, 3.7vw, 2.8rem);
- }
-}
-
-@media (max-width: 820px) {
- .section {
- padding: 0 max(14px, env(safe-area-inset-right)) 0 max(14px, env(safe-area-inset-left));
- }
-
- .card {
- grid-template-columns: minmax(0, 1fr);
- gap: 16px;
- width: min(100%, 620px);
- padding: 22px;
- }
-
- .identity {
- display: flex;
- align-items: center;
- gap: 13px;
- }
-
- .logo {
- --pip-face-size: 44px;
- --pip-wordmark-size: 60px;
- --pip-logo-gap: 8px;
- }
-
- .identity > span:last-child {
- font-size: 10px;
- }
-
- .copy h2 {
- font-size: clamp(2rem, 8vw, 2.75rem);
- }
-
- .copy p {
- margin-top: 10px;
- font-size: 14px;
- }
-
- .actions {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
- gap: 10px 16px;
- min-width: 0;
- }
-
- .actions::before {
- display: none;
- }
-
- .actions > span {
- grid-row: auto;
- }
-
- .actions :global(a) {
- min-width: min(100%, 150px);
- }
-}
-
-@media (max-width: 370px) {
- .card {
- padding: 18px;
- }
-
- .actions {
- align-items: stretch;
- }
-
- .actions :global(a) {
- width: 100%;
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .actions :global(a),
- .actionContent svg {
- transition: none;
- }
-
- .actions :global(a):hover,
- .actions :global(a):active,
- .actions :global(a):hover .actionContent svg {
- transform: none;
- }
-}
diff --git a/components/pip/pip-feature.tsx b/components/pip/pip-feature.tsx
deleted file mode 100644
index 89557ed..0000000
--- a/components/pip/pip-feature.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { ArrowUpRight } from "lucide-react";
-import { PipLogo } from "./pip-logo";
-import { Button } from "@/components/ui/button";
-import deskStyles from "@/components/concepts/signal-desk/signal-desk.module.css";
-import styles from "./pip-feature.module.css";
-
-export function PipFeature() {
- return (
-
-
-
-
-
A little help. A little banter.
-
Meet Pip, your AI mate on Telegram. Quick questions, useful reminders, and a little everyday company.
-
-
-
-
- );
-}
diff --git a/components/pip/pip-logo.module.css b/components/pip/pip-logo.module.css
deleted file mode 100644
index 879a1c0..0000000
--- a/components/pip/pip-logo.module.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.logo {
- display: inline-flex;
- align-items: center;
- gap: var(--pip-logo-gap, 10px);
- color: #f6be91;
-}
-
-.logo svg {
- flex-shrink: 0;
- width: var(--pip-face-size, 34px);
- height: var(--pip-face-size, 34px);
-}
-
-.logo > span {
- font-family: var(--font-pixelify), sans-serif;
- font-size: var(--pip-wordmark-size, 46px);
- font-weight: 400;
- line-height: 1;
- letter-spacing: -3px;
- transform: translateY(var(--pip-wordmark-offset, 0));
-}
diff --git a/components/pip/pip-logo.tsx b/components/pip/pip-logo.tsx
deleted file mode 100644
index 0d94a7c..0000000
--- a/components/pip/pip-logo.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import styles from "./pip-logo.module.css";
-
-export function PipFace({ className = "" }: { className?: string }) {
- return (
-
-
-
-
- );
-}
-
-export function PipLogo({ className = "" }: { className?: string }) {
- return (
-
-
- pip
-
- );
-}
diff --git a/components/pip/pip-web-search-demo.module.css b/components/pip/pip-web-search-demo.module.css
deleted file mode 100644
index 953c306..0000000
--- a/components/pip/pip-web-search-demo.module.css
+++ /dev/null
@@ -1,319 +0,0 @@
-.demo {
- width: 100%;
- margin-top: 14px;
-}
-.stage {
- height: 280px;
- position: relative;
- isolation: isolate;
- overflow: hidden;
- background: radial-gradient(
- ellipse at 50% 60%,
- var(--glass-selected),
- transparent 70%
- );
-}
-.stage canvas {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- z-index: 2;
- pointer-events: none;
-}
-.browser {
- position: absolute;
- left: 29%;
- top: 10%;
- width: 69%;
- height: 77%;
- border: 1px solid var(--glass-rim);
- border-radius: 14px;
- background: var(--site-surface);
- box-shadow:
- var(--site-shadow),
- inset 0 1px var(--glass-rim);
- overflow: hidden;
- z-index: 1;
-}
-.chrome {
- height: 28px;
- display: flex;
- align-items: center;
- gap: 4px;
- padding: 0 10px;
- border-bottom: 1px solid var(--site-border);
- color: var(--site-muted);
-}
-.chrome i {
- width: 4px;
- height: 4px;
- border-radius: 50%;
- background: var(--site-accent);
- opacity: 0.6;
-}
-.chrome > span {
- flex: 1;
- text-align: center;
- font-size: 8px;
- letter-spacing: 0.03em;
-}
-.chrome svg {
- width: 11px;
- height: 11px;
-}
-.content {
- padding: 14px;
- color: var(--site-text);
- font-size: 10px;
-}
-.heading {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 6px;
- font-size: 11px;
- font-weight: 600;
- margin-bottom: 12px;
-}
-.heading span {
- color: var(--site-muted);
- font-size: 8px;
-}
-.heading svg {
- width: 13px;
- height: 13px;
- color: var(--site-accent);
-}
-.search {
- height: 27px;
- padding: 6px 8px;
- border: 1px solid var(--site-accent);
- border-radius: 6px;
- font-size: 9px;
- margin-bottom: 12px;
-}
-.product {
- display: grid;
- grid-template-columns: 32% 1fr;
- gap: 12px;
- align-items: center;
-}
-.productArt {
- display: grid;
- place-items: center;
- height: 82px;
- border: 1px solid var(--site-border);
- background: var(--glass-selected);
- border-radius: 9px;
-}
-.book,
-.smallBook {
- border-radius: 3px;
- background: linear-gradient(100deg, #a38e61 8%, #dec998 12%, #c8b07d);
- box-shadow: 3px 5px 8px #0002;
-}
-.book {
- width: 32px;
- height: 47px;
- transform: rotate(-9deg);
-}
-.book::after {
- content: "";
- display: block;
- width: 16px;
- height: 1px;
- margin: 19px auto;
- background: #887447;
-}
-.product strong,
-.cart strong {
- display: block;
- font-size: 10px;
- font-weight: 600;
- line-height: 1.4;
- margin-bottom: 7px;
-}
-.skeleton,
-.shortSkeleton {
- height: 4px;
- border-radius: 4px;
- background: var(--site-border);
- margin-bottom: 5px;
- width: 85%;
-}
-.shortSkeleton {
- width: 55%;
-}
-.product small,
-.cart small {
- display: block;
- color: var(--site-muted);
- font-size: 9px;
- margin: 8px 0;
-}
-.action {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-height: 27px;
- padding: 5px 9px;
- border: 1px solid var(--glass-rim);
- background: var(--site-accent);
- color: var(--site-contrast);
- border-radius: 7px;
- font-size: 9px;
- font-weight: 600;
- white-space: nowrap;
-}
-.cart {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 16px 0;
- border-block: 1px solid var(--site-border);
-}
-.cart > div:last-child {
- flex: 1;
-}
-.smallBook {
- width: 26px;
- height: 37px;
- flex-shrink: 0;
-}
-.bottom {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 5px;
- margin-top: 15px;
-}
-.bottom small {
- font-size: 8px;
- color: var(--site-muted);
-}
-.field {
- color: var(--site-muted);
- font-size: 8px;
- margin-top: 6px;
-}
-.field span {
- display: block;
- min-height: 24px;
- padding: 5px 7px;
- margin-top: 3px;
- border: 1px solid var(--site-border);
- border-radius: 5px;
- color: var(--site-text);
- font-size: 9px;
-}
-.field [data-typed] {
- border-color: var(--site-accent);
-}
-.fields {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 7px;
-}
-.content:has(.fields) {
- padding-block: 8px;
- line-height: 1.2;
-}
-.content:has(.fields) .heading {
- margin-bottom: 4px;
-}
-.content:has(.fields) .field {
- margin-top: 4px;
-}
-.content:has(.fields) .field span {
- min-height: 22px;
- padding-block: 4px;
-}
-.content:has(.fields) .bottom {
- margin-top: 6px;
-}
-.done {
- text-align: center;
- padding-top: 13px;
-}
-.seal {
- width: 42px;
- height: 42px;
- display: grid;
- place-items: center;
- background: var(--glass-selected);
- color: var(--site-accent);
- border: 1px solid var(--site-border);
- border-radius: 50%;
- margin: 0 auto 12px;
-}
-.seal svg {
- width: 22px;
- height: 22px;
-}
-.done strong {
- font-size: 14px;
-}
-.done p {
- font-size: 10px;
- color: var(--site-muted);
- margin: 8px 0 18px;
-}
-.done .skeleton,
-.done .shortSkeleton {
- margin-inline: auto;
-}
-.companion {
- position: absolute;
- top: 28%;
- left: 2%;
- width: 22%;
- display: flex;
- align-items: center;
- flex-direction: column;
- gap: 12px;
- z-index: 3;
-}
-.companion svg {
- width: 85%;
- color: var(--site-accent);
- filter: drop-shadow(0 6px 8px #1b122720);
-}
-.companion span {
- font-size: 10px;
- line-height: 1.4;
- text-align: center;
- color: var(--site-muted);
- max-width: 11ch;
-}
-.status {
- position: absolute;
- bottom: 4%;
- left: 29%;
- right: 2%;
- font-size: 10px;
- color: var(--site-muted);
- text-align: center;
-}
-@media (max-width: 370px) {
- .content {
- padding: 11px 9px;
- }
- .product {
- gap: 8px;
- }
- .product strong,
- .cart strong {
- font-size: 9px;
- }
- .action {
- padding-inline: 7px;
- font-size: 8px;
- }
- .companion span {
- font-size: 9px;
- }
- .heading {
- font-size: 10px;
- }
-}
diff --git a/components/pip/pip-web-search-demo.tsx b/components/pip/pip-web-search-demo.tsx
deleted file mode 100644
index 6d1233b..0000000
--- a/components/pip/pip-web-search-demo.tsx
+++ /dev/null
@@ -1,428 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import { Check, Search } from "lucide-react";
-import { PipFace } from "./pip-logo";
-import type { createDemoRenderer } from "./pip-web-search-renderer";
-import styles from "./pip-web-search-demo.module.css";
-
-type Phase = "search" | "product" | "addcart" | "postage" | "review" | "done";
-type Step = {
- phase: Phase;
- start: number;
- duration: number;
- label: string;
- speech: string;
-};
-const timeline: readonly Step[] = [
- {
- phase: "search",
- start: 0,
- duration: 2400,
- label: "Searching the web",
- speech: "I'll find a match.",
- },
- {
- phase: "product",
- start: 2400,
- duration: 2200,
- label: "Opening a result",
- speech: "Found the one.",
- },
- {
- phase: "addcart",
- start: 4600,
- duration: 2000,
- label: "Adding to the basket",
- speech: "Into the basket.",
- },
- {
- phase: "postage",
- start: 6600,
- duration: 2800,
- label: "Filling postage details",
- speech: "Details filled in.",
- },
- {
- phase: "review",
- start: 9400,
- duration: 2400,
- label: "Reviewing checkout",
- speech: "One final check.",
- },
- {
- phase: "done",
- start: 11800,
- duration: 6500,
- label: "All sorted",
- speech: "One less thing.",
- },
-];
-const completedStep = timeline[timeline.length - 1];
-const sequenceDuration = completedStep.start + completedStep.duration;
-
-function sampleFrame(elapsed: number) {
- const time = elapsed % sequenceDuration;
- const step =
- timeline.find(
- (entry) => time >= entry.start && time < entry.start + entry.duration,
- ) ?? completedStep;
- return {
- step,
- portion: (time - step.start) / step.duration,
- drift: Math.sin(time / 1800),
- };
-}
-
-function BrowserContent({ phase }: { phase: Phase }) {
- if (phase === "done")
- return (
-
-
-
-
-
All sorted.
-
One less thing on your list.
-
-
-
- );
- if (phase === "postage")
- return (
- <>
-
- Postage details 1 / 2
-
-
- NameAlex Example
-
-
- Address12 Example Lane
-
-
-
- TownSampleton
-
-
- PostcodeAB1 2CD
-
-
-
- Standard delivery
-
- Continue
-
-
- >
- );
- if (phase === "review" || phase === "addcart")
- return (
- <>
-
- {phase === "review" ? "Review checkout" : "Your basket"}
- {phase === "review" ? "2 / 2" : "1 item"}
-
-
-
-
-
Everyday notebook
-
-
£12.00
-
-
-
-
- {phase === "review" ? "Total · £15" : "Ready when you are"}
-
-
- {phase === "review" ? "Confirm order" : "Checkout"}
-
-
- >
- );
- return (
- <>
-
- {phase === "search" ? "Find something useful" : "Everyday notebook"}
-
-
- {phase === "search" && (
-
- a notebook for my desk
-
- )}
-
-
-
-
- {phase === "search"
- ? "A notebook for your desk"
- : "Everyday notebook"}
-
-
-
-
£12.00
-
- {phase === "search" ? "View notebook" : "Add to cart"}
-
-
-
- >
- );
-}
-
-export function PipWebSearchDemo() {
- const stageRef = useRef(null);
- const companionRef = useRef(null);
- const [step, setStep] = useState(completedStep);
- const [rendererKind, setRendererKind] = useState<
- "pending" | "three" | "fallback"
- >("pending");
- const [playing, setPlaying] = useState(true);
-
- useEffect(() => {
- const stageElement = stageRef.current;
- if (!stageElement) return;
- const stage = stageElement;
- const motion = matchMedia("(prefers-reduced-motion: reduce)");
- const theme = matchMedia("(prefers-color-scheme: light)");
- let renderer: ReturnType | undefined;
- let visible = false;
- let disposed = false;
- let failed = false;
- let loading = false;
- let generation = 0;
- let raf = 0;
- let elapsed = 0;
- let previous = 0;
- let current: Phase = "done";
- let cursorX = 0.4;
- let cursorY = 0.3;
-
- function stop() {
- cancelAnimationFrame(raf);
- raf = 0;
- previous = 0;
- }
-
- function fail() {
- failed = true;
- stop();
- renderer?.dispose();
- renderer = undefined;
- setRendererKind("fallback");
- setStep(completedStep);
- setPlaying(false);
- }
-
- function resize() {
- try {
- renderer?.resize(stage.clientWidth, stage.clientHeight, theme.matches);
- if (renderer) draw(elapsed, 0);
- } catch {
- fail();
- }
- }
-
- function draw(time: number, delta: number) {
- const frame = sampleFrame(time);
- if (current !== frame.step.phase) {
- current = frame.step.phase;
- setStep(frame.step);
- }
- const typed = stage.querySelector("[data-typed]");
- if (typed) {
- const text =
- typed.dataset.typed === "search"
- ? "a notebook for my desk"
- : "12 Example Lane";
- const nextText = text.slice(
- 0,
- Math.floor(Math.min(1, frame.portion * 2.2) * text.length),
- );
- if (typed.textContent !== nextText) typed.textContent = nextText;
- }
- const bounds = stage.getBoundingClientRect();
- const target = stage
- .querySelector("[data-demo-target]")
- ?.getBoundingClientRect();
- const targetX = target
- ? (target.left + target.width * 0.65 - bounds.left) / bounds.width
- : 0.82;
- const targetY = target
- ? (target.top + target.height * 0.6 - bounds.top) / bounds.height
- : 0.65;
- const ease = 1 - Math.exp(-delta / 180);
- cursorX += (targetX - cursorX) * ease;
- cursorY += (targetY - cursorY) * ease;
- if (companionRef.current)
- companionRef.current.style.transform = `translateY(${frame.drift * 3}px) rotate(${frame.drift * 1.5}deg)`;
- const ripple =
- frame.portion > 0.72 && frame.portion < 0.96
- ? (frame.portion - 0.72) / 0.24
- : 0;
- renderer?.render({
- cursorX,
- cursorY,
- ripple,
- drift: frame.drift,
- complete: frame.step.phase === "done",
- });
- }
-
- function tick(now: number) {
- raf = 0;
- if (disposed) return;
- if (motion.matches) {
- motionChanged();
- return;
- }
- if (!visible || document.hidden || !renderer) return;
- const delta = previous ? Math.min(now - previous, 60) : 0;
- previous = now;
- elapsed += delta;
- try {
- draw(elapsed, delta);
- } catch {
- fail();
- return;
- }
- raf = requestAnimationFrame(tick);
- }
-
- async function loadRenderer() {
- if (
- renderer ||
- loading ||
- failed ||
- motion.matches ||
- !visible ||
- document.hidden
- )
- return;
- loading = true;
- const requestGeneration = generation;
- try {
- const module = await import("./pip-web-search-renderer");
- if (
- disposed ||
- requestGeneration !== generation ||
- motion.matches ||
- !visible ||
- document.hidden
- )
- return;
- renderer = module.createDemoRenderer(stage, fail);
- resize();
- if (failed) return;
- setRendererKind("three");
- wake();
- } catch {
- if (!disposed && requestGeneration === generation) fail();
- } finally {
- loading = false;
- if (
- !disposed &&
- !renderer &&
- !failed &&
- visible &&
- !motion.matches &&
- !document.hidden
- )
- wake();
- }
- }
-
- function wake() {
- stop();
- setPlaying(visible && !document.hidden && !motion.matches && !failed);
- if (disposed || motion.matches || failed || !visible || document.hidden)
- return;
- if (!renderer) {
- void loadRenderer();
- return;
- }
- raf = requestAnimationFrame(tick);
- }
-
- function motionChanged() {
- generation += 1;
- stop();
- if (motion.matches) {
- renderer?.dispose();
- renderer = undefined;
- current = "done";
- elapsed = 0;
- setStep(completedStep);
- setRendererKind("fallback");
- setPlaying(false);
- if (companionRef.current) companionRef.current.style.transform = "";
- } else {
- setRendererKind(failed ? "fallback" : "pending");
- wake();
- }
- }
-
- const observer = new IntersectionObserver(([entry]) => {
- visible = entry.isIntersecting;
- wake();
- });
- const resizeObserver = new ResizeObserver(resize);
- observer.observe(stage);
- resizeObserver.observe(stage);
- document.addEventListener("visibilitychange", wake);
- motion.addEventListener("change", motionChanged);
- theme.addEventListener("change", resize);
- if (motion.matches) motionChanged();
- return () => {
- disposed = true;
- generation += 1;
- stop();
- observer.disconnect();
- resizeObserver.disconnect();
- document.removeEventListener("visibilitychange", wake);
- motion.removeEventListener("change", motionChanged);
- theme.removeEventListener("change", resize);
- renderer?.dispose();
- };
- }, []);
-
- return (
-
-
-
-
-
-
-
-
- pip / browsing
-
-
-
-
-
-
-
{step.label}
-
-
- );
-}
diff --git a/components/pip/pip-web-search-renderer.ts b/components/pip/pip-web-search-renderer.ts
deleted file mode 100644
index c7763db..0000000
--- a/components/pip/pip-web-search-renderer.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-import * as THREE from "three";
-
-export type DemoRenderFrame = {
- cursorX: number;
- cursorY: number;
- ripple: number;
- drift: number;
- complete: boolean;
-};
-
-export function createDemoRenderer(stage: HTMLElement, onFailure: () => void) {
- const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
- const geometries: THREE.BufferGeometry[] = [];
- const materials: THREE.Material[] = [];
- try {
- renderer.setClearColor(0x000000, 0);
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
- renderer.domElement.setAttribute("aria-hidden", "true");
- const scene = new THREE.Scene();
- const camera = new THREE.OrthographicCamera(0, 1, 1, 0, 0.1, 10);
- camera.position.z = 4;
- scene.add(new THREE.AmbientLight(0xffffff, 2));
- const light = new THREE.DirectionalLight(0xeee3ff, 3);
- light.position.set(-1, 2, 4);
- scene.add(light);
-
- function rounded(
- width: number,
- height: number,
- radius: number,
- frame = false,
- ) {
- const shape = new THREE.Shape();
- shape.moveTo(radius, 0);
- shape.lineTo(width - radius, 0);
- shape.quadraticCurveTo(width, 0, width, radius);
- shape.lineTo(width, height - radius);
- shape.quadraticCurveTo(width, height, width - radius, height);
- shape.lineTo(radius, height);
- shape.quadraticCurveTo(0, height, 0, height - radius);
- shape.lineTo(0, radius);
- shape.quadraticCurveTo(0, 0, radius, 0);
- if (frame) {
- const inset = 0.01;
- const hole = new THREE.Path();
- hole.moveTo(radius, inset);
- hole.lineTo(width - radius, inset);
- hole.quadraticCurveTo(width - inset, inset, width - inset, radius);
- hole.lineTo(width - inset, height - radius);
- hole.quadraticCurveTo(
- width - inset,
- height - inset,
- width - radius,
- height - inset,
- );
- hole.lineTo(radius, height - inset);
- hole.quadraticCurveTo(inset, height - inset, inset, height - radius);
- hole.lineTo(inset, radius);
- hole.quadraticCurveTo(inset, inset, radius, inset);
- shape.holes.push(hole);
- }
- const geometry = new THREE.ExtrudeGeometry(shape, {
- depth: 0.025,
- bevelEnabled: true,
- bevelThickness: 0.009,
- bevelSize: 0.006,
- bevelSegments: 3,
- steps: 1,
- curveSegments: 10,
- });
- geometries.push(geometry);
- return geometry;
- }
-
- const chassisMaterial = new THREE.MeshStandardMaterial({
- color: 0x665279,
- roughness: 0.35,
- metalness: 0.15,
- transparent: true,
- opacity: 0.55,
- });
- materials.push(chassisMaterial);
- const backingMaterial = new THREE.MeshStandardMaterial({
- color: 0x9c83c7,
- roughness: 0.4,
- metalness: 0.08,
- });
- materials.push(backingMaterial);
- const cursorMaterial = new THREE.MeshStandardMaterial({
- color: 0xf4eaff,
- roughness: 0.3,
- });
- materials.push(cursorMaterial);
- const rippleMaterial = new THREE.MeshBasicMaterial({
- color: 0xbda3ed,
- transparent: true,
- opacity: 0,
- depthWrite: false,
- });
- materials.push(rippleMaterial);
- const chassis = new THREE.Mesh(
- rounded(0.69, 0.77, 0.045, true),
- chassisMaterial,
- );
- chassis.position.set(0.29, 0.13, 0);
- scene.add(chassis);
- const backing = new THREE.Mesh(rounded(0.19, 0.29, 0.06), backingMaterial);
- backing.position.set(0.035, 0.43, 0.04);
- scene.add(backing);
- const cursorShape = new THREE.Shape();
- cursorShape.moveTo(0, 0);
- cursorShape.lineTo(0.027, -0.033);
- cursorShape.lineTo(0.014, -0.032);
- cursorShape.lineTo(0.009, -0.046);
- cursorShape.lineTo(0, 0);
- const cursorGeometry = new THREE.ExtrudeGeometry(cursorShape, {
- depth: 0.005,
- bevelEnabled: false,
- });
- geometries.push(cursorGeometry);
- const ringGeometry = new THREE.RingGeometry(0.017, 0.02, 32);
- geometries.push(ringGeometry);
- const cursor = new THREE.Mesh(cursorGeometry, cursorMaterial);
- const ring = new THREE.Mesh(ringGeometry, rippleMaterial);
- scene.add(cursor, ring);
- let disposed = false;
- let aspect = 1;
- let ringScale = 1;
-
- function dispose() {
- if (disposed) return;
- disposed = true;
- renderer.domElement.removeEventListener("webglcontextlost", contextLost);
- geometries.forEach((geometry) => geometry.dispose());
- materials.forEach((material) => material.dispose());
- renderer.dispose();
- renderer.domElement.remove();
- }
-
- function contextLost(event: Event) {
- event.preventDefault();
- dispose();
- onFailure();
- }
-
- renderer.domElement.addEventListener("webglcontextlost", contextLost);
- stage.appendChild(renderer.domElement);
- return {
- resize(width: number, height: number, lightMode: boolean) {
- renderer.setSize(width, height, false);
- const stageBounds = stage.getBoundingClientRect();
- const avatarBounds = stage
- .querySelector("[data-demo-companion] svg")
- ?.getBoundingClientRect();
- if (avatarBounds) {
- backing.scale.set(
- (avatarBounds.width + 4) / width / 0.19,
- (avatarBounds.height + 4) / height / 0.29,
- 1,
- );
- backing.position.set(
- (avatarBounds.left - stageBounds.left - 2) / width,
- 1 - (avatarBounds.bottom - stageBounds.top + 2) / height,
- 0.04,
- );
- }
- aspect = width / height;
- const cursorScale = 15 / (width * 0.027);
- cursor.scale.set(cursorScale, cursorScale * aspect, 1);
- ringScale = 10 / (width * 0.02);
- chassisMaterial.color.set(lightMode ? 0xd0bcdf : 0x665279);
- backingMaterial.color.set(lightMode ? 0xc7afdf : 0x9c83c7);
- },
- render(frame: DemoRenderFrame) {
- if (disposed) return;
- backing.rotation.z = frame.drift * 0.035;
- cursor.position.set(frame.cursorX, 1 - frame.cursorY, 0.3);
- ring.position.set(frame.cursorX, 1 - frame.cursorY, 0.25);
- ring.scale.x = ringScale * (1 + frame.ripple * 2);
- ring.scale.y = ringScale * aspect * (1 + frame.ripple * 2);
- rippleMaterial.opacity =
- frame.ripple > 0 ? (1 - frame.ripple) * 0.7 : 0;
- cursor.visible = !frame.complete;
- renderer.render(scene, camera);
- },
- dispose,
- };
- } catch (error) {
- geometries.forEach((geometry) => geometry.dispose());
- materials.forEach((material) => material.dispose());
- renderer.dispose();
- renderer.domElement.remove();
- throw error;
- }
-}
diff --git a/components/ui/skullet-image.tsx b/components/ui/skullet-image.tsx
index bf6b9b6..0802717 100644
--- a/components/ui/skullet-image.tsx
+++ b/components/ui/skullet-image.tsx
@@ -26,7 +26,7 @@ export function loadSkulletOverlay() {
}
// Loaded only after an image is still incomplete. A static import pulls Skullet
-// into Pip's initial script. A failed chunk drops the skeleton and leaves the image.
+// into the initial script. A failed chunk drops the skeleton and leaves the image.
const SkulletOverlay = lazy(() =>
import("./skullet-overlay")
.then((mod) => ({ default: rememberOverlay(mod.default) }))
diff --git a/lib/pip.ts b/lib/pip.ts
deleted file mode 100644
index 5b28434..0000000
--- a/lib/pip.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-// Pip's public Telegram bot username, supplied by Lu. No leading @ required.
-// Rebuild the static site after changing it. Never put credentials here.
-export const PIP_TELEGRAM_HANDLE = "PIPSOFTWARE_BOT";
-
-export function getPipContact(value: string = PIP_TELEGRAM_HANDLE) {
- const username = value.trim().replace(/^@/, "");
-
- if (!/^[a-zA-Z][a-zA-Z0-9_]{4,31}$/.test(username)) return null;
-
- const telegramUrl = `https://t.me/${username}`;
- return {
- handle: `@${username}`,
- telegramUrl,
- qrUrl: `/qr/${username}.svg`,
- };
-}
diff --git a/package-lock.json b/package-lock.json
index a833ee7..79d721b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,7 +35,6 @@
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^16.5.0",
- "qrcode": "1.5.4",
"typescript": "5.9.3",
"typescript-eslint": "^8.48.1",
"vite": "8.3.3"
@@ -2714,6 +2713,72 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+ "version": "2.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
@@ -3458,16 +3523,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -3578,16 +3633,6 @@
"node": ">=6"
}
},
- "node_modules/camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
- "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/caniuse-lite": {
"version": "1.0.30001815",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz",
@@ -3638,18 +3683,6 @@
"url": "https://polar.sh/cva"
}
},
- "node_modules/cliui": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
- "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.0",
- "wrap-ansi": "^6.2.0"
- }
- },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -3745,16 +3778,6 @@
}
}
},
- "node_modules/decamelize": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
- "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -3778,13 +3801,6 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
- "node_modules/dijkstrajs": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
- "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/electron-to-chromium": {
"version": "1.5.452",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.452.tgz",
@@ -3792,13 +3808,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/enhanced-resolve": {
"version": "5.26.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz",
@@ -4141,16 +4150,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": "6.* || 8.* || >= 10.*"
- }
- },
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
@@ -4267,16 +4266,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -4588,16 +4577,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -4638,16 +4617,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/pngjs": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
- "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10.13.0"
- }
- },
"node_modules/postcss": {
"version": "8.5.29",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz",
@@ -4697,24 +4666,6 @@
"node": ">=6"
}
},
- "node_modules/qrcode": {
- "version": "1.5.4",
- "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
- "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "dijkstrajs": "^1.0.1",
- "pngjs": "^5.0.0",
- "yargs": "^15.3.1"
- },
- "bin": {
- "qrcode": "bin/qrcode"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
"node_modules/radix-ui": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.7.0.tgz",
@@ -4882,23 +4833,6 @@
}
}
},
- "node_modules/require-directory": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/require-main-filename": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
- "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -4959,13 +4893,6 @@
"semver": "bin/semver.js"
}
},
- "node_modules/set-blocking": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
- "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -5025,34 +4952,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -5694,13 +5593,6 @@
"node": ">= 8"
}
},
- "node_modules/which-module": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
- "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -5711,28 +5603,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/wrap-ansi": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
- "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/y18n": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
- "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -5740,99 +5610,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/yargs": {
- "version": "15.4.1",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
- "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cliui": "^6.0.0",
- "decamelize": "^1.2.0",
- "find-up": "^4.1.0",
- "get-caller-file": "^2.0.1",
- "require-directory": "^2.1.1",
- "require-main-filename": "^2.0.0",
- "set-blocking": "^2.0.0",
- "string-width": "^4.2.0",
- "which-module": "^2.0.0",
- "y18n": "^4.0.0",
- "yargs-parser": "^18.1.2"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs-parser": {
- "version": "18.1.3",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
- "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "camelcase": "^5.0.0",
- "decamelize": "^1.2.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/yargs/node_modules/find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
- "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
- "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-locate": "^4.1.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/p-limit": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
- "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-try": "^2.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/yargs/node_modules/p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
- "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-limit": "^2.2.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 9a7f28f..c82efdf 100644
--- a/package.json
+++ b/package.json
@@ -4,9 +4,8 @@
"private": true,
"type": "module",
"scripts": {
- "predev": "node scripts/generate-pip-qr.mjs",
"dev": "vite --host 127.0.0.1",
- "prebuild": "node scripts/generate-sitemap.js && node scripts/generate-pip-qr.mjs",
+ "prebuild": "node scripts/generate-sitemap.js",
"build": "tsc --noEmit && vite build && vite build --ssr app/entry-server.tsx && node scripts/prerender.mjs",
"check": "tsc --noEmit",
"start": "node scripts/serve-preview.mjs",
@@ -14,7 +13,6 @@
"lint": "eslint",
"test:e2e:setup": "python3 -m pip install -r requirements-test.txt && python3 -m playwright install chromium",
"test:e2e": "npm run build && python3 -m unittest discover -s tests/browser -p '*_test.py' -v",
- "test:pip": "node --test tests/pip-contact.test.mts",
"test:pond": "node --test tests/pond-world.test.mts"
},
"dependencies": {
@@ -45,7 +43,6 @@
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^16.5.0",
- "qrcode": "1.5.4",
"typescript": "5.9.3",
"typescript-eslint": "^8.48.1",
"vite": "8.3.3"
diff --git a/pip.html b/pip.html
deleted file mode 100644
index 1349850..0000000
--- a/pip.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/images/portfolio/pip-everyday.webp b/public/images/portfolio/pip-everyday.webp
deleted file mode 100644
index c03206588f24e9daef3e29b5276455ce49f11ee6..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 280228
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 NECHe1{;F?
z&6tN`V||PN?~sfMg+)Xe>0|Q9q;{VHw}1H>YHqZOEc+ki6I@>j?~e>Pv5Zu7AFAlL
zy;G24cChMKHw9UZl5%OGN6Ei0*2AYrNDK+MFl#^=9R~DE{gTcxQBag;)$d7u5tO_{
z8?VnMPjfNc+FW@4uVU|MVc^-9Pc0{(XZ
zNjx}t|F>z@3x`08qfRED+04soa8H6RZ4(t*>{^@)b-7ycN-S^Nw09$ke7?$*-v>=%IIeoor5fYcsy44C;vsZXl~MvKBVY?%!n6>K
zSFnakRbpOAgbyK=n{YE17_0MMIN~Z_5!9yiF9!s5~{I_VTJP#h8^(s+%=9n-8~}CAK%q(r;Iv
zU+-$F{A7+d*0e~kcruMX63bTvVoYj)jD9ocLu)VKNKLo(35$`fO0=4s3`|drRs26k
zSwTtjY|FQFoxP_D+YPw+{4%nFIwTsH(j^v=i+uM|GJQ1bMFouU62VI7ii;>*?bDI;
za?-f4)x9%|FOUDyOF$3e8NFCI?E>VK*Bv5xzOszb(G)E`@vhU8I#$kh>V&8O1&SA6KVeBL1
zCgDqB`GzXd`|*M_dL?051H!Qoc+?j!zmMp%8=Cq-^;@-HT+vG2M)~(L
z$V&jmXO;pmWpIp{6Vy)rN}o!=GO(s->Ob&fD^W60!$hhkWU!_>nc*6bmQcHXI~sq^
zH`AeraV~e8nh|XvaZVn?!_RMi0eKAl_A;54i-T(qR-aSnKI-cp6PYH@gLenMzf*O4
z%f_Bxv7*Xn4jWJygqXy&U$r=U5%SA@*SK#)NocIOsa=Z5AMN14&)_bO^>fq(nj2FkjID@i4sarsg1
z96@AG!w~6^LG=}Y$VDXTopDY=12iJS&BV%AXA;|z7i?ydm*-o=W$t9VU1$1y4vx_1
zQhL^7D^Q-HjR-l-?TLG87IDgN!qpPuOK6WXt2TakVbkY&%{fJ!KxAdb#r9e|^ys`=
zhq8j~w7mq(3Df4c&H2bT9iXLdVy>cq{vq_`=$_f(php+%yinM%e37FYm+!8bT$oi(
z^iIoj!?GciNO`h}*!3R-0G&1}pKwOFL-M@}9fx{<>!1$r^bfGzon=lej+^(~R^gfu
z(Q^RJ#Q3kga9FZo4}%iC7XX7_5GwB0Q4S|?XWX>~vXjkky5W#xF%7mbr%-oac!y!WOM
z(OtRqbSvw<-cILf?2K=3U41`EXv}CEqj2nYRfX4XN1LN#!5JFC_xs#Dl>6kAXc>8~
zgy%)N%FANfXxfdw$>_|1N+OY?@X`~LX;s0-c#Nc)msQw(dYWLk!Lpye(
zf1YXMg*$ztV&e>c7R!@J{~o*9R4Q?B#h>Gf9m@?8K};{MIyQx8(N3E4scjmS#52r-
zRAT>S03(0GnfIsIEXxl|=b>1(2j$^~m5aURCgXUpo2sYZ=q-E<#CdkkDU|dF($oSs
zUjLoeHnA@u@Q=rY*sI;>JANn(W27U^s#*c*cX*oPyT~xlb!dAMwRL~Z(8QqIO6#ep
zJbREboP~4B9vqFzmCWbY4!VLR;B(5LT|GeW27;e_5G21DX;gI-{->M%_B<+Ub&Z&w
zvYD@ofSMUiusawZ2ED#?zbddQ9!1WfjPo94C8Mp^2xH+O9C}TWl$t<6c9$9gZ&L=SONU#KZnQg$_Lk<}jpH4{Ub|CKf=ek(9S!4T;Bz=1y`0X@
zI(fpLgvF|CbMJaU9mcS7xz&&lAYLwpn3VEg6alkQ#MD1anqTZy=PY6Hz6_Xc_!i%&
zqLh}^#KnQYN3!yR1VLR!0`-8G?~6;7@~3N`kX;B1EM}OvhtCb5VX!!gFrdNpepUEv
zQ4NPky^VvU2cgl;7Cn%7LogcUr8Xa;KK8zxEjGrdX`M$G)9ao^y1Ky1~o-=jWr>E*5-Fqy+s
ztkTt~vODJhoSsyAu*r=Rf+>+p%omup%RM_h{blls4QnU}Kt?l&>3ts)3AR{X)f43?
zE_wf`;cp{Uv53QSE2fv>G_0ofLkeIAb(Dt3f!lQ$piCzBdElH?;r0=kEXK;e%&!32Xt(f?tVXtH8fCHA4S@c8UWVAdAYMUm&h)NSkxUbWF}bJP>_
z>z~s{e)_mm7%qX(Uj)Fq1y5j$25eZ+xH!KXlbT8R^nai+uVR*yV5Bziw1-dS2vJ|VSG7QfQ~Xwqir6sbfco~mLtVFP!p-=NN99TPbg8Q2t00R2*yVmXfYiZEuv)>a9;
z{68n(LplH;P38#BX
z@~e4}5y>7ZGU-r4#d(Mz;UkUmw2o;7{kUdZ8fny2R@gF`kF&Py5c;1shv^?3uIV${wOU0*o)wH87DHSofW+CW`%u#dlA`O
z>#mNC^mHzUje-44l%m`Qzg4)kaFt-!ti*2BI|@|qFNCurn$61YS=4C#QLw?$R$YZ$
zm8$+om1VOAO;q`G@jWfB(b6f?bf%DqKQU%z4jwe?dvRK1#?jrQVAd@LsF<8S9%yL2
z1alzp7j^mx5N%U+IMxbvl>~kWCI*veJ&P
zEare9SbKQ}^XzfSGd$vyE{^>geX=BJz_A!bG8%`%5JwWZ!qz4_i#H@32gH}`cfdO@
z$s(v#S?lc6I8IiYxWz2xw>!Rgf6Sk+%V8d2PR%S`6RMLuLiQeS0+Xwk*)nu)zNX~%
zV4U}S_M2@;-GYAN5gDaQ$(S{1r(N@mt2VoO=E$b0asX`se>1d|o6(kSh1T8d`^I7U
z4QxX>f?3Y&MxY|m#DR+Hmh4gz3*KPOFEbY^gMop+~%OiaePt!Ly86I{>O|UfM0r{ov&f_=1w!g0o5>{^g
z#gcfR`ZAac;WgfxTVKh^{;SUCmeVJUUg(1DFv*5f*&LtsC4_)mYFS?tjq38*Hk)&{
zP|G~JXyN#$)^U@}@(;n6Dy6@R>&ioK(EFCE%f!(vQ9aYk4I^V5XU2j(kDJobR@L(O
z=g0XzfK`gB8SEHROrRvM+hV{zMk3+naMbnuH^SQqt!bdJ5&YLSSLizdE@xuU4%ObI
zjGPloA*S`nrI3}?-J@<`16LyfXjt1GYh7JU%6f(!I1TJ{AH(oiG68M4`q;L<5Z$M{
zRnCiRr}w!BzP7GcWXZkwk#8t2nw!5mY}R$8i&)hw-BVcoz4w$}&Q2P*#
z+^N@n*Oun-xBHt^W@(T&dd)DPhg2=U#wa%mQE`ejwuZ5gKwR2}ws+ba+Vp09v`Qpd
zWGk_N^cA8#@EK`zXA?IiT?)T5nTD
z?p^Ed7h@IphjiY_y!TAMwmY2pSJOp2vj&-Cj++I3RPi9sVt2b>{15-P%DT$VOYV~W
zau~1KNU~ryXICM#6+A{$Ob4g7cfY(`r;AlB{dR1ks$24R(FHsM|XOAyYX3U|>le&e*S#uwc~OqmH)
zWAMi(`C`V~{gEx3E>T8RIijMOmVLmEh2-mUhwpZWn>U7ah}%cB)!Ot)2h^~(GRLn9
z13J6mIoKW14?BS*s$z+Rg|>vRxEb^x95u_Qd5ka|$WyE{;0WFb`o_m;{*fbvR7UFZ
zvIZ!Z*B(`6bCnTv8s1FQc|$>v@mZazn5UEo9#9}W51cx$GV7D`i>Q=ufz}Q}HFe-UQy}`lpmi=6|0AU9c~AGypa|t%
zxo8&7fqg24R0np+=F}DKfn^LDfj&I;FtRl9z$oFD+$dxM?Wt&KF~S@XV<<(dfj9-O
zZbpmB9M=&CDl?|DeM6tZGVk~olCLv0N5lZ__&x3V(gP{)0B!^axg^hH^7fpp>i@CL9b1|FZ*bveJN=cG@$UzRd%K1OP<1g-!`LMg*6I)!
z?Q4e;9$l8`<*-`tyN6(ldd{apqD)`(>lo0d^N=&^*R;J#xdR9%w7>6!zT(24-VpX{
z^yyAc%bk6h`REE${MjZ-GZdzc_0kk{DXO>f?Sk(iryYh*xc}VUbm
z_-vjYKc){R=j+|9DQdzb2c-O+VRk1cy#Pmlt?vQH?vEeafqZyGG>HTASeBQi)KpFO
z&aI!Vbdn#U*#XacrCkYZ2WjWFBq73N26B76z8qcZ-;dVhfkPDUDaNj!7A0JNsc@}k
zy_1@K%?m#PZJ<`xQC8ma8U^>-(8n1App-zaIGeIXc!=@E{paW1V?&bLYp+l2u9-x)
zk4IVE{w3D4Qhmm=+@*>}8eA_?U_4)13LnK-KI+wx)u(q=j;bN)_NOZYyEc90iK>Fr
zx6~C4jPge-YOZTJSw-vkO4Z4iUX{#A;_mEB&DBX)dZHu0>FB#NHpr`Z0yU(1tTGBc
z$=h$7F$j&?;+DWX_|RpU7ta+i&tc&5Zn*f{iuZ=$_QFs`vF!p4F?Sh`YrTRz3PJ4L
z!O>@xYE^c}1*lQ3^LdU!S1Of~f4EvXo?TDY6E+pEi>2-i8UE9pPzY?M2WtLA4N5rl{Bd8Cv2cs75aZR&6J+0ek>1!x&Lgxz{M}l%m|L?Y
zL)T%wh5dFs1!oITi9q3`)EQo3gF}g+7^B7XFQItcMH~_PdE&b3eU{K!4AR#<{3p@O
zLspAo7NSV@2QMN>NXbXW0ZuE-l+^12Va}B}@gBF7^#oJb*%9{2cF51hTS=HV0>8tX
zqo#xwq=Fz^`4$N*1sduhFCDrgrBH}al&(k=l`{ET-;N4XL%})K*Sqk=vhhB;emRh+SIpxsB2qLhar>xq)y0qur~zCWMd-~XU_jw)d#!P*+;PvpO_
z*(qswGT2;P7!k1<+}I@a=Xpmtp}n;pwz(%4b9bBVHV>-Gb-r7+8
zQc4Vu@C2m5pNQ%@{Dlh%6YQq8sZn#JOVnjAOz11!06Rd$zd^zIaLbv}x}XDyAWwIR
zB3v3#qNs17wM8?d$I_SqRY~!S>@4#EwjeO1Qfi-TK25Ms)t4d5mKiS_MNq;F&qt!)oObJu@y|`v*|#uMhpbHv9RBB@phZR
z8#Xq?qmLM52=&~_{^NLq1p{PRf)=q)k?R3@@6%R!?yqOeAgteX*ja%(xH(;y_D$*q
zc@6s6V)^Z&PC?zyPbRk-=>$8Lo^uAbn8x9j84;r4`IWO_VJ&TGDhZ?balh&{oP^#+
zsnyWy#7Ol}_0GIzKAS{P5gM;laUP^I6_&(vmq2}llkIzSi2V-Im2;uIG9nUBu*GzS
z47HUF`nvqNB6|hUEj@=qraK1q6GZ*5E>p-qxQq+kxo_y6Q5?M<2oV_igEy)2FrS`x
zk0Nx{4gm;B>Nv~*+h_pk^_#infL;El)c6#?RlzM-J_$~V2Ej|sCm9zNNVF_b!=L`w{8r=p5SbU>muCISJboAj
z+J1ol^lJg#d1|>1zN@+)+@d^_hNoo8+)CYW_|P%xx@AX0n{q_6o)1i)v<3VAIVSn&
zF6>5WKRR1RqJ$YtvHJ8J@V`@H+eJ8+E^uF&TMLjTix06Ww&{r1_@~$0|DEXuo5sZ0
z4AmZSd?_#x@b|k2FRoe;bqmUTIf?cbbH)F6|8gZ^==_v>N!kQS@VKKIK1F}}2+m`Y
zx@^+gcz3j`a?sMu*jJwU92?QD^a|oxTn`&iR-Cs2CmH(<44Z@yAQd(*dAf?hj|#d(
z$aq-<2aUR|bAHUU-+3M4c59E^!#z5-h{YWJK{&8GFRc%+p4a)AUbmYcdzheyDwnaB
ze1mWU=uWbqi6%XC)0SXXkytF9!>b8iPtihRx?1{K*0YE@SJk&xXIYdujSgL5ZzMja
z*HftmfG(LLQCO@aEl_e%F6=AnNj4`88FY&@C|9|*3Z0-}Io%(bo2wUFMFUP@Ioix^
zF!Ya3xv>Swr)ENj6i9I`NUZiPz-EgH3zo_)q!+5wZAsh!})R+Y&lud-TB)P
zt2V3oHM}}8jT9oKXU+dhk@Wni&+f8IeLN-9!b}~8!8NTJZ#psz870wD_Bm2m`0f;s(-5!Z&Q-9c-5YpZz@X6=J=P3+8`H
zi|Kx>{eUd}q*
zwpw^Z1vDupS2wT`;tMTCRaw+|=cPu*
zOFacS=GHIT@`ncuMwvh
zG-^%!E*sefRVMuAJ!b!NMTV2QerlpoI=S~ITuRQ^x+nH8VN@K7cz~l_08Jp}i3;DC{9oC3#}@dOUj~r__e1C{
zQY&^k)QpD}zEB;yVBK{2=@mpnp>Ti9!;IlT)->~iH1pZ6RwdQDpK}7*2`QM!X!(WJ
zp$x92Zh})bAV&Njeucry|5{@Qv<_gj^yWGLlKWHSvUHhjKe|`ar(j1HWF`1A(@5J7
zQL3Z4M>bQ1o`kyNV*4fMzokShe45Y}C8gUpO&7Hq)FZTLF+zP4^{=?xD0lXXDNwOF
z=rvDwbfAs+jJ{#Se6wMOY>+^drh)`WV_&QS+B7
zKg&1u4uxuB_JG0y@OIqFv{V=MMXV$-V_%?Knn&7YDi&J5F??{MNa@0~^1n2gQPP}R<-Nm$4Nm@Q&G$8u3H5?ouL)gwRmi}GiVqPF>(iK>GKBw-BC{eF(y!STx}
zqo2cdB9&vR60+sTBP*yB60k;|by5%gRwq?B8vneF2n_itmuVPD6Vm!nfL_IjZXyR@
zCld!zWo#-{GCX%sKcg*y9}aeXDY}9YiWfCkfF*@WFPQw#O6ok=bf?ft43Pqsl&9SE
z)$E#}H}|Ncnz{4V=N8mMBFeJ^_3f8RpWK<(9d4_iTrqzbIIVQuXi(HSU0EO|MMXYK
zIt|=qOB3`D16xqvEE<4022`6hboXDgbt#$8-BLD9j;d0(QemZ$u2$OkfC=@#+eR<%
zi+ZVy;xsj3m&B5@pYRyH|-Na*8-EqscoU&u$yLQr&ARLI4j6UQ`-^TS1`9N^+|
zMD3MExxPI!$J=Xy&!qHM9wH1*WuToR9E$O9;M%`@zk(J^O!vh&pVpE3@dV&TnEmq6
z+0i&CV+JA=8xX(f`71JO!DCi)I7?b2gC@(2}eI1NmK
zUiC}d=uTJn>N(4u*VxC`>H4-okNz1Sjtj;n5FyZ5rZ^Mvk(?`D?99V<7MK8k1|#Xx
zGWov%+>1lz09pEnFYH$U#=%TDziMHWn)V_1!=3oxO~DHVC*evchS9IS@c;eB{~2Nb-2AZmupXn&q?xc7C*jV&l_h4(>3;*7fmFbneDtQyJ9
zO|fb9~mA?z{i}7HB?1umu*o}MVwc<)4X&k$+MkJrH-{m$c7a+UC}@o
z&sqYWp2$|h_kR_NB$C`m0m)Uz@7}hEWwx>whVYuaxsIn0bIa?co)d757QT17m?+hKn{<4aSV(5&QoGB6a36_*S16%)!C@Cq0hW_IlRVH#^
z8$8jg`%6?2b#4npitta5+G6)I#ohD
zo~?rYceW3Mn_MtrN!6i$#X%7>UEw)E6~DPt+1gz3>=*Rvtb5XDJWHU)FX$tI#T#6L
z0N&}xpf_q|o?K&?BVuFr5y2wUlasL;66&aXa-Zh6X9NVLAu-uzedGJ%jwqcRz54?X
z>KP+a38uiqMDh1jFP3)HyreIEmW}CtYu$#TiPY_@*uZ072$yTRB>O}RBzryMt@w3Z
zpnf;??y~q3TMhhZkv$Gq{l-_&orkP>yFe~422tBor*}27YO6)&>{cnh{kt!?8SV5f
znSZZq57J-a<@zHlVO{VnvrK0CA7!S2VWW(uyP6*CbDFR7Gfvk39!EIB1M2+017xcycestIyK|w|ny{Tz0zIXLD
z!19v#p)%dPB_>@qgm2KE*(6AA2YM~84y0CV_ZMXYozk-RxP9C5WC~g)nSBuX8!T4d
z2fpO(_D$3D&1by+U5AD8mUWdG-Bw0Cuw$X$er@yE~o!_MZr;H`l$DjOhMoB@IUEms+|
zF-Ko(GzKRMZPd4w-kJDiA8@y-EmR&otU!24ZZTo9B$qpbypX-R2W`n3GIgwVQUw(A
z=K^zN&h-BTWM@m!MSJ-X^O5)h8Q0#KC9G?8zwfehkN6{Lj!TI2s3wYtzhlR}
zdtdybqp0Ad(?Lit4PJy36RPIkmtYV@@5Db@ED}qnR-B;f{Ke6WmY1WjtMtK6%zELsv
z!y&nCw_Nq~5-gK3+4_hRm+t5@L>L{5Mhde{fM)gLy~TFSwp99aXqx4%
zdR@2%R*+dy_AaqxK%#70o8
zFqoK}?W0W*0Iyi}2Jouq>Cs4fFyiA9p2@7U+w+K`0Cfrkj@If#`jl3a6dzvAdab_KXpIQ@npS@buKT4AZ
zo~Csg!dqnv^*ba@+pE5#r>Q*qnc0MP8rf2B2;PoC)VLPZSQ1@~)49#`wg{PxGGph|
zs76ZioFIVlZd63^5u3N7^~bClO}6nu%Qu?VomV>
z8w855R4~zWQV?D9@00S&Dn_4SVH$;Hlx`!ML|sX!47HPonW`7!*MoB$ws0wdsQg)A
zuA}T)ScP=2k4fV~t)eAC5f7fR
zyN8Zw$~bN3XDJJ0gQ%Ijd!qo9ZiPnF;<(ML@*%7
zSo!rN>_wTxDNSFIf#W#kfn&MgW-abAKY@+Z?(tCsILV!G
zEuy9MKKj~8W5F#uZ!Qn-lUT7=!;k0LBND!x{exp{xjB?&A7Nd;#zwO@c?-Jswu)b$
zZ<8G4-^NAxgz5pKc8v&!(mKRZ2#?!nlsf5yho(wxqRJub&&y
z^!96}vhy&_Ze!nWay^MzvthAv#Hz~_jE!QxpCbKaMAaB8|U7Z^(TAMdtGgoxQ`Co
z;xN-cA5#|4h}Vj!G5e$wsh&!1HVO;gz2eM|1Qgu&mV?@jxY(~>nR$DxlOMJYbvEI)
zkitj}De#cZppMPDnr{6;#W%T5EyCG~7xbP|yEFDl-G>>0SjB15T-f1zT{rGGbTWQ;
zm=31bv8HA*@%vanSiYxzP=O+g*hy^gF9tMB6gwwObVF!%>3``y0qXJNh(Fr6!gN)<
zAu`L|*Pvm(yL^+Ch-AgqC}9&19!hUPFk`SDkIzY{swiA%3jM%vbp5KC_qAl=zKX4p
za?gMi6wy0UZdFw=Aog+0C(sTHLt*Gui!#49UnOgN9EPr6>M
zz#EV|KCYOqt3-VxU_YQM6e9+V;EjUWw?LZcN2$vn`LM86jAM`py@_c++!+FwGsa*Y
z4k(@6d;HXT&bN(%Y5H06zAXp_`uAir+7MFZJ3rh<)IRM7PmMG;bD-lh1}mOH$b{`r
zXq#quVirHYf`^TFT@i3(W>_pmiSCA|{IZqBaN;ls{4EBV&
z%lq9peTo=wG$kP@nzq3(g?gBIRjPavp9`x@i*tB|FCV{o_Xvj_Bv+TjH!v4yld03k
z*+qN(bH!wO{d{36#rPM(S9JQu!ZojrF6}j9r?+-9i(Pe|vzKTFpj+C}Qnz9BJU-
zF^~J4u>E_<#Dcu$OcmfQM4b<>kYR-HG>>abj|Y@W0WN3{y=B5-0iq3)9~Yc~?tv(+
zMXFwkWOxRa?2hoxAR)+7K@bbBHx@fXFQ*{hJ~x0LAorHxM5v#``^W$Q%=EH+bU+S%
zMO461qIAFfQ8$OTi*1OZ2X5q>h&j<;2an6r_E@PSzZbEjW@~?nSXQh(z~gOiA|DuP
zTKkcvl!PVkOD#oR4Iz)Ax#APS^Vwur09D^g$P`S%FxCa7(swlrgEY$F+nj>R)ZdAH
zUbwK;tMor=OV+807BPEbUpV2og@&&t6jHCQCVcf#lid!@E^M&OPIFP}0P3czRddeW
z?8}Bm#6>;Js`{^fk+L#Fv?^B{q$?146hGny;o;OHy0ydja0X6}iV0Y9}XaXLXT^$V{UR
zH50*bIB$G`vzyFNgg3C?sv+PV~M$GDV9dog#8=3?Q6jveR
ztmz%vB3%h%UWcU8-s-*(rZ1yA&>NP2V3QDjT@ymv6Nh7GF$&YSklYZ8Ecv87iGt}u
z&b`c(q)n$!LTgJMIzv$?b7(EnXs0zALr8s~D!jIibV>FTfRug!NYQEJlWX5o6=Mdi
zPpB_!5AuWjY~!GKpW%9fCCCYkYE<^VC7vu%U9
z|2sU?8udJ~LuTt9Mxo5+g~=e&59TGXpeT`1u7J@mO-Yb0aXe$+q5*J=j9}Z^>;51%
zbGW!|&9qa~8Sb5?7g`5=EqaGp5c6RnaDGjPxgVQaj{Rh|Zuk8Z!;TCi;($<%MDj`=
zQ5f)z5(^oC%BtQ`?XlUXXXV#19kK;)xwEc|s86?M6>NPAQeb2EN#YLyN!N&^74FP|
z=#UCwbh5*bHU{veq2&YV;ux4q_MyI^Qjxz~2fJeRGCI_xIQuN{89I6)Kq=*8IueLssECZA(?<4>J-5JT|W;D4Q7U_fT848~x
z1gwy{<^G1APnE6}F^_1lBg&FO=oWFOl@oY5_h)31*12dV6@I1}k*CmiwFASZT{QOF
zpZvY`IA%AF(vuQ*(cnGvcCsK7Fj#g{HbaT^owRQ^_|lS5Q|notX~?TyB%MeKX@Fgq
zVA`Y%!Me>`6qbjSyCIbM;|jIl6Bh;1tTnj(p^e}L7WgcW><@o0Z?(7QkI`vhDfM9RQ3)b+Sw(ylHLYgvG8EPUQx7ulo{k=jrX=VYntN+stlkgQ>_J-;iG
z2Y#PPydoS(A!#dt%_11huJ!2}k76(R9mSPlVr4
z2=G5D{>XbgT+eT&IP0#z<{1?H&9+a$E#P??S4xwFb7VsNqeD!{)vu@~RO4H0o$h9MYa*6wzadKN`pToG@#1z~ys;RR6T7fg!eFeV8?3th
z-VIefu1xhx(VR6OX_W$6d_)a=&Y4P9uf-a7(k|_THr3X22duL@w={ee799p;{a|V5
zF!~y1_5IA^okwCu{!$jxLA$>KYtm#arR|dK{p|F<4>OIzt>c%*7TfG&5~5f(lXK!}
ztq@nts_!oA-%`10H^^8WR+2Nj>>dmB1`l|6ih79%h@wn4`>M9Fy-uBTM;6fA$})B0
z@@x7>SzSkeD!|fw71F0|5;?rx=P6|OnQN=aTeRlr@jzIh(2HBl{5g$^FWMPHk^Gao
z7x!o6SeJ&)Bn<#>*}l!9srsCZ;0_+&I-@Z+^KvdWG_YBr_j2L&y
z!QmNAawz`FHaoa6>0#E?CBBV^OSV`TZCl;zbKM?_FBr62bpEB$5B
zo`zAYi{%+1K-AZg6|uOKhA@~2ihsRtgwy?;+zefs6}3|wogGYF$NyrLA+=6n&L5M-
z_vTI0he9E);e;sQw#<&?o3A#x`os0tQnf4$L^AS9Z9HfY