[object Object]

← back to Nineoh Guide

professional redesign: original luxe design system (globals.css), Playfair+Inter fonts, animated SVG dusk hero + art-deco brand mark, sticky header/nav, stat+nav cards, dynamic OG image, favicon

e86cb341842d1b2567e87b67a5781d14600ec5e7 · 2026-07-25 11:32:01 -0700 · Steve

Files touched

Diff

commit e86cb341842d1b2567e87b67a5781d14600ec5e7
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jul 25 11:32:01 2026 -0700

    professional redesign: original luxe design system (globals.css), Playfair+Inter fonts, animated SVG dusk hero + art-deco brand mark, sticky header/nav, stat+nav cards, dynamic OG image, favicon
---
 apps/web/app/globals.css         | 155 +++++++++++++++++++++++++++++++++++++++
 apps/web/app/icon.svg            |  11 +++
 apps/web/app/layout.tsx          | 111 +++++++++++++++-------------
 apps/web/app/opengraph-image.tsx |  58 +++++++++++++++
 apps/web/app/page.tsx            |  89 ++++++++++++----------
 apps/web/components/HeroArt.tsx  |  72 ++++++++++++++++++
 6 files changed, 409 insertions(+), 87 deletions(-)

diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
new file mode 100644
index 0000000..1d1f235
--- /dev/null
+++ b/apps/web/app/globals.css
@@ -0,0 +1,155 @@
+/* ── Unofficial 90210 Guide — original editorial design system ─────────────
+   A Beverly-Hills-luxe aesthetic (dusk sky, gold, blush) that is deliberately
+   OUR OWN brand — no official logos, title-card type, or key art. */
+
+:root {
+  --ink: #17130f;
+  --ink-soft: #3a332c;
+  --paper: #faf6ef;
+  --paper-2: #fffdf9;
+  --line: #e9e1d4;
+  --gold: #c19a3e;
+  --gold-2: #e4c877;
+  --maroon: #7a2233;
+  --dusk-1: #2b1e3a;
+  --dusk-2: #7b3f6e;
+  --dusk-3: #d9668a;
+  --sun-1: #ff9a63;
+  --sun-2: #ffd27a;
+  --muted: #7c746b;
+  --radius: 12px;
+  --shadow: 0 1px 2px rgba(23, 19, 15, 0.04), 0 8px 24px rgba(23, 19, 15, 0.06);
+  --maxw: 940px;
+}
+
+* { box-sizing: border-box; }
+html { -webkit-text-size-adjust: 100%; }
+body {
+  margin: 0;
+  background: var(--paper);
+  color: var(--ink);
+  font-family: var(--font-body, system-ui, -apple-system, Segoe UI, Roboto, sans-serif);
+  line-height: 1.65;
+  font-size: 16px;
+  -webkit-font-smoothing: antialiased;
+}
+a { color: var(--maroon); text-decoration: none; }
+a:hover { text-decoration: underline; text-underline-offset: 2px; }
+
+h1, h2, h3 {
+  font-family: var(--font-display, Georgia, "Times New Roman", serif);
+  font-weight: 600;
+  line-height: 1.15;
+  letter-spacing: -0.01em;
+  color: var(--ink);
+}
+h1 { font-size: clamp(28px, 5vw, 40px); margin: 0 0 8px; }
+h2 { font-size: clamp(19px, 3vw, 23px); margin: 28px 0 10px; }
+
+/* ── Layout chrome ─────────────────────────────────────────────────────── */
+.site-header {
+  position: sticky; top: 0; z-index: 20;
+  background: rgba(250, 246, 239, 0.82);
+  backdrop-filter: saturate(1.4) blur(10px);
+  border-bottom: 1px solid var(--line);
+}
+.site-header .bar {
+  max-width: var(--maxw); margin: 0 auto;
+  display: flex; align-items: center; gap: 16px;
+  padding: 12px 20px;
+}
+.brand { display: inline-flex; align-items: center; gap: 10px; }
+.brand .badge {
+  font-size: 10px; font-weight: 700; letter-spacing: 0.12em;
+  color: var(--maroon); border: 1px solid currentColor;
+  padding: 2px 6px; border-radius: 999px;
+}
+.nav { margin-left: auto; display: flex; gap: 20px; font-size: 14px; font-weight: 500; }
+.nav a { color: var(--ink-soft); }
+
+.wrap { max-width: var(--maxw); margin: 0 auto; padding: 28px 20px 60px; }
+
+.site-footer {
+  max-width: var(--maxw); margin: 0 auto; padding: 28px 20px 48px;
+  border-top: 1px solid var(--line); font-size: 12.5px; color: var(--muted);
+}
+.site-footer a { color: var(--muted); text-decoration: underline; }
+
+/* ── Hero ──────────────────────────────────────────────────────────────── */
+.hero {
+  position: relative; overflow: hidden;
+  border-radius: 18px; padding: 56px 32px 48px; margin-bottom: 8px;
+  color: #fff5ec; isolation: isolate;
+  box-shadow: var(--shadow);
+}
+.hero .sky {
+  position: absolute; inset: 0; z-index: -2;
+  background: linear-gradient(180deg, var(--dusk-1) 0%, var(--dusk-2) 46%, var(--dusk-3) 70%, var(--sun-1) 90%, var(--sun-2) 100%);
+  background-size: 100% 220%;
+  animation: skyDrift 24s ease-in-out infinite alternate;
+}
+.hero .art { position: absolute; inset: 0; z-index: -1; width: 100%; height: 100%; }
+.hero .eyebrow {
+  font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
+  color: var(--gold-2); font-weight: 700; margin-bottom: 12px;
+}
+.hero h1 { color: #fff8f0; font-size: clamp(32px, 6vw, 52px); text-shadow: 0 2px 18px rgba(0,0,0,0.28); }
+.hero p.lede { max-width: 40ch; font-size: 16px; color: rgba(255,246,236,0.9); }
+.hero .cta {
+  display: inline-flex; align-items: center; gap: 8px; margin-top: 20px;
+  background: linear-gradient(180deg, var(--gold-2), var(--gold));
+  color: #241a06; font-weight: 700; font-size: 14px;
+  padding: 11px 20px; border-radius: 999px; box-shadow: 0 6px 20px rgba(0,0,0,0.22);
+  transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+.hero .cta:hover { transform: translateY(-1px); text-decoration: none; box-shadow: 0 10px 26px rgba(0,0,0,0.28); }
+
+/* ── Cards / stats ─────────────────────────────────────────────────────── */
+.stat-row { display: flex; flex-wrap: wrap; gap: 14px; margin: 22px 0 6px; }
+.stat {
+  background: var(--paper-2); border: 1px solid var(--line);
+  border-radius: var(--radius); padding: 14px 18px; min-width: 150px;
+  box-shadow: var(--shadow);
+}
+.stat .num { font-family: var(--font-display, serif); font-size: 30px; font-weight: 700; color: var(--maroon); }
+.stat .lab { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
+
+.card {
+  background: var(--paper-2); border: 1px solid var(--line);
+  border-radius: var(--radius); padding: 14px 16px; box-shadow: var(--shadow);
+  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
+}
+.card:hover { transform: translateY(-2px); border-color: var(--gold); box-shadow: 0 10px 28px rgba(23,19,15,0.10); }
+
+.pill {
+  display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: 0.04em;
+  color: var(--maroon); background: #f3e7e6; border-radius: 999px; padding: 2px 8px;
+}
+.se { color: var(--maroon); font-weight: 700; }
+
+input[type="search"], input[type="number"] {
+  font-family: inherit; border: 1px solid var(--line); border-radius: 8px; padding: 9px 11px; font-size: 14px; background: var(--paper-2);
+}
+button {
+  font-family: inherit; cursor: pointer; border-radius: 8px;
+  border: 1px solid var(--line); background: var(--paper-2); color: var(--ink);
+  padding: 7px 12px; font-size: 13px; transition: background 0.12s ease, border-color 0.12s ease;
+}
+button:hover { border-color: var(--gold); }
+
+/* ── Motion ────────────────────────────────────────────────────────────── */
+@keyframes skyDrift { from { background-position: 0 0; } to { background-position: 0 100%; } }
+@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
+@keyframes twinkle { 0%,100% { opacity: 0.25; } 50% { opacity: 0.9; } }
+@keyframes sway { 0%,100% { transform: rotate(-1.2deg); } 50% { transform: rotate(1.2deg); } }
+@keyframes shimmer { from { background-position: -400px 0; } to { background-position: 400px 0; } }
+
+.rise { animation: rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; }
+.rise-2 { animation: rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) 0.08s both; }
+.rise-3 { animation: rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) 0.16s both; }
+.palm { transform-origin: bottom center; animation: sway 7s ease-in-out infinite; }
+.star { animation: twinkle 4s ease-in-out infinite; }
+
+@media (prefers-reduced-motion: reduce) {
+  *, .hero .sky, .palm, .star { animation: none !important; }
+}
diff --git a/apps/web/app/icon.svg b/apps/web/app/icon.svg
new file mode 100644
index 0000000..fe2bf22
--- /dev/null
+++ b/apps/web/app/icon.svg
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
+  <rect width="40" height="40" rx="9" fill="#17130f"/>
+  <circle cx="20" cy="23" r="9" fill="#c19a3e"/>
+  <g stroke="#e4c877" stroke-width="2" stroke-linecap="round">
+    <line x1="20" y1="6" x2="20" y2="10"/>
+    <line x1="9" y1="10" x2="12" y2="13"/>
+    <line x1="31" y1="10" x2="28" y2="13"/>
+    <line x1="5" y1="23" x2="9" y2="23"/>
+    <line x1="35" y1="23" x2="31" y2="23"/>
+  </g>
+</svg>
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 9f35afb..99ffaa7 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,7 +1,22 @@
 import type { Metadata } from "next";
-import { APP } from "@nineoh/core";
+import { Playfair_Display, Inter } from "next/font/google";
+import { APP, SITE_URL } from "@nineoh/core";
+import "./globals.css";
+
+const display = Playfair_Display({
+  subsets: ["latin"],
+  weight: ["600", "700"],
+  variable: "--font-display",
+  display: "swap",
+});
+const body = Inter({
+  subsets: ["latin"],
+  variable: "--font-body",
+  display: "swap",
+});
 
 export const metadata: Metadata = {
+  metadataBase: new URL(SITE_URL),
   title: {
     default: `${APP.displayName} — Unofficial Episode & Cast Guide`,
     template: `%s · ${APP.displayName}`,
@@ -11,56 +26,31 @@ export const metadata: Metadata = {
   robots: { index: true, follow: true },
 };
 
-export default function RootLayout({
-  children,
-}: {
-  children: React.ReactNode;
-}) {
+export default function RootLayout({ children }: { children: React.ReactNode }) {
   return (
-    <html lang="en">
-      <body
-        style={{
-          margin: 0,
-          fontFamily:
-            "system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
-          color: "#1a1a1a",
-          background: "#fafafa",
-        }}
-      >
-        <header
-          style={{
-            padding: "12px 20px",
-            borderBottom: "1px solid #e5e5e5",
-            display: "flex",
-            gap: 12,
-            alignItems: "baseline",
-          }}
-        >
-          <a href="/" style={{ textDecoration: "none", color: "inherit" }}>
-            <strong style={{ fontSize: 18 }}>{APP.displayName}</strong>
-          </a>
-          <span style={{ fontSize: 12, color: "#8a1c1c", fontWeight: 600 }}>
-            UNOFFICIAL
-          </span>
-          <nav style={{ marginLeft: "auto", display: "flex", gap: 14, fontSize: 14 }}>
-            <a href="/episodes">Episodes</a>
-            <a href="/cast">Cast</a>
-            <a href="/search">Search</a>
-          </nav>
+    <html lang="en" className={`${display.variable} ${body.variable}`}>
+      <body>
+        <header className="site-header">
+          <div className="bar">
+            <a className="brand" href="/" aria-label={APP.displayName}>
+              <Mark />
+              <strong style={{ fontFamily: "var(--font-display)", fontSize: 19 }}>
+                90210 Guide
+              </strong>
+              <span className="badge">Unofficial</span>
+            </a>
+            <nav className="nav">
+              <a href="/episodes">Episodes</a>
+              <a href="/cast">Cast</a>
+              <a href="/news">News</a>
+              <a href="/search">Search</a>
+            </nav>
+          </div>
         </header>
-        <main style={{ maxWidth: 820, margin: "0 auto", padding: "24px 20px" }}>
-          {children}
-        </main>
-        <footer
-          style={{
-            maxWidth: 820,
-            margin: "0 auto",
-            padding: "24px 20px",
-            borderTop: "1px solid #e5e5e5",
-            fontSize: 12,
-            color: "#666",
-          }}
-        >
+
+        <main className="wrap">{children}</main>
+
+        <footer className="site-footer">
           {APP.disclaimerShort}{" "}
           <a href="/legal/disclaimer">Disclaimer</a> ·{" "}
           <a href="/legal/dmca">Copyright/DMCA</a> ·{" "}
@@ -70,3 +60,26 @@ export default function RootLayout({
     </html>
   );
 }
+
+/* Original art-deco sun mark — our brand glyph, not the show's. */
+function Mark() {
+  return (
+    <svg width="26" height="26" viewBox="0 0 40 40" aria-hidden="true">
+      <circle cx="20" cy="24" r="10" fill="#c19a3e" />
+      <g stroke="#c19a3e" strokeWidth="2" strokeLinecap="round">
+        {Array.from({ length: 7 }).map((_, i) => {
+          const a = Math.PI - (Math.PI / 6) * i;
+          return (
+            <line
+              key={i}
+              x1={20 + Math.cos(a) * 13}
+              y1={24 - Math.sin(a) * 13}
+              x2={20 + Math.cos(a) * 18}
+              y2={24 - Math.sin(a) * 18}
+            />
+          );
+        })}
+      </g>
+    </svg>
+  );
+}
diff --git a/apps/web/app/opengraph-image.tsx b/apps/web/app/opengraph-image.tsx
new file mode 100644
index 0000000..cba8829
--- /dev/null
+++ b/apps/web/app/opengraph-image.tsx
@@ -0,0 +1,58 @@
+import { ImageResponse } from "next/og";
+
+export const size = { width: 1200, height: 630 };
+export const contentType = "image/png";
+export const alt = "Unofficial 90210 Guide";
+
+// Dynamically-generated, ORIGINAL branded social card — no show imagery.
+export default function OG() {
+  return new ImageResponse(
+    (
+      <div
+        style={{
+          width: "100%",
+          height: "100%",
+          display: "flex",
+          flexDirection: "column",
+          justifyContent: "center",
+          padding: "70px",
+          background:
+            "linear-gradient(160deg, #2b1e3a 0%, #7b3f6e 52%, #d9668a 76%, #ff9a63 100%)",
+          color: "#fff6ec",
+          fontFamily: "serif",
+        }}
+      >
+        <div
+          style={{
+            display: "flex",
+            alignItems: "center",
+            gap: 16,
+            fontSize: 26,
+            letterSpacing: 6,
+            textTransform: "uppercase",
+            color: "#ffd27a",
+            fontWeight: 700,
+          }}
+        >
+          <div
+            style={{
+              width: 34,
+              height: 34,
+              borderRadius: 999,
+              background: "#c19a3e",
+              display: "flex",
+            }}
+          />
+          Unofficial Fan Guide
+        </div>
+        <div style={{ fontSize: 84, fontWeight: 700, marginTop: 20, lineHeight: 1.05 }}>
+          Every episode of 90210, recapped.
+        </div>
+        <div style={{ fontSize: 30, marginTop: 22, color: "rgba(255,246,236,0.9)" }}>
+          Original recaps · cast profiles · spoiler guard
+        </div>
+      </div>
+    ),
+    { ...size }
+  );
+}
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index d1fb4e9..c5f5662 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -1,4 +1,5 @@
 import { pool } from "@/lib/db";
+import { HeroArt } from "@/components/HeroArt";
 
 export const dynamic = "force-dynamic";
 
@@ -9,18 +10,22 @@ async function getStats() {
     );
     const eps = await pool.query("select count(*)::int as n from episodes");
     const cast = await pool.query("select count(*)::int as n from cast_people");
+    const recapped = await pool.query(
+      "select count(*)::int as n from episodes where summary_short_original is not null"
+    );
     return {
       show: show.rows[0] ?? null,
       episodes: eps.rows[0]?.n ?? 0,
       cast: cast.rows[0]?.n ?? 0,
+      recapped: recapped.rows[0]?.n ?? 0,
     };
   } catch {
-    return { show: null, episodes: 0, cast: 0 };
+    return { show: null, episodes: 0, cast: 0, recapped: 0 };
   }
 }
 
 export default async function Home() {
-  const { show, episodes, cast } = await getStats();
+  const { episodes, cast, recapped } = await getStats();
   const jsonLd = {
     "@context": "https://schema.org",
     "@type": "TVSeries",
@@ -30,54 +35,62 @@ export default async function Home() {
     numberOfSeasons: 5,
     numberOfEpisodes: episodes,
   };
+
   return (
-    <section>
-      <script
-        type="application/ld+json"
-        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
-      />
-      <h1 style={{ fontSize: 30, marginBottom: 4 }}>
-        {show?.display_title ?? "Unofficial 90210 Guide"}
-      </h1>
-      <p style={{ color: "#444", lineHeight: 1.6 }}>
-        {show?.description_original ??
-          "A fan-made companion to the 90210 series — original episode recaps, cast bios, and news. This is an unofficial guide, written independently."}
-      </p>
+    <>
+      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
 
-      <div style={{ display: "flex", gap: 16, margin: "20px 0" }}>
-        <Stat label="Episodes indexed" value={episodes} />
-        <Stat label="Cast profiles" value={cast} />
+      <section className="hero">
+        <div className="sky" />
+        <HeroArt />
+        <p className="eyebrow rise">Unofficial Fan Guide</p>
+        <h1 className="rise-2">Every episode of 90210, recapped.</h1>
+        <p className="lede rise-3">
+          An independent companion to the 2008 Beverly Hills teen drama —
+          original recaps, cast profiles, and a spoiler guard that keeps you one
+          step ahead. Written by fans, for fans.
+        </p>
+        <a className="cta rise-3" href="/episodes">
+          Browse the episode guide →
+        </a>
+      </section>
+
+      <div className="stat-row">
+        <Stat num={episodes} lab="Episodes indexed" />
+        <Stat num={recapped} lab="Original recaps" />
+        <Stat num={cast} lab="Cast profiles" />
       </div>
 
-      <nav style={{ display: "grid", gap: 8 }}>
-        <a href="/episodes">Browse episode guide →</a>
-        <a href="/cast">Browse cast →</a>
-        <a href="/news">Latest news →</a>
-      </nav>
+      <h2>Start exploring</h2>
+      <div style={{ display: "grid", gap: 12, gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))" }}>
+        <NavCard href="/episodes" title="Episode Guide" sub="All 5 seasons · spoiler-safe recaps" />
+        <NavCard href="/cast" title="Cast & Characters" sub="Who played who, with original bios" />
+        <NavCard href="/search" title="Search" sub="Find any episode, character, or moment" />
+      </div>
 
       {episodes === 0 && (
-        <p style={{ marginTop: 24, fontSize: 13, color: "#8a1c1c" }}>
-          Content is being curated. Episode and cast data will populate as our
-          editors write original recaps and clear image rights.
+        <p style={{ marginTop: 20, color: "var(--maroon)", fontSize: 13 }}>
+          Content is being curated — recaps and profiles will populate shortly.
         </p>
       )}
-    </section>
+    </>
   );
 }
 
-function Stat({ label, value }: { label: string; value: number }) {
+function Stat({ num, lab }: { num: number; lab: string }) {
   return (
-    <div
-      style={{
-        border: "1px solid #e5e5e5",
-        borderRadius: 8,
-        padding: "12px 16px",
-        background: "#fff",
-        minWidth: 140,
-      }}
-    >
-      <div style={{ fontSize: 24, fontWeight: 700 }}>{value}</div>
-      <div style={{ fontSize: 12, color: "#666" }}>{label}</div>
+    <div className="stat">
+      <div className="num">{num}</div>
+      <div className="lab">{lab}</div>
     </div>
   );
 }
+
+function NavCard({ href, title, sub }: { href: string; title: string; sub: string }) {
+  return (
+    <a className="card" href={href} style={{ display: "block", color: "inherit" }}>
+      <strong style={{ fontFamily: "var(--font-display)", fontSize: 17 }}>{title}</strong>
+      <div style={{ color: "var(--muted)", fontSize: 13, marginTop: 4 }}>{sub}</div>
+    </a>
+  );
+}
diff --git a/apps/web/components/HeroArt.tsx b/apps/web/components/HeroArt.tsx
new file mode 100644
index 0000000..b3ef001
--- /dev/null
+++ b/apps/web/components/HeroArt.tsx
@@ -0,0 +1,72 @@
+/**
+ * Original hero artwork — a stylized Beverly Hills dusk (art-deco sun, palm
+ * silhouettes, stars). Entirely our own vector art: no show imagery, logos, or
+ * key art. Motion comes from the .palm / .star CSS keyframes in globals.css.
+ */
+export function HeroArt() {
+  return (
+    <svg
+      className="art"
+      viewBox="0 0 1000 500"
+      preserveAspectRatio="xMidYMax slice"
+      aria-hidden="true"
+    >
+      <defs>
+        <radialGradient id="sun" cx="50%" cy="72%" r="42%">
+          <stop offset="0%" stopColor="#ffe6a8" />
+          <stop offset="45%" stopColor="#ffcf72" />
+          <stop offset="100%" stopColor="#ff9a63" stopOpacity="0" />
+        </radialGradient>
+        <linearGradient id="palm" x1="0" y1="0" x2="0" y2="1">
+          <stop offset="0%" stopColor="#2a1526" />
+          <stop offset="100%" stopColor="#120a12" />
+        </linearGradient>
+      </defs>
+
+      {/* sun disc + deco rays */}
+      <g>
+        <circle cx="500" cy="360" r="150" fill="url(#sun)" />
+        <g stroke="#ffd27a" strokeOpacity="0.35" strokeWidth="2">
+          {Array.from({ length: 13 }).map((_, i) => {
+            const a = (Math.PI / 12) * i;
+            const x = 500 + Math.cos(a) * 175;
+            const y = 360 - Math.sin(a) * 175;
+            const x2 = 500 + Math.cos(a) * 230;
+            const y2 = 360 - Math.sin(a) * 230;
+            return <line key={i} x1={x} y1={y} x2={x2} y2={y2} />;
+          })}
+        </g>
+      </g>
+
+      {/* stars */}
+      {[
+        [120, 70], [230, 40], [360, 90], [700, 55], [820, 95], [900, 45], [60, 130],
+      ].map(([cx, cy], i) => (
+        <circle key={i} className="star" cx={cx} cy={cy} r="2.2" fill="#fff3d6" style={{ animationDelay: `${i * 0.5}s` }} />
+      ))}
+
+      {/* palm silhouettes */}
+      <PalmLeft />
+      <PalmRight />
+
+      {/* horizon ground */}
+      <rect x="0" y="470" width="1000" height="30" fill="#120a12" />
+    </svg>
+  );
+}
+
+function PalmLeft() {
+  return (
+    <g className="palm" style={{ transformBox: "fill-box", transformOrigin: "bottom" }}>
+      <path d="M120 480 C126 400 132 330 128 300 C150 320 150 300 118 292 C150 280 150 262 120 286 C146 254 130 244 112 282 C120 246 96 250 106 290 C86 264 74 286 108 300 C78 300 78 322 112 302 C104 360 110 420 108 480 Z" fill="url(#palm)" />
+    </g>
+  );
+}
+
+function PalmRight() {
+  return (
+    <g className="palm" style={{ transformBox: "fill-box", transformOrigin: "bottom", animationDelay: "1.5s" }}>
+      <path d="M885 480 C879 400 873 330 877 300 C855 320 855 300 887 292 C855 280 855 262 885 286 C859 254 875 244 893 282 C885 246 909 250 899 290 C919 264 931 286 897 300 C927 300 927 322 893 302 C901 360 895 420 897 480 Z" fill="url(#palm)" />
+    </g>
+  );
+}

← 2a1d19a internal search: /search (SSR) across episode titles+recaps  ·  back to Nineoh Guide  ·  news: index 760 90210 articles (Google News RSS, original+re 8dc29d7 →