diff --git a/assets/site.css b/assets/site.css index 3ee1fd0..2ac8401 100644 --- a/assets/site.css +++ b/assets/site.css @@ -106,7 +106,7 @@ main{max-width:var(--maxw);margin:0 auto;padding:10px 24px 70px} position:relative;overflow:hidden} .btn.concierge::before{content:"";position:absolute;inset:0; background:linear-gradient(135deg,rgba(50,38,22,.95),rgba(65,48,25,.95)); - opacity:0;transition:opacity .25s;pointer-events:none} + opacity:0;transition:opacity .25s;pointer-events:none;z-index:-1} .btn.concierge:hover::before{opacity:1} .btn.concierge:hover{color:var(--gold);box-shadow:inset 0 0 0 1px rgba(212,165,116,.5),0 8px 28px rgba(0,0,0,.55)} diff --git a/body.html b/body.html index 9c67bf7..43e8619 100644 --- a/body.html +++ b/body.html @@ -22,7 +22,7 @@ display: flex; align-items: center; flex-wrap: wrap; - gap: 16px; + gap: var(--sp4); margin-bottom: 10px; } .body-title-row h1 { @@ -44,8 +44,8 @@ /* spec block panel */ .spec-panel { - background: var(--panel); - border: 1px solid var(--line); + background: var(--s2); + border: 1px solid var(--b-subtle); border-radius: 13px; padding: 26px 30px; margin-top: 28px; @@ -82,7 +82,7 @@ .archive-note { font-size: 13px; color: var(--muted); - margin-top: 16px; + margin-top: var(--sp4); letter-spacing: 0.5px; } .archive-note strong { color: var(--cream); } diff --git a/browse.html b/browse.html index 28a6542..94d48bb 100644 --- a/browse.html +++ b/browse.html @@ -17,7 +17,7 @@ } .browse-hero .eyebrow{margin-bottom:14px} .browse-hero h1{font-size:clamp(30px,5.5vw,58px);font-weight:700;letter-spacing:1px} -.browse-hero .sub{color:var(--muted);margin-top:12px;font-size:14px;letter-spacing:1px} +.browse-hero .sub{color:var(--muted);margin-top:12px;font-size:var(--t-base);letter-spacing:1px} .browse-hero .back{display:inline-block;margin-top:18px;font-size:11px;letter-spacing:2px; text-transform:uppercase;color:var(--muted);border-bottom:1px solid var(--line);padding-bottom:2px;transition:.2s} .browse-hero .back:hover{color:var(--text);border-color:var(--text)} @@ -26,16 +26,16 @@ .filterbar{ position:sticky;top:62px;z-index:40; background:rgba(18,18,18,.97);backdrop-filter:blur(12px); - border-bottom:1px solid var(--line); + border-bottom:1px solid var(--b-subtle); padding:14px 24px; } .filterbar-inner{max-width:var(--maxw);margin:0 auto;display:flex;flex-wrap:wrap;gap:10px;align-items:center} .filter-group{display:flex;align-items:center;gap:6px;flex-wrap:wrap} -.filter-label{font-size:10px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);white-space:nowrap;margin-right:2px} +.filter-label{font-size:var(--t-xs);letter-spacing:2px;text-transform:uppercase;color:var(--muted);white-space:nowrap;margin-right:2px} .pill{ - padding:6px 16px;border:1px solid var(--line);border-radius:30px; + padding:6px 16px;border:1px solid var(--b-subtle);border-radius:var(--r-pill); background:transparent;color:var(--muted); - cursor:pointer;font-size:12px;letter-spacing:.5px; + cursor:pointer;font-size:var(--t-sm);letter-spacing:.5px; font-family:inherit;transition:.2s;white-space:nowrap; } .pill:hover{color:var(--text);border-color:var(--muted)} @@ -44,7 +44,7 @@ .filter-sep{width:1px;height:22px;background:var(--line);margin:0 4px;flex-shrink:0} .toggle-wrap{display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none} .toggle-wrap input[type=checkbox]{accent-color:var(--gold);width:15px;height:15px;cursor:pointer} -.toggle-label{font-size:12px;color:var(--muted);letter-spacing:.5px;transition:.2s} +.toggle-label{font-size:var(--t-sm);color:var(--muted);letter-spacing:.5px;transition:.2s} .toggle-wrap:hover .toggle-label{color:var(--text)} .search-wrap{margin-left:auto;position:relative;flex-shrink:0} .search-wrap input{ @@ -72,7 +72,7 @@ } .count-text{font-size:13px;color:var(--muted)} .count-text strong{color:var(--text)} -.reset-link{font-size:12px;letter-spacing:1px;text-transform:uppercase;color:var(--gold); +.reset-link{font-size:var(--t-sm);letter-spacing:1px;text-transform:uppercase;color:var(--gold); cursor:pointer;border-bottom:1px solid transparent;transition:.2s} .reset-link:hover{border-color:var(--gold)} @@ -85,7 +85,7 @@ } .empty-state .big{font-family:'Playfair Display',serif;font-style:italic; font-size:clamp(22px,4vw,36px);color:var(--muted);margin-bottom:16px} -.empty-state p{color:var(--muted);font-size:14px;max-width:400px;margin:0 auto 24px} +.empty-state p{color:var(--muted);font-size:var(--t-base);max-width:400px;margin:0 auto 24px} diff --git a/character.html b/character.html index cbf16c1..01c45d1 100644 --- a/character.html +++ b/character.html @@ -9,17 +9,17 @@ diff --git a/contact.html b/contact.html index b798640..6e1ab89 100644 --- a/contact.html +++ b/contact.html @@ -18,11 +18,11 @@ @media(max-width:900px){.contact-layout{grid-template-columns:1fr;gap:36px}} /* character chip (prefill) */ - .char-chip{display:flex;align-items:center;gap:14px;background:var(--panel2);border:1px solid rgba(212,165,116,.35);border-radius:10px;padding:12px 16px;margin-bottom:24px} - .char-chip img{width:52px;height:70px;object-fit:cover;object-position:top center;border-radius:6px;flex-shrink:0;background:#0d0d0d} - .char-chip .chip-thumb-ph{width:52px;height:70px;border-radius:6px;flex-shrink:0;background:var(--panel);display:flex;align-items:center;justify-content:center;font-family:'Playfair Display',serif;font-size:22px;color:var(--gold)} + .char-chip{display:flex;align-items:center;gap:14px;background:var(--panel2);border:1px solid color-mix(in srgb,var(--gold) 35%,transparent);border-radius:var(--r-md);padding:12px 16px;margin-bottom:24px} + .char-chip img{width:52px;height:70px;object-fit:cover;object-position:top center;border-radius:var(--r-sm);flex-shrink:0;background:#0d0d0d} + .char-chip .chip-thumb-ph{width:52px;height:70px;border-radius:var(--r-sm);flex-shrink:0;background:var(--s2);display:flex;align-items:center;justify-content:center;font-family:'Playfair Display',serif;font-size:22px;color:var(--gold)} .char-chip .chip-info{} - .char-chip .chip-label{font-size:10px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);margin-bottom:3px} + .char-chip .chip-label{font-size:var(--t-xs);letter-spacing:2px;text-transform:uppercase;color:var(--gold);margin-bottom:3px} .char-chip .chip-name{font-family:'Playfair Display',serif;font-size:18px;font-weight:600;line-height:1.2} .char-chip .chip-sub{font-size:11px;color:var(--muted);margin-top:2px;letter-spacing:.5px} @@ -103,9 +103,9 @@

Begin a Conversation

-

Your inquiry is in.

-

Thank you — we'll reply personally within one business day.
- If your email client didn't open automatically, you can also write us directly at +

Your inquiry has been received.

+

We will be in touch discreetly within one business day.
+ You may also reach us directly at .

@@ -374,7 +374,7 @@

No pressure

} else { showFieldError('f-email','err-email',false); } var msg = (document.getElementById('f-message') || {}).value || ''; - if (!msg.trim()) { + if (msg.trim().length < 10) { showFieldError('f-message','err-message',true); if (!firstBad) firstBad = document.getElementById('f-message'); ok = false; @@ -439,9 +439,9 @@

No pressure

if (form) form.style.display = 'none'; if (success) success.style.display = 'block'; if (!usedMailto) { - // hide the "if email didn't open" fallback sentence for endpoint path + // endpoint path: hide the direct-email fallback line var p = success.querySelector('p'); - if (p) p.innerHTML = 'Thank you — we\'ll reply personally within one business day.'; + if (p) p.innerHTML = 'We will be in touch discreetly within one business day.'; } // Update success email link with real href var email = ZX.INQUIRY_EMAIL || 'inquiries@zelexdoll.com'; diff --git a/craft.html b/craft.html index cea7031..71c0e46 100644 --- a/craft.html +++ b/craft.html @@ -25,7 +25,7 @@ .craft-hero .inner{ position:relative;max-width:var(--maxw);margin:0 auto;width:100%;padding:0 40px 10vh; } -.craft-hero .eyebrow{color:var(--gold);letter-spacing:6px;text-transform:uppercase;font-size:12px;margin-bottom:22px;} +.craft-hero .eyebrow{color:var(--gold);letter-spacing:6px;text-transform:uppercase;font-size:var(--t-sm);margin-bottom:22px;} .craft-hero h1{ font-family:'Playfair Display',serif;font-size:clamp(42px,8vw,92px); line-height:1.03;font-weight:400;max-width:15ch; @@ -51,7 +51,7 @@ /* ---- method section ---- */ .method{max-width:var(--maxw);margin:0 auto;padding:100px 40px 60px;} .method-head{text-align:center;max-width:740px;margin:0 auto 80px;} -.method-head .eyebrow{color:var(--gold);letter-spacing:5px;text-transform:uppercase;font-size:12px;} +.method-head .eyebrow{color:var(--gold);letter-spacing:5px;text-transform:uppercase;font-size:var(--t-sm);} .method-head h2{font-family:'Playfair Display',serif;font-size:clamp(28px,4.2vw,46px);margin:14px 0;} .method-head p{color:var(--muted);font-weight:300;font-size:15px;} @@ -63,8 +63,8 @@ .method{padding:70px 20px 40px;} } .method-img-wrap{ - aspect-ratio:3/4;background:#0d0d0d;border-radius:10px;overflow:hidden; - border:1px solid var(--line); + aspect-ratio:3/4;background:#0d0d0d;border-radius:var(--r-md);overflow:hidden; + border:1px solid var(--b-subtle); } .method-img-wrap img{width:100%;height:100%;object-fit:cover;object-position:top center;transition:.4s;} .method-img-wrap:hover img{transform:scale(1.04);} @@ -74,8 +74,8 @@ .method-txt p+p{margin-top:12px;} .method-txt .inline-stat{ display:inline-block;margin-top:18px;padding:9px 18px; - background:var(--panel2);border:1px solid var(--line);border-radius:8px; - font-size:12px;letter-spacing:1.5px;color:var(--cream); + background:var(--s3);border:1px solid var(--b-subtle);border-radius:8px; + font-size:var(--t-sm);letter-spacing:1.5px;color:var(--cream); } .method-txt .inline-stat b{color:var(--gold);} diff --git a/docs/PDR-005-sculpt-acquisition-brief.md b/docs/PDR-005-sculpt-acquisition-brief.md new file mode 100644 index 0000000..f1ad5a3 --- /dev/null +++ b/docs/PDR-005-sculpt-acquisition-brief.md @@ -0,0 +1,97 @@ +# PDR-005 — The Sculpt: Acquisition Brief + +**Status:** Draft · **Date:** 2026-06-05 · **Author:** Product + +--- + +## Executive Summary + +ZELEX is the only Catalog Depth Leader in the 30-brand matrix with zero Sculpt bodies — a family now present in 20 of 30 catalogues surveyed and led by MD Doll with 37 entries. The gap is not a niche oversight; it is a structural absence that routes buyers seeking athletic, defined proportions to competitors who have already committed the SKU depth. This brief makes the case for a single, precisely specified hero Sculpt body as ZELEX's first entry, positioned at the premium end of a market that has largely served the segment with lower-cost TPE products. + +--- + +## Market Signal + +Across 30 brands and 495 classified body-style rows, The Sculpt family is the second-most-represented family in competitor catalogues by raw SKU count, driven almost entirely by MD Doll: + +| Brand | Sculpt bodies | Sculpt share of catalogue | Material | Median price | +|---|---:|---:|---|---:| +| MD Doll | 37 | 53.6% | Mixed (silicone 33%) | $2,599 | +| FunWest | 17 | 16.0% | Mixed (silicone 46%) | $1,599 | +| Tayu | 5 | 23.8% | Silicone 86% | $3,800 | +| Lusandy Doll | 5 | 14.3% | Silicone 83% | $2,699 | +| Gynoid | 4 | 40.0% | Silicone 100% | $5,391 | +| Angel Kiss | 3 | 30.0% | Silicone 100% | $2,390 | +| XT Doll | 3 | 30.0% | Silicone 100% | $2,190 | +| YL Doll | 3 | 30.0% | Mixed (silicone 20%) | $1,740 | +| 6YE Premium | 3 | 30.0% | TPE 100% | $1,490 | + +Nine additional brands carry 1–2 Sculpt bodies each: AS Doll, Dime Doll, Game Lady, Irokebijin, Jiusheng, JY Doll, Lilydoll, Piper Doll, SE Doll, SM Doll, Sanhui. + +**Taxonomy note.** "The Sculpt" is a ZELEX internal classification applied retroactively to competitor measurements. Competitors do not use a shared "Sculpt" label — MD Doll presents these as fitness or athletic body variants; Gynoid markets them under its broader realism positioning. The classification is measurement-driven (WHR 0.65–0.68, BWR 1.45–1.55), not a label competitors themselves promote. The convergence of 20 brands arriving at bodies that fall in this range independently is the signal, not the terminology. + +**Price tier observation.** The volume leader in this segment (MD Doll, 37 bodies at $2,599 median) and the mass-market providers (FunWest at $1,599, 6YE at $1,490) serve the segment at meaningfully different price points. The silicone-first, boutique-premium segment — Gynoid ($5,391), Tayu ($3,800) — validates that material quality commands a significant premium in this family. The mid-premium corridor ($2,000–$2,700) in silicone is underserved by brands with ZELEX's catalogue depth. + +--- + +## ZELEX's Gap + +The coverage matrix is unambiguous: + +- **ZELEX (official): 19 bodies classified. Sculpt count: 0.** +- **ZELEX (Dollstudio): 10 bodies classified. Sculpt count: 0.** +- Family status in `family_taxonomy.json`: `in_development`, 0 members. +- ZELEX's 84.2% top-two concentration (Muse + Icon) is the highest in the matrix — a focused catalogue that is strong where it plays but absent from the athletic/defined segment entirely. + +A buyer who arrives at ZELEX's quiz or family page having self-identified as wanting athletic, muscular definition — the buyer Gynoid and MD Doll are actively serving — currently hits a dead end. The quiz routing has no Sculpt destination to send them to. They leave. + +ZELEX is classified as a Catalog Depth Leader (156 full-body SKUs, median $1,999). Every other Catalog Depth Leader in the matrix — FunWest, Irontech, SE Doll, WM Doll — covers the Sculpt family. ZELEX is the exception. + +--- + +## Entry Specification + +The Sculpt's hard measurement boundaries, as defined in `db/family_taxonomy.json` and surfaced in `family.html`: + +| Axis | Floor | Ceiling | Context | +|---|---|---|---| +| WHR | 0.65 | 0.68 | Lower floor than The Muse (0.65–0.70); ceiling 0.02 below Muse ceiling | +| BWR | 1.45 | 1.55 | Above Muse (1.30–1.40); below Icon (1.50–1.60) at the high end | + +**WHR–Muse boundary.** The Sculpt and The Muse share the same WHR lower bound (0.65) but diverge immediately on BWR: Muse bodies sit at 1.30–1.40, Sculpt bodies at 1.45–1.55. The visual difference is a slightly fuller bust relative to the waist — athletic torso geometry rather than the long, lean European line. A classifier placing a body at WHR 0.66 / BWR 1.35 lands in Muse. The same WHR at BWR 1.48 lands in Sculpt. The distinction is real and measurable. + +**Surface geometry requirements** (from `FAM_META` description and family brand voice): +- Articulated abdominals — visible muscle definition, not smooth belly surface +- Defined deltoids and shoulder musculature +- Sculpted, lifted posterior with visible gluteal separation + +These are render/sculpt-level requirements, not derivable from WHR/BWR alone. A body can hit the measurement ranges with a smooth, soft surface and still fail the family entry criteria. + +**Height range.** The taxonomy does not set a Sculpt-specific height floor or ceiling. Competitor data spans 150–175 cm for classified Sculpt bodies. An entry at 163–168 cm sits within the ZELEX range (153–175 cm) and avoids the height extremes that narrow buyable configurations. + +**Minimum viable entry:** +- WHR: 0.66–0.67 (centre of range, unambiguous classification, no border risk) +- BWR: 1.48–1.52 (mid-range, clear separation from Muse, below Icon overlap) +- Height: 163–168 cm +- Surface: articulated abs, defined deltoid caps, sculpted glutes — sculpt-reviewed at prototype stage +- Material: silicone (mandatory — TPE cannot render the surface detail that defines the family) + +--- + +## Competitive Differentiation Angle + +MD Doll's 37-body Sculpt catalogue is a volume play on a single family. It wins on choice; it does not win on character, narrative, or routing intelligence. Its Sculpt bodies are listed as product variants — SKUs to scroll through — not personas with backstory, aesthetic context, or quiz-matched entry points. + +ZELEX's existing advantage is the named-character architecture: each body is a character with a profile, a skin match, and a quiz pathway. The Sculpt entry should not be "athletic body variant 001." It should be a named character — a persona designed around fitness realism, with a defined visual identity that the buyer encounters before they see measurements. + +This matters because the buyer for a Sculpt body is different from the Muse or Icon buyer in one specific way: they arrived with a concrete physical idea in mind. They are not browsing aesthetics; they know they want definition. A quiz that identifies this preference and routes them to a character — rather than a measurement table — closes the sale that MD Doll's scrollable grid leaves open. + +The differentiation is not a larger Sculpt catalogue than MD Doll. It is one Sculpt character, launched with ZELEX's full character-skin-quiz treatment, at a price point ($1,999–$2,199) that the silicone-first mid-premium segment has not yet occupied for this family. + +--- + +## Recommended Next Step + +Commission one hero Sculpt body at WHR 0.67 / BWR 1.50, height 165 cm, in silicone, with prototype sculpt review gated on surface geometry criteria (articulated abs, defined deltoids, sculpted glutes). Pair the body with two character skins at launch. Integrate the character into the body quiz as a Sculpt routing destination, making it the first family the quiz can actively close rather than redirect. Target debut price: $2,099. + +This single entry closes the gap in the coverage matrix, activates the `in_development` Sculpt taxonomy status, and gives the quiz a destination it currently lacks — without committing to a MD Doll-style volume build before the demand signal is confirmed. diff --git a/docs/PDR-006-irontech-differentiation.md b/docs/PDR-006-irontech-differentiation.md new file mode 100644 index 0000000..f2fb03c --- /dev/null +++ b/docs/PDR-006-irontech-differentiation.md @@ -0,0 +1,92 @@ +# PDR-006 — The Icon vs Irontech: Differentiation Brief + +**Date:** 2026-06-05 +**Status:** Draft — do not publish +**Scope:** ZELEX Icon family versus Irontech Doll; curation model vs catalog depth model + +--- + +## Situation + +The glamour-model silhouette — long torso, compressed waist, hip-dominant — is the most contested body type in the premium silicone market. Any buyer who searches for this form will encounter Irontech before they find ZELEX. + +Irontech's own numbers confirm the concentration: of their 11 classified body styles, 9 fall into the Icon family and 2 into the Muse, producing a 100% top-two concentration rate — the narrowest catalog spread of any brand in the 30-brand competitive set. They are, structurally, an Icon-specialist in all but name. + +ZELEX's Icon family is its second largest by body count: 4 distinct body codes (ZK159D, ZX163E, ZX165D, ZX172E), each carrying 4 named characters — 16 curated personas across heights 159–172 cm, WHR 0.541–0.630, BWR 1.438–1.547. The family sits at the precise geometric intersection that defines "glamour model" in measurement terms: WHR 0.60–0.65, BWR 1.50–1.60. Irontech reaches the same buyer. The question is what each brand gives that buyer once they arrive. + +--- + +## Irontech's Approach + +**Catalog profile.** 124 total SKUs, 122 full-body, classified as Catalog Depth Leader. Median price across the full catalog: $2,386. The competitor-analysis research (sourced from irontechdoll.com) places their referenced silicone body range at $2,750–$3,379. + +**Taxonomy.** Irontech organizes by informal tags — BBW, Skinny, MILF, Curvy — alongside series and material tiers. These are orientation tags, not measurement-grounded families. There is no published WHR or BWR threshold that defines "Skinny" vs "Curvy"; the buyer must visually estimate. + +**Persona layer.** Irontech does name characters: Eileen, Luna, Misaki are cited as examples in competitor research, each with an occupation narrative attached. This is the closest any competitor comes to ZELEX's planned character model. The weakness is architecture: occupation tags are a surface gesture, not a body-family system. Irontech has no discovery quiz to route a buyer from "I know what I want to feel" to a specific character and body code. No measurement-grounded WHR/BWR ranges underpin the character assignments. The persona and the body exist in parallel tracks — related by listing proximity, not by structural logic. + +**Material and tech.** Silicone-only catalog (100% silicone share in the classified set). Feature stack includes AquaBounce, ROS MAX head tech, custom configurator. US stock availability. These are genuine strengths. + +**Structural weakness.** A catalog of 122 full-body SKUs concentrated in two body-type families, organized by vague aesthetic tags, with character names that float above — rather than anchor into — a measurement system, is ultimately a volume play wearing persona clothing. The buyer who wants a specific waist-to-hip ratio documented, or who wants to purchase a named character with a consistent physical identity across future pieces, is not served. + +--- + +## ZELEX's Approach + +**Family definition.** The Icon is defined by measurement in ZELEX's published family architecture: WHR 0.60–0.65, BWR 1.50–1.60. The brand-voice descriptor: "Glamour-model geometry: dramatic waist suppression, elongated torso, a silhouette built for the lens." Every body assigned to The Icon is there because its measurements fall within that range — not because a product manager called it glamorous. + +**Body roster.** +- ZK159D — 159 cm, D-cup, WHR 0.624, BWR 1.438. Characters: Sora ("The Companion — Built for the lens"), Lian ("The Starlet"), Mira, Zhen. +- ZX163E — 163 cm, E-cup, WHR 0.630, BWR 1.466. Characters: Eva, Fiona, Gia, Helena. +- ZX165D — 165 cm, D-cup, WHR 0.541, BWR 1.547. Four characters. +- ZX172E — 172 cm, E-cup, WHR 0.608, BWR 1.504. Four characters. + +**Character architecture.** Each body carries exactly 4 characters. Characters are not naming exercises — they have distinct titles, taglines, buyer positioning, and energy profiles. Sora is "The Companion," tagged for the photographer and first-time premium buyer. Lian is "The Starlet," theatrical and unapologetic — same 159 cm Icon geometry, entirely different presence. The body is a fixed substrate; the character is what the buyer forms a relationship with. + +**Discovery routing.** The site's discovery quiz and FAMILIES array route the buyer through a structured decision: family first (measurement-grounded), then character (persona-grounded), then configuration. The buyer arrives at Sora or Helena because the quiz resolved their preferences against the taxonomy — not because they scrolled past 122 SKUs and picked a thumbnail. + +**Pricing.** ZELEX's catalog median sits at $1,999 across 156 full-body SKUs, with the premium tier extending to $2,500–$3,499. Icon-family bodies with character overlays are positioned above the commodity midline without requiring the buyer to choose bespoke. + +--- + +## The Curation Argument + +Irontech's model is: broad catalog + loose persona names + material/tech differentiation. It serves buyers who already know what they want visually and are comparison-shopping within a price range. The catalog is the answer. + +ZELEX's model is: measurement-grounded families + named characters with consistent physical specs + guided discovery. It serves buyers who know a feeling or an aesthetic archetype but have not yet resolved it to a body code. The system is the answer — and the catalog becomes legible through it. + +The distinction matters most for the serious collector. A collector who owns multiple pieces wants each acquisition to be deliberate and documentable. If they purchase Sora today, they know her body code (ZK159D), her exact WHR (0.624), her height-cup pairing, and her position in the Icon family — measurements that hold across any future variant of that body. That is a specification. Irontech's Eileen has a name and an occupation. She does not have a measurement contract. + +The measurement-grounded taxonomy also enables comparison across families. A buyer deciding between The Icon and The Muse can read the specs: Icon WHR 0.60–0.65 vs Muse WHR 0.65–0.70; Icon BWR 1.50–1.60 vs Muse BWR 1.30–1.40. That is a decision frame. No competitor exposes this level of structural clarity — including Irontech, who is otherwise the closest analog. + +--- + +## Buyer Scenarios + +**Scenario 1: The editorial photographer.** +A photographer building a studio portfolio wants a specific waist-to-hip ratio for a fashion editorial — something that reads as glamour-model without being anatomically implausible. They need to know the measurement before ordering, because the visual argument of the shoot depends on it. Irontech's tags ("Curvy," "Skinny") require visual estimation. ZELEX's Icon family publishes WHR 0.60–0.65 and BWR 1.50–1.60. The photographer selects ZX165D (WHR 0.541, BWR 1.547) because it hits the tightest waist relative to shoulder — confirmed before purchase, reproduced if needed. The measurement is the specification. + +**Scenario 2: The deliberate first collector.** +A buyer entering the premium silicone market for the first time has done significant research. They want a named character — something with an identity they can grow attached to — but they are overwhelmed by catalogs that list dozens of similar bodies with product codes instead of personalities. The ZELEX discovery quiz resolves their stated aesthetic preference ("dramatic, camera-ready, not too tall") to The Icon family, then surfaces Sora: The Companion, 159 cm, D-cup, WHR 0.624, tagline "Built for the lens." Sora has a title, a tagline, a target-buyer profile, and a documented body. The buyer is not choosing a SKU — they are choosing a character who happens to have documented measurements. + +**Scenario 3: The upgrading collector.** +A collector who already owns a TPE piece from a volume brand (WM, FunWest) is trading up to silicone. They have a specific body type in mind — the elongated torso proportion they could not find in TPE — and a budget of $2,500–$3,500. Irontech is their first search result. Irontech's configurator is functional; the character names are present. But the collector wants to know whether "Luna" is a different body from "Misaki," and what the measurement difference is, before committing. That information is not surfaced. They land on ZELEX, where the family page for The Icon displays WHR and BWR ranges alongside named characters with consistent body specs. They can verify the geometry before purchase. They buy Fiona (ZX163E, WHR 0.630) because the measurement matches what they saw in the TPE piece they want to upgrade from. + +--- + +## Recommended Messaging + +**For the Icon family landing page or a comparison context:** + +1. "The Icon is defined by measurement, not mood. WHR 0.60–0.65, BWR 1.50–1.60 — the geometry of a glamour-model silhouette, published before you purchase." + +2. "Four bodies. Sixteen named characters. Each character is a consistent physical identity: same body code, same measurements, its own title and presence. You are not buying a SKU — you are choosing who she is." + +3. "Every character in The Icon family ships on a documented body spec. If you need to reference the WHR in two years, it is in the product record. Collectors call this provenance." + +4. "Irontech names their models. We define our families. The difference is whether the name points to a measurement or floats above one." + +5. "The discovery quiz routes you here because your stated preference — elongated torso, dramatic waist, editorial proportion — is a measurement, not a mood board. The Icon is where those numbers live." + +--- + +*Sources: docs/competitor-analysis.md, docs/research/competitor-family-coverage-matrix.md, db/independent_catalog_groupings.json, db/characters.json, family.html FAM_META, assets/site.js.* diff --git a/docs/PDR-007-classic-pricing-brief.md b/docs/PDR-007-classic-pricing-brief.md new file mode 100644 index 0000000..ea52cf7 --- /dev/null +++ b/docs/PDR-007-classic-pricing-brief.md @@ -0,0 +1,140 @@ +# PDR-007 — The Classic: Pricing & Entry Brief + +**Date:** 2026-06-05 +**Status:** Draft — do not publish +**Spec reference:** `db/family_taxonomy.json` · `family.html` FAM_META · `docs/research/competitor-family-coverage-matrix.md` + +--- + +## The Gap + +The Classic is the most-wanted body shape in the market — the timeless hourglass, balanced curves, WHR 0.68–0.72, BWR 1.40–1.50 — and it has exactly one credible silicone incumbent at premium price: Gynoid Doll, at $4,401–$7,038. Below that level, Classic coverage is thin and predominantly TPE. + +The $2,000–$3,000 silicone window is structurally empty. Of the 30 brands tracked across 495 classified body rows, 17 brands carry at least one Classic body in their catalogues. Of those 17, only a handful offer silicone at all, and none price a silicone Classic body in the $2,000–$3,000 band with real catalogue depth. The coverage matrix makes this concrete: across all 30 brands, Gynoid is the only pure-silicone operator with Classic as its primary family (5 of 10 bodies, 50% concentration) — and Gynoid's floor is $4,401. + +ZELEX today has zero Classic bodies (confirmed: `db/family_taxonomy.json` `member_count: 0`; `db/characters.json` yields 0 characters with `body.family = "The Classic"`). The family is marked `"status": "in_development"` in both the taxonomy and the `family.html` dev-card. The site copy is written and ready: *"The timeless hourglass — balanced curves, centred mass, a body that reads as effortlessly complete."* The product does not yet exist to put behind it. + +Entering at ~$2,299 would be a market first: the only silicone Classic body priced accessibly to the first-time premium buyer. + +--- + +## Competitor Pricing Landscape + +### Gynoid Doll — the silicone incumbent + +Gynoid is the dominant silicone Classic operator in the dataset: + +- **Classic bodies:** 5 of 10 classified bodies (50.0% of catalogue), the highest Classic concentration of any brand in the matrix +- **Material:** 100% silicone +- **Price range:** $4,401–$7,038 +- **Median price:** $5,391 + +Gynoid's positioning is ultra-premium artisan. Their Classic bodies compete on hyper-realism and Japanese/European collector demand. They do not compete on accessibility. The $4,401 floor is the lowest a silicone Classic currently enters the market from any brand with meaningful catalogue depth. + +### Other silicone brands with Classic presence + +Several silicone or mixed-material brands carry isolated Classic bodies, but none with catalogue commitment or pricing that fills the $2–3k window: + +| Brand | Classic bodies | Silicone share | Median price (all bodies) | Notes | +|---|---:|---:|---:|---| +| Gynoid | 5 | 100% | $5,391 | Ultra-premium; Classic is primary family | +| Jiusheng | 2 | 100% | $2,290 | 2 Classic bodies in an 8-body catalogue; not Classic-specialist | +| Hitdoll | 1 | 100% | $1,437 | Single Classic body; budget tier | +| ILdoll | 1 | 100% | $1,437 | Single Classic body; budget tier | +| Sanhui | 1 | 100% | $2,599 | 1 Classic in a 7-body silicone catalogue | +| Piper Doll | 2 | 90% | $1,290 | 2 Classic bodies; broad catalogue, budget positioning | +| Tayu | 1 | 85.7% | $3,800 | 1 Classic body; catalogue is Muse/Icon-heavy | + +No silicone brand owns the $2,000–$3,000 Classic segment with catalogue depth. The closest is Jiusheng ($2,290 median, 2 Classic bodies) and Sanhui ($2,599 median, 1 Classic body) — neither is Classic-specialist; both are general silicone catalogues where Classic bodies appear by accident of measurement, not strategic intent. + +### The TPE tier — what fills the gap today + +The majority of Classic coverage in the matrix is TPE-material, priced at $1,400–$1,999: + +| Brand | Classic bodies | Silicone share | Median price | Material note | +|---|---:|---:|---:|---| +| Lilydoll | 6 | 0% (unknown) | $1,199 | Largest Classic count after Gynoid; not silicone | +| Jarliet | 2 | 0% TPE | $1,558 | Classic as secondary family | +| Dime Doll | 2 | 0% TPE | $1,699 | Classic as minor family | +| WM Doll | 1 | 0% TPE | $1,694 | Single Classic; volume brand | +| AS Doll | 1 | 0% TPE | $1,993 | Single Classic | + +The TPE Classic tier clusters under $2,000. It serves buyers who want the Classic silhouette but cannot or will not pay for silicone. This is a different buyer from ZELEX's target — but it defines the floor expectation: a Classic body "should" be accessible. ZELEX at ~$2,299 in silicone would be ~$300–$500 above the TPE ceiling and $2,100 below Gynoid's silicone floor. That is a defensible positioning band. + +--- + +## ZELEX's Position + +**Current state:** 0 Classic bodies. The family is in development. The site has the family descriptor live but routes buyers to a dev-card with no product. Any quiz or discovery flow that routes a buyer to The Classic today hits a dead end. + +**Price target:** ZELEX's overall retailer median is approximately $1,999 (ZELEX Dollstudio proxy: median $1,840, range $1,390–$2,450; independent catalogue price_matrix shows 102 ZELEX SKUs in the $1,800–$2,499 band, 40 in $1,200–$1,799, 13 in $2,500–$3,499). A Classic body at the top of ZELEX's natural band — $2,199–$2,499 — is consistent with the +20% premium marker assigned to The Classic in `family_taxonomy.json` applied to a ~$1,999 base. + +**Positioning vs Gynoid:** Gynoid owns the ultra-premium Classic at $4,401+. ZELEX at $2,299 is not competing with Gynoid's collector market — it is opening a new tier. The positioning is: *silicone Classic, accessible premium, first-time buyer*. Gynoid is the artisan reference; ZELEX is the entry to the silhouette category in silicone at all. + +**Positioning vs TPE:** The TPE Classic floor is $1,199–$1,699. ZELEX at $2,299 commands a ~$600–$1,100 premium over TPE alternatives. The material justification (silicone skin feel, durability, oil-free surface) plus the ZELEX brand architecture (named characters, quiz-matched) supports this spread cleanly. The buyer trading from TPE to silicone in The Classic body shape has no current destination — ZELEX fills it. + +**WHR/BWR spec (source of truth: `db/family_taxonomy.json`):** +- WHR range: 0.68–0.72 +- BWR range: 1.40–1.50 +- Silhouette: "Timeless hourglass" +- Premium marker: +20% +- Target buyer: "First-time premium buyer" + +--- + +## Entry Specification + +**Minimum viable Classic body:** + +The Classic is the most geometrically "centred" of the six families — the intersection point between the Muse (hip-dominant, lower BWR) and the Icon (waist-suppressed, higher BWR). It demands precision manufacturing: the silhouette reads as complete only when both axes land inside the window simultaneously. + +| Axis | Target | Range | +|---|---|---| +| WHR | 0.70 | 0.68–0.72 | +| BWR | 1.45 | 1.40–1.50 | +| Cup | D–E | D minimum for proportional credibility at WHR 0.70 | +| Height | 160–168 cm | Consistent with Classic's "balanced, not dramatic" posture | + +**Manufacturing implications:** + +- A WHR of 0.70 at a standard waist of 58 cm implies hips of ~83 cm. At BWR 1.45, bust = 58 × 1.45 ≈ 84 cm. These are close-set proportions: bust and hip within ~1 cm of each other. The mould must be sculpted to balance without either dimension reading as dominant — this is harder to execute credibly than either the Siren (bust-dominant, obvious) or the Muse (hip-dominant, obvious). +- Silicone-specific: The Classic's tactile read depends on the breast fill — a D or E cup at this waist-to-hip geometry should feel substantive without overwhelming. Platinum-cure silicone Shore hardness in the 0–10A range (consistent with ZELEX's SLE line) achieves this. TPE cannot replicate the self-supporting feel at this cup size and WHR. +- The family descriptor phrase "effortlessly complete" is the manufacturing brief in two words: no feature should call attention to itself. + +**Suggested first SKU:** 165 cm, D cup, WHR 0.70, BWR 1.45 — the geometric centre of the family, the safest first proof-of-concept, and a height that photographs neutrally across all campaign contexts. + +--- + +## Revenue Opportunity + +**Caveat:** Figures below are estimated from catalogue data, not from ZELEX transaction data. They are directional, not financial projections. + +**Demand signal — catalogue gap:** +Across 30 brands and 495 classified bodies, 17 brands have at least one Classic body. Classic is the second-most-present family in competitor catalogues after Muse. Buyer demand for the Classic silhouette is broad enough that even volume-generic brands (WM, SE, AS, Jarliet) add at least one Classic body to their catalogues. There is a demonstrated pattern: Classic buyers exist in sufficient numbers that competitors add Classic SKUs even when it is not their strategy. + +**Quiz dead-end cost (estimated):** +The Classic's WHR/BWR range (0.68–0.72 / 1.40–1.50) sits at the geometric centre of the six-family space — the most common natural body shape in the population. In a quiz routing buyers by proportion preference, The Classic is the most likely modal answer for a first-time buyer describing "realistic, balanced proportions." If 20–35% of quiz completions route to Classic (estimated — no quiz data exists yet), and if 100 buyers per month complete the quiz at launch, that is 20–35 monthly Classic-first buyers with no product to purchase. At a $2,299 target price and an optimistic 10% conversion (standard e-commerce baseline for high-consideration product), the per-month opportunity cost is approximately $460–$805/month — or $5,500–$9,700/year — from a single SKU gap. This is a conservative, illustrative estimate based on general e-commerce benchmarks and proportional geometry logic, not observed ZELEX traffic data. + +**Competitive urgency:** +No brand currently owns the $2–3k silicone Classic tier with strategic intent. This window is open today. It will not remain structurally empty indefinitely — any silicone manufacturer with Classic-range measurement profiles already in production can enter this tier with a price and a landing page. ZELEX's advantage is the taxonomy infrastructure (quiz, family architecture, character layer) that a price-cut alone cannot replicate. + +--- + +## Recommended Next Step + +**Immediate action:** Prioritize one Classic hero body at WHR 0.70 ± 0.01, BWR 1.45 ± 0.03 for the next production cycle. Target retail price $2,299. Commission 2 character slots against this body on debut (not all 4 — launch lean, add characters after sales validation). + +**Sequencing:** +1. Production brief to factory: 165 cm, D cup, WHR 0.70 target — confirm measurement feasibility against existing SLE tooling. +2. Two character profiles written and shot against the hero body before site launch. +3. Family landing page (`/family?f=classic`) exits dev-card status; the quiz routing to Classic resolves to a live product page, not a dead end. +4. Pricing set at $2,299 retail — positioned explicitly as "silicone hourglass, premium accessible" in copy, not competing upward with Gynoid and not discounting into TPE territory. +5. PDR-003 landing copy (if applicable) debuts alongside the Classic family activation. + +**Positioning headline (draft):** *"The Classic — the silhouette every buyer recognises, in silicone, under $2,500. The first of its kind."* + +This is the positioning ZELEX can own today. No other brand has claimed it. + +--- + +*Sources: `db/family_taxonomy.json` · `db/characters.json` · `db/independent_catalog_groupings.json` · `docs/research/competitor-family-coverage-matrix.md` · `docs/competitor-analysis.md` · `family.html` FAM_META* diff --git a/docs/PDR-010-competitor-lineup-brief.md b/docs/PDR-010-competitor-lineup-brief.md index bc6a6e7..f6e7365 100644 --- a/docs/PDR-010-competitor-lineup-brief.md +++ b/docs/PDR-010-competitor-lineup-brief.md @@ -39,7 +39,7 @@ Price-ladder breadth analysis highlights which brands compete across multiple co ## Visual Matrix Artifacts -- Line architecture heatmap: docs\research\independent-catalog-line-heatmap.png -- Price-band heatmap: docs\research\independent-catalog-price-heatmap.png -- Full matrix CSV: docs\research\independent-catalog-grouping-matrix.csv -- Narrative matrix report: docs\research\independent-catalog-grouping-heatmap.md +- Line architecture heatmap: docs/research/independent-catalog-line-heatmap.png +- Price-band heatmap: docs/research/independent-catalog-price-heatmap.png +- Full matrix CSV: docs/research/independent-catalog-grouping-matrix.csv +- Narrative matrix report: docs/research/independent-catalog-grouping-heatmap.md diff --git a/docs/agent-source-material/README.md b/docs/agent-source-material/README.md new file mode 100644 index 0000000..305afbc --- /dev/null +++ b/docs/agent-source-material/README.md @@ -0,0 +1,35 @@ +# Agent Source Material Bundle + +This folder groups the current competitor-analysis artifacts into a handoff package for other agents. + +## Primary Entry Points + +- Human overview: `docs/PDR-010-competitor-lineup-brief.md` +- Narrative matrix: `docs/research/independent-catalog-grouping-heatmap.md` +- Machine payload: `db/independent_catalog_groupings.json` +- Bundle manifest: `docs/agent-source-material/manifest.json` + +## Grouping Model + +The manifest groups artifacts into six buckets: + +1. Executive Context +2. Independent Competitor Groupings +3. Competitor Family Coverage +4. Visual Assets +5. Source Catalog Datasets +6. Generation Pipeline + +## Recommended Consumption Order + +1. Read executive context for current findings and ranking logic. +2. Load machine payload and CSV for structured downstream analysis. +3. Use SQLite databases for custom SQL slices. +4. Consult source datasets only when lineage validation is needed. +5. Re-run pipeline scripts to refresh grouped outputs when data updates. + +## Notes + +- WM Doll depth and ZELEX normalization are already reflected in grouped artifacts. +- PNG heatmaps are included for presentation and quick inspection workflows. +- Keep this bundle updated whenever new grouped outputs are generated. diff --git a/docs/agent-source-material/handoff-prompt.md b/docs/agent-source-material/handoff-prompt.md new file mode 100644 index 0000000..6405bd1 --- /dev/null +++ b/docs/agent-source-material/handoff-prompt.md @@ -0,0 +1,24 @@ +# Handoff Prompt For Downstream Agent + +Use this prompt when starting a new agent session for competitor-line analysis: + +--- +You are continuing analysis using a pre-grouped source bundle. + +Start with these files in order: +1. docs/PDR-010-competitor-lineup-brief.md +2. docs/research/independent-catalog-grouping-heatmap.md +3. db/independent_catalog_groupings.json +4. docs/agent-source-material/manifest.json + +Rules: +- Treat manifest groups as the source-of-truth context map. +- Preserve WM Doll and ZELEX normalization assumptions already encoded in grouped outputs. +- If you need custom segmentation, query db/independent_competitor.sqlite. +- If findings depend on updated data, rerun scripts/analyze_independent_groupings.py and refresh derived artifacts. + +Expected output: +- A concise insight memo. +- Any new segmentation tables and rationale. +- A change list of regenerated artifacts. +--- diff --git a/docs/agent-source-material/manifest.json b/docs/agent-source-material/manifest.json new file mode 100644 index 0000000..e36207d --- /dev/null +++ b/docs/agent-source-material/manifest.json @@ -0,0 +1,86 @@ +{ + "package": { + "name": "howiezz-competitor-source-material", + "version": "2026-06-05", + "purpose": "Grouped source material for downstream agents focused on competitor lineup, line architecture, and pricing analysis.", + "entrypoints": { + "human_brief": "docs/PDR-010-competitor-lineup-brief.md", + "matrix_report": "docs/research/independent-catalog-grouping-heatmap.md", + "machine_payload": "db/independent_catalog_groupings.json" + } + }, + "groups": [ + { + "id": "executive-context", + "title": "Executive Context", + "description": "Topline conclusions and ranked competitive findings.", + "files": [ + "docs/PDR-010-competitor-lineup-brief.md", + "docs/competitor-analysis.md", + "docs/PDR-009-six-family-product-taxonomy.md" + ] + }, + { + "id": "independent-groupings", + "title": "Independent Competitor Groupings", + "description": "Line architecture and pricing groupings for independent competitors, including WM and ZELEX normalization outcomes.", + "files": [ + "docs/research/independent-catalog-grouping-heatmap.md", + "docs/research/independent-catalog-grouping-matrix.csv", + "db/independent_catalog_groupings.json", + "db/independent_competitor.sqlite", + "db/independent_competitor_report.json" + ] + }, + { + "id": "family-coverage", + "title": "Competitor Family Coverage", + "description": "Cross-competitor family coverage matrix and structured coverage payloads.", + "files": [ + "docs/research/competitor-family-coverage-matrix.md", + "docs/research/competitor-family-coverage-matrix.csv", + "db/competitor_family_coverage.json", + "db/competitor_family_coverage.sqlite" + ] + }, + { + "id": "visual-assets", + "title": "Visual Assets", + "description": "Heatmap artifacts for rapid visual comparison and slide-ready reuse.", + "files": [ + "docs/research/independent-catalog-line-heatmap.png", + "docs/research/independent-catalog-price-heatmap.png" + ] + }, + { + "id": "source-datasets", + "title": "Source Catalog Datasets", + "description": "Raw or near-raw catalog and crawl outputs used to construct grouped analytics.", + "files": [ + "db/catalog.json", + "db/live_products.json", + "db/live_variants.json", + "db/competitor_web_scrape.json", + "db/family_taxonomy.json" + ] + }, + { + "id": "generation-pipeline", + "title": "Generation Pipeline", + "description": "Scripts to rebuild taxonomy, competitor coverage, and independent grouping outputs.", + "files": [ + "scripts/build_competitor_catalog_taxonomy.py", + "scripts/analyze_independent_groupings.py", + "scripts/build_taxonomy.py", + "scripts/build_db.py" + ] + } + ], + "recommended_agent_flow": [ + "Read docs/PDR-010-competitor-lineup-brief.md for current conclusions.", + "Load db/independent_catalog_groupings.json for machine-consumable grouped metrics.", + "Use docs/research/independent-catalog-grouping-heatmap.md and CSV for table-ready context.", + "Consult db/independent_competitor.sqlite for custom SQL segmentation.", + "Rebuild with scripts/analyze_independent_groupings.py if source data changes." + ] +} diff --git a/docs/pdr/PDR-001-luxury-design-system-v2.md b/docs/pdr/PDR-001-luxury-design-system-v2.md new file mode 100644 index 0000000..3babb14 --- /dev/null +++ b/docs/pdr/PDR-001-luxury-design-system-v2.md @@ -0,0 +1,70 @@ +# PDR-001: Luxury Design System v2 + +**Status:** Implemented +**Branch:** feat/pdr-001-design-system-v2 +**File:** `assets/site.css` + +--- + +## What Changed + +### v2 Token Layer (`:root` extension) +A semantic token layer sits on top of the raw palette — use these in new components. + +| Token group | Tokens | +|---|---| +| Surfaces | `--s0`…`--s3` (deepest → elevated) | +| Borders | `--b-soft`, `--b-accent`, `--b-subtle` | +| Type scale | `--t-xs`…`--t-3xl` | +| Spacing | `--sp1`…`--sp8` (4px → 64px) | +| Radii | `--r-sm` (6) · `--r-md` (10) · `--r-lg` (14) · `--r-pill` (100) | +| Shadows | `--sh-card`, `--sh-card-hover`, `--sh-focus` | +| Status | `--st-live`, `--st-concept`, `--st-pending`, `--st-estimated`, `--st-verified` | + +### Card Glow — Quieted at Rest +Cards previously always emitted a gold ambient glow (`box-shadow: 0 0 26px rgba(212,165,116,.08)`). +Now cards rest with `--sh-card` (a neutral dark shadow); glow activates only on `:hover` via `--sh-card-hover`. + +### Button Hierarchy (4 variants) +| Class | Use | +|---|---| +| `.btn` | Primary — gold border, transparent fill | +| `.btn.solid` | Secondary — filled cream/gold with sheen sweep | +| `.btn.ghost` | Low-emphasis — muted border | +| `.btn.concierge` | Final CTA — Playfair italic, dark gradient, premium shadow | + +### Status Primitives (`.stat`) +Five pill badges with dot indicator, colored by state: + +```html +Live +Concept +Shoot Pending +Estimated Spec +Verified Spec +``` + +### Focus States +`:focus-visible` outlines (gold, 2px, offset 3px) on all interactive elements. +Cards additionally get `--sh-focus` shadow ring. Zero regression on no-JS/reduced-motion paths. + +### Reduced-Motion +Added `transition:border-color .15s` to cards under `prefers-reduced-motion` so state still communicates without motion. + +--- + +## Acceptance Criteria Checklist + +- [x] `assets/site.css` has a documented v2 token section +- [x] Character cards and body cards: quiet at rest, glow on hover only +- [x] Buttons: primary, secondary, ghost, concierge variants +- [x] Status primitives: live, concept, pending, estimated, verified +- [x] Accessible focus states via `:focus-visible` +- [x] Reduced-motion safe (cards, reveal, ken-burns, sheen) +- [x] No-JS behavior unchanged (reveal only hidden under `.js`) + +--- + +## Dependencies Consumed +- `assets/site.css` (modified in place — backward compat maintained) +- `assets/site.js` (unchanged) diff --git a/family.html b/family.html index 4cd619e..da09135 100644 --- a/family.html +++ b/family.html @@ -15,12 +15,12 @@ .hero p{color:var(--muted);max-width:620px;margin:14px auto 0;font-size:15px} .hero .stats{display:flex;gap:38px;justify-content:center;flex-wrap:wrap;margin-top:28px;color:var(--muted)} .hero .stats .n{font-family:'Playfair Display',serif;font-size:30px;color:var(--cream);display:block;line-height:1} - .hero .stats .l{font-size:10px;letter-spacing:2px;text-transform:uppercase;margin-top:6px} + .hero .stats .l{font-size:var(--t-xs);letter-spacing:2px;text-transform:uppercase;margin-top:6px} /* ---- index family card ---- */ .fam-grid{display:grid;gap:20px;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));margin-top:36px} .fam-card{position:relative;overflow:hidden;isolation:isolate; - background:var(--panel);border:1px solid rgba(212,165,116,.28);border-radius:14px;padding:28px 24px; + background:var(--s2);border:1px solid color-mix(in srgb,var(--gold) 28%,transparent);border-radius:var(--r-lg);padding:28px 24px; display:flex;flex-direction:column;gap:10px;transition:.25s;cursor:pointer;min-height:240px; box-shadow:0 0 0 1px rgba(212,165,116,.10),0 0 24px rgba(212,165,116,.06)} .fam-card:hover{transform:translateY(-5px);border-color:var(--gold);box-shadow:0 16px 40px rgba(0,0,0,.55),0 0 30px rgba(212,165,116,.14)} @@ -30,26 +30,29 @@ .fam-card:hover .fc-bg{opacity:.46;transform:scale(1.06)} .fam-card::after{content:'';position:absolute;inset:0;z-index:1;pointer-events:none; background:linear-gradient(180deg,rgba(20,18,16,.48) 0%,rgba(18,18,18,.82) 58%,rgba(18,18,18,.95) 100%)} - .fam-card .fc-name,.fam-card .fc-sig,.fam-card .fc-desc,.fam-card .fc-count{position:relative;z-index:2} + .fam-card .fc-name,.fam-card .fc-sig,.fam-card .fc-desc,.fam-card .fc-count,.fam-card .fc-ratio{position:relative;z-index:2} .fam-card.dev{opacity:.55;cursor:default} .fam-card.dev::after{display:none} .fam-card.dev:hover{transform:none;border-color:rgba(212,165,116,.28);box-shadow:0 0 0 1px rgba(212,165,116,.10)} .fam-card .fc-name{font-family:'Playfair Display',serif;font-size:26px;font-weight:700;line-height:1.1} .fam-card .fc-sig{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--muted);margin-top:2px} - .fam-card .fc-desc{font-size:14px;color:var(--muted);line-height:1.55;flex:1} - .fam-card .fc-count{font-size:12px;letter-spacing:1px;color:var(--muted);border-top:1px solid var(--line);padding-top:10px;margin-top:auto} + .fam-card .fc-desc{font-size:var(--t-base);color:var(--muted);line-height:1.55;flex:1} + .fam-card .fc-count{font-size:var(--t-sm);letter-spacing:1px;color:var(--muted);border-top:1px solid var(--b-subtle);padding-top:10px;margin-top:auto} .fam-card .fc-dev{font-size:11px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);font-style:italic} + .fam-card .fc-ratio{font-size:11px;letter-spacing:1.4px;color:var(--muted);margin-top:4px;opacity:.75} + .dev-narrative{color:var(--muted);font-size:14px;line-height:1.7;max-width:600px;margin:0 auto 28px} + .dev-cta{text-align:center;margin-top:8px} /* ---- family landing header (compact, mirrors series.html proportions) ---- */ .fam-hero{padding:34px 32px 20px;background:radial-gradient(ellipse at top,#1c1c1c,#121212 70%);text-align:center} .fam-hero .eyebrow{margin-bottom:9px;font-size:11px} .fam-hero h1{font-size:clamp(26px,4.4vw,44px);font-weight:700;line-height:1;letter-spacing:.5px} .fam-hero .sig-line{font-size:11px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin-top:7px} - .fam-hero p{color:var(--muted);max-width:620px;margin:9px auto 0;font-size:14px;line-height:1.65} + .fam-hero p{color:var(--muted);max-width:620px;margin:9px auto 0;font-size:var(--t-base);line-height:1.65} .fam-hero .prop-badge{display:inline-flex;gap:18px;flex-wrap:wrap;justify-content:center; - background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:10px 18px;margin-top:16px;font-size:13px} + background:var(--s2);border:1px solid var(--b-subtle);border-radius:var(--r-md);padding:10px 18px;margin-top:16px;font-size:13px} .fam-hero .prop-badge .pb-item .pb-label{font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);display:block} - .fam-hero .prop-badge .pb-item .pb-val{font-family:'Playfair Display',serif;font-size:16px;color:var(--cream)} + .fam-hero .prop-badge .pb-item .pb-val{font-family:'Playfair Display',serif;font-size:var(--t-md);color:var(--cream)} /* ---- section heads ---- */ .sec{margin-top:28px} @@ -60,41 +63,41 @@ .actions{display:flex;gap:14px;flex-wrap:wrap;margin-top:32px;justify-content:center} /* ---- no-content tile ---- */ - .empty-tile{background:var(--panel2);border:1px dashed var(--line);border-radius:12px;padding:36px 20px; + .empty-tile{background:var(--s3);border:1px dashed var(--b-subtle);border-radius:12px;padding:36px 20px; text-align:center;color:var(--muted);font-size:13px;font-style:italic;grid-column:1/-1} /* ---- landing sticky controls bar (view toggle lives here) ---- */ .landing-controls{position:sticky;top:58px;z-index:40;display:flex;align-items:center; justify-content:flex-end;gap:14px;padding:11px 24px; - background:rgba(18,18,18,.94);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)} + background:rgba(18,18,18,.94);backdrop-filter:blur(10px);border-bottom:1px solid var(--b-subtle)} @media(max-width:600px){.landing-controls{justify-content:center}} /* ---- segmented view toggle (pill, matches series.html) ---- */ - .view-toggle{display:inline-flex;border:1px solid var(--line);border-radius:30px;overflow:hidden; - background:var(--panel2);flex-shrink:0} + .view-toggle{display:inline-flex;border:1px solid var(--b-subtle);border-radius:var(--r-pill);overflow:hidden; + background:var(--s3);flex-shrink:0} .view-toggle button{padding:6px 18px;border:none;background:transparent; color:var(--muted);font-size:11px;letter-spacing:1.2px;text-transform:uppercase; font-family:inherit;cursor:pointer;transition:.2s;white-space:nowrap} - .view-toggle button:first-child{border-right:1px solid var(--line)} + .view-toggle button:first-child{border-right:1px solid var(--b-subtle)} .view-toggle button.active{background:var(--cream);color:#111;font-weight:600} .view-toggle button:not(.active):hover{color:var(--text)} /* ---- chars-mode filter bar (below sticky bar) ---- */ .char-filters{max-width:var(--maxw);margin:14px auto 0;padding:0 24px; display:flex;flex-wrap:wrap;gap:10px;align-items:center} - .char-filters .filter-label{font-size:10px;letter-spacing:2px;text-transform:uppercase; + .char-filters .filter-label{font-size:var(--t-xs);letter-spacing:2px;text-transform:uppercase; color:var(--muted);white-space:nowrap} .cpill{ - padding:5px 14px;border:1px solid var(--line);border-radius:30px; + padding:5px 14px;border:1px solid var(--b-subtle);border-radius:var(--r-pill); background:transparent;color:var(--muted); - cursor:pointer;font-size:12px;letter-spacing:.4px; + cursor:pointer;font-size:var(--t-sm);letter-spacing:.4px; font-family:inherit;transition:.2s;white-space:nowrap; } .cpill:hover{color:var(--text);border-color:var(--muted)} .cpill.active{background:var(--cream);color:#111;border-color:var(--cream);font-weight:600} .toggle-wrap{display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none} .toggle-wrap input[type=checkbox]{accent-color:var(--gold);width:15px;height:15px;cursor:pointer} - .toggle-label{font-size:12px;color:var(--muted);letter-spacing:.4px;transition:.2s} + .toggle-label{font-size:var(--t-sm);color:var(--muted);letter-spacing:.4px;transition:.2s} .toggle-wrap:hover .toggle-label{color:var(--text)} /* ---- char-mode count bar ---- */ @@ -233,11 +236,15 @@

Six Architectures

const d = `d${Math.min(fi + 1, 6)}`; if (!hasBodies) { + const ratioLine = meta.whr + ? `
WHR ${ZX.esc(meta.whr)} · ${ZX.esc((meta.sig || '').split(' · ')[0])}
` + : ''; return `
${ZX.esc(fam)}
${ZX.esc((meta.sig || '').split(' · ').slice(1).join(' · '))}
${ZX.esc(meta.desc || '')}
-
In development
+ ${ratioLine} +
In Development
`; } @@ -377,9 +384,30 @@

${ZX.esc(fam)}

renderView(); }); + const DEV_COPY = { + "The Classic": { + narrative: "The Classic is the proportion ZELEX returns to first — a true hourglass where waist, hip, and shoulder resolve into a single, self-evident harmony. WHR 0.68–0.72 places it at the centre of the feminine ideal: nothing exaggerated, nothing withheld. Its architectures are currently in final pre-production review; the first bodies are expected to reach the catalogue within the coming season.", + subject: "ZELEX — Notify me: The Classic" + }, + "The Sculpt": { + narrative: "The Sculpt renders athletic musculature at fine-craft resolution — articulated abdominals, defined deltoids, sculpted posterior — not as caricature but as the body a serious athlete actually inhabits. At WHR 0.65–0.68 it carries the density of the Muse with the surface geometry of the gym. Engineering is underway; first production bodies will enter the catalogue once silicone durability trials are complete.", + subject: "ZELEX — Notify me: The Sculpt" + } + }; + /* ---- BODY ARCHITECTURES section ---- */ function buildBodySec() { if (famBodyCodes.length === 0) { + const copy = DEV_COPY[fam]; + if (copy) { + return `
+
Body Architectures
+

${ZX.esc(copy.narrative)}

+
+ Notify me when available +
+
`; + } return `
Body Architectures
No architectures catalogued yet — check back soon.
diff --git a/index.html b/index.html index 07810c4..4ab1ff0 100644 --- a/index.html +++ b/index.html @@ -15,15 +15,15 @@ .hero .inner{position:relative;z-index:2} .hero h1{font-size:clamp(40px,7vw,76px);font-weight:700;letter-spacing:1px;margin:16px 0} .hero h1 .em{font-style:italic;color:var(--gold)} - .hero p{color:var(--muted);max-width:640px;margin:0 auto;font-size:16px} + .hero p{color:var(--muted);max-width:640px;margin:0 auto;font-size:var(--t-md)} .hero .cta{display:flex;gap:14px;justify-content:center;flex-wrap:wrap;margin-top:32px} .stats{display:flex;gap:40px;justify-content:center;flex-wrap:wrap;margin-top:40px} .stats .n{font-family:'Playfair Display',serif;font-size:32px;color:var(--cream);display:block;line-height:1} - .stats .l{font-size:10px;letter-spacing:2px;text-transform:uppercase;margin-top:6px;color:var(--muted)} + .stats .l{font-size:var(--t-xs);letter-spacing:2px;text-transform:uppercase;margin-top:6px;color:var(--muted)} /* doors */ .doors{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin:54px 0 10px} @media(max-width:880px){.doors{grid-template-columns:1fr}} - .door{position:relative;border:1px solid var(--line);border-radius:16px;overflow:hidden;min-height:300px; + .door{position:relative;border:1px solid var(--b-subtle);border-radius:16px;overflow:hidden;min-height:300px; display:flex;flex-direction:column;justify-content:flex-end;padding:26px;transition:.25s;background:#0d0d0d; box-shadow:0 0 0 1px rgba(212,165,116,.12),0 0 26px rgba(212,165,116,.08)} .door .dimg{position:absolute;inset:0;background-size:cover;background-position:top center;opacity:.42;transition:.4s} @@ -46,11 +46,11 @@ .srow:hover .srow-bg{opacity:.42;transform:scale(1.06)} .srow::after{content:'';position:absolute;inset:0;z-index:1;pointer-events:none;background:linear-gradient(180deg,rgba(20,18,16,.46),rgba(18,18,18,.86) 60%,rgba(18,18,18,.95))} .srow h4,.srow .sc,.srow .sm{position:relative;z-index:2} - .srow h4{font-size:20px} - .srow .sc{color:var(--muted);font-size:12px;margin:6px 0 10px;min-height:48px} + .srow h4{font-size:var(--t-lg)} + .srow .sc{color:var(--muted);font-size:var(--t-sm);margin:6px 0 10px;min-height:48px} .srow .sm{color:var(--blue);font-size:11px;letter-spacing:1px} .featrow{display:flex;align-items:baseline;justify-content:space-between;margin:46px 0 6px} - .featrow a{color:var(--gold);font-size:12px;letter-spacing:1px;text-transform:uppercase} + .featrow a{color:var(--gold);font-size:var(--t-sm);letter-spacing:1px;text-transform:uppercase} diff --git a/quiz.html b/quiz.html index e3bc6d1..0e65f9a 100644 --- a/quiz.html +++ b/quiz.html @@ -37,7 +37,7 @@ padding: 16px 42px; background: var(--gold); border: none; - border-radius: 10px; + border-radius: var(--r-md); color: #111; font-family: 'Montserrat', sans-serif; font-size: 13px; @@ -51,7 +51,7 @@ .intro .note { margin-top: 20px; color: var(--muted); - font-size: 12px; + font-size: var(--t-sm); letter-spacing: 1px; } @@ -77,7 +77,7 @@ .qprogress-track { flex: 1; height: 3px; - background: var(--panel2); + background: var(--s3); border-radius: 3px; overflow: hidden; } @@ -98,8 +98,8 @@ } .qcard { - background: var(--panel); - border: 1px solid var(--line); + background: var(--s2); + border: 1px solid var(--b-subtle); border-radius: 16px; padding: 36px; } @@ -205,6 +205,10 @@ .qcard { padding: 22px 18px; } .qprogress-label { min-width: auto; } } + +.match-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-bottom:28px} +.match-divider{grid-column:1/-1;display:flex;align-items:center;gap:12px;margin:4px 0;color:var(--muted);font-size:11px;letter-spacing:1.8px;text-transform:uppercase} +.match-divider::before,.match-divider::after{content:'';flex:1;height:1px;background:var(--line)} @@ -367,40 +371,58 @@

// pick winner — highest-scoring family with at least one live char const win = ranked.find(f => liveByFam(f).length > 0) || ranked[0]; - // pick up to 3 matches — diversified by body_code + // pick up to 4 matches from the top family — diversified by body_code first const pool = liveByFam(win); const seenBody = new Set(); const matches = []; for (const c of pool) { if (!seenBody.has(c.body_code)) { matches.push(c); seenBody.add(c.body_code); } - if (matches.length === 3) break; + if (matches.length === 4) break; } - // if still < 3, fill from same family without body-code diversity for (const c of pool) { - if (matches.length >= 3) break; + if (matches.length >= 4) break; if (!matches.includes(c)) matches.push(c); } - // final fallback: any live character + + // fill remaining slots from second-ranked family before any cross-family fallback + const runner = ranked.find(f => f !== win && liveByFam(f).length > 0); + const fillFrom = runner ? liveByFam(runner) : []; + let fromSecond = 0; + for (const c of fillFrom) { + if (matches.length >= 4) break; + if (!matches.includes(c)) { matches.push(c); fromSecond++; } + } + + // final fallback: any remaining live character if (matches.length < 3) { const anyLive = m.characters.filter(c => c.status === 'live'); for (const c of anyLive) { - if (matches.length >= 3) break; - if (!matches.includes(c)) matches.push(c); + if (matches.length >= 4) break; + if (!matches.includes(c)) { matches.push(c); fromSecond++; } } } + const splitAt = matches.length - fromSecond; + // update progress bar to 100% document.getElementById('pbar').style.width = '100%'; hide('qscreen'); - showResult(win, matches); + showResult(win, runner, matches, splitAt); } - function showResult(win, matches) { + function showResult(win, runner, matches, splitAt) { const famColor = ZX.famColor(win); const desc = FAM_DESC[win] || ''; const rscreen = document.getElementById('rscreen'); + const cardItems = matches.map((c, i) => { + const divider = (i === splitAt && i > 0 && runner) + ? `
Also from ${ZX.esc(runner)}
` + : ''; + return `${divider}
${ZX.charCard(c)}
`; + }).join(''); + rscreen.innerHTML = `
Your match
@@ -408,12 +430,13 @@

You are drawn to ${ZX.esc(win)}

${ZX.esc(desc)}

-
- ${matches.map((c, i) => `
${ZX.charCard(c)}
`).join('')} +
+ ${cardItems}
`; diff --git a/scripts/analyze_independent_groupings.py b/scripts/analyze_independent_groupings.py index 4f08f9f..a9efbc2 100644 --- a/scripts/analyze_independent_groupings.py +++ b/scripts/analyze_independent_groupings.py @@ -11,9 +11,15 @@ import sqlite3 from pathlib import Path -import matplotlib.pyplot as plt -import pandas as pd -import seaborn as sns +try: + import matplotlib.pyplot as plt + import pandas as pd + import seaborn as sns +except ImportError as exc: + raise SystemExit( + "This script requires matplotlib, pandas, and seaborn. " + "Install with: pip install matplotlib pandas seaborn" + ) from exc ROOT = Path(__file__).parent.parent DB_PATH = ROOT / "db" / "independent_competitor.sqlite" @@ -180,10 +186,10 @@ def write_pdr_brief( lines.append("## Visual Matrix Artifacts") lines.append("") - lines.append(f"- Line architecture heatmap: {OUT_LINE_PNG.relative_to(ROOT)}") - lines.append(f"- Price-band heatmap: {OUT_PRICE_PNG.relative_to(ROOT)}") - lines.append(f"- Full matrix CSV: {OUT_CSV.relative_to(ROOT)}") - lines.append(f"- Narrative matrix report: {OUT_MD.relative_to(ROOT)}") + lines.append(f"- Line architecture heatmap: {OUT_LINE_PNG.relative_to(ROOT).as_posix()}") + lines.append(f"- Price-band heatmap: {OUT_PRICE_PNG.relative_to(ROOT).as_posix()}") + lines.append(f"- Full matrix CSV: {OUT_CSV.relative_to(ROOT).as_posix()}") + lines.append(f"- Narrative matrix report: {OUT_MD.relative_to(ROOT).as_posix()}") OUT_PDR_BRIEF.parent.mkdir(parents=True, exist_ok=True) OUT_PDR_BRIEF.write_text("\n".join(lines) + "\n", encoding="utf-8") @@ -414,8 +420,8 @@ def main() -> None: print( "Wrote " - f"{OUT_CSV.relative_to(ROOT)}, {OUT_MD.relative_to(ROOT)}, {OUT_JSON.relative_to(ROOT)}, " - f"{OUT_LINE_PNG.relative_to(ROOT)}, {OUT_PRICE_PNG.relative_to(ROOT)}, and {OUT_PDR_BRIEF.relative_to(ROOT)}" + f"{OUT_CSV.relative_to(ROOT).as_posix()}, {OUT_MD.relative_to(ROOT).as_posix()}, {OUT_JSON.relative_to(ROOT).as_posix()}, " + f"{OUT_LINE_PNG.relative_to(ROOT).as_posix()}, {OUT_PRICE_PNG.relative_to(ROOT).as_posix()}, and {OUT_PDR_BRIEF.relative_to(ROOT).as_posix()}" ) diff --git a/scripts/build_competitor_catalog_taxonomy.py b/scripts/build_competitor_catalog_taxonomy.py index 0756279..6893e39 100644 --- a/scripts/build_competitor_catalog_taxonomy.py +++ b/scripts/build_competitor_catalog_taxonomy.py @@ -436,14 +436,20 @@ def scrape_supplier(config: SupplierConfig, session: requests.Session, families: if title.startswith("‣"): continue - block = measurement_text_from_card(anchor) + try: + block = measurement_text_from_card(anchor) + except Exception as exc: + print(f"[scrape_supplier] skip {url}: {exc}") + continue if not block: # Avoid deep per-product fetches in supplier mode; rely on inline card specs. continue height = parse_number(r"Height:\s*([\d.]+)\s*cm", block) or parse_number(r"Body height[^\d]*([\d.]+)\s*cm", block) + if height is not None and height < 100: + height = None # guard against partial regex matches (e.g. "16.0" instead of "160") weight = parse_number(r"Weight:\s*([\d.]+)\s*kg", block) or parse_number(r"weights?\s*(?:ca\.)?\s*([\d.]+)\s*kg", block) - bust = parse_number(r"Breasts?:\s*([\d.]+)\s*cm", block) or parse_number(r"Bust/Chest\s*([\d.]+)\s*cm", block) + bust = parse_number(r"Breasts?:\s*([\d.]+)\s*cm", block) or parse_number(r"(?:Bust|Chest):\s*([\d.]+)\s*cm", block) waist = parse_number(r"Waist:\s*([\d.]+)\s*cm", block) hips = parse_number(r"Hips?:\s*([\d.]+)\s*cm", block) underbust = parse_number(r"under bust\)?[:\s]*([\d.]+)\s*cm", block) @@ -526,7 +532,11 @@ def scrape_irontech(session: requests.Session, families: list[dict]) -> list[dic rows = [] seen_codes: set[str] = set() for url in irontech_product_links(session): - html = fetch_text(url, session) + try: + html = fetch_text(url, session) + except Exception as exc: + print(f"[scrape_irontech] skip {url}: {exc}") + continue soup = BeautifulSoup(html, "html.parser") specs = parse_irontech_specs(soup) if not specs: @@ -546,6 +556,8 @@ def scrape_irontech(session: requests.Session, families: list[dict]) -> list[dic cup = cup_match.group(1).upper() if cup_match else None height = parse_number(r"([\d.]+)\s*cm", specs.get("height", "")) + if height is not None and height < 100: + height = None bust = parse_number(r"([\d.]+)\s*cm", specs.get("breastline", "")) underbust = parse_number(r"([\d.]+)\s*cm", specs.get("under breastline", "")) waist = parse_number(r"([\d.]+)\s*cm", specs.get("waistline", "")) @@ -625,7 +637,11 @@ def scrape_tayu(session: requests.Session, families: list[dict]) -> list[dict]: representative_urls.setdefault(key, url) for url in representative_urls.values(): - html = fetch_text(url, session) + try: + html = fetch_text(url, session) + except Exception as exc: + print(f"[scrape_tayu] skip {url}: {exc}") + continue soup = BeautifulSoup(html, "html.parser") text = normalize_space(soup.get_text(" ", strip=True)) @@ -637,6 +653,8 @@ def scrape_tayu(session: requests.Session, families: list[dict]) -> list[dict]: continue height = parse_number(r"Height:\s*([\d.]+)\s*cm", text) + if height is not None and height < 100: + height = None bust = parse_number(r"Bust:\s*([\d.]+)\s*cm", text) waist = parse_number(r"Waist:\s*([\d.]+)\s*cm", text) hips = parse_number(r"Hips:\s*([\d.]+)\s*cm", text) @@ -1311,7 +1329,8 @@ def main() -> None: rows.extend(scrape_siliconwives(sw_config, session, families)) realdoll_inventory = capture_realdoll_inventory(session) - unavailable = list(UNAVAILABLE_COMPETITORS) + scraped_brands = {r["brand"] for r in rows} + unavailable = [u for u in UNAVAILABLE_COMPETITORS if u["brand"] not in scraped_brands] if realdoll_inventory: unavailable.insert( 0, diff --git a/scripts/build_independent_db.py b/scripts/build_independent_db.py index c0be6cf..1b9ec0a 100644 --- a/scripts/build_independent_db.py +++ b/scripts/build_independent_db.py @@ -1,6 +1,6 @@ """ Build independent competitor database from web-scraped catalog data. -Sources: SiliconWives (Shopify API, 1731 products), FantasyWives (60 products) +Sources: SiliconWives (Shopify API), FantasyWives, BestRealDoll Output: db/independent_competitor.sqlite + db/independent_competitor_report.json """ diff --git a/scripts/inspect_scrape.py b/scripts/inspect_scrape.py index d855924..497a9aa 100644 --- a/scripts/inspect_scrape.py +++ b/scripts/inspect_scrape.py @@ -1,6 +1,8 @@ import json, statistics +from pathlib import Path -with open('db/competitor_web_scrape.json', encoding='utf-8') as f: +ROOT = Path(__file__).resolve().parent.parent +with open(ROOT / 'db' / 'competitor_web_scrape.json', encoding='utf-8') as f: d = json.load(f) brands = {} diff --git a/scripts/print_report.py b/scripts/print_report.py index b160e31..ca79101 100644 --- a/scripts/print_report.py +++ b/scripts/print_report.py @@ -1,5 +1,8 @@ import json -with open('db/independent_competitor_report.json', encoding='utf-8') as f: +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +with open(ROOT / 'db' / 'independent_competitor_report.json', encoding='utf-8') as f: r = json.load(f) print('Sources:') diff --git a/scripts/scrape_competitors.js b/scripts/scrape_competitors.js index 6bc7140..1f58013 100644 --- a/scripts/scrape_competitors.js +++ b/scripts/scrape_competitors.js @@ -107,20 +107,6 @@ async function fetchShopifyCatalog(baseUrl) { return all; } -// ── Playwright: product page deep-dive for measurements ────────────────────── -async function deepMeasurements(page, productUrl, baseUrl) { - try { - await page.goto(productUrl.startsWith('http') ? productUrl : baseUrl + productUrl, { - waitUntil: 'domcontentloaded', timeout: 20000, - }); - await page.waitForTimeout(1500); - const bodyHtml = await page.evaluate(() => document.body.innerHTML); - return parseMeasurements(bodyHtml); - } catch { - return {}; - } -} - // ── Playwright: full catalog scrape fallback ────────────────────────────────── async function playwrightCatalog(browser, target) { const ctx = await browser.newContext({ userAgent: UA, locale: 'en-US' }); @@ -144,7 +130,7 @@ async function playwrightCatalog(browser, target) { const selectors = ['.product-card', '.product-item', '.grid__item', 'li.grid-item', '.product', '[class*="product-card"]', 'article.product']; for (const sel of selectors) { const els = document.querySelectorAll(sel); - if (els.length > 3) { + if (els.length > 0) { return Array.from(els).slice(0, 300).map(el => ({ title: el.querySelector('[class*="title"],[class*="name"],h2,h3,h4')?.textContent?.trim() || '', price: el.querySelector('[class*="price"],.price,.amount')?.textContent?.trim() || '', @@ -200,7 +186,9 @@ async function enrichMeasurements(browser, baseUrl, products, sampleSize = 20) { for (const product of toSample) { const handle = product.handle; if (!handle) continue; - const productUrl = handle.startsWith('http') ? handle : `${baseUrl}/products/${handle}`; + const productUrl = handle.startsWith('http') ? handle + : handle.startsWith('/products/') ? `${baseUrl}${handle}` + : `${baseUrl}/products/${handle}`; try { await page.goto(productUrl, { waitUntil: 'domcontentloaded', timeout: 20000 }); await page.waitForTimeout(1000); diff --git a/series.html b/series.html index 8dbd34c..811af23 100644 --- a/series.html +++ b/series.html @@ -12,7 +12,7 @@ .hero{padding:34px 32px 20px;text-align:center;background:radial-gradient(ellipse at top,#1c1c1c,#121212 70%)} .hero .eyebrow{margin-bottom:9px;font-size:11px} .hero h1{font-size:clamp(26px,4.4vw,44px);font-weight:700;letter-spacing:.5px} - .hero p{color:var(--muted);max-width:600px;margin:9px auto 0;font-size:14px} + .hero p{color:var(--muted);max-width:600px;margin:9px auto 0;font-size:var(--t-base)} .stats{display:flex;gap:28px;justify-content:center;flex-wrap:wrap;margin-top:18px;color:var(--muted)} .stats .n{font-family:'Playfair Display',serif;font-size:23px;color:var(--cream);display:block;line-height:1} .stats .l{font-size:9px;letter-spacing:2px;text-transform:uppercase;margin-top:5px} @@ -20,29 +20,29 @@ /* ---- consolidated sticky controls bar (series switcher + view toggle) ---- */ .controls-bar{position:sticky;top:58px;z-index:40;display:flex;align-items:center; justify-content:space-between;gap:14px 18px;flex-wrap:wrap;padding:11px 24px; - background:rgba(18,18,18,.94);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)} + background:rgba(18,18,18,.94);backdrop-filter:blur(10px);border-bottom:1px solid var(--b-subtle)} .series-tabs{display:flex;gap:7px;flex-wrap:wrap;align-items:center} - .tab{padding:6px 16px;border:1px solid var(--line);border-radius:30px;background:transparent; - color:var(--muted);font-size:12px;letter-spacing:.5px;font-family:inherit;transition:.2s;cursor:pointer;display:inline-block} + .tab{padding:6px 16px;border:1px solid var(--b-subtle);border-radius:var(--r-pill);background:transparent; + color:var(--muted);font-size:var(--t-sm);letter-spacing:.5px;font-family:inherit;transition:.2s;cursor:pointer;display:inline-block} .tab:hover{color:var(--text);border-color:var(--text)} .tab.active{background:var(--cream);color:#111;border-color:var(--cream);font-weight:600} /* ---- view-mode toggle (segmented pill) ---- */ - .view-toggle{display:inline-flex;border:1px solid var(--line);border-radius:30px;overflow:hidden; - background:var(--panel2);flex-shrink:0} + .view-toggle{display:inline-flex;border:1px solid var(--b-subtle);border-radius:var(--r-pill);overflow:hidden; + background:var(--s3);flex-shrink:0} .view-toggle button{padding:6px 18px;border:none;background:transparent; color:var(--muted);font-size:11px;letter-spacing:1.2px;text-transform:uppercase; font-family:inherit;cursor:pointer;transition:.2s;white-space:nowrap} - .view-toggle button:first-child{border-right:1px solid var(--line)} + .view-toggle button:first-child{border-right:1px solid var(--b-subtle)} .view-toggle button.active{background:var(--cream);color:#111;font-weight:600} .view-toggle button:not(.active):hover{color:var(--text)} @media(max-width:600px){.controls-bar{justify-content:center}} /* ---- body-architecture mode ---- */ - .body-group{border-top:1px solid var(--line);padding:32px 0 8px} + .body-group{border-top:1px solid var(--b-subtle);padding:32px 0 8px} .body-group-head{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:20px} .body-group-head h3{font-size:22px} - .body-group-head .meta{color:var(--muted);font-size:12px} + .body-group-head .meta{color:var(--muted);font-size:var(--t-sm)} /* ---- characters mode filter bar ---- */ .char-filterbar{ @@ -51,9 +51,9 @@ } .chip-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center} .pill{ - padding:6px 16px;border:1px solid var(--line);border-radius:30px; + padding:6px 16px;border:1px solid var(--b-subtle);border-radius:var(--r-pill); background:transparent;color:var(--muted); - cursor:pointer;font-size:12px;letter-spacing:.5px; + cursor:pointer;font-size:var(--t-sm);letter-spacing:.5px; font-family:inherit;transition:.2s;white-space:nowrap; } .pill:hover{color:var(--text);border-color:var(--muted)} @@ -61,7 +61,7 @@ .filter-sep{width:1px;height:20px;background:var(--line);margin:0 4px;flex-shrink:0;align-self:center} .toggle-wrap{display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none} .toggle-wrap input[type=checkbox]{accent-color:var(--gold);width:15px;height:15px;cursor:pointer} - .toggle-label{font-size:12px;color:var(--muted);letter-spacing:.5px;transition:.2s} + .toggle-label{font-size:var(--t-sm);color:var(--muted);letter-spacing:.5px;transition:.2s} .toggle-wrap:hover .toggle-label{color:var(--text)} /* ---- count bar ---- */