Created
April 24, 2026 01:57
-
-
Save mrexodia/1c8719e19e3717ceff4ad8a3907e2227 to your computer and use it in GitHub Desktop.
Invitation taste walkthrough for Monika and Duncan
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Invitation Taste Walkthrough</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #f6f3ed; | |
| --panel: #fbf9f5; | |
| --panel-strong: #f1ece3; | |
| --line: #d9d0c2; | |
| --line-strong: #c7bcab; | |
| --text: #2f2c27; | |
| --muted: #6f675d; | |
| --accent: #66745f; | |
| --accent-soft: #e7ece3; | |
| --danger: #8b5a50; | |
| --shadow: 0 1px 2px rgba(47, 44, 39, 0.05); | |
| --radius: 8px; | |
| --space-1: 4px; | |
| --space-2: 8px; | |
| --space-3: 12px; | |
| --space-4: 16px; | |
| --space-5: 24px; | |
| --space-6: 32px; | |
| --space-7: 40px; | |
| --body: "Source Sans 3", sans-serif; | |
| --display: "Cormorant Garamond", serif; | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { | |
| margin: 0; | |
| padding: 0; | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--body); | |
| font-size: 16px; | |
| line-height: 1.6; | |
| } | |
| body { | |
| min-height: 100vh; | |
| padding: var(--space-5); | |
| } | |
| .shell { | |
| max-width: 1120px; | |
| margin: 0 auto; | |
| } | |
| .masthead { | |
| display: grid; | |
| gap: var(--space-4); | |
| margin-bottom: var(--space-5); | |
| } | |
| .eyebrow { | |
| font-size: 0.84rem; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| margin: 0; | |
| } | |
| h1 { | |
| margin: 0; | |
| font-family: var(--display); | |
| font-size: clamp(2.2rem, 4vw, 3.5rem); | |
| line-height: 0.98; | |
| font-weight: 600; | |
| letter-spacing: -0.02em; | |
| } | |
| .intro { | |
| max-width: 760px; | |
| color: var(--muted); | |
| margin: 0; | |
| font-size: 1.02rem; | |
| } | |
| .app { | |
| background: var(--panel); | |
| border: 1px solid var(--line); | |
| border-radius: 10px; | |
| box-shadow: var(--shadow); | |
| overflow: hidden; | |
| } | |
| .topbar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: var(--space-4); | |
| padding: var(--space-4) var(--space-5); | |
| border-bottom: 1px solid var(--line); | |
| background: rgba(255,255,255,0.42); | |
| } | |
| .topbar-meta { | |
| display: grid; | |
| gap: var(--space-1); | |
| min-width: 0; | |
| } | |
| .step-label { | |
| font-size: 0.88rem; | |
| color: var(--muted); | |
| } | |
| .step-title { | |
| font-weight: 600; | |
| font-size: 1rem; | |
| } | |
| .progress { | |
| width: min(280px, 42vw); | |
| height: 6px; | |
| background: var(--panel-strong); | |
| border-radius: 999px; | |
| overflow: hidden; | |
| flex: 0 0 auto; | |
| } | |
| .progress > span { | |
| display: block; | |
| height: 100%; | |
| background: var(--accent); | |
| width: 0%; | |
| transition: width 180ms ease; | |
| } | |
| .content { | |
| padding: var(--space-5); | |
| display: grid; | |
| gap: var(--space-5); | |
| } | |
| .question { | |
| display: grid; | |
| gap: var(--space-2); | |
| max-width: 760px; | |
| } | |
| .question h2 { | |
| margin: 0; | |
| font-family: var(--display); | |
| font-size: clamp(1.55rem, 3vw, 2.2rem); | |
| line-height: 1.06; | |
| font-weight: 600; | |
| letter-spacing: -0.01em; | |
| } | |
| .question p { | |
| margin: 0; | |
| color: var(--muted); | |
| } | |
| .grid { | |
| display: grid; | |
| gap: var(--space-4); | |
| } | |
| .grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } | |
| .grid.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } | |
| .grid.cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); } | |
| .option { | |
| border: 1px solid var(--line); | |
| border-radius: var(--radius); | |
| background: #fffdfa; | |
| padding: var(--space-3); | |
| display: grid; | |
| gap: var(--space-3); | |
| align-content: start; | |
| min-width: 0; | |
| } | |
| .option.selected-top { | |
| border-color: var(--accent); | |
| background: #fdfdf9; | |
| } | |
| .option-meta { | |
| display: grid; | |
| gap: 2px; | |
| } | |
| .option-kicker { | |
| font-size: 0.76rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: var(--muted); | |
| } | |
| .option-title { | |
| font-weight: 700; | |
| line-height: 1.2; | |
| } | |
| .option-subtitle { | |
| color: var(--muted); | |
| font-size: 0.92rem; | |
| line-height: 1.45; | |
| } | |
| .preview { | |
| background: #f2ece2; | |
| border: 1px solid #ddd3c6; | |
| border-radius: 6px; | |
| padding: 14px; | |
| } | |
| .paper { | |
| position: relative; | |
| aspect-ratio: 5 / 7; | |
| overflow: hidden; | |
| border: 1px solid var(--paper-line, #d8cfbf); | |
| background: var(--paper-bg, #fbf8f1); | |
| color: var(--paper-text, #2e2a24); | |
| padding: 18px 16px; | |
| border-radius: 4px; | |
| display: grid; | |
| align-content: start; | |
| gap: 6px; | |
| text-align: center; | |
| box-shadow: inset 0 0 0 1px rgba(255,255,255,0.24); | |
| } | |
| .paper::before, | |
| .paper::after { | |
| content: ""; | |
| position: absolute; | |
| inset: auto; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: opacity 150ms ease; | |
| } | |
| .paper .frame { | |
| position: absolute; | |
| inset: 10px; | |
| border: 1px solid transparent; | |
| pointer-events: none; | |
| opacity: 0; | |
| } | |
| .paper .vellum { | |
| position: absolute; | |
| inset: 42% 8% auto 8%; | |
| height: 20%; | |
| background: rgba(247, 244, 238, 0.72); | |
| border: 1px solid rgba(194, 186, 173, 0.45); | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .paper .monogram { | |
| font-family: var(--paper-display, var(--display)); | |
| font-size: 1rem; | |
| letter-spacing: 0.18em; | |
| text-transform: uppercase; | |
| color: var(--paper-accent, #6b7a64); | |
| margin-top: 2px; | |
| } | |
| .paper .tiny-rule { | |
| width: 46px; | |
| height: 1px; | |
| background: var(--paper-accent, #6b7a64); | |
| margin: 4px auto 6px; | |
| opacity: 0.6; | |
| } | |
| .paper .names { | |
| font-family: var(--paper-display, var(--display)); | |
| font-size: 1.42rem; | |
| line-height: 0.98; | |
| letter-spacing: -0.01em; | |
| font-weight: 600; | |
| margin-top: 2px; | |
| } | |
| .paper .amp { | |
| font-style: italic; | |
| font-weight: 400; | |
| margin: 0 3px; | |
| } | |
| .paper .invite-line { | |
| font-size: 0.64rem; | |
| letter-spacing: 0.14em; | |
| text-transform: uppercase; | |
| color: var(--paper-muted, #6f675d); | |
| margin-top: 2px; | |
| } | |
| .paper .detail-block { | |
| margin-top: 12px; | |
| display: grid; | |
| gap: 4px; | |
| justify-items: center; | |
| } | |
| .paper .detail-primary { | |
| font-size: 0.74rem; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| color: var(--paper-muted, #6f675d); | |
| } | |
| .paper .detail-secondary { | |
| font-size: 0.84rem; | |
| max-width: 13ch; | |
| line-height: 1.28; | |
| } | |
| .paper .footer-note { | |
| margin-top: auto; | |
| font-size: 0.64rem; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| color: var(--paper-muted, #6f675d); | |
| padding-top: 12px; | |
| } | |
| .theme-botanical .paper { | |
| --paper-bg: #f7f3ea; | |
| --paper-line: #d7d0c2; | |
| --paper-accent: #6f7e67; | |
| --paper-muted: #736b61; | |
| --paper-display: var(--display); | |
| } | |
| .theme-editorial .paper { | |
| --paper-bg: #f8f5ef; | |
| --paper-line: #d6cdbe; | |
| --paper-accent: #646e60; | |
| --paper-muted: #70685e; | |
| --paper-display: var(--display); | |
| } | |
| .theme-romantic .paper { | |
| --paper-bg: #f9f5ee; | |
| --paper-line: #d8cfbf; | |
| --paper-accent: #748067; | |
| --paper-muted: #786f64; | |
| --paper-display: var(--display); | |
| } | |
| .theme-botanical .paper::before, | |
| .theme-botanical .paper::after, | |
| .deco-accent .paper::before, | |
| .deco-accent .paper::after, | |
| .deco-full .paper::before, | |
| .deco-full .paper::after { | |
| opacity: 1; | |
| width: 64px; | |
| height: 80px; | |
| background-repeat: no-repeat; | |
| background-size: contain; | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='84' height='104' viewBox='0 0 84 104' fill='none'%3E%3Cpath d='M8 95c20-11 33-30 40-57 4-17 12-27 28-31-11 12-15 25-16 40-1 17-8 33-22 48' stroke='%23788970' stroke-width='1.4' stroke-linecap='round'/%3E%3Cpath d='M18 68c14-5 24-15 30-30' stroke='%23788970' stroke-width='1.1' stroke-linecap='round'/%3E%3Cpath d='M15 79c11-3 20-10 28-22' stroke='%23788970' stroke-width='1.1' stroke-linecap='round'/%3E%3C/svg%3E"); | |
| } | |
| .theme-botanical .paper::before, | |
| .deco-accent .paper::before, | |
| .deco-full .paper::before { | |
| top: 6px; | |
| left: 4px; | |
| transform: rotate(-3deg); | |
| } | |
| .theme-botanical .paper::after, | |
| .deco-accent .paper::after, | |
| .deco-full .paper::after { | |
| right: 2px; | |
| bottom: 4px; | |
| transform: rotate(176deg); | |
| } | |
| .theme-editorial .paper { | |
| padding-top: 22px; | |
| } | |
| .theme-editorial .paper .monogram { | |
| letter-spacing: 0.26em; | |
| font-size: 0.92rem; | |
| } | |
| .theme-editorial .paper .names { | |
| font-size: 1.3rem; | |
| line-height: 1.04; | |
| } | |
| .theme-editorial .paper .tiny-rule { | |
| width: 100%; | |
| max-width: 112px; | |
| background: rgba(100, 110, 96, 0.45); | |
| } | |
| .theme-romantic .paper .names { | |
| font-size: 1.36rem; | |
| } | |
| .theme-romantic .paper .tiny-rule { | |
| width: 54px; | |
| } | |
| .palette-sage .paper { | |
| --paper-bg: #f7f4ed; | |
| --paper-accent: #6f7d68; | |
| --paper-line: #d6cfc1; | |
| } | |
| .palette-olive .paper { | |
| --paper-bg: #f6f2e9; | |
| --paper-accent: #667159; | |
| --paper-line: #d4ccbb; | |
| } | |
| .palette-neutral .paper { | |
| --paper-bg: #f9f6ef; | |
| --paper-accent: #8a7c6b; | |
| --paper-line: #d8cfbf; | |
| } | |
| .palette-champagne .paper { | |
| --paper-bg: #faf5ee; | |
| --paper-accent: #887d66; | |
| --paper-line: #ded5c8; | |
| } | |
| .type-balanced .paper { | |
| --paper-display: var(--display); | |
| } | |
| .type-balanced .paper .detail-secondary, | |
| .type-balanced .paper .detail-primary, | |
| .type-balanced .paper .footer-note, | |
| .type-balanced .paper .invite-line { | |
| font-family: var(--body); | |
| } | |
| .type-italic .paper .names { | |
| font-weight: 500; | |
| } | |
| .type-italic .paper .amp { | |
| font-style: italic; | |
| font-size: 0.95em; | |
| } | |
| .type-highcontrast .paper .names { | |
| letter-spacing: -0.02em; | |
| font-weight: 700; | |
| } | |
| .type-highcontrast .paper .detail-secondary, | |
| .type-highcontrast .paper .detail-primary, | |
| .type-highcontrast .paper .footer-note, | |
| .type-highcontrast .paper .invite-line { | |
| font-family: var(--body); | |
| letter-spacing: 0.11em; | |
| } | |
| .type-script .paper .names { | |
| font-weight: 500; | |
| } | |
| .type-script .paper .amp { | |
| font-family: var(--display); | |
| font-style: italic; | |
| font-size: 1em; | |
| } | |
| .deco-none .paper::before, | |
| .deco-none .paper::after { | |
| opacity: 0; | |
| } | |
| .deco-none .paper .frame, | |
| .deco-accent .paper .frame { | |
| opacity: 0; | |
| } | |
| .deco-border .paper::before, | |
| .deco-border .paper::after { | |
| opacity: 0; | |
| } | |
| .deco-border .paper .frame { | |
| opacity: 1; | |
| border-color: rgba(114, 126, 103, 0.44); | |
| } | |
| .deco-full .paper .frame { | |
| opacity: 1; | |
| border-color: rgba(114, 126, 103, 0.32); | |
| } | |
| .finish-cotton .paper { | |
| background-image: linear-gradient(rgba(255,255,255,0.14), rgba(255,255,255,0.14)), linear-gradient(90deg, rgba(0,0,0,0.016) 1px, transparent 1px), linear-gradient(rgba(0,0,0,0.014) 1px, transparent 1px); | |
| background-size: auto, 8px 8px, 8px 8px; | |
| } | |
| .finish-handmade .paper { | |
| background-image: linear-gradient(rgba(255,255,255,0.16), rgba(255,255,255,0.16)), radial-gradient(circle at 20% 20%, rgba(117,112,100,0.05) 0 1px, transparent 1px), radial-gradient(circle at 78% 64%, rgba(117,112,100,0.05) 0 1px, transparent 1px); | |
| background-size: auto, 16px 16px, 18px 18px; | |
| } | |
| .finish-deckled .paper { | |
| border-radius: 7px; | |
| clip-path: polygon(1% 2%, 6% 1%, 13% 2%, 22% 0.5%, 31% 2%, 40% 1%, 49% 2%, 58% 0.5%, 68% 1.5%, 79% 1%, 88% 2%, 98% 1%, 99% 8%, 98.5% 17%, 99% 25%, 98% 35%, 99% 45%, 98.2% 56%, 99% 67%, 98.3% 77%, 99% 88%, 98% 98%, 89% 99%, 79% 98.5%, 69% 99%, 59% 98%, 49% 99%, 39% 98%, 29% 99%, 20% 98%, 10% 99%, 1.5% 98%, 1% 89%, 2% 79%, 1% 69%, 1.8% 59%, 1% 49%, 2% 39%, 1% 28%, 1.8% 18%, 1% 8%); | |
| background-image: linear-gradient(rgba(255,255,255,0.14), rgba(255,255,255,0.14)); | |
| } | |
| .finish-vellum .paper .vellum { | |
| opacity: 1; | |
| } | |
| .finish-letterpress .paper { | |
| text-shadow: 0 1px 0 rgba(255,255,255,0.45); | |
| box-shadow: inset 0 0 0 1px rgba(255,255,255,0.2), inset 0 -1px 1px rgba(0,0,0,0.03); | |
| } | |
| .rating-group { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: var(--space-2); | |
| } | |
| .rating-button { | |
| border: 1px solid var(--line); | |
| background: transparent; | |
| color: var(--muted); | |
| border-radius: 6px; | |
| padding: 7px 10px; | |
| font: inherit; | |
| font-size: 0.88rem; | |
| line-height: 1; | |
| cursor: pointer; | |
| transition: border-color 160ms ease, color 160ms ease, background 160ms ease; | |
| } | |
| .rating-button:hover, | |
| .rating-button:focus-visible { | |
| border-color: var(--line-strong); | |
| color: var(--text); | |
| outline: none; | |
| } | |
| .rating-button.active { | |
| border-color: var(--accent); | |
| background: var(--accent-soft); | |
| color: var(--text); | |
| } | |
| .step-actions { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: var(--space-3); | |
| border-top: 1px solid var(--line); | |
| padding-top: var(--space-4); | |
| margin-top: var(--space-2); | |
| } | |
| .actions-left, | |
| .actions-right { | |
| display: flex; | |
| gap: var(--space-2); | |
| flex-wrap: wrap; | |
| } | |
| .button { | |
| border: 1px solid var(--line-strong); | |
| background: #fffdf9; | |
| color: var(--text); | |
| border-radius: 6px; | |
| padding: 10px 14px; | |
| font: inherit; | |
| font-size: 0.95rem; | |
| cursor: pointer; | |
| transition: background 160ms ease, border-color 160ms ease, color 160ms ease; | |
| } | |
| .button:hover, | |
| .button:focus-visible { | |
| background: #fbf8f1; | |
| border-color: var(--accent); | |
| outline: none; | |
| } | |
| .button.primary { | |
| background: var(--accent); | |
| color: #f8f6f0; | |
| border-color: var(--accent); | |
| } | |
| .button.primary:hover, | |
| .button.primary:focus-visible { | |
| background: #596653; | |
| border-color: #596653; | |
| } | |
| .button[disabled] { | |
| opacity: 0.45; | |
| cursor: not-allowed; | |
| } | |
| .helper { | |
| font-size: 0.9rem; | |
| color: var(--muted); | |
| } | |
| .result { | |
| display: grid; | |
| gap: var(--space-5); | |
| } | |
| .result-header { | |
| display: grid; | |
| gap: var(--space-2); | |
| max-width: 760px; | |
| } | |
| .result-header h2 { | |
| margin: 0; | |
| font-family: var(--display); | |
| font-size: clamp(1.8rem, 3vw, 2.6rem); | |
| line-height: 1.04; | |
| font-weight: 600; | |
| } | |
| .profile-card { | |
| background: #fffdfa; | |
| border: 1px solid var(--line); | |
| border-radius: var(--radius); | |
| padding: var(--space-4); | |
| display: grid; | |
| gap: var(--space-3); | |
| } | |
| .profile-grid { | |
| display: grid; | |
| gap: var(--space-4); | |
| grid-template-columns: 1.15fr 0.85fr; | |
| } | |
| .chip-list { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: var(--space-2); | |
| } | |
| .chip { | |
| border: 1px solid var(--line); | |
| background: var(--panel); | |
| border-radius: 999px; | |
| padding: 6px 10px; | |
| font-size: 0.88rem; | |
| color: var(--text); | |
| } | |
| .list { | |
| margin: 0; | |
| padding-left: 18px; | |
| display: grid; | |
| gap: 6px; | |
| color: var(--text); | |
| } | |
| .muted { | |
| color: var(--muted); | |
| } | |
| .footer-note-global { | |
| color: var(--muted); | |
| font-size: 0.9rem; | |
| margin-top: var(--space-3); | |
| } | |
| @media (max-width: 980px) { | |
| body { padding: var(--space-3); } | |
| .content, .topbar { padding: var(--space-4); } | |
| .grid.cols-3, .grid.cols-4, .grid.cols-5, .profile-grid { grid-template-columns: 1fr; } | |
| .progress { width: 38vw; } | |
| } | |
| @media (max-width: 640px) { | |
| .topbar { | |
| align-items: start; | |
| flex-direction: column; | |
| } | |
| .progress { width: 100%; } | |
| .step-actions { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .actions-left, .actions-right { | |
| width: 100%; | |
| } | |
| .actions-right .button, | |
| .actions-left .button { | |
| flex: 1 1 auto; | |
| } | |
| .rating-group { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| } | |
| .rating-button:last-child { | |
| grid-column: 1 / -1; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="shell"> | |
| <header class="masthead"> | |
| <p class="eyebrow">Invitation walkthrough</p> | |
| <h1>Find the invitation style that actually feels like Monika.</h1> | |
| <p class="intro">A calm, phone-friendly visual walkthrough for narrowing taste. Rate each direction, go a level deeper, and the page will return a deduced style profile with palette, typography, decoration level, and paper finish.</p> | |
| </header> | |
| <main class="app"> | |
| <div class="topbar"> | |
| <div class="topbar-meta"> | |
| <div class="step-label" id="stepLabel">Step 1 of 6</div> | |
| <div class="step-title" id="stepTitle">Broad invitation direction</div> | |
| </div> | |
| <div class="progress" aria-hidden="true"><span id="progressBar"></span></div> | |
| </div> | |
| <div class="content" id="appContent"></div> | |
| </main> | |
| <p class="footer-note-global">Built around cues from the consultation: cozy, warm lights, greenery over flowers, low contrast, and a preference for something elegant but not overly formal or glittery.</p> | |
| </div> | |
| <script> | |
| const STORAGE_KEY = "invitation-taste-walkthrough-v1"; | |
| const RATING_VALUES = { love: 3, like: 2, neutral: 0, dislike: -2, strongno: -3 }; | |
| const RATING_LABELS = { | |
| love: "Love", | |
| like: "Like", | |
| neutral: "Neutral", | |
| dislike: "Dislike", | |
| strongno: "Strong no" | |
| }; | |
| const RATING_ORDER = ["love", "like", "neutral", "dislike", "strongno"]; | |
| const steps = [ | |
| { | |
| id: "broad", | |
| label: "Step 1 of 6", | |
| title: "Broad invitation direction", | |
| question: "Start with the overall feel.", | |
| description: "Rate each option. Try to respond instinctively rather than analytically.", | |
| grid: "cols-3", | |
| options: [ | |
| { | |
| id: "broad_a", | |
| kicker: "A", | |
| title: "Cozy Botanical Minimal", | |
| subtitle: "Soft greenery, warm paper, airy composition, natural elegance.", | |
| classes: "theme-botanical palette-sage type-balanced deco-accent finish-cotton", | |
| tags: { botanical: 3, cozy: 3, natural: 3, warm: 3, minimal: 2, airy: 2, lowContrast: 2, relaxed: 2, refined: 1 }, | |
| traits: ["delicate greenery", "airy spacing", "candlelit warmth", "soft sage accents", "welcoming mood"] | |
| }, | |
| { | |
| id: "broad_b", | |
| kicker: "B", | |
| title: "Modern Editorial Warm", | |
| subtitle: "Typography-first, pared back, polished, contemporary but not cold.", | |
| classes: "theme-editorial palette-neutral type-highcontrast deco-none finish-letterpress", | |
| tags: { editorial: 4, minimal: 3, refined: 3, modern: 3, typography: 3, structured: 2, lowContrast: 2, warm: 1 }, | |
| traits: ["typography-led layout", "restrained ornament", "clean lines", "polished composition", "contemporary elegance"] | |
| }, | |
| { | |
| id: "broad_c", | |
| kicker: "C", | |
| title: "Romantic Garden Classic", | |
| subtitle: "Softer, more graceful, lightly framed, timeless with gentle decoration.", | |
| classes: "theme-romantic palette-olive type-italic deco-border finish-handmade", | |
| tags: { romantic: 4, classic: 4, botanical: 2, decorative: 2, soft: 3, timeless: 3, warm: 2 }, | |
| traits: ["framed composition", "romantic detailing", "garden influence", "graceful symmetry", "softened tradition"] | |
| } | |
| ] | |
| }, | |
| { | |
| id: "branch", | |
| label: "Step 2 of 6", | |
| title: "Refine the preferred direction", | |
| question: "Now narrow the shape of that preference.", | |
| description: "The second gallery adapts to the strongest preference from step one.", | |
| grid: "cols-3", | |
| dynamic: true | |
| }, | |
| { | |
| id: "palette", | |
| label: "Step 3 of 6", | |
| title: "Color palette", | |
| question: "Which palette feels most right for the invitation?", | |
| description: "Same invitation structure, different color families.", | |
| grid: "cols-4", | |
| options: [ | |
| { | |
| id: "palette_sage", | |
| kicker: "Palette 1", | |
| title: "Sage + ivory + ecru", | |
| subtitle: "Soft, airy, botanical and very easy to live with.", | |
| classes: "theme-botanical palette-sage type-balanced deco-accent finish-cotton", | |
| tags: { paletteSage: 3, botanical: 2, warm: 2, airy: 1, lowContrast: 2 }, | |
| traits: ["sage green", "airy neutrals", "soft botanical palette"] | |
| }, | |
| { | |
| id: "palette_olive", | |
| kicker: "Palette 2", | |
| title: "Olive + cream + taupe", | |
| subtitle: "Slightly deeper and more grounded, still warm and natural.", | |
| classes: "theme-botanical palette-olive type-balanced deco-accent finish-cotton", | |
| tags: { paletteOlive: 3, natural: 2, grounded: 2, warm: 2, refined: 1 }, | |
| traits: ["olive green", "earthy taupe", "grounded warmth"] | |
| }, | |
| { | |
| id: "palette_neutral", | |
| kicker: "Palette 3", | |
| title: "Warm neutral monochrome", | |
| subtitle: "Very restrained, elegant, subtle, almost colorless.", | |
| classes: "theme-editorial palette-neutral type-highcontrast deco-none finish-letterpress", | |
| tags: { paletteNeutral: 3, minimal: 2, editorial: 1, understated: 2, refined: 1 }, | |
| traits: ["warm monochrome", "very restrained palette", "quiet elegance"] | |
| }, | |
| { | |
| id: "palette_champagne", | |
| kicker: "Palette 4", | |
| title: "Muted green + soft champagne", | |
| subtitle: "A little more romantic and dressy without going flashy.", | |
| classes: "theme-romantic palette-champagne type-italic deco-border finish-handmade", | |
| tags: { paletteChampagne: 3, romantic: 2, soft: 1, elegant: 1, classic: 1 }, | |
| traits: ["soft champagne", "dressier warmth", "muted romantic contrast"] | |
| } | |
| ] | |
| }, | |
| { | |
| id: "type", | |
| label: "Step 4 of 6", | |
| title: "Typography", | |
| question: "Which typographic voice feels best?", | |
| description: "Only the type system changes here.", | |
| grid: "cols-4", | |
| options: [ | |
| { | |
| id: "type_balanced", | |
| kicker: "Type 1", | |
| title: "Elegant serif + clean sans", | |
| subtitle: "Balanced, legible, refined, widely appealing.", | |
| classes: "theme-botanical palette-sage type-balanced deco-accent finish-cotton", | |
| tags: { typeBalanced: 3, refined: 2, typography: 2, approachable: 1, timeless: 1 }, | |
| traits: ["balanced hierarchy", "clear readability", "refined but easy"] | |
| }, | |
| { | |
| id: "type_italic", | |
| kicker: "Type 2", | |
| title: "Romantic serif + subtle italic", | |
| subtitle: "Softer and more graceful, with a gentle lyrical note.", | |
| classes: "theme-romantic palette-olive type-italic deco-border finish-handmade", | |
| tags: { typeRomantic: 3, romantic: 2, soft: 2, classic: 1, typography: 2 }, | |
| traits: ["subtle italic", "softly romantic type", "graceful cadence"] | |
| }, | |
| { | |
| id: "type_highcontrast", | |
| kicker: "Type 3", | |
| title: "High-contrast editorial serif", | |
| subtitle: "Sharper, cleaner, more fashion-editorial and composed.", | |
| classes: "theme-editorial palette-neutral type-highcontrast deco-none finish-letterpress", | |
| tags: { typeEditorial: 3, editorial: 3, modern: 2, typography: 3, structured: 1 }, | |
| traits: ["editorial serif", "sharper contrast", "confident typography"] | |
| }, | |
| { | |
| id: "type_script", | |
| kicker: "Type 4", | |
| title: "Classic serif + script accent", | |
| subtitle: "The most decorative and ceremonial of the type options.", | |
| classes: "theme-romantic palette-champagne type-script deco-border finish-handmade", | |
| tags: { typeScript: 3, classic: 2, decorative: 2, romantic: 2, formal: 2 }, | |
| traits: ["script accent", "ceremonial feel", "decorative typography"] | |
| } | |
| ] | |
| }, | |
| { | |
| id: "decoration", | |
| label: "Step 5 of 6", | |
| title: "Decoration level", | |
| question: "How much ornament is enough?", | |
| description: "Same core invitation, different levels of visual detail.", | |
| grid: "cols-4", | |
| options: [ | |
| { | |
| id: "deco_none", | |
| kicker: "Decoration 1", | |
| title: "Typography only", | |
| subtitle: "The cleanest, least decorated route.", | |
| classes: "theme-editorial palette-neutral type-highcontrast deco-none finish-letterpress", | |
| tags: { decorationNone: 3, minimal: 4, editorial: 2, understated: 2 }, | |
| traits: ["no illustration", "pure typography", "maximum restraint"] | |
| }, | |
| { | |
| id: "deco_accent", | |
| kicker: "Decoration 2", | |
| title: "Tiny botanical accent", | |
| subtitle: "A quiet touch of greenery without making it floral.", | |
| classes: "theme-botanical palette-sage type-balanced deco-accent finish-cotton", | |
| tags: { decorationAccent: 3, botanical: 2, minimal: 2, refined: 1 }, | |
| traits: ["quiet greenery", "subtle botanical detail", "restrained decoration"] | |
| }, | |
| { | |
| id: "deco_border", | |
| kicker: "Decoration 3", | |
| title: "Soft framing border", | |
| subtitle: "A little more classical, more formal, still tidy.", | |
| classes: "theme-romantic palette-olive type-italic deco-border finish-handmade", | |
| tags: { decorationBorder: 3, classic: 2, romantic: 1, decorative: 1 }, | |
| traits: ["framing border", "tidy ornament", "classical structure"] | |
| }, | |
| { | |
| id: "deco_full", | |
| kicker: "Decoration 4", | |
| title: "Fuller botanical treatment", | |
| subtitle: "The most expressive decorative option in the set.", | |
| classes: "theme-botanical palette-olive type-italic deco-full finish-handmade", | |
| tags: { decorationFull: 3, botanical: 3, decorative: 2, romantic: 2 }, | |
| traits: ["more visible greenery", "expressive border detail", "fuller ornament"] | |
| } | |
| ] | |
| }, | |
| { | |
| id: "finish", | |
| label: "Step 6 of 6", | |
| title: "Paper and finishing", | |
| question: "Which production feel is most worth having?", | |
| description: "This is about tactile impression, not layout.", | |
| grid: "cols-5", | |
| options: [ | |
| { | |
| id: "finish_cotton", | |
| kicker: "Finish 1", | |
| title: "Matte cotton paper", | |
| subtitle: "Quiet, premium, versatile.", | |
| classes: "theme-botanical palette-sage type-balanced deco-accent finish-cotton", | |
| tags: { finishCotton: 3, refined: 2, understated: 2, minimal: 1 }, | |
| traits: ["matte cotton stock", "quiet premium feel", "versatile finish"] | |
| }, | |
| { | |
| id: "finish_handmade", | |
| kicker: "Finish 2", | |
| title: "Light handmade texture", | |
| subtitle: "Organic and tactile, a bit softer and warmer.", | |
| classes: "theme-romantic palette-olive type-balanced deco-accent finish-handmade", | |
| tags: { finishHandmade: 3, natural: 2, tactile: 2, soft: 1 }, | |
| traits: ["organic texture", "handmade feel", "warm tactile paper"] | |
| }, | |
| { | |
| id: "finish_deckled", | |
| kicker: "Finish 3", | |
| title: "Deckled edges", | |
| subtitle: "More noticeable, more romantic, more of a statement.", | |
| classes: "theme-romantic palette-champagne type-italic deco-border finish-deckled", | |
| tags: { finishDeckled: 3, romantic: 2, tactile: 2, decorative: 1 }, | |
| traits: ["deckled edges", "visible craft detail", "romantic finish"] | |
| }, | |
| { | |
| id: "finish_vellum", | |
| kicker: "Finish 4", | |
| title: "Vellum wrap + seal", | |
| subtitle: "The most ceremonial and elaborate finishing option.", | |
| classes: "theme-romantic palette-champagne type-script deco-border finish-vellum", | |
| tags: { finishVellum: 3, romantic: 2, decorative: 2, formal: 2 }, | |
| traits: ["vellum wrap", "wax-seal mood", "ceremonial finishing"] | |
| }, | |
| { | |
| id: "finish_letterpress", | |
| kicker: "Finish 5", | |
| title: "Letterpress / blind emboss", | |
| subtitle: "Subtle luxury through craft rather than decoration.", | |
| classes: "theme-editorial palette-neutral type-highcontrast deco-none finish-letterpress", | |
| tags: { finishLetterpress: 3, refined: 3, timeless: 1, understated: 1 }, | |
| traits: ["letterpress feel", "quiet craftsmanship", "subtle luxury"] | |
| } | |
| ] | |
| } | |
| ]; | |
| const branchOptions = { | |
| botanical: [ | |
| { | |
| id: "branch_botanical_line", | |
| kicker: "A1", | |
| title: "Fine line botanical", | |
| subtitle: "Light illustration and a clean, airy page.", | |
| classes: "theme-botanical palette-sage type-balanced deco-accent finish-cotton", | |
| tags: { botanical: 3, minimal: 2, airy: 2, refined: 2, natural: 2 }, | |
| traits: ["fine line botanicals", "light illustration", "subtle detailing"] | |
| }, | |
| { | |
| id: "branch_botanical_wash", | |
| kicker: "A2", | |
| title: "Soft foliage wash", | |
| subtitle: "More atmosphere, more softness, still restrained.", | |
| classes: "theme-botanical palette-olive type-italic deco-full finish-handmade", | |
| tags: { botanical: 3, cozy: 2, warm: 2, soft: 2, romantic: 1 }, | |
| traits: ["soft foliage wash", "gentle atmosphere", "painterly greenery"] | |
| }, | |
| { | |
| id: "branch_botanical_bare", | |
| kicker: "A3", | |
| title: "Barely-there botanical accent", | |
| subtitle: "Almost all calm space, just a tiny natural cue.", | |
| classes: "theme-botanical palette-sage type-highcontrast deco-accent finish-letterpress", | |
| tags: { minimal: 3, botanical: 2, editorial: 1, refined: 2, lowContrast: 2 }, | |
| traits: ["barely-there accent", "quiet space", "very restrained decoration"] | |
| } | |
| ], | |
| editorial: [ | |
| { | |
| id: "branch_editorial_serif", | |
| kicker: "B1", | |
| title: "Serif-led editorial", | |
| subtitle: "Typography does most of the work, but warmth remains.", | |
| classes: "theme-editorial palette-neutral type-highcontrast deco-none finish-letterpress", | |
| tags: { editorial: 3, refined: 2, typography: 3, timeless: 2, warm: 1 }, | |
| traits: ["serif-led hierarchy", "editorial composition", "type focus"] | |
| }, | |
| { | |
| id: "branch_editorial_grid", | |
| kicker: "B2", | |
| title: "Modern grid minimal", | |
| subtitle: "More structure, sharper margins, very composed.", | |
| classes: "theme-editorial palette-neutral type-highcontrast deco-none finish-cotton", | |
| tags: { editorial: 3, minimal: 3, structured: 3, modern: 3 }, | |
| traits: ["grid alignment", "clean margins", "sharp restraint"] | |
| }, | |
| { | |
| id: "branch_editorial_monogram", | |
| kicker: "B3", | |
| title: "Monogram-led refined typography", | |
| subtitle: "A slightly more ceremonial editorial take.", | |
| classes: "theme-editorial palette-neutral type-balanced deco-none finish-letterpress", | |
| tags: { editorial: 2, refined: 3, typography: 3, romantic: 1, classic: 1 }, | |
| traits: ["monogram detail", "typographic centerpiece", "ceremonial restraint"] | |
| } | |
| ], | |
| romantic: [ | |
| { | |
| id: "branch_romantic_frame", | |
| kicker: "C1", | |
| title: "Framed garden border", | |
| subtitle: "A soft border with classic garden influence.", | |
| classes: "theme-romantic palette-olive type-italic deco-border finish-handmade", | |
| tags: { romantic: 3, classic: 3, botanical: 2, decorative: 2 }, | |
| traits: ["garden border", "classic framing", "romantic softness"] | |
| }, | |
| { | |
| id: "branch_romantic_soft", | |
| kicker: "C2", | |
| title: "Soft traditional elegance", | |
| subtitle: "Traditional bones, softened and simplified.", | |
| classes: "theme-romantic palette-champagne type-script deco-border finish-cotton", | |
| tags: { classic: 3, soft: 3, timeless: 2, warm: 2, decorative: 1 }, | |
| traits: ["traditional composition", "soft femininity", "graceful order"] | |
| }, | |
| { | |
| id: "branch_romantic_modern", | |
| kicker: "C3", | |
| title: "Modernized romantic classic", | |
| subtitle: "Less ornament, more breathing room, still graceful.", | |
| classes: "theme-romantic palette-olive type-balanced deco-border finish-letterpress", | |
| tags: { romantic: 2, classic: 2, minimal: 1, editorial: 1, refined: 2 }, | |
| traits: ["modern classic balance", "lighter ornament", "updated tradition"] | |
| } | |
| ] | |
| }; | |
| const state = loadState() || { | |
| currentStep: 0, | |
| responses: {} | |
| }; | |
| const appContent = document.getElementById("appContent"); | |
| const stepLabel = document.getElementById("stepLabel"); | |
| const stepTitle = document.getElementById("stepTitle"); | |
| const progressBar = document.getElementById("progressBar"); | |
| render(); | |
| function loadState() { | |
| try { | |
| const raw = localStorage.getItem(STORAGE_KEY); | |
| return raw ? JSON.parse(raw) : null; | |
| } catch (error) { | |
| return null; | |
| } | |
| } | |
| function saveState() { | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); | |
| } | |
| function resetState() { | |
| state.currentStep = 0; | |
| state.responses = {}; | |
| saveState(); | |
| render(); | |
| } | |
| function getStep(index) { | |
| const step = steps[index]; | |
| if (!step) return null; | |
| if (step.id !== "branch") return step; | |
| const branch = getWinningBranch(); | |
| return { | |
| ...step, | |
| options: branchOptions[branch], | |
| question: branch === "botanical" | |
| ? "You leaned botanical — now refine the exact kind of botanical." | |
| : branch === "editorial" | |
| ? "You leaned editorial — now refine the exact kind of editorial." | |
| : "You leaned romantic/classic — now refine the exact softness level." | |
| }; | |
| } | |
| function getWinningBranch() { | |
| const step = steps[0]; | |
| const broadScores = step.options.map((option) => { | |
| const ratingKey = state.responses[option.id]; | |
| return { optionId: option.id, score: RATING_VALUES[ratingKey] ?? -999 }; | |
| }).sort((a, b) => b.score - a.score); | |
| const winner = broadScores[0]?.optionId || "broad_a"; | |
| if (winner === "broad_b") return "editorial"; | |
| if (winner === "broad_c") return "romantic"; | |
| return "botanical"; | |
| } | |
| function render() { | |
| saveState(); | |
| const totalInteractiveSteps = steps.length; | |
| const onResult = state.currentStep >= totalInteractiveSteps; | |
| const current = onResult ? null : getStep(state.currentStep); | |
| stepLabel.textContent = onResult ? "Result" : current.label; | |
| stepTitle.textContent = onResult ? "Deduced taste profile" : current.title; | |
| progressBar.style.width = onResult ? "100%" : `${((state.currentStep) / totalInteractiveSteps) * 100}%`; | |
| appContent.innerHTML = onResult ? renderResult() : renderStep(current); | |
| attachEvents(); | |
| } | |
| function renderStep(step) { | |
| const optionsHtml = step.options.map((option) => { | |
| const selectedRating = state.responses[option.id]; | |
| return ` | |
| <article class="option ${isTopSelection(step.id, option.id) ? "selected-top" : ""}"> | |
| <div class="option-meta"> | |
| <div class="option-kicker">${option.kicker}</div> | |
| <div class="option-title">${option.title}</div> | |
| <div class="option-subtitle">${option.subtitle}</div> | |
| </div> | |
| ${renderPreview(option.classes)} | |
| <div class="rating-group" role="group" aria-label="Rate ${option.title}"> | |
| ${RATING_ORDER.map((key) => ` | |
| <button class="rating-button ${selectedRating === key ? "active" : ""}" data-option-id="${option.id}" data-rating="${key}" type="button">${RATING_LABELS[key]}</button> | |
| `).join("")} | |
| </div> | |
| </article> | |
| `; | |
| }).join(""); | |
| return ` | |
| <section class="question"> | |
| <h2>${step.question}</h2> | |
| <p>${step.description}</p> | |
| </section> | |
| <section class="grid ${step.grid}"> | |
| ${optionsHtml} | |
| </section> | |
| <div class="step-actions"> | |
| <div class="actions-left"> | |
| <button class="button" data-action="reset" type="button">Start over</button> | |
| ${state.currentStep > 0 ? '<button class="button" data-action="back" type="button">Back</button>' : ''} | |
| </div> | |
| <div class="actions-right"> | |
| <div class="helper">${isStepComplete(step) ? 'Looks good — move on.' : 'Rate every option to continue.'}</div> | |
| <button class="button primary" data-action="next" type="button" ${isStepComplete(step) ? '' : 'disabled'}>${state.currentStep === steps.length - 1 ? 'See result' : 'Continue'}</button> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function renderPreview(classes) { | |
| return ` | |
| <div class="preview ${classes}"> | |
| <div class="paper"> | |
| <div class="frame"></div> | |
| <div class="vellum"></div> | |
| <div class="monogram">M D</div> | |
| <div class="tiny-rule"></div> | |
| <div class="invite-line">Wedding invitation</div> | |
| <div class="names">Monika <span class="amp">&</span> Duncan</div> | |
| <div class="detail-block"> | |
| <div class="detail-primary">Saturday · October 10, 2026</div> | |
| <div class="detail-secondary">Wrocław, Poland</div> | |
| </div> | |
| <div class="footer-note">Dinner and celebration to follow</div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function isTopSelection(stepId, optionId) { | |
| if (stepId !== "broad") return false; | |
| const scores = steps[0].options.map(option => ({ id: option.id, score: RATING_VALUES[state.responses[option.id]] ?? -999 })); | |
| scores.sort((a, b) => b.score - a.score); | |
| return scores[0]?.id === optionId && scores[0]?.score > -999; | |
| } | |
| function isStepComplete(step) { | |
| return step.options.every((option) => Boolean(state.responses[option.id])); | |
| } | |
| function attachEvents() { | |
| document.querySelectorAll(".rating-button").forEach((button) => { | |
| button.addEventListener("click", () => { | |
| state.responses[button.dataset.optionId] = button.dataset.rating; | |
| render(); | |
| }); | |
| }); | |
| document.querySelectorAll("[data-action='reset']").forEach((button) => { | |
| button.addEventListener("click", resetState); | |
| }); | |
| document.querySelectorAll("[data-action='back']").forEach((button) => { | |
| button.addEventListener("click", () => { | |
| state.currentStep = Math.max(0, state.currentStep - 1); | |
| render(); | |
| }); | |
| }); | |
| document.querySelectorAll("[data-action='next']").forEach((button) => { | |
| button.addEventListener("click", () => { | |
| if (state.currentStep < steps.length) { | |
| const current = getStep(state.currentStep); | |
| if (!isStepComplete(current)) return; | |
| state.currentStep += 1; | |
| render(); | |
| } | |
| }); | |
| }); | |
| document.querySelectorAll("[data-action='copy']").forEach((button) => { | |
| button.addEventListener("click", async () => { | |
| try { | |
| await navigator.clipboard.writeText(buildShareText()); | |
| button.textContent = "Copied"; | |
| setTimeout(() => { button.textContent = "Copy summary"; }, 1400); | |
| } catch (error) { | |
| button.textContent = "Could not copy"; | |
| setTimeout(() => { button.textContent = "Copy summary"; }, 1400); | |
| } | |
| }); | |
| }); | |
| } | |
| function getAllInteractiveOptions() { | |
| return [ | |
| ...steps[0].options, | |
| ...branchOptions[getWinningBranch()], | |
| ...steps[2].options, | |
| ...steps[3].options, | |
| ...steps[4].options, | |
| ...steps[5].options | |
| ]; | |
| } | |
| function getOptionById(id) { | |
| return getAllInteractiveOptions().find(option => option.id === id); | |
| } | |
| function summarizeScores() { | |
| const tagScores = {}; | |
| const positiveTraits = {}; | |
| const negativeTraits = {}; | |
| Object.entries(state.responses).forEach(([optionId, ratingKey]) => { | |
| const option = getOptionById(optionId); | |
| if (!option) return; | |
| const rating = RATING_VALUES[ratingKey] ?? 0; | |
| Object.entries(option.tags || {}).forEach(([tag, weight]) => { | |
| tagScores[tag] = (tagScores[tag] || 0) + (weight * rating); | |
| }); | |
| (option.traits || []).forEach((trait) => { | |
| if (rating > 0) positiveTraits[trait] = (positiveTraits[trait] || 0) + rating; | |
| if (rating < 0) negativeTraits[trait] = (negativeTraits[trait] || 0) + Math.abs(rating); | |
| }); | |
| }); | |
| return { tagScores, positiveTraits, negativeTraits }; | |
| } | |
| function getTopTraitList(bucket, limit = 5) { | |
| return Object.entries(bucket) | |
| .sort((a, b) => b[1] - a[1]) | |
| .slice(0, limit) | |
| .map(([label]) => label); | |
| } | |
| function highestRatedOption(options) { | |
| return options | |
| .map(option => ({ option, score: RATING_VALUES[state.responses[option.id]] ?? -999 })) | |
| .sort((a, b) => b.score - a.score)[0]?.option; | |
| } | |
| function pickCoreProfile(tagScores) { | |
| const botanical = tagScores.botanical || 0; | |
| const editorial = tagScores.editorial || 0; | |
| const romantic = tagScores.romantic || 0; | |
| const classic = tagScores.classic || 0; | |
| const minimal = tagScores.minimal || 0; | |
| const cozy = tagScores.cozy || 0; | |
| const warm = tagScores.warm || 0; | |
| const refined = tagScores.refined || 0; | |
| if (editorial > botanical && editorial > romantic) { | |
| return minimal > 8 | |
| ? "Warm editorial minimal with restrained elegance" | |
| : "Editorially refined with a soft, warm edge"; | |
| } | |
| if ((romantic + classic) > editorial && (romantic + classic) > botanical) { | |
| return minimal > 4 | |
| ? "Romantic classic with restrained detail" | |
| : "Soft romantic classic with garden influence"; | |
| } | |
| if (botanical >= editorial && botanical >= romantic) { | |
| return (cozy + warm) > 10 | |
| ? "Warm botanical minimal with cozy natural elegance" | |
| : "Botanical minimal with airy, understated warmth"; | |
| } | |
| return refined > 8 | |
| ? "Understated, refined, softly natural" | |
| : "Balanced warm minimal with quiet romantic notes"; | |
| } | |
| function pickPaletteRecommendation() { | |
| const winner = highestRatedOption(steps[2].options); | |
| const map = { | |
| palette_sage: "sage, ivory and ecru", | |
| palette_olive: "olive, cream and taupe", | |
| palette_neutral: "warm neutral monochrome", | |
| palette_champagne: "muted green with soft champagne" | |
| }; | |
| return map[winner?.id] || "sage, ivory and ecru"; | |
| } | |
| function pickTypographyRecommendation() { | |
| const winner = highestRatedOption(steps[3].options); | |
| const map = { | |
| type_balanced: "classic serif for headings paired with a clean sans for body copy", | |
| type_italic: "soft serif with a restrained italic note", | |
| type_highcontrast: "high-contrast editorial serif with crisp supporting sans text", | |
| type_script: "classic serif with only a very small script accent" | |
| }; | |
| return map[winner?.id] || "classic serif with clean supporting sans text"; | |
| } | |
| function pickFinishRecommendation() { | |
| const winner = highestRatedOption(steps[5].options); | |
| const map = { | |
| finish_cotton: "matte cotton paper", | |
| finish_handmade: "light handmade-style texture", | |
| finish_deckled: "deckled edges", | |
| finish_vellum: "vellum wrap and seal", | |
| finish_letterpress: "letterpress or blind emboss" | |
| }; | |
| return map[winner?.id] || "matte cotton paper"; | |
| } | |
| function pickDirectionRecommendation() { | |
| const broadWinner = highestRatedOption(steps[0].options)?.id; | |
| const branchWinner = highestRatedOption(branchOptions[getWinningBranch()])?.id; | |
| if (broadWinner === "broad_b") { | |
| if (branchWinner === "branch_editorial_grid") return "Go for a clean editorial invitation with disciplined spacing, minimal ornament and strong typography."; | |
| if (branchWinner === "branch_editorial_monogram") return "Go for an editorial invitation with a small monogram and ceremonial restraint rather than visible decoration."; | |
| return "Go for a serif-led editorial invitation where typography does most of the work."; | |
| } | |
| if (broadWinner === "broad_c") { | |
| if (branchWinner === "branch_romantic_modern") return "Go for a modernized romantic classic: graceful structure, soft border, lighter ornament."; | |
| if (branchWinner === "branch_romantic_soft") return "Go for a soft traditional invitation with simplified romantic detail and warmth."; | |
| return "Go for a lightly framed romantic garden classic with controlled botanical detail."; | |
| } | |
| if (branchWinner === "branch_botanical_wash") return "Go for a cozy botanical invitation with a soft foliage mood and warm, low-contrast color."; | |
| if (branchWinner === "branch_botanical_bare") return "Go for an understated botanical invitation with only the lightest natural cue and plenty of calm space."; | |
| return "Go for a fine-line botanical invitation with airy spacing and gentle greenery details."; | |
| } | |
| function calculateConfidence(tagScores) { | |
| const families = [ | |
| { name: "botanical", score: tagScores.botanical || 0 }, | |
| { name: "editorial", score: tagScores.editorial || 0 }, | |
| { name: "romantic", score: (tagScores.romantic || 0) + (tagScores.classic || 0) } | |
| ].sort((a, b) => b.score - a.score); | |
| const diff = (families[0]?.score || 0) - (families[1]?.score || 0); | |
| const intensity = Object.values(state.responses).reduce((sum, key) => sum + Math.abs(RATING_VALUES[key] || 0), 0); | |
| if (diff >= 8 && intensity >= 24) return "High confidence"; | |
| if (diff >= 4 && intensity >= 16) return "Medium confidence"; | |
| return "Low confidence"; | |
| } | |
| function buildNuance(tagScores) { | |
| const botanical = tagScores.botanical || 0; | |
| const editorial = tagScores.editorial || 0; | |
| const romantic = tagScores.romantic || 0; | |
| const classic = tagScores.classic || 0; | |
| const minimal = tagScores.minimal || 0; | |
| if (botanical > 0 && editorial > 0 && botanical > editorial - 3 && editorial > botanical - 3) { | |
| return "There is a clear blend here: the warmth and greenery cues matter, but they probably need to be expressed in a cleaner, more controlled way rather than with full decorative treatment."; | |
| } | |
| if ((romantic + classic) > 0 && minimal > 4) { | |
| return "You seem to like romantic softness only when it stays tidy and disciplined. That suggests controlled classic detail rather than overtly decorative styling."; | |
| } | |
| if (minimal > 8 && romantic < 2) { | |
| return "You are consistently trimming away decoration. The final invitation should feel polished and thoughtful, not embellished for its own sake."; | |
| } | |
| return "The strongest pattern is consistency around warmth, restraint and a dislike of anything that feels too formal, flashy or overworked."; | |
| } | |
| function buildShareText() { | |
| const summary = summarizeScores(); | |
| const keywords = Object.entries(summary.tagScores) | |
| .sort((a, b) => b[1] - a[1]) | |
| .filter(([, score]) => score > 0) | |
| .slice(0, 8) | |
| .map(([tag]) => humanizeTag(tag)); | |
| return [ | |
| "Invitation taste profile", | |
| "", | |
| `Core profile: ${pickCoreProfile(summary.tagScores)}`, | |
| `Recommended direction: ${pickDirectionRecommendation()}`, | |
| `Palette: ${pickPaletteRecommendation()}`, | |
| `Typography: ${pickTypographyRecommendation()}`, | |
| `Finish: ${pickFinishRecommendation()}`, | |
| `Confidence: ${calculateConfidence(summary.tagScores)}`, | |
| `Keywords: ${keywords.join(", ")}` | |
| ].join("\n"); | |
| } | |
| function humanizeTag(tag) { | |
| return tag | |
| .replace(/([A-Z])/g, " $1") | |
| .replace(/^./, s => s.toUpperCase()) | |
| .trim() | |
| .replace("Low Contrast", "low contrast") | |
| .replace("Palette ", "") | |
| .replace("Type ", "") | |
| .replace("Finish ", "") | |
| .replace("Decoration ", ""); | |
| } | |
| function renderResult() { | |
| const { tagScores, positiveTraits, negativeTraits } = summarizeScores(); | |
| const topKeywords = Object.entries(tagScores) | |
| .sort((a, b) => b[1] - a[1]) | |
| .filter(([, score]) => score > 0) | |
| .slice(0, 10) | |
| .map(([tag]) => humanizeTag(tag).toLowerCase()); | |
| const strongYes = getTopTraitList(positiveTraits, 6); | |
| const strongNo = getTopTraitList(negativeTraits, 6); | |
| const coreProfile = pickCoreProfile(tagScores); | |
| const palette = pickPaletteRecommendation(); | |
| const typography = pickTypographyRecommendation(); | |
| const finish = pickFinishRecommendation(); | |
| const confidence = calculateConfidence(tagScores); | |
| const recommendation = pickDirectionRecommendation(); | |
| const nuance = buildNuance(tagScores); | |
| return ` | |
| <section class="result"> | |
| <div class="result-header"> | |
| <h2>${coreProfile}</h2> | |
| <p class="muted">This is the deduced invitation direction based on the visual ratings above.</p> | |
| </div> | |
| <div class="profile-grid"> | |
| <div class="profile-card"> | |
| <div class="option-kicker">Core style profile</div> | |
| <div class="option-title">${coreProfile}</div> | |
| <p class="muted" style="margin:0;">${recommendation}</p> | |
| <div class="chip-list"> | |
| ${topKeywords.map(keyword => `<span class="chip">${keyword}</span>`).join("")} | |
| </div> | |
| </div> | |
| <div class="profile-card"> | |
| <div class="option-kicker">Confidence</div> | |
| <div class="option-title">${confidence}</div> | |
| <p class="muted" style="margin:0;">${nuance}</p> | |
| </div> | |
| </div> | |
| <div class="profile-grid"> | |
| <div class="profile-card"> | |
| <div class="option-kicker">Strong yes</div> | |
| <ul class="list"> | |
| ${strongYes.map(item => `<li>${item}</li>`).join("")} | |
| </ul> | |
| </div> | |
| <div class="profile-card"> | |
| <div class="option-kicker">Strong no</div> | |
| <ul class="list"> | |
| ${strongNo.map(item => `<li>${item}</li>`).join("")} | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="profile-grid"> | |
| <div class="profile-card"> | |
| <div class="option-kicker">Recommended palette</div> | |
| <div class="option-title">${palette}</div> | |
| <p class="muted" style="margin:0;">Keep the color family low-contrast and let the paper tone do some of the work.</p> | |
| </div> | |
| <div class="profile-card"> | |
| <div class="option-kicker">Recommended typography</div> | |
| <div class="option-title">${typography}</div> | |
| <p class="muted" style="margin:0;">Use type to establish tone first, then add decoration only if it still feels necessary.</p> | |
| </div> | |
| </div> | |
| <div class="profile-grid"> | |
| <div class="profile-card"> | |
| <div class="option-kicker">Recommended finish</div> | |
| <div class="option-title">${finish}</div> | |
| <p class="muted" style="margin:0;">Finish should support the mood quietly rather than turning the invitation into a costume.</p> | |
| </div> | |
| <div class="profile-card"> | |
| <div class="option-kicker">Next design move</div> | |
| <div class="option-title">Build one refined invitation route</div> | |
| <p class="muted" style="margin:0;">Take the winning direction and produce one polished invitation, then test only small refinements around color, type and detail level.</p> | |
| </div> | |
| </div> | |
| <div class="step-actions"> | |
| <div class="actions-left"> | |
| <button class="button" data-action="reset" type="button">Start over</button> | |
| </div> | |
| <div class="actions-right"> | |
| <button class="button" data-action="copy" type="button">Copy summary</button> | |
| </div> | |
| </div> | |
| </section> | |
| `; | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment