Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95826a1f19 | ||
|
|
cd5b21514d | ||
|
|
8c9bf89676 | ||
|
|
b5f5518c1e | ||
|
|
251b3af5ed |
No files matched your search
@@ -1 +0,0 @@
|
||||
ff37d1cd
|
||||
@@ -0,0 +1,45 @@
|
||||
/* CycleWise AU - scroll animations (Step 1).
|
||||
Purely cosmetic: if GSAP fails to load or the visitor prefers reduced
|
||||
motion, this file does nothing and the page looks exactly as before. */
|
||||
(function(){
|
||||
"use strict";
|
||||
if (!window.gsap || !window.ScrollTrigger) return;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
var done = new WeakSet();
|
||||
|
||||
function reveal(panel){
|
||||
if (!panel || done.has(panel)) return;
|
||||
done.add(panel);
|
||||
|
||||
// Top of the tab: hero text and picture fade up one after another.
|
||||
gsap.from(panel.querySelectorAll(".hero > *"), {
|
||||
opacity: 0, y: 24, duration: 0.7, stagger: 0.12, ease: "power2.out",
|
||||
clearProps: "all"
|
||||
});
|
||||
|
||||
// Everything below: fade up when it scrolls into view.
|
||||
panel.querySelectorAll(
|
||||
".ref-heading, .ref-grid, .timeline, .week-panel, .filmstrip, .cal-card, .detail-card"
|
||||
).forEach(function(el){
|
||||
gsap.from(el, {
|
||||
opacity: 0, y: 32, duration: 0.7, ease: "power2.out",
|
||||
clearProps: "all",
|
||||
scrollTrigger: { trigger: el, start: "top 88%", once: true }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function activePanel(){ return document.querySelector(".mode-panel.active"); }
|
||||
|
||||
reveal(activePanel());
|
||||
|
||||
document.querySelectorAll(".mode-btn").forEach(function(btn){
|
||||
btn.addEventListener("click", function(){
|
||||
// Wait one tick so the page's own code switches the tab first.
|
||||
setTimeout(function(){ reveal(activePanel()); ScrollTrigger.refresh(); }, 0);
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,539 @@
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
/* ---------------- date helpers ---------------- */
|
||||
function toDate(y,m,d){ return new Date(y,m,d); }
|
||||
function fromInput(str){
|
||||
if(!str) return null;
|
||||
var p = str.split("-");
|
||||
return new Date(+p[0], +p[1]-1, +p[2]);
|
||||
}
|
||||
function toInputVal(d){
|
||||
return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0");
|
||||
}
|
||||
function addDays(d,n){ var r=new Date(d); r.setDate(r.getDate()+n); return r; }
|
||||
function daysBetween(a,b){ // b - a, in whole days, ignoring time
|
||||
var A = new Date(a.getFullYear(),a.getMonth(),a.getDate());
|
||||
var B = new Date(b.getFullYear(),b.getMonth(),b.getDate());
|
||||
return Math.round((B-A)/86400000);
|
||||
}
|
||||
function sameDay(a,b){ return a.getFullYear()===b.getFullYear() && a.getMonth()===b.getMonth() && a.getDate()===b.getDate(); }
|
||||
function mod(n,m){ return ((n % m) + m) % m; }
|
||||
var MONTH_NAMES = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
||||
var today = new Date(); today.setHours(0,0,0,0);
|
||||
|
||||
function loadLS(key, fallback){
|
||||
try{ var v = localStorage.getItem(key); return v===null ? fallback : v; }catch(e){ return fallback; }
|
||||
}
|
||||
function saveLS(key, val){ try{ localStorage.setItem(key,val); }catch(e){} }
|
||||
|
||||
/* ---------------- mode switching ---------------- */
|
||||
var modeBtns = document.querySelectorAll(".mode-btn");
|
||||
var panels = { period: document.getElementById("mode-period"), ovulation: document.getElementById("mode-ovulation"), pregnancy: document.getElementById("mode-pregnancy") };
|
||||
modeBtns.forEach(function(btn){
|
||||
btn.addEventListener("click", function(){
|
||||
modeBtns.forEach(function(b){ b.classList.remove("active"); b.setAttribute("aria-selected","false"); });
|
||||
btn.classList.add("active"); btn.setAttribute("aria-selected","true");
|
||||
Object.keys(panels).forEach(function(k){ panels[k].classList.remove("active"); });
|
||||
panels[btn.dataset.mode].classList.add("active");
|
||||
saveLS("ca-mode", btn.dataset.mode);
|
||||
});
|
||||
});
|
||||
var savedMode = loadLS("ca-mode", null);
|
||||
if(savedMode && panels[savedMode]){
|
||||
modeBtns.forEach(function(b){ b.classList.toggle("active", b.dataset.mode===savedMode); b.setAttribute("aria-selected", b.dataset.mode===savedMode ? "true":"false"); });
|
||||
Object.keys(panels).forEach(function(k){ panels[k].classList.toggle("active", k===savedMode); });
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
PHASE + OVULATION REFERENCE DATA
|
||||
================================================================ */
|
||||
var PHASES = {
|
||||
menstrual: {
|
||||
title:"Menstrual", color:"var(--period)", soft:"var(--period-soft)",
|
||||
window:"Cycle day 1 through the end of bleeding",
|
||||
desc:"The uterine lining sheds because no pregnancy occurred last cycle. Estrogen and progesterone are both at their lowest point of the month.",
|
||||
hormone:"Estrogen & progesterone: low", energy:"Energy: lowest",
|
||||
symptoms:"Cramping, fatigue, lower back ache, mood dips",
|
||||
tip:"Iron-rich food, gentle movement, and heat on the lower abdomen ease cramping better than rest alone."
|
||||
},
|
||||
follicular: {
|
||||
title:"Follicular", color:"var(--ovulation)", soft:"var(--ovulation-soft)",
|
||||
window:"Cycle day 1 through ovulation (overlaps menstrual)",
|
||||
desc:"The pituitary releases FSH, prompting several ovarian follicles to mature. Estrogen climbs steadily as one dominant follicle takes the lead.",
|
||||
hormone:"Estrogen: rising", energy:"Energy: climbing",
|
||||
symptoms:"Clearer skin, sharper focus, rising motivation",
|
||||
tip:"This is the physiologically best window for starting new training blocks or demanding projects."
|
||||
},
|
||||
ovulatory: {
|
||||
title:"Ovulatory", color:"var(--ovulation-strong)", soft:"var(--ovulation-soft)",
|
||||
window:"Roughly one day, mid-cycle",
|
||||
desc:"A surge in luteinizing hormone (LH) triggers the dominant follicle to release its egg. This is the single most fertile day of the cycle.",
|
||||
hormone:"LH: sharp surge", energy:"Energy & libido: peak",
|
||||
symptoms:"Egg-white cervical mucus, one-sided pelvic twinge (mittelschmerz)",
|
||||
tip:"Ovulation predictor kits detect the LH surge roughly 24–36 hours before the egg is released."
|
||||
},
|
||||
luteal: {
|
||||
title:"Luteal", color:"var(--pregnancy)", soft:"var(--pregnancy-soft)",
|
||||
window:"Ovulation through the day before your next period",
|
||||
desc:"The ruptured follicle becomes the corpus luteum and secretes progesterone to prepare the uterine lining. If no implantation occurs, hormone levels fall and the cycle restarts.",
|
||||
hormone:"Progesterone: rises then falls", energy:"Energy: gradually declining",
|
||||
symptoms:"Bloating, breast tenderness, irritability, food cravings (PMS)",
|
||||
tip:"Basal body temperature stays elevated ~0.3–0.5°C through this phase — a useful way to confirm ovulation already happened."
|
||||
}
|
||||
};
|
||||
|
||||
// offset relative to ovulation day (0 = ovulation)
|
||||
var OVULATION_DAYS = {
|
||||
"-5": {fert:"Low", fertClass:"fert-low", bbt:"Baseline (pre-shift)", mucus:"Dry to sticky", note:"Sperm can survive up to 5 days, so conception is technically possible starting here."},
|
||||
"-4": {fert:"Low–moderate", fertClass:"fert-low", bbt:"Baseline", mucus:"Sticky, cloudy", note:"Fertility begins climbing as estrogen rises toward its pre-ovulation peak."},
|
||||
"-3": {fert:"Moderate", fertClass:"fert-mid", bbt:"Baseline", mucus:"Creamy, increasing in volume", note:"Cervical mucus is becoming more hospitable to sperm."},
|
||||
"-2": {fert:"High", fertClass:"fert-high", bbt:"Baseline", mucus:"Thin, stretchy, egg-white-like", note:"One of the two most fertile days of the entire cycle."},
|
||||
"-1": {fert:"Peak", fertClass:"fert-peak", bbt:"Baseline (often the day of the LH surge)", mucus:"Clear, slippery, maximum stretch", note:"Statistically the single highest-probability day for conception."},
|
||||
"0": {fert:"Peak", fertClass:"fert-peak", bbt:"Still baseline — the rise hasn't happened yet", mucus:"Egg-white, abundant", note:"The egg is released and is viable for roughly 12–24 hours. Some feel a one-sided twinge (mittelschmerz)."},
|
||||
"1": {fert:"Dropping fast", fertClass:"fert-mid", bbt:"Beginning to rise ~0.3–0.5°C", mucus:"Becoming tacky again", note:"The egg's fertile window is closing; BBT starts its post-ovulation climb."},
|
||||
"2": {fert:"Very low", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry or minimal", note:"Progesterone from the corpus luteum keeps temperature elevated through the luteal phase."},
|
||||
"3": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"If fertilization happened, the embryo is traveling toward the uterus."},
|
||||
"4": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"Too late in the cycle for a new ovulation; luteal phase symptoms may start appearing."},
|
||||
"5": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"Implantation, if it happens this cycle, typically occurs around days 6–10 after ovulation."}
|
||||
};
|
||||
var FERT_COLOR = {"fert-low":"var(--ink-soft)","fert-mid":"var(--fertile)","fert-high":"var(--fertile-strong)","fert-peak":"var(--ovulation-strong)"};
|
||||
|
||||
function phaseForDayInCycle(dic, periodLen, ovulationDic){
|
||||
if(dic < periodLen) return "menstrual";
|
||||
if(dic < ovulationDic) return "follicular";
|
||||
if(dic === ovulationDic) return "ovulatory";
|
||||
return "luteal";
|
||||
}
|
||||
function isFertile(dic, ovulationDic){
|
||||
return dic >= ovulationDic - 5 && dic <= ovulationDic;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
PREGNANCY WEEK DATA (1–40)
|
||||
================================================================ */
|
||||
var WEEKS = [
|
||||
{c:"a poppy seed (not yet fertilized)", m:"n/a", dev:"Pregnancy is dated from the first day of this period, before conception happens — your body is shedding last cycle's lining and preparing a fresh egg.", body:"This week looks and feels like an ordinary period.", tip:"If you're trying to conceive, this is a good week to start a prenatal vitamin with folic acid."},
|
||||
{c:"an egg cell", m:"~0.1 mm", dev:"Rising estrogen prompts an egg to mature; ovulation typically occurs near the end of this week, roughly two weeks before a missed period.", body:"Cervical mucus becomes clear and stretchy around ovulation — a natural fertility sign.", tip:"This is the fertile window if you're trying to conceive — see the Ovulation tab for day-by-day detail."},
|
||||
{c:"a grain of sand", m:"~0.1–0.2 mm", dev:"If sperm meets egg this week, fertilization occurs in the fallopian tube and the resulting cell begins dividing as it travels toward the uterus.", body:"No symptoms are detectable yet — even a sensitive test won't turn positive for another week or two.", tip:"Implantation, if it happens, usually occurs 6–10 days after ovulation."},
|
||||
{c:"a poppy seed", m:"~0.1 mm", dev:"The fertilized egg implants in the uterine lining and begins producing hCG, the hormone home pregnancy tests detect.", body:"Most people notice no symptoms yet; a sensitive test may already turn positive near the end of this week.", tip:"A missed period is usually the first sign — testing a few days after that gives the most reliable result."},
|
||||
{c:"a sesame seed", m:"~2 mm", dev:"The neural tube — the earliest structure of the brain and spinal cord — begins to form, along with the first blood vessels.", body:"A missed period is usually the first clear sign; fatigue and tender breasts may begin.", tip:"Avoid alcohol and confirm any medications with a clinician — this window is critical for early development."},
|
||||
{c:"a lentil", m:"~4 mm", dev:"A primitive heart tube starts beating, and the beginnings of the face, arms, and legs appear as small buds.", body:"Morning sickness and smell sensitivity often begin here, though timing varies widely.", tip:"Small, frequent, bland meals tend to sit better than three large ones if nausea has started."},
|
||||
{c:"a blueberry", m:"~1.3 cm", dev:"The brain is dividing into distinct sections, and the arm and leg buds are elongating into recognizable limbs.", body:"Bloating and mild cramping are common as the uterus makes room to grow.", tip:"This is when most people schedule their first prenatal appointment — dating scans are most accurate now."},
|
||||
{c:"a raspberry", m:"~1.6 cm", dev:"Fingers and toes are forming, still webbed; the heart now has four distinct chambers.", body:"Fatigue often peaks around this point as progesterone rises sharply.", tip:"Rest when you can — first-trimester exhaustion is real and temporary, not a sign anything is wrong."},
|
||||
{c:"a grape", m:"~2.3 cm · 4 g", dev:"The embryo is officially called a fetus starting around now; essential organs are all present in early form and the tail has disappeared.", body:"Nausea, food aversions, and a heightened sense of smell are often at their strongest.", tip:"Ginger tea, vitamin B6, and staying hydrated are commonly recommended first-line options for nausea."},
|
||||
{c:"a kumquat", m:"~3 cm · 7 g", dev:"Vital organs — kidneys, liver, brain — are functioning in a basic form, and tiny fingernail beds are forming.", body:"A visible baby bump is still unlikely, though clothes may feel tighter.", tip:"Ask your provider about first-trimester screening options available in this window."},
|
||||
{c:"a fig", m:"~4.1 cm · 10 g", dev:"The fetus can now bend its arms and legs, and tooth buds are forming beneath the gums.", body:"Round ligament twinges may start as the uterus stretches.", tip:"Gentle stretching and changing positions slowly can ease early ligament discomfort."},
|
||||
{c:"a lime", m:"~5.4 cm · 14 g", dev:"Reflexes are developing — fingers can curl, and toes can wiggle, though it can't be felt yet.", body:"The end of the first trimester is near, and miscarriage risk drops meaningfully from here.", tip:"Many people choose to share their pregnancy news around now, though the choice is entirely personal."},
|
||||
{c:"a plum", m:"~6.7 cm · 23 g", dev:"Vocal cords are forming, and unique fingerprints are being etched onto tiny fingertips.", body:"Some early nausea may start easing as the placenta takes over hormone production.", tip:"The nuchal translucency scan, if chosen, is typically done between now and week 13."},
|
||||
{c:"a passion fruit", m:"~10 cm · 45 g", dev:"Baby can open and close fists and may even suck a thumb; genitals are differentiating, though not yet visible on ultrasound.", body:"For many, first-trimester nausea and fatigue begin to lift.", tip:"Second-trimester energy often supports resuming or starting light-to-moderate exercise — check in with your provider first."},
|
||||
{c:"a peach", m:"~11.6 cm · 58 g", dev:"Vocal cords are complete and the intestines, which developed in the umbilical cord, move into the abdomen.", body:"Welcome to the second trimester — often called the most comfortable stretch of pregnancy.", tip:"An anatomy scan is usually scheduled in the coming weeks; ask your clinic when it's booked."},
|
||||
{c:"a lemon", m:"~13 cm · 70 g", dev:"Fine, downy hair called lanugo begins covering the body, and the kidneys are producing urine into the amniotic fluid.", body:"A small bump may finally become visible; appetite often improves noticeably.", tip:"Iron needs rise sharply now — leafy greens, legumes, and lean meat help meet increased demand."},
|
||||
{c:"an apple", m:"~14.2 cm · 95 g", dev:"The skeleton continues hardening and baby can sense light through closed eyelids.", body:"Skin changes like a faint linea nigra or darkened areolas may appear as pigment increases.", tip:"Daily SPF helps limit pregnancy-related skin pigmentation (melasma) from sun exposure."},
|
||||
{c:"an avocado", m:"~16 cm · 135 g", dev:"The placenta is now fully formed and functioning as baby's life-support system.", body:"First flutters of movement ('quickening') may be felt any time from now through week 20.", tip:"Movement timing varies a lot — first pregnancies and anterior placentas often mean feeling it later."},
|
||||
{c:"a pomegranate", m:"~17.7 cm · 160 g", dev:"The skeleton is converting from cartilage to true bone, and fat stores are just beginning to accumulate.", body:"The uterus is now roughly at the level of the belly button.", tip:"A pregnancy support pillow can meaningfully improve sleep as side-sleeping becomes more comfortable."},
|
||||
{c:"a bell pepper", m:"~19 cm · 190 g", dev:"Ears have reached their final position and baby may begin startling at loud external sounds.", body:"Quickening is commonly felt by now — often described as bubbles or light taps.", tip:"Try narrating your day out loud — hearing is developing and familiar voices become recognizable over time."},
|
||||
{c:"a mango", m:"~20.3 cm · 240 g", dev:"A waxy coating called vernix caseosa begins protecting the skin, and taste buds are forming.", body:"Backaches can start as posture shifts to balance the growing bump.", tip:"A supportive, flatter shoe and a pillow between the knees at night both help low back strain."},
|
||||
{c:"a banana", m:"~25.6 cm · 300 g", dev:"The detailed anatomy ultrasound is often done around now if it hasn't happened already, and sex can typically be identified on it.", body:"Movements are stronger and more frequent; the uterus is now level with the belly button.", tip:"If you want to know the sex and haven't had the anatomy scan yet, ask your clinic to schedule it soon — it's usually done weeks 18–22."},
|
||||
{c:"a carrot", m:"~27 cm · 360 g", dev:"Baby is swallowing small amounts of amniotic fluid, giving the digestive system practice.", body:"Stretch marks may start appearing on the belly, hips, or chest as skin stretches.", tip:"Moisturizing won't prevent stretch marks, but it can ease the itchiness that comes with stretching skin."},
|
||||
{c:"a papaya", m:"~28.9 cm · 430 g", dev:"Eyebrows and eyelids are fully formed, and the sense of touch is developing rapidly.", body:"Braxton Hicks — brief, painless practice contractions — may be felt occasionally from here on.", tip:"Braxton Hicks usually ease with a change of position or a glass of water; true labor contractions don't."},
|
||||
{c:"an ear of corn", m:"~32.2 cm · 600 g", dev:"The lungs begin producing surfactant, a substance essential for breathing air after birth — a key viability milestone.", body:"The linea nigra (a dark vertical line on the belly) may become more visible.", tip:"This week is often cited as the threshold of viability with intensive neonatal care, should birth happen very early."},
|
||||
{c:"a cauliflower head", m:"~33.7 cm · 660 g", dev:"Baby can now respond to your voice, and hand grip strength is developing.", body:"The glucose screening test for gestational diabetes is typically scheduled around now.", tip:"If asked to fast beforehand, follow your clinic's instructions closely for an accurate glucose result."},
|
||||
{c:"a head of lettuce", m:"~35.6 cm · 760 g", dev:"Eyes begin to open after being fused shut since early development, and hair may be visibly growing.", body:"Round ligament pain and rib discomfort can increase as space gets tighter.", tip:"A maternity support band can ease rib and ligament pressure during long periods of standing or walking."},
|
||||
{c:"a bunch of scallions", m:"~36.6 cm · 875 g", dev:"Brain tissue is developing rapidly, and baby is developing regular hiccups you may be able to feel.", body:"Third trimester begins soon — appointments typically become more frequent from here.", tip:"Hiccup-like twitches are completely normal and are the diaphragm practicing its rhythm."},
|
||||
{c:"an eggplant", m:"~37.6 cm · 1.1 kg", dev:"Eyes can blink now, and baby may cycle through active REM sleep.", body:"Welcome to the third trimester — the home stretch begins.", tip:"This is a common time to start planning a hospital bag and reviewing birth preferences."},
|
||||
{c:"a butternut squash", m:"~38.6 cm · 1.2 kg", dev:"Bones are fully formed but still soft and flexible, ready to move through the birth canal.", body:"Frequent urination often returns as the head engages lower in the pelvis.", tip:"Kick counts become a useful daily check-in — note baby's typical pattern of movement."},
|
||||
{c:"a large cabbage", m:"~39.9 cm · 1.3 kg", dev:"The brain is developing at its fastest rate yet, adding folds and surface area.", body:"Sleep can become harder to find as the bump limits comfortable positions.", tip:"A full-body pregnancy pillow supports the belly, back, and knees simultaneously."},
|
||||
{c:"a coconut", m:"~41.1 cm · 1.5 kg", dev:"All five senses are now functioning, and baby is gaining roughly half a pound a week.", body:"Colostrum, the first form of breast milk, may begin to leak slightly.", tip:"A few nursing pads in your bag can help if colostrum leaking becomes noticeable."},
|
||||
{c:"a jicama", m:"~42.4 cm · 1.7 kg", dev:"Baby is practicing breathing movements against a closed airway, strengthening the lungs.", body:"Swelling and mild carpal tunnel tingling in the hands can appear from fluid retention.", tip:"Report sudden or severe swelling to your provider promptly — it can occasionally signal preeclampsia."},
|
||||
{c:"a pineapple", m:"~43.7 cm · 1.9 kg", dev:"Bones are hardening overall, though the skull stays deliberately soft and flexible for delivery.", body:"Pelvic pressure increases as baby continues descending.", tip:"Prenatal massage or a warm bath can ease the deep pelvic ache many feel this week."},
|
||||
{c:"a cantaloupe", m:"~45 cm · 2.1 kg", dev:"The central nervous system is maturing quickly, and baby's grip is now quite strong.", body:"Braxton Hicks contractions often become more noticeable and frequent.", tip:"Time any contractions that feel different — regular, intensifying, closer together warrants a call to your provider."},
|
||||
{c:"a honeydew melon", m:"~46.2 cm · 2.4 kg", dev:"The kidneys are fully developed, and the liver is processing waste products on its own.", body:"You may feel baby 'drop' lower as it settles into position for birth.", tip:"Lightening (the baby dropping) can ease heartburn but often increases bathroom trips."},
|
||||
{c:"a head of romaine", m:"~47.4 cm · 2.6 kg", dev:"Most physical development is complete; the focus now is steady weight gain.", body:"You've just reached 'early term' — babies born from here on tend to do very well.", tip:"Pack your hospital bag if you haven't yet — early-term babies can arrive with little warning."},
|
||||
{c:"a leek", m:"~49.8 cm · 3.1 kg", dev:"Baby continues laying down fat for temperature regulation after birth, and the brain is still rapidly developing.", body:"Nesting instinct and a burst of energy are common right before labor.", tip:"Balance nesting energy with rest — labor's timing is unpredictable."},
|
||||
{c:"a mini watermelon", m:"~50.7 cm · 3.3 kg", dev:"Full term. The lungs are completing final maturation in preparation for that first breath.", body:"Increased pelvic pressure and more frequent, intense Braxton Hicks are common.", tip:"Watch for your labor signs: regular contractions, water breaking, or a bloody show."},
|
||||
{c:"a small pumpkin", m:"~51.2 cm · 3.5 kg", dev:"Baby is fully developed and simply continuing to grow, ready to meet you.", body:"If labor hasn't started, your provider will discuss monitoring or induction options.", tip:"Most babies arrive within two weeks of their due date in either direction — both are entirely normal."}
|
||||
];
|
||||
|
||||
/* ================================================================
|
||||
SVG helpers
|
||||
================================================================ */
|
||||
function polarToXY(cx,cy,r,angleDeg){
|
||||
var rad = (angleDeg-90) * Math.PI/180;
|
||||
return [cx + r*Math.cos(rad), cy + r*Math.sin(rad)];
|
||||
}
|
||||
function arcPath(cx,cy,r,startDeg,endDeg){
|
||||
if(endDeg <= startDeg) endDeg = startDeg + 0.01;
|
||||
var s = polarToXY(cx,cy,r,startDeg), e = polarToXY(cx,cy,r,endDeg);
|
||||
var large = (endDeg-startDeg) > 180 ? 1 : 0;
|
||||
return "M "+s[0].toFixed(2)+" "+s[1].toFixed(2)+" A "+r+" "+r+" 0 "+large+" 1 "+e[0].toFixed(2)+" "+e[1].toFixed(2);
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
SHARED STATE: period start / cycle length / period length
|
||||
================================================================ */
|
||||
var exampleStart = addDays(today, -10);
|
||||
var startInput = document.getElementById("p-start");
|
||||
var cycleLenInput = document.getElementById("p-cyclelen");
|
||||
var periodLenInput = document.getElementById("p-periodlen");
|
||||
|
||||
var savedStart = loadLS("ca-start", null);
|
||||
var savedCycleLen = loadLS("ca-cyclelen", null);
|
||||
var savedPeriodLen = loadLS("ca-periodlen", null);
|
||||
var usingExample = !savedStart;
|
||||
|
||||
startInput.value = savedStart || toInputVal(exampleStart);
|
||||
cycleLenInput.value = savedCycleLen || 28;
|
||||
periodLenInput.value = savedPeriodLen || 5;
|
||||
|
||||
function getCycleState(){
|
||||
return {
|
||||
start: fromInput(startInput.value) || exampleStart,
|
||||
cycleLen: Math.max(21, Math.min(35, parseInt(cycleLenInput.value,10) || 28)),
|
||||
periodLen: Math.max(2, Math.min(10, parseInt(periodLenInput.value,10) || 5))
|
||||
};
|
||||
}
|
||||
|
||||
var pCalMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
var oCalMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
var pSelectedDate = new Date(today);
|
||||
var oSelectedDate = new Date(today);
|
||||
|
||||
[startInput,cycleLenInput,periodLenInput].forEach(function(inp){
|
||||
inp.addEventListener("input", function(){
|
||||
saveLS("ca-start", startInput.value);
|
||||
saveLS("ca-cyclelen", cycleLenInput.value);
|
||||
saveLS("ca-periodlen", periodLenInput.value);
|
||||
usingExample = false;
|
||||
document.getElementById("p-example-note").style.display = "none";
|
||||
document.getElementById("pr-example-note").style.display = "none";
|
||||
renderAll();
|
||||
});
|
||||
});
|
||||
if(!usingExample) document.getElementById("p-example-note").style.display = "none";
|
||||
|
||||
/* ================================================================
|
||||
CALENDAR RENDERING (shared builder)
|
||||
================================================================ */
|
||||
function buildCalendar(gridEl, weekdayHeaderIgnored, monthDate, dayClassifier, onSelect, selectedDate){
|
||||
gridEl.innerHTML = "";
|
||||
var year = monthDate.getFullYear(), month = monthDate.getMonth();
|
||||
var firstDow = new Date(year,month,1).getDay();
|
||||
var daysInMonth = new Date(year,month+1,0).getDate();
|
||||
var totalCells = Math.ceil((firstDow+daysInMonth)/7)*7;
|
||||
for(var i=0;i<totalCells;i++){
|
||||
var dayNum = i - firstDow + 1;
|
||||
var cell = document.createElement("button");
|
||||
cell.className = "day-cell";
|
||||
if(dayNum < 1 || dayNum > daysInMonth){
|
||||
cell.classList.add("pad");
|
||||
gridEl.appendChild(cell);
|
||||
continue;
|
||||
}
|
||||
var d = new Date(year,month,dayNum);
|
||||
var info = dayClassifier(d);
|
||||
cell.innerHTML = "<span>"+dayNum+"</span>";
|
||||
if(info.period) cell.classList.add("is-period");
|
||||
if(info.fertile) cell.classList.add("is-fertile");
|
||||
if(info.ovulation) cell.classList.add("is-ovulation");
|
||||
if(sameDay(d,today)) cell.classList.add("today");
|
||||
if(selectedDate && sameDay(d,selectedDate)) cell.classList.add("selected");
|
||||
cell.setAttribute("aria-label", d.toDateString());
|
||||
cell.addEventListener("click", function(dd){ return function(){ onSelect(dd); }; }(d));
|
||||
gridEl.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
function classifyForCycle(d, state){
|
||||
var dic = mod(daysBetween(state.start, d), state.cycleLen);
|
||||
var ovDic = mod(state.cycleLen - 14, state.cycleLen);
|
||||
return {
|
||||
period: dic < state.periodLen,
|
||||
fertile: isFertile(dic, ovDic),
|
||||
ovulation: dic === ovDic,
|
||||
dic: dic, ovDic: ovDic
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------------- PERIOD tab render ---------------- */
|
||||
function renderPeriodCalendar(){
|
||||
var state = getCycleState();
|
||||
document.getElementById("p-cal-title").textContent = MONTH_NAMES[pCalMonth.getMonth()] + " " + pCalMonth.getFullYear();
|
||||
buildCalendar(document.getElementById("p-daygrid"), null, pCalMonth,
|
||||
function(d){ return classifyForCycle(d, state); },
|
||||
function(d){ pSelectedDate = d; renderPeriodDetail(); renderPeriodCalendar(); },
|
||||
pSelectedDate
|
||||
);
|
||||
}
|
||||
|
||||
function renderPeriodDetail(){
|
||||
var state = getCycleState();
|
||||
var info = classifyForCycle(pSelectedDate, state);
|
||||
var phaseKey = phaseForDayInCycle(info.dic, state.periodLen, info.ovDic);
|
||||
var phase = PHASES[phaseKey];
|
||||
var el = document.getElementById("p-detail");
|
||||
el.innerHTML =
|
||||
'<span class="d-date">'+pSelectedDate.toDateString()+'</span>'+
|
||||
'<span class="pill" style="background:'+phase.soft+';color:'+phase.color+'">'+phase.title+' phase'+(info.fertile?' · fertile window':'')+'</span>'+
|
||||
'<div class="d-phase">Cycle day '+(info.dic+1)+'</div>'+
|
||||
'<p class="d-desc">'+phase.desc+'</p>'+
|
||||
'<div class="d-grid">'+
|
||||
'<div class="d-mini"><div class="k">Hormones</div><div class="v">'+phase.hormone+'</div></div>'+
|
||||
'<div class="d-mini"><div class="k">Typical symptoms</div><div class="v" style="font-weight:500;font-size:0.82rem;">'+phase.symptoms+'</div></div>'+
|
||||
'</div>'+
|
||||
'<p class="d-desc"><strong style="color:var(--ink)">Care note — </strong>'+phase.tip+'</p>';
|
||||
}
|
||||
|
||||
function renderPeriodStats(){
|
||||
var state = getCycleState();
|
||||
var dic = mod(daysBetween(state.start, today), state.cycleLen);
|
||||
var ovDic = mod(state.cycleLen - 14, state.cycleLen);
|
||||
var daysToNextPeriod = mod(state.cycleLen - dic, state.cycleLen);
|
||||
if(daysToNextPeriod === 0 && dic !== 0) daysToNextPeriod = state.cycleLen;
|
||||
var daysToOvulation = mod(ovDic - dic, state.cycleLen);
|
||||
var phaseKey = phaseForDayInCycle(dic, state.periodLen, ovDic);
|
||||
document.getElementById("period-stats").innerHTML =
|
||||
statTile(dic+1, "Cycle day today") +
|
||||
statTile(PHASES[phaseKey].title, "Current phase") +
|
||||
statTile(dic===0 ? "Today" : (daysToNextPeriod+"d"), "Until next period") +
|
||||
statTile(dic===ovDic ? "Today" : (daysToOvulation+"d"), "Until ovulation");
|
||||
|
||||
// hero ring
|
||||
var cx=100, cy=100, r=88;
|
||||
var perStart=0, perEnd = state.periodLen/state.cycleLen*360;
|
||||
var follEnd = ovDic/state.cycleLen*360;
|
||||
var fertStart = Math.max(0,(ovDic-5))/state.cycleLen*360;
|
||||
var lutStart = (ovDic+1)/state.cycleLen*360;
|
||||
document.getElementById("phase-arc-menstrual").setAttribute("d", arcPath(cx,cy,r,perStart,perEnd));
|
||||
document.getElementById("phase-arc-follicular").setAttribute("d", arcPath(cx,cy,r,perEnd,fertStart));
|
||||
document.getElementById("phase-arc-fertile").setAttribute("d", arcPath(cx,cy,r,fertStart,follEnd+ (360/state.cycleLen)));
|
||||
document.getElementById("phase-arc-luteal").setAttribute("d", arcPath(cx,cy,r,lutStart,360));
|
||||
var todayAngle = dic/state.cycleLen*360;
|
||||
var tp = polarToXY(cx,cy,r,todayAngle);
|
||||
document.getElementById("today-marker").setAttribute("cx", tp[0]);
|
||||
document.getElementById("today-marker").setAttribute("cy", tp[1]);
|
||||
document.getElementById("cycle-day-big").textContent = dic+1;
|
||||
// hand the same numbers to the optional 3D ring (ring3d.js)
|
||||
window.cycleRingData = {perEnd:perEnd, fertStart:fertStart, fertEnd:follEnd+(360/state.cycleLen),
|
||||
lutStart:lutStart, todayAngle:todayAngle};
|
||||
window.dispatchEvent(new CustomEvent("cycle:ring", {detail: window.cycleRingData}));
|
||||
}
|
||||
|
||||
function statTile(num,label){
|
||||
return '<div class="stat-tile"><div class="num">'+num+'</div><div class="lbl">'+label+'</div></div>';
|
||||
}
|
||||
|
||||
document.getElementById("p-prev").addEventListener("click", function(){ pCalMonth.setMonth(pCalMonth.getMonth()-1); renderPeriodCalendar(); });
|
||||
document.getElementById("p-next").addEventListener("click", function(){ pCalMonth.setMonth(pCalMonth.getMonth()+1); renderPeriodCalendar(); });
|
||||
|
||||
function renderPhaseRefGrid(){
|
||||
var order = ["menstrual","follicular","ovulatory","luteal"];
|
||||
document.getElementById("phase-ref-grid").innerHTML = order.map(function(key,i){
|
||||
var p = PHASES[key];
|
||||
return '<div class="ref-card"><span class="r-num">0'+(i+1)+'</span><h4 style="color:'+p.color+'">'+p.title+'</h4>'+
|
||||
'<p>'+p.desc+'</p>'+
|
||||
'<div class="tag-row"><span class="tagchip">'+p.window+'</span></div></div>';
|
||||
}).join("");
|
||||
}
|
||||
|
||||
/* ---------------- OVULATION tab render ---------------- */
|
||||
var ovSelectedOffset = 0;
|
||||
function renderOvTimeline(){
|
||||
var wrap = document.getElementById("ov-timeline");
|
||||
wrap.innerHTML = "";
|
||||
for(var off=-5; off<=5; off++){
|
||||
var d = OVULATION_DAYS[String(off)];
|
||||
var btn = document.createElement("button");
|
||||
btn.className = "tl-btn" + (off===ovSelectedOffset ? " active":"");
|
||||
btn.innerHTML = '<div class="tl-day">'+(off===0?"OVUL.":(off>0?"+":"")+off)+'</div>'+
|
||||
'<div class="tl-fert" style="background:'+FERT_COLOR[d.fertClass]+'"></div>';
|
||||
btn.addEventListener("click", function(o){ return function(){ ovSelectedOffset=o; renderOvTimeline(); renderOvDetailFromOffset(); }; }(off));
|
||||
wrap.appendChild(btn);
|
||||
}
|
||||
}
|
||||
function renderOvDetailFromOffset(){
|
||||
var d = OVULATION_DAYS[String(ovSelectedOffset)];
|
||||
var el = document.getElementById("o-detail");
|
||||
var label = ovSelectedOffset===0 ? "Ovulation day" : (ovSelectedOffset>0 ? ovSelectedOffset+" day"+(ovSelectedOffset>1?"s":"")+" after ovulation" : Math.abs(ovSelectedOffset)+" day"+(ovSelectedOffset<-1?"s":"")+" before ovulation");
|
||||
el.innerHTML =
|
||||
'<span class="d-date">'+label+'</span>'+
|
||||
'<span class="pill" style="background:var(--ovulation-soft);color:'+FERT_COLOR[d.fertClass]+'">Fertility: '+d.fert+'</span>'+
|
||||
'<div class="d-phase">'+(ovSelectedOffset===0?"Egg release":(ovSelectedOffset<0?"Building fertility":"Fertility fading"))+'</div>'+
|
||||
'<div class="d-grid">'+
|
||||
'<div class="d-mini"><div class="k">Basal body temp</div><div class="v" style="font-size:0.82rem;">'+d.bbt+'</div></div>'+
|
||||
'<div class="d-mini"><div class="k">Cervical mucus</div><div class="v" style="font-size:0.82rem;">'+d.mucus+'</div></div>'+
|
||||
'</div>'+
|
||||
'<p class="d-desc">'+d.note+'</p>';
|
||||
}
|
||||
|
||||
function renderOvulationCalendar(){
|
||||
var state = getCycleState();
|
||||
document.getElementById("o-cal-title").textContent = MONTH_NAMES[oCalMonth.getMonth()] + " " + oCalMonth.getFullYear();
|
||||
buildCalendar(document.getElementById("o-daygrid"), null, oCalMonth,
|
||||
function(d){ return classifyForCycle(d, state); },
|
||||
function(d){
|
||||
oSelectedDate = d;
|
||||
var info = classifyForCycle(d, state);
|
||||
var offset = info.dic - info.ovDic;
|
||||
if(offset < -5) offset = -5; if(offset > 5) offset = 5;
|
||||
ovSelectedOffset = offset;
|
||||
renderOvTimeline(); renderOvDetailFromOffset(); renderOvulationCalendar();
|
||||
},
|
||||
oSelectedDate
|
||||
);
|
||||
}
|
||||
|
||||
function renderOvulationStats(){
|
||||
var state = getCycleState();
|
||||
var dic = mod(daysBetween(state.start, today), state.cycleLen);
|
||||
var ovDic = mod(state.cycleLen - 14, state.cycleLen);
|
||||
var daysToOvulation = mod(ovDic - dic, state.cycleLen);
|
||||
var fertileStartDic = mod(ovDic-5, state.cycleLen);
|
||||
var inFertile = isFertile(dic, ovDic);
|
||||
var daysToFertile = inFertile ? 0 : mod(fertileStartDic - dic, state.cycleLen);
|
||||
document.getElementById("ovulation-stats").innerHTML =
|
||||
statTile(inFertile ? "Yes" : "No", "In fertile window") +
|
||||
statTile(dic===ovDic ? "Today" : daysToOvulation+"d", "Until ovulation") +
|
||||
statTile(inFertile ? "Now" : daysToFertile+"d", "Fertile window") +
|
||||
statTile("6 days", "Window length");
|
||||
}
|
||||
|
||||
document.getElementById("o-prev").addEventListener("click", function(){ oCalMonth.setMonth(oCalMonth.getMonth()-1); renderOvulationCalendar(); });
|
||||
document.getElementById("o-next").addEventListener("click", function(){ oCalMonth.setMonth(oCalMonth.getMonth()+1); renderOvulationCalendar(); });
|
||||
|
||||
/* ================================================================
|
||||
PREGNANCY tab
|
||||
================================================================ */
|
||||
var lmpInput = document.getElementById("pr-lmp");
|
||||
var exampleLMP = addDays(today, -20*7 - 3);
|
||||
var savedLMP = loadLS("ca-lmp", null);
|
||||
lmpInput.value = savedLMP || toInputVal(exampleLMP);
|
||||
if(savedLMP) document.getElementById("pr-example-note").style.display = "none";
|
||||
|
||||
var slider = document.getElementById("week-slider");
|
||||
var prCalMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
|
||||
lmpInput.addEventListener("input", function(){
|
||||
saveLS("ca-lmp", lmpInput.value);
|
||||
document.getElementById("pr-example-note").style.display = "none";
|
||||
document.getElementById("p-example-note").style.display = "none";
|
||||
syncSliderToLMP();
|
||||
renderPregnancy();
|
||||
});
|
||||
|
||||
function getLMP(){ return fromInput(lmpInput.value) || exampleLMP; }
|
||||
function currentGestWeek(){
|
||||
var lmp = getLMP();
|
||||
var w = Math.floor(daysBetween(lmp,today)/7) + 1;
|
||||
return Math.max(1, Math.min(40, w));
|
||||
}
|
||||
function syncSliderToLMP(){ slider.value = currentGestWeek(); }
|
||||
syncSliderToLMP();
|
||||
|
||||
function renderPregnancyStats(){
|
||||
var lmp = getLMP();
|
||||
var due = addDays(lmp, 280);
|
||||
var week = currentGestWeek();
|
||||
var daysLeft = Math.max(0, daysBetween(today, due));
|
||||
document.getElementById("pregnancy-stats").innerHTML =
|
||||
statTile(week, "Weeks along") +
|
||||
statTile(Math.floor((week-1)/13)+1 <= 3 ? ["First","Second","Third"][Math.min(2,Math.floor((week-1)/13))] : "Third", "Trimester") +
|
||||
statTile(daysLeft, "Days to due date") +
|
||||
statTile(due.toLocaleDateString(undefined,{month:"short",day:"numeric"}), "Estimated due date");
|
||||
|
||||
var pct = (week-1)/39;
|
||||
var pathLen = 230;
|
||||
var offset = pathLen - pct*pathLen;
|
||||
var path = document.getElementById("pregnancy-progress-path");
|
||||
path.setAttribute("stroke-dashoffset", offset.toFixed(1));
|
||||
try{
|
||||
var totalLen = path.getTotalLength ? path.getTotalLength() : pathLen;
|
||||
var pt = path.getPointAtLength ? path.getPointAtLength(pct*totalLen) : null;
|
||||
if(pt) { document.getElementById("pregnancy-progress-dot").setAttribute("cx",pt.x); document.getElementById("pregnancy-progress-dot").setAttribute("cy",pt.y); }
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
function renderWeekPanel(week){
|
||||
var w = WEEKS[week-1];
|
||||
document.getElementById("wk-badge").textContent = "WEEK " + week;
|
||||
document.getElementById("wk-compare").textContent = "size of " + w.c;
|
||||
document.getElementById("wk-metric").textContent = w.m === "n/a" ? "not yet fertilized" : "≈ " + w.m;
|
||||
document.getElementById("wk-dev").textContent = w.dev;
|
||||
document.getElementById("wk-body").textContent = w.body;
|
||||
document.getElementById("wk-tip").textContent = w.tip;
|
||||
var tri = week<=13?1:(week<=27?2:3);
|
||||
var triColor = "var(--pregnancy)";
|
||||
document.getElementById("wk-blob").innerHTML =
|
||||
'<svg width="96" height="96" viewBox="0 0 96 96"><circle cx="48" cy="48" r="'+(14+week*0.9)+'" fill="'+ (tri===1?"var(--pregnancy-soft)":tri===2?"var(--pregnancy)":"var(--pregnancy-strong)") +'" opacity="0.85"/></svg>';
|
||||
document.querySelectorAll(".film-item").forEach(function(f){ f.classList.toggle("active", +f.dataset.week===week); });
|
||||
var active = document.querySelector('.film-item[data-week="'+week+'"]');
|
||||
if(active) active.scrollIntoView({inline:"center", block:"nearest", behavior:"smooth"});
|
||||
}
|
||||
|
||||
slider.addEventListener("input", function(){ renderWeekPanel(+slider.value); });
|
||||
|
||||
function buildFilmstrip(){
|
||||
var wrap = document.getElementById("week-filmstrip");
|
||||
wrap.innerHTML = "";
|
||||
for(var w=1; w<=40; w++){
|
||||
var item = document.createElement("button");
|
||||
item.className = "film-item";
|
||||
item.dataset.week = w;
|
||||
item.innerHTML = '<div class="fw">WK</div><div class="fn">'+w+'</div>';
|
||||
item.addEventListener("click", function(ww){ return function(){ slider.value=ww; renderWeekPanel(ww); }; }(w));
|
||||
wrap.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPregnancyCalendar(){
|
||||
var lmp = getLMP();
|
||||
document.getElementById("pr-cal-title").textContent = MONTH_NAMES[prCalMonth.getMonth()] + " " + prCalMonth.getFullYear();
|
||||
var grid = document.getElementById("pr-daygrid");
|
||||
grid.innerHTML = "";
|
||||
var year = prCalMonth.getFullYear(), month = prCalMonth.getMonth();
|
||||
var firstDow = new Date(year,month,1).getDay();
|
||||
var daysInMonth = new Date(year,month+1,0).getDate();
|
||||
var totalCells = Math.ceil((firstDow+daysInMonth)/7)*7;
|
||||
for(var i=0;i<totalCells;i++){
|
||||
var dayNum = i - firstDow + 1;
|
||||
var cell = document.createElement("button");
|
||||
cell.className = "day-cell";
|
||||
if(dayNum<1 || dayNum>daysInMonth){ cell.classList.add("pad"); grid.appendChild(cell); continue; }
|
||||
var d = new Date(year,month,dayNum);
|
||||
var wk = Math.floor(daysBetween(lmp,d)/7)+1;
|
||||
var inRange = wk>=1 && wk<=42;
|
||||
cell.innerHTML = "<span>"+dayNum+"</span>" + (inRange ? '<span class="dot-row" style="position:static;margin-top:2px;font-size:0.55rem;color:var(--pregnancy-strong)">W'+wk+'</span>' : "");
|
||||
if(inRange && wk<=40) cell.classList.add("is-fertile");
|
||||
if(sameDay(d,today)) cell.classList.add("today");
|
||||
cell.addEventListener("click", function(ww){ return function(){ if(ww>=1&&ww<=40){ slider.value=ww; renderWeekPanel(ww); } }; }(wk));
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
}
|
||||
document.getElementById("pr-prev").addEventListener("click", function(){ prCalMonth.setMonth(prCalMonth.getMonth()-1); renderPregnancyCalendar(); });
|
||||
document.getElementById("pr-next").addEventListener("click", function(){ prCalMonth.setMonth(prCalMonth.getMonth()+1); renderPregnancyCalendar(); });
|
||||
|
||||
function renderPregnancy(){
|
||||
renderPregnancyStats();
|
||||
renderWeekPanel(currentGestWeek());
|
||||
renderPregnancyCalendar();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
INIT
|
||||
================================================================ */
|
||||
function renderAll(){
|
||||
renderPeriodStats();
|
||||
renderPeriodCalendar();
|
||||
renderPeriodDetail();
|
||||
renderOvulationStats();
|
||||
renderOvTimeline();
|
||||
renderOvDetailFromOffset();
|
||||
renderOvulationCalendar();
|
||||
renderPregnancy();
|
||||
}
|
||||
|
||||
renderPhaseRefGrid();
|
||||
buildFilmstrip();
|
||||
slider.value = currentGestWeek();
|
||||
renderAll();
|
||||
})();
|
||||
@@ -1 +0,0 @@
|
||||
.hallmark/shots/
|
||||
@@ -1,5 +0,0 @@
|
||||
shots/
|
||||
before/
|
||||
before.json
|
||||
before.txt
|
||||
compare.html
|
||||
@@ -1,31 +0,0 @@
|
||||
[
|
||||
{
|
||||
"shape": "Marquee Hero",
|
||||
"theme": "squeeze",
|
||||
"axes": "mid/geometric-sans/warm",
|
||||
"hero": "Banner",
|
||||
"nav": "edge-aligned",
|
||||
"footer": "letter close",
|
||||
"imagery": "photographs, library 1, natural",
|
||||
"photos": [
|
||||
"worm-bay"
|
||||
],
|
||||
"entry": "BeingOne front page",
|
||||
"sections": [
|
||||
"Hero",
|
||||
"Path",
|
||||
"Finder",
|
||||
"Checks",
|
||||
"Privacy",
|
||||
"Talk",
|
||||
"Family+Safety",
|
||||
"Questions+Join",
|
||||
"Footer"
|
||||
],
|
||||
"signature": "match finder with a tappable map of Australia and a moving score",
|
||||
"waived": [],
|
||||
"prompt": "BeingOne marriage site for Australians",
|
||||
"date": "2026-10-23",
|
||||
"mode": "build"
|
||||
}
|
||||
]
|
||||
@@ -1,65 +0,0 @@
|
||||
# BeingOne: plan (PRD)
|
||||
|
||||
Tagline: Real people. Real profiles. Real marriage.
|
||||
For: Australians, and the families of Australians, who want to find a life partner. English only. Australia only.
|
||||
Written for: Keith. Plain words on purpose.
|
||||
|
||||
## The big idea
|
||||
|
||||
BeingOne is a marriage website where every profile is checked to be a real person. People find matches, talk safely, and families can be part of it.
|
||||
|
||||
## The 13 core features (from Keith's plan)
|
||||
|
||||
1. Sign up and log in with email.
|
||||
2. Make a profile: photos, age, city, job, things you love, a short "about me".
|
||||
3. Check the profile is real: phone check, photo check, ID check, and a quick look by a person on the team.
|
||||
4. Search by Australian city, age and shared interests.
|
||||
5. Match score: a number showing how well two people fit.
|
||||
6. Photo privacy: you decide who sees your photos.
|
||||
7. Send an interest. The other person accepts or says no thanks.
|
||||
8. Private chat that opens only when both say yes.
|
||||
9. Family view: a parent or sibling helps look at profiles.
|
||||
10. Safety tools: block, report, hide your profile.
|
||||
11. Free plan and paid plan (payments).
|
||||
12. Admin area for the BeingOne team: review profiles, handle reports, see numbers.
|
||||
13. Emails and phone alerts: new matches, interests, messages.
|
||||
|
||||
## Phase 1 (today): the new look, with working demos
|
||||
|
||||
What you get: one front page in a new design. Everything on it is a demo that runs in your browser, with made-up sample people.
|
||||
|
||||
| Feature | Where it shows up on the page |
|
||||
| --- | --- |
|
||||
| 1 Sign up, log in | The join card at the bottom (Join and Log in tabs). It does not save anything yet. |
|
||||
| 2 Profile | Shown through the sample people and the checks demo. The real profile editor is Phase 3. |
|
||||
| 3 Checks | "Every profile is checked": run the four checks on a sample profile. |
|
||||
| 4 Search | "Find your match across Australia": map of Australia, city, ages, interests. |
|
||||
| 5 Match score | The score and bar on every sample person. |
|
||||
| 6 Photo privacy | "You choose who sees your photos": pick who can see, pick who is looking. |
|
||||
| 7 Interest, accept | "Chat opens when both say yes": accept or say no thanks. |
|
||||
| 8 Private chat | Same section: the chat unlocks after Accept. |
|
||||
| 9 Family view | "Family view" tile: pick who helps you look. |
|
||||
| 10 Safety tools | "Safety tools" tile: block, report, hide. |
|
||||
| 11 Plans | The "What does it cost?" answer. The paid price is left blank until you give it. |
|
||||
| 12 Admin | Mentioned in "Who runs the site?". The real admin area is Phase 7 and is a separate private page. |
|
||||
| 13 Alerts | The "Will I get alerts?" answer. Real alerts are Phase 6. |
|
||||
|
||||
Tests, in this order:
|
||||
1. The page loads with no errors.
|
||||
2. Every button, chip, slider, tab and form reacts.
|
||||
3. Real connections: none yet, so nothing to test.
|
||||
4. Security: no passwords, no keys, no calls to other servers for data. The forms only show a message on screen.
|
||||
|
||||
## Phases 2 to 8 (one at a time, each must work before the next)
|
||||
|
||||
2. Real sign-up and login (needs a host, a domain, a privacy policy and a consent step).
|
||||
3. Real profiles and photos, with private photo storage.
|
||||
4. Real checks (phone code, ID check through a trusted provider, team review screen).
|
||||
5. Real search and matching on real profiles.
|
||||
6. Private chat, block and report, email and phone alerts.
|
||||
7. Payments (test mode first) and the admin area.
|
||||
8. Launch checks: speed, phones, privacy rules, backups, a trial with friends and family.
|
||||
|
||||
## Important
|
||||
|
||||
Marriage sites hold very private details. In Australia some of them are "sensitive information" under the Privacy Act 1988, so people must clearly agree to how they are used. Plan the privacy policy and consent step in Phase 2. Please check with a lawyer before launch. This is not legal advice.
|
||||
@@ -1,42 +0,0 @@
|
||||
# BeingOne
|
||||
|
||||
Real people. Real profiles. Real marriage.
|
||||
|
||||
BeingOne is a marriage website for Australians and their families. Every profile is checked to be a real person. People find matches, talk safely, and families can be part of it.
|
||||
|
||||
This is **Phase 1**: the front page, in its new design, with working demos that run in the browser. The people on it are made up. Nothing is saved and nothing is sent anywhere yet.
|
||||
|
||||
## See it
|
||||
|
||||
There is no build step. Open `index.html` in a browser.
|
||||
|
||||
## What is in this folder
|
||||
|
||||
| File | What it is |
|
||||
| --- | --- |
|
||||
| `index.html` | The page |
|
||||
| `style.css` | How it looks (colours, fonts, spacing, motion) |
|
||||
| `tokens.css`, `base.css` | The colour and font settings, and the basic rules |
|
||||
| `app.js` | What the buttons, map, search, checks, chat and forms do |
|
||||
| `images/hero.jpg` | The photo at the top of the page (swap in the original) |
|
||||
| `PRD.md` | The plan: 13 features and the 8 phases |
|
||||
|
||||
## What the demos do
|
||||
|
||||
- **Search bar and map of Australia:** pick a city, ages and interests. Made-up people appear with a match score.
|
||||
- **Four checks:** run the phone, photo, ID and team checks on a sample profile.
|
||||
- **Photo privacy:** choose who can see a photo, and who is looking.
|
||||
- **Interest and chat:** accept an interest and a private chat opens.
|
||||
- **Family view and safety tools:** pick who helps you look; block, report and hide.
|
||||
- **Join and log in:** forms that do not send anything yet.
|
||||
|
||||
## Before it goes live
|
||||
|
||||
- The fonts load from Google Fonts. The beach photo loads from Wikimedia Commons (CC BY-SA 4.0, credited in the footer). Download both into this folder if you want the page to make no outside calls.
|
||||
- Replace `images/hero.jpg` with the original photo file.
|
||||
- The paid-plan price is a blank box on the page until you decide it.
|
||||
- Marriage sites hold very private details. In Australia some of them count as "sensitive information" under the Privacy Act 1988, so people must clearly agree to how it is used. Plan the privacy policy and consent step in Phase 2, and check with a lawyer before launch. This is not legal advice.
|
||||
|
||||
## Next: Phase 2
|
||||
|
||||
Real sign-up and login. This needs a hosting place (for example Vercel and Supabase) and a domain name.
|
||||
-313
@@ -1,313 +0,0 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
const $ = (s, r = document) => r.querySelector(s);
|
||||
const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
|
||||
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
/* made-up sample people; nothing here is a real person (g: w = woman, m = man) */
|
||||
const PEOPLE = [
|
||||
{ n: 'Mia', g: 'w', a: 28, c: 'Sydney', l: ['beach', 'coffee', 'travel'], t: 'a' },
|
||||
{ n: 'Arjun', g: 'm', a: 31, c: 'Melbourne', l: ['footy', 'cooking', 'music'], t: 'b' },
|
||||
{ n: 'Hannah', g: 'w', a: 27, c: 'Brisbane', l: ['beach', 'hiking', 'pets'], t: 'c' },
|
||||
{ n: 'Wei', g: 'm', a: 30, c: 'Perth', l: ['coffee', 'travel', 'hiking'], t: 'd' },
|
||||
{ n: 'Sana', g: 'w', a: 29, c: 'Adelaide', l: ['cooking', 'music', 'pets'], t: 'a' },
|
||||
{ n: 'Tom', g: 'm', a: 33, c: 'Hobart', l: ['hiking', 'footy', 'coffee'], t: 'b' },
|
||||
{ n: 'Priya', g: 'w', a: 26, c: 'Sydney', l: ['music', 'cooking', 'beach'], t: 'c' },
|
||||
{ n: 'Jack', g: 'm', a: 35, c: 'Melbourne', l: ['footy', 'beach', 'pets'], t: 'd' },
|
||||
{ n: 'Elena', g: 'w', a: 32, c: 'Brisbane', l: ['travel', 'coffee', 'music'], t: 'a' },
|
||||
{ n: 'Dev', g: 'm', a: 34, c: 'Perth', l: ['footy', 'cooking', 'hiking'], t: 'b' },
|
||||
{ n: 'Grace', g: 'w', a: 30, c: 'Canberra', l: ['hiking', 'coffee', 'pets'], t: 'c' },
|
||||
{ n: 'Sam', g: 'm', a: 28, c: 'Darwin', l: ['beach', 'footy', 'travel'], t: 'd' },
|
||||
{ n: 'Lena', g: 'w', a: 38, c: 'Sydney', l: ['travel', 'music', 'pets'], t: 'a' },
|
||||
{ n: 'Omar', g: 'm', a: 29, c: 'Melbourne', l: ['coffee', 'cooking', 'hiking'], t: 'c' }
|
||||
];
|
||||
|
||||
/* ---------- scroll reveal, once ---------- */
|
||||
const revealEls = $$('[data-reveal]');
|
||||
if ('IntersectionObserver' in window) {
|
||||
const io = new IntersectionObserver((es) => es.forEach((e) => {
|
||||
if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); }
|
||||
}), { rootMargin: '0px 0px -10% 0px' });
|
||||
revealEls.forEach((el) => io.observe(el));
|
||||
} else {
|
||||
revealEls.forEach((el) => el.classList.add('is-in'));
|
||||
}
|
||||
|
||||
/* ---------- mobile menu ---------- */
|
||||
const menu = $('.nav__menu'), links = $('#nav-links');
|
||||
menu.addEventListener('click', () => {
|
||||
const open = links.classList.toggle('is-open');
|
||||
menu.setAttribute('aria-expanded', String(open));
|
||||
menu.textContent = open ? 'Close' : 'Menu';
|
||||
});
|
||||
links.addEventListener('click', (e) => {
|
||||
if (e.target.closest('a')) { links.classList.remove('is-open'); menu.setAttribute('aria-expanded', 'false'); menu.textContent = 'Menu'; }
|
||||
});
|
||||
|
||||
/* ---------- match finder (the quick search bar drives it too) ---------- */
|
||||
const st = { city: 'Anywhere', gender: 'any', from: 25, to: 40, likes: new Set(), sent: new Set() };
|
||||
const from = $('#f-from'), to = $('#f-to'), oFrom = $('#o-from'), oTo = $('#o-to');
|
||||
const results = $('#results'), status = $('#finder-status'), mapCity = $('#map-city');
|
||||
const qFor = $('#q-for'), qAge = $('#q-age'), qCity = $('#q-city');
|
||||
const pins = $$('.pin');
|
||||
|
||||
function scoreOf(p) {
|
||||
const mid = (st.from + st.to) / 2, half = Math.max((st.to - st.from) / 2, 1);
|
||||
const ageFit = Math.max(0, 1 - Math.abs(p.a - mid) / (half + 4));
|
||||
const shared = p.l.filter((x) => st.likes.has(x)).length;
|
||||
const likeFit = st.likes.size ? Math.min(1, shared / Math.min(st.likes.size, 3)) : 0.5;
|
||||
const cityFit = st.city === 'Anywhere' ? 0.7 : 1;
|
||||
return Math.round(55 + 45 * (0.5 * likeFit + 0.3 * ageFit + 0.2 * cityFit));
|
||||
}
|
||||
|
||||
function tile(r, i) {
|
||||
const p = r.p, sent = st.sent.has(p.n);
|
||||
const tags = p.l.map((x) => '<li' + (st.likes.has(x) ? ' class="is-shared"' : '') + '>' + cap(x) + '</li>').join('');
|
||||
return '<article class="person" style="--i:' + i + '">' +
|
||||
'<div class="person__who"><span class="avatar" style="--tone:var(--tone-' + p.t + ')" aria-hidden="true">' + p.n.charAt(0) + '</span>' +
|
||||
'<div><h3>' + p.n + ', ' + p.a + '</h3><p class="label">' + p.c + '</p></div></div>' +
|
||||
'<div><div class="score"><b data-to="' + r.score + '">' + r.score + '</b><span class="label" title="Counts shared interests, city and age">match</span></div>' +
|
||||
'<div class="bar" role="img" aria-label="Match score ' + r.score + ' out of 100"><i style="--s:' + r.score + '"></i></div></div>' +
|
||||
'<ul class="tags">' + tags + '</ul>' +
|
||||
'<button class="btn btn--sm" type="button" data-interest="' + p.n + '" aria-pressed="' + sent + '">' + (sent ? 'Interest sent' : 'Send interest') + '</button>' +
|
||||
'</article>';
|
||||
}
|
||||
|
||||
/* scores count up from 55 to their value, once per render */
|
||||
function countUp(el) {
|
||||
const to = +el.dataset.to;
|
||||
if (reduced) { el.textContent = to; return; }
|
||||
const t0 = performance.now(), dur = 900, start = 55;
|
||||
el.textContent = start;
|
||||
(function step(now) {
|
||||
const k = Math.min(1, (now - t0) / dur), e = 1 - Math.pow(1 - k, 3);
|
||||
el.textContent = Math.round(start + (to - start) * e);
|
||||
if (k < 1) requestAnimationFrame(step);
|
||||
})(t0);
|
||||
}
|
||||
|
||||
function renderFinder() {
|
||||
const pool = PEOPLE
|
||||
.filter((p) => (st.city === 'Anywhere' || p.c === st.city) && (st.gender === 'any' || p.g === st.gender) && p.a >= st.from && p.a <= st.to)
|
||||
.map((p) => ({ p, score: scoreOf(p) }))
|
||||
.sort((x, y) => y.score - x.score || x.p.n.localeCompare(y.p.n));
|
||||
const top = pool.slice(0, 4);
|
||||
results.innerHTML = top.length
|
||||
? top.map(tile).join('')
|
||||
: '<p class="results__empty">No one fits yet. Widen the ages or try another city.</p>';
|
||||
$$('.score b', results).forEach(countUp);
|
||||
status.textContent = top.length ? top.length + ' of ' + pool.length + ' shown.' : '';
|
||||
mapCity.textContent = st.city === 'Anywhere' ? 'All of Australia' : st.city;
|
||||
qCity.value = st.city;
|
||||
pins.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.city === st.city)));
|
||||
}
|
||||
|
||||
function setAges(a, b) {
|
||||
st.from = a; st.to = b; from.value = a; to.value = b;
|
||||
oFrom.textContent = a; oTo.textContent = b;
|
||||
}
|
||||
|
||||
pins.forEach((b) => b.addEventListener('click', () => {
|
||||
st.city = st.city === b.dataset.city ? 'Anywhere' : b.dataset.city;
|
||||
if (st.city === b.dataset.city && !reduced) {
|
||||
const ping = document.createElement('span');
|
||||
ping.className = 'ping';
|
||||
ping.addEventListener('animationend', () => ping.remove(), { once: true });
|
||||
b.appendChild(ping);
|
||||
}
|
||||
renderFinder();
|
||||
}));
|
||||
from.addEventListener('input', () => {
|
||||
st.from = +from.value;
|
||||
if (st.from > st.to) { st.to = st.from; to.value = st.to; }
|
||||
oFrom.textContent = st.from; oTo.textContent = st.to; renderFinder();
|
||||
});
|
||||
to.addEventListener('input', () => {
|
||||
st.to = +to.value;
|
||||
if (st.to < st.from) { st.from = st.to; from.value = st.from; }
|
||||
oFrom.textContent = st.from; oTo.textContent = st.to; renderFinder();
|
||||
});
|
||||
$$('#f-likes input').forEach((c) => c.addEventListener('change', () => {
|
||||
c.checked ? st.likes.add(c.value) : st.likes.delete(c.value);
|
||||
renderFinder();
|
||||
}));
|
||||
results.addEventListener('click', (e) => {
|
||||
const b = e.target.closest('[data-interest]');
|
||||
if (!b) return;
|
||||
const name = b.dataset.interest;
|
||||
st.sent.has(name) ? st.sent.delete(name) : st.sent.add(name);
|
||||
const on = st.sent.has(name);
|
||||
b.setAttribute('aria-pressed', String(on));
|
||||
b.textContent = on ? 'Interest sent' : 'Send interest';
|
||||
});
|
||||
|
||||
/* quick search bar: each choice updates the finder, the button also scrolls to the people */
|
||||
function applyQuick() {
|
||||
st.gender = qFor.value;
|
||||
st.city = qCity.value;
|
||||
const [a, b] = qAge.value.split('-').map(Number);
|
||||
setAges(a, b);
|
||||
renderFinder();
|
||||
}
|
||||
[qFor, qAge, qCity].forEach((el) => el.addEventListener('change', applyQuick));
|
||||
$('#quick').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
applyQuick();
|
||||
results.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'center' });
|
||||
});
|
||||
renderFinder();
|
||||
|
||||
/* ---------- the four checks ---------- */
|
||||
const rows = $$('[data-check]'), vStatus = $('#v-status'), vText = $('#v-status-text'), vBar = $('#v-progress');
|
||||
const timers = new Map();
|
||||
function updateChecks() {
|
||||
const n = rows.filter((r) => r.classList.contains('is-done')).length;
|
||||
vStatus.dataset.state = n === rows.length ? 'done' : 'idle';
|
||||
vText.textContent = n === rows.length ? 'Verified' : n ? n + ' of 4 passed' : 'Not verified yet';
|
||||
vBar.style.setProperty('--p', n / rows.length);
|
||||
}
|
||||
function runCheck(row) {
|
||||
if (row.classList.contains('is-done') || timers.has(row)) return;
|
||||
const btn = $('[data-run]', row);
|
||||
btn.setAttribute('aria-busy', 'true'); btn.disabled = true; btn.textContent = 'Checking…';
|
||||
timers.set(row, setTimeout(() => {
|
||||
timers.delete(row);
|
||||
row.classList.add('is-done');
|
||||
btn.hidden = true; btn.removeAttribute('aria-busy');
|
||||
$('.passed', row).hidden = false;
|
||||
updateChecks();
|
||||
}, 800));
|
||||
}
|
||||
rows.forEach((r) => $('[data-run]', r).addEventListener('click', () => runCheck(r)));
|
||||
$('#v-all').addEventListener('click', () => rows.forEach((r, i) => setTimeout(() => runCheck(r), i * 250)));
|
||||
$('#v-reset').addEventListener('click', () => {
|
||||
timers.forEach((t) => clearTimeout(t)); timers.clear();
|
||||
rows.forEach((r) => {
|
||||
r.classList.remove('is-done');
|
||||
const btn = $('[data-run]', r);
|
||||
btn.hidden = false; btn.disabled = false; btn.removeAttribute('aria-busy'); btn.textContent = 'Run';
|
||||
$('.passed', r).hidden = true;
|
||||
});
|
||||
updateChecks();
|
||||
});
|
||||
|
||||
/* ---------- photo privacy ---------- */
|
||||
const photo = $('#photo'), photoState = $('#photo-state');
|
||||
const rank = { stranger: 0, verified: 1, accepted: 2 };
|
||||
function renderPhoto() {
|
||||
const setting = $('input[name="setting"]:checked').value;
|
||||
const viewer = $('input[name="viewer"]:checked').value;
|
||||
const can = setting === 'everyone' ||
|
||||
(setting === 'verified' && rank[viewer] >= 1) ||
|
||||
(setting === 'accepted' && rank[viewer] >= 2);
|
||||
photo.classList.toggle('is-hidden', !can);
|
||||
photoState.textContent = can ? 'Visible to this visitor' : 'Hidden from this visitor';
|
||||
}
|
||||
$$('input[name="setting"], input[name="viewer"]').forEach((r) => r.addEventListener('change', renderPhoto));
|
||||
renderPhoto();
|
||||
|
||||
/* ---------- interest, accept, chat ---------- */
|
||||
const chat = $('#chat'), log = $('#chat-log'), tResult = $('#t-result');
|
||||
const accept = $('#t-accept'), decline = $('#t-decline'), tReset = $('#t-reset');
|
||||
const chatForm = $('#chat-form'), chatInput = $('#chat-input');
|
||||
function addLine(text, cls) {
|
||||
const li = document.createElement('li');
|
||||
if (cls) li.className = cls;
|
||||
li.textContent = text;
|
||||
log.appendChild(li);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
function decide(yes) {
|
||||
accept.hidden = true; decline.hidden = true; tReset.hidden = false;
|
||||
tResult.textContent = yes ? 'You both said yes. Say hello.' : 'You said no thanks. Nothing opens.';
|
||||
if (yes) {
|
||||
chat.classList.remove('is-locked');
|
||||
addLine('You and Priya both said yes.', 'note');
|
||||
chatInput.focus();
|
||||
}
|
||||
}
|
||||
accept.addEventListener('click', () => decide(true));
|
||||
decline.addEventListener('click', () => decide(false));
|
||||
tReset.addEventListener('click', () => {
|
||||
accept.hidden = false; decline.hidden = false; tReset.hidden = true;
|
||||
tResult.textContent = ''; log.textContent = '';
|
||||
chat.classList.add('is-locked'); chatInput.value = '';
|
||||
});
|
||||
chatForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const v = chatInput.value.trim();
|
||||
if (!v) { chatInput.focus(); return; }
|
||||
addLine(v, 'me'); chatInput.value = ''; chatInput.focus();
|
||||
});
|
||||
|
||||
/* ---------- family view and safety tools ---------- */
|
||||
const famStatus = $('#fam-status');
|
||||
function renderFam() {
|
||||
const on = $$('#fam input:checked').map((c) => c.value);
|
||||
famStatus.textContent = !on.length ? 'Just you for now.'
|
||||
: (on.length === 1 ? on[0] : on.slice(0, -1).join(', ') + ' and ' + on[on.length - 1]) +
|
||||
(on.length === 1 ? ' is' : ' are') + ' looking with you.';
|
||||
}
|
||||
$$('#fam input').forEach((c) => c.addEventListener('change', renderFam));
|
||||
|
||||
const safe = $('#safe-status'), sBlock = $('#s-block'), sHide = $('#s-hide'), sReport = $('#s-report');
|
||||
let blocked = false;
|
||||
sBlock.addEventListener('click', () => {
|
||||
blocked = !blocked;
|
||||
$('span', sBlock).textContent = blocked ? 'Unblock' : 'Block';
|
||||
sBlock.setAttribute('aria-pressed', String(blocked));
|
||||
safe.textContent = blocked ? 'Jack is blocked. He cannot message you.' : 'Jack is unblocked.';
|
||||
});
|
||||
sReport.addEventListener('click', () => { safe.textContent = 'Demo only: nothing was sent to our team.'; });
|
||||
sHide.addEventListener('click', () => {
|
||||
const on = sHide.getAttribute('aria-pressed') !== 'true';
|
||||
sHide.setAttribute('aria-pressed', String(on));
|
||||
$('span', sHide).textContent = on ? 'Show my profile' : 'Hide my profile';
|
||||
safe.textContent = on ? 'Your profile is hidden from search.' : 'Your profile shows in search.';
|
||||
});
|
||||
|
||||
/* ---------- join card: tabs and forms that post nowhere yet ---------- */
|
||||
const tabs = $$('[role="tab"]', $('#join-card'));
|
||||
const panels = { join: $('#panel-join'), login: $('#panel-login') };
|
||||
function openTab(name, focus) {
|
||||
tabs.forEach((t) => {
|
||||
const on = t.id === 'tab-' + name;
|
||||
t.setAttribute('aria-selected', String(on));
|
||||
t.tabIndex = on ? 0 : -1;
|
||||
if (on && focus) t.focus();
|
||||
});
|
||||
Object.keys(panels).forEach((k) => { panels[k].hidden = k !== name; });
|
||||
}
|
||||
tabs.forEach((t, i) => {
|
||||
t.addEventListener('click', () => openTab(t.id.replace('tab-', '')));
|
||||
t.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
|
||||
e.preventDefault();
|
||||
const next = tabs[(i + (e.key === 'ArrowRight' ? 1 : tabs.length - 1)) % tabs.length];
|
||||
openTab(next.id.replace('tab-', ''), true);
|
||||
});
|
||||
});
|
||||
$$('[data-open-tab]').forEach((a) => a.addEventListener('click', () => openTab(a.dataset.openTab)));
|
||||
openTab('join');
|
||||
|
||||
$$('form[data-endpoint]').forEach((form) => {
|
||||
const help = $('.form-help', form);
|
||||
form.addEventListener('input', (e) => { e.target.removeAttribute && e.target.removeAttribute('aria-invalid'); });
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
help.classList.remove('is-error');
|
||||
const bad = $$('input', form).filter((i) => !i.checkValidity());
|
||||
$$('input', form).forEach((i) => i.toggleAttribute('aria-invalid', bad.includes(i)));
|
||||
if (bad.length) {
|
||||
help.classList.add('is-error');
|
||||
help.textContent = bad[0].type === 'email'
|
||||
? 'That email looks incomplete. Write it as name@example.com.'
|
||||
: 'Please fill in the highlighted field.';
|
||||
bad[0].focus();
|
||||
return;
|
||||
}
|
||||
if (!form.dataset.endpoint) { help.textContent = 'Nothing was sent: this form is not connected yet.'; return; }
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -1,23 +0,0 @@
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { overflow-x: clip; }
|
||||
html { scrollbar-color: var(--color-muted) var(--color-paper); scroll-padding-top: var(--space-3xl); }
|
||||
body { margin: 0; background: var(--color-paper); color: var(--color-ink); font-family: var(--font-body); font-size: var(--text-md); line-height: 1.55; -webkit-font-smoothing: antialiased; caret-color: var(--color-accent); }
|
||||
::selection { background: color-mix(in oklab, var(--color-accent) 22%, transparent); color: var(--color-ink); }
|
||||
:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
|
||||
a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 0.16em; }
|
||||
h1, h2, h3 { margin: 0; font-family: var(--font-display); font-synthesis: none; overflow-wrap: break-word; min-width: 0; text-wrap: balance; }
|
||||
p { margin: 0; text-wrap: pretty; }
|
||||
p:not([class]) { max-width: 65ch; }
|
||||
img, video { max-width: 100%; height: auto; display: block; }
|
||||
svg { display: block; }
|
||||
[hidden] { display: none !important; }
|
||||
button, input, select, textarea { font: inherit; }
|
||||
.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); box-sizing: border-box; }
|
||||
.num, .figure, td.num { font-variant-numeric: tabular-nums; }
|
||||
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
||||
[data-slot] { position: relative; overflow: hidden; background: linear-gradient(160deg, var(--color-paper-2, var(--color-paper)), var(--color-paper-3, var(--color-paper))); }
|
||||
[data-slot] > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1; }
|
||||
[data-slot]:has(> img) > :not(img):not([data-keep]) { display: none !important; }
|
||||
@media (max-width: 60rem) { :root { --gutter: 24px; --section-gap-s: 80px; --section-gap: 96px; --section-gap-l: 120px; } }
|
||||
@media (max-width: 40rem) { :root { --gutter: 16px; --section-gap-s: 56px; --section-gap: 72px; --section-gap-l: 96px; --gap-columns: 16px; } }
|
||||
@media (prefers-reduced-motion: reduce) { *:not(.keep-motion), *:not(.keep-motion)::before, *:not(.keep-motion)::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB |
@@ -1,346 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en-AU">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BeingOne: marriage profiles for Australians, every one checked</title>
|
||||
<meta name="description" content="BeingOne is a marriage website for Australians and their families where every profile is checked to be a real person.">
|
||||
<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=DM+Serif+Display:ital@0;1&family=Figtree:wght@500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script>document.documentElement.classList.add('js');</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
|
||||
<symbol id="i-check" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></symbol>
|
||||
<symbol id="i-arrow" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></symbol>
|
||||
<symbol id="i-lock" viewBox="0 0 24 24"><rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol>
|
||||
<symbol id="i-eye" viewBox="0 0 24 24"><path d="M2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 19.88 0 1 1 0 0 1 0 .7 10.75 10.75 0 0 1-19.88 0"/><circle cx="12" cy="12" r="3"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
<symbol id="i-phone" viewBox="0 0 24 24"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"/></symbol>
|
||||
<symbol id="i-camera" viewBox="0 0 24 24"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></symbol>
|
||||
<symbol id="i-id" viewBox="0 0 24 24"><rect x="2" y="5" width="20" height="14" rx="2"/><circle cx="9" cy="11" r="2"/><path d="M5.5 16c.5-1.5 2-2.5 3.5-2.5s3 1 3.5 2.5M15 10h4M15 14h3"/></symbol>
|
||||
<symbol id="i-users" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></symbol>
|
||||
<symbol id="i-ban" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="m4.9 4.9 14.2 14.2"/></symbol>
|
||||
<symbol id="i-flag" viewBox="0 0 24 24"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><path d="M4 22v-7"/></symbol>
|
||||
<symbol id="i-send" viewBox="0 0 24 24"><path d="m22 2-7 20-4-9-9-4z"/><path d="M22 2 11 13"/></symbol>
|
||||
</svg>
|
||||
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
|
||||
<header class="nav">
|
||||
<div class="container nav__bar">
|
||||
<a class="wordmark" href="#top" aria-label="BeingOne, back to the top">
|
||||
<svg class="mark" viewBox="0 0 32 32" aria-hidden="true"><circle cx="12" cy="16" r="8"/><circle cx="20" cy="16" r="8"/></svg>
|
||||
<span>Being<b>One</b></span>
|
||||
</a>
|
||||
<nav class="nav__links" id="nav-links" aria-label="Main">
|
||||
<a href="#find">Discover</a>
|
||||
<a href="#results">Curated matches</a>
|
||||
<a href="#talk">Messages</a>
|
||||
<a href="#safety">Safety centre</a>
|
||||
<a href="#help">Help</a>
|
||||
<a class="only-sm" href="#join" data-open-tab="login">Sign in</a>
|
||||
</nav>
|
||||
<div class="nav__actions">
|
||||
<a class="nav__signin" href="#join" data-open-tab="login">Sign in</a>
|
||||
<a class="btn btn--sm" href="#join" data-open-tab="join">Join</a>
|
||||
<button class="nav__menu btn btn--sm" type="button" aria-expanded="false" aria-controls="nav-links">Menu</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
|
||||
<section class="hero" id="top" aria-labelledby="hero-title">
|
||||
<figure class="hero__photo" data-slot="hero" data-alt="A bride and groom holding hands outdoors as guests throw petals">
|
||||
<img src="images/hero.jpg" width="998" height="770" alt="A bride and groom holding hands outdoors as guests throw petals" fetchpriority="high">
|
||||
</figure>
|
||||
<div class="container hero__inner">
|
||||
<div class="hero__text">
|
||||
<h1 id="hero-title">
|
||||
<span class="line" style="--i:0"><span>Real people.</span></span>
|
||||
<span class="line" style="--i:1"><span>Real profiles.</span></span>
|
||||
<span class="line line--it" style="--i:2"><span>Real marriage.</span></span>
|
||||
</h1>
|
||||
<p class="lead hero__lead">Every profile is checked, so you meet real people across Australia.</p>
|
||||
<a class="btn btn--primary hero__cta" href="#join" data-open-tab="join">Create your profile<svg class="icon" aria-hidden="true"><use href="#i-arrow"/></svg></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container quick__wrap">
|
||||
<form class="quick" id="quick" onsubmit="return false" aria-label="Quick search">
|
||||
<div class="quick__field">
|
||||
<label for="q-for">I’m looking for</label>
|
||||
<select id="q-for"><option value="any">Anyone</option><option value="w">A woman</option><option value="m">A man</option></select>
|
||||
</div>
|
||||
<div class="quick__field">
|
||||
<label for="q-age">Age</label>
|
||||
<select id="q-age"><option value="21-60">Any age</option><option value="21-29">21 to 29</option><option value="30-39">30 to 39</option><option value="40-49">40 to 49</option><option value="50-60">50 to 60</option></select>
|
||||
</div>
|
||||
<div class="quick__field">
|
||||
<label for="q-city">Location</label>
|
||||
<select id="q-city"><option>Anywhere</option><option>Adelaide</option><option>Brisbane</option><option>Canberra</option><option>Darwin</option><option>Hobart</option><option>Melbourne</option><option>Perth</option><option>Sydney</option></select>
|
||||
</div>
|
||||
<button class="btn btn--primary quick__go" type="submit"><svg class="icon" aria-hidden="true"><use href="#i-search"/></svg>Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="band band--path" id="path" aria-label="How it goes">
|
||||
<div class="container">
|
||||
<ol class="path">
|
||||
<li data-reveal style="--i:0"><a href="#join">Join</a></li>
|
||||
<li data-reveal style="--i:1"><a href="#verified">Get checked</a></li>
|
||||
<li data-reveal style="--i:2"><a href="#find">Find</a></li>
|
||||
<li data-reveal style="--i:3"><a href="#talk">Say yes</a></li>
|
||||
<li data-reveal style="--i:4"><a href="#talk">Chat</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sec finder" id="find" aria-labelledby="find-title">
|
||||
<div class="container">
|
||||
<div class="grid finder__top">
|
||||
<h2 id="find-title" data-reveal>Find your match across Australia</h2>
|
||||
<p class="lead" data-reveal style="--i:1">Pick a city, ages and what you love.</p>
|
||||
</div>
|
||||
<div class="grid finder__body">
|
||||
<div class="map" data-reveal role="group" aria-label="Map of Australia. Tap a city to search there.">
|
||||
<svg class="map__svg" viewBox="-40 -20 605 516" role="img" aria-label="Outline of Australia">
|
||||
<path d="M381.3 9.8L393.8 56.0L416.3 70.0L422.5 96.6L435.0 130.2L458.7 144.2L465.0 155.4L485.0 187.6L505.0 208.6L513.7 224.0L512.5 245.0L520.0 260.4L512.5 284.2L506.3 308.0L490.0 334.6L485.0 348.6L476.2 366.8L473.8 385.0L450.0 392.0L430.0 407.4L412.5 397.6L393.8 403.2L362.5 393.4L350.0 385.0L343.8 358.4L326.2 359.1L331.3 347.2L321.2 336.0L300.0 347.2L277.5 322.0L237.5 301.0L212.5 303.8L175.0 312.2L143.8 333.2L123.8 334.6L87.5 348.6L73.8 350.0L38.7 341.6L46.3 326.2L47.5 308.0L32.5 263.2L17.5 224.0L18.8 196.0L26.2 165.2L58.8 149.8L82.5 144.2L118.8 126.0L127.5 112.0L147.5 91.0L168.8 77.0L186.3 54.6L212.5 70.0L221.2 68.6L225.0 46.2L235.0 35.0L256.3 21.0L275.0 28.0L310.0 30.8L306.3 49.0L297.5 72.8L322.5 91.0L343.8 105.0L360.0 103.6L370.0 77.0L368.8 49.0L376.2 30.8ZM408.7 429.8L453.8 432.6L453.8 450.8L441.3 469.0L425.0 470.4L415.0 450.8Z"/>
|
||||
</svg>
|
||||
<button class="pin" type="button" data-city="Perth" aria-pressed="false" aria-label="Perth" title="Perth" style="--x:14.6%;--y:63.4%;--i:0"></button>
|
||||
<button class="pin" type="button" data-city="Adelaide" aria-pressed="false" aria-label="Adelaide" title="Adelaide" style="--x:61.6%;--y:71.5%;--i:1"></button>
|
||||
<button class="pin" type="button" data-city="Darwin" aria-pressed="false" aria-label="Darwin" title="Darwin" style="--x:45.5%;--y:10.5%;--i:2"></button>
|
||||
<button class="pin" type="button" data-city="Brisbane" aria-pressed="false" aria-label="Brisbane" title="Brisbane" style="--x:91.4%;--y:51.3%;--i:3"></button>
|
||||
<button class="pin" type="button" data-city="Sydney" aria-pressed="false" aria-label="Sydney" title="Sydney" style="--x:87.6%;--y:68.6%;--i:4"></button>
|
||||
<button class="pin" type="button" data-city="Canberra" aria-pressed="false" aria-label="Canberra" title="Canberra" style="--x:83.3%;--y:72.5%;--i:5"></button>
|
||||
<button class="pin" type="button" data-city="Melbourne" aria-pressed="false" aria-label="Melbourne" title="Melbourne" style="--x:74.7%;--y:79.3%;--i:6"></button>
|
||||
<button class="pin" type="button" data-city="Hobart" aria-pressed="false" aria-label="Hobart" title="Hobart" style="--x:79.6%;--y:93.1%;--i:7"></button>
|
||||
<p class="map__city" id="map-city">All of Australia</p>
|
||||
</div>
|
||||
|
||||
<form class="controls" id="finder-form" data-reveal style="--i:1" onsubmit="return false" aria-label="Match finder">
|
||||
<div class="controls__ages">
|
||||
<div class="field">
|
||||
<div class="field__row"><label for="f-from">From</label><output id="o-from" for="f-from">25</output></div>
|
||||
<input id="f-from" type="range" min="21" max="60" value="25">
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field__row"><label for="f-to">To</label><output id="o-to" for="f-to">40</output></div>
|
||||
<input id="f-to" type="range" min="21" max="60" value="40">
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="choices" id="f-likes">
|
||||
<legend>You love</legend>
|
||||
<input type="checkbox" id="l-beach" value="beach"><label for="l-beach">Beach</label>
|
||||
<input type="checkbox" id="l-footy" value="footy"><label for="l-footy">Footy</label>
|
||||
<input type="checkbox" id="l-coffee" value="coffee"><label for="l-coffee">Coffee</label>
|
||||
<input type="checkbox" id="l-hiking" value="hiking"><label for="l-hiking">Hiking</label>
|
||||
<input type="checkbox" id="l-cooking" value="cooking"><label for="l-cooking">Cooking</label>
|
||||
<input type="checkbox" id="l-travel" value="travel"><label for="l-travel">Travel</label>
|
||||
<input type="checkbox" id="l-music" value="music"><label for="l-music">Music</label>
|
||||
<input type="checkbox" id="l-pets" value="pets"><label for="l-pets">Pets</label>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="results" id="results" aria-live="polite"></div>
|
||||
<p class="visually-hidden" id="finder-status" role="status"></p>
|
||||
<p class="finder__note">Sample people, made up for this demo.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sec band--navy on-dark checks" id="verified" aria-labelledby="verified-title">
|
||||
<div class="container grid">
|
||||
<div class="head">
|
||||
<h2 id="verified-title" data-reveal>Every profile is checked</h2>
|
||||
<p class="lead" data-reveal style="--i:1">Try the four checks.</p>
|
||||
</div>
|
||||
<div class="checks__panel" data-reveal style="--i:2">
|
||||
<div class="checks__who">
|
||||
<span class="avatar" style="--tone:var(--tone-a)" aria-hidden="true">M</span>
|
||||
<div><h3>Mia, 28</h3><p class="label">Sydney</p></div>
|
||||
<span class="status" id="v-status" data-state="idle" role="status"><svg class="icon icon--sm" aria-hidden="true"><use href="#i-shield"/></svg><span id="v-status-text">Not verified yet</span></span>
|
||||
</div>
|
||||
<div class="progress" aria-hidden="true"><i id="v-progress" style="--p:0"></i></div>
|
||||
<ul class="rows" id="v-rows">
|
||||
<li data-check>
|
||||
<svg class="icon idle" aria-hidden="true"><use href="#i-phone"/></svg><svg class="icon tick" aria-hidden="true"><use href="#i-check"/></svg>
|
||||
<div><h3>Phone check</h3><p>A code arrives by text.</p></div>
|
||||
<button class="btn btn--sm" type="button" data-run>Run</button><span class="passed" hidden>Passed</span>
|
||||
</li>
|
||||
<li data-check>
|
||||
<svg class="icon idle" aria-hidden="true"><use href="#i-camera"/></svg><svg class="icon tick" aria-hidden="true"><use href="#i-check"/></svg>
|
||||
<div><h3>Photo check</h3><p>Checked as real.</p></div>
|
||||
<button class="btn btn--sm" type="button" data-run>Run</button><span class="passed" hidden>Passed</span>
|
||||
</li>
|
||||
<li data-check>
|
||||
<svg class="icon idle" aria-hidden="true"><use href="#i-id"/></svg><svg class="icon tick" aria-hidden="true"><use href="#i-check"/></svg>
|
||||
<div><h3>ID check</h3><p>Matched to your photo.</p></div>
|
||||
<button class="btn btn--sm" type="button" data-run>Run</button><span class="passed" hidden>Passed</span>
|
||||
</li>
|
||||
<li data-check>
|
||||
<svg class="icon idle" aria-hidden="true"><use href="#i-users"/></svg><svg class="icon tick" aria-hidden="true"><use href="#i-check"/></svg>
|
||||
<div><h3>Team look</h3><p>A quick look by our team.</p></div>
|
||||
<button class="btn btn--sm" type="button" data-run>Run</button><span class="passed" hidden>Passed</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="checks__foot">
|
||||
<button class="btn btn--sm" type="button" id="v-all">Run all four</button>
|
||||
<button class="btn btn--sm" type="button" id="v-reset">Start again</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sec sec--tall" id="privacy" aria-labelledby="privacy-title">
|
||||
<div class="container grid">
|
||||
<figure class="privacy__photo" id="photo" data-reveal>
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Peterborough_%28AU%29%2C_Port_Campbell_National_Park%2C_Worm_Bay_--_2019_--_0863.jpg/1280px-Peterborough_%28AU%29%2C_Port_Campbell_National_Park%2C_Worm_Bay_--_2019_--_0863.jpg" srcset="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Peterborough_%28AU%29%2C_Port_Campbell_National_Park%2C_Worm_Bay_--_2019_--_0863.jpg/960px-Peterborough_%28AU%29%2C_Port_Campbell_National_Park%2C_Worm_Bay_--_2019_--_0863.jpg 960w, https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Peterborough_%28AU%29%2C_Port_Campbell_National_Park%2C_Worm_Bay_--_2019_--_0863.jpg/1280px-Peterborough_%28AU%29%2C_Port_Campbell_National_Park%2C_Worm_Bay_--_2019_--_0863.jpg 1280w, https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Peterborough_%28AU%29%2C_Port_Campbell_National_Park%2C_Worm_Bay_--_2019_--_0863.jpg/1920px-Peterborough_%28AU%29%2C_Port_Campbell_National_Park%2C_Worm_Bay_--_2019_--_0863.jpg 1920w" sizes="(min-width: 60rem) 58vw, 100vw" width="6718" height="4479" alt="A beach below orange sandstone cliffs, standing in for a profile photo" loading="lazy">
|
||||
<img class="blur" src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Peterborough_%28AU%29%2C_Port_Campbell_National_Park%2C_Worm_Bay_--_2019_--_0863.jpg/960px-Peterborough_%28AU%29%2C_Port_Campbell_National_Park%2C_Worm_Bay_--_2019_--_0863.jpg" width="6718" height="4479" alt="" aria-hidden="true" loading="lazy">
|
||||
<span class="privacy__lock" aria-hidden="true"><span class="privacy__disc"><svg class="icon"><use href="#i-lock"/></svg></span></span>
|
||||
<span class="privacy__badge" role="status"><svg class="icon icon--sm icon-open" aria-hidden="true"><use href="#i-eye"/></svg><svg class="icon icon--sm icon-shut" aria-hidden="true"><use href="#i-lock"/></svg><span id="photo-state">Hidden from this visitor</span></span>
|
||||
</figure>
|
||||
<form class="privacy__panel" onsubmit="return false" aria-label="Photo privacy">
|
||||
<h2 id="privacy-title" data-reveal>You choose who sees your photos</h2>
|
||||
<fieldset class="choices choices--stack" id="p-setting">
|
||||
<legend>Photos are visible to</legend>
|
||||
<input type="radio" name="setting" id="s-everyone" value="everyone"><label for="s-everyone">Everyone</label>
|
||||
<input type="radio" name="setting" id="s-verified" value="verified" checked><label for="s-verified">Verified members</label>
|
||||
<input type="radio" name="setting" id="s-accepted" value="accepted"><label for="s-accepted">People I accept</label>
|
||||
<input type="radio" name="setting" id="s-me" value="me"><label for="s-me">Only me</label>
|
||||
</fieldset>
|
||||
<fieldset class="choices" id="p-viewer">
|
||||
<legend>Right now, a visitor is</legend>
|
||||
<input type="radio" name="viewer" id="v-stranger" value="stranger" checked><label for="v-stranger">A stranger</label>
|
||||
<input type="radio" name="viewer" id="v-member" value="verified"><label for="v-member">A verified member</label>
|
||||
<input type="radio" name="viewer" id="v-accepted" value="accepted"><label for="v-accepted">Someone I accepted</label>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="band talk" id="talk" aria-labelledby="talk-title">
|
||||
<div class="container">
|
||||
<div class="talk__top"><h2 id="talk-title" data-reveal>Chat opens when both say yes</h2></div>
|
||||
<div class="grid talk__body">
|
||||
<div class="invite" data-reveal>
|
||||
<div class="invite__who">
|
||||
<span class="avatar" style="--tone:var(--tone-c)" aria-hidden="true">P</span>
|
||||
<div><h3>Priya sent you an interest</h3><p class="label">26, Sydney</p></div>
|
||||
</div>
|
||||
<div class="invite__actions" id="invite-actions">
|
||||
<button class="btn" type="button" id="t-accept">Accept</button>
|
||||
<button class="btn" type="button" id="t-decline">No thanks</button>
|
||||
<button class="btn btn--sm" type="button" id="t-reset" hidden>Start again</button>
|
||||
</div>
|
||||
<p class="invite__result" id="t-result" role="status"></p>
|
||||
</div>
|
||||
<div class="chat on-dark is-locked" id="chat" data-reveal style="--i:1">
|
||||
<ul class="chat__log" id="chat-log" role="log" aria-label="Chat with Priya"></ul>
|
||||
<form class="chat__form" id="chat-form" autocomplete="off">
|
||||
<label class="visually-hidden" for="chat-input">Message</label>
|
||||
<input id="chat-input" name="message" type="text" maxlength="300" enterkeyhint="send">
|
||||
<button class="btn btn--sm btn--primary" type="submit"><svg class="icon" aria-hidden="true"><use href="#i-send"/></svg>Send</button>
|
||||
</form>
|
||||
<div class="chat__lock"><svg class="icon" aria-hidden="true"><use href="#i-lock"/></svg><span>Chat opens when you both say yes.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sec sec--tall" id="family" aria-label="Family view and safety tools">
|
||||
<div class="container grid together">
|
||||
<div class="tile tile--wide" data-reveal>
|
||||
<h2>Family view</h2>
|
||||
<p class="lead">A parent or sibling can look at profiles with you.</p>
|
||||
<fieldset class="choices" id="fam">
|
||||
<legend>Who helps you look</legend>
|
||||
<input type="checkbox" id="h-mum" value="Mum"><label for="h-mum">Mum</label>
|
||||
<input type="checkbox" id="h-dad" value="Dad"><label for="h-dad">Dad</label>
|
||||
<input type="checkbox" id="h-sister" value="Sister"><label for="h-sister">Sister</label>
|
||||
<input type="checkbox" id="h-brother" value="Brother"><label for="h-brother">Brother</label>
|
||||
</fieldset>
|
||||
<p class="live" id="fam-status" role="status">Just you for now.</p>
|
||||
</div>
|
||||
<div class="tile tile--narrow on-dark" id="safety" data-reveal style="--i:1">
|
||||
<h2>Safety tools</h2>
|
||||
<div class="safety__who">
|
||||
<span class="avatar" style="--tone:var(--tone-d)" aria-hidden="true">J</span>
|
||||
<div><h3>Jack, 35</h3><p class="label">Melbourne</p></div>
|
||||
</div>
|
||||
<div class="safety__actions">
|
||||
<button class="btn btn--sm" type="button" id="s-block"><svg class="icon" aria-hidden="true"><use href="#i-ban"/></svg><span>Block</span></button>
|
||||
<button class="btn btn--sm" type="button" id="s-report"><svg class="icon" aria-hidden="true"><use href="#i-flag"/></svg><span>Report</span></button>
|
||||
<button class="btn btn--sm" type="button" id="s-hide" aria-pressed="false"><svg class="icon" aria-hidden="true"><use href="#i-lock"/></svg><span>Hide my profile</span></button>
|
||||
</div>
|
||||
<p class="live" id="safe-status" role="status"></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="band join" id="join" aria-labelledby="join-title">
|
||||
<div class="container grid">
|
||||
<div class="head">
|
||||
<h2 id="join-title" data-reveal>Make your profile</h2>
|
||||
<dl class="qa" id="help" data-reveal style="--i:1">
|
||||
<div><dt>What does it cost?</dt><dd>A free plan and a paid plan. Paid price: <span class="fill" data-fill="paid-price">to be announced</span></dd></div>
|
||||
<div><dt>Will I get alerts?</dt><dd>Email and phone alerts for matches, interests and messages.</dd></div>
|
||||
<div><dt>Who runs the site?</dt><dd>Our team reviews profiles and reports.</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="card" id="join-card" data-reveal style="--i:2">
|
||||
<div class="tabs" role="tablist" aria-label="Join or log in">
|
||||
<button type="button" role="tab" id="tab-join" aria-selected="true" aria-controls="panel-join">Join</button>
|
||||
<button type="button" role="tab" id="tab-login" aria-selected="false" aria-controls="panel-login" tabindex="-1">Log in</button>
|
||||
</div>
|
||||
<div class="panel" role="tabpanel" id="panel-join" aria-labelledby="tab-join">
|
||||
<form data-endpoint="" novalidate class="panel">
|
||||
<div class="field"><label for="j-name">First name</label><input id="j-name" name="name" type="text" autocomplete="given-name" required></div>
|
||||
<div class="field"><label for="j-email">Email address</label><input id="j-email" name="email" type="email" autocomplete="email" inputmode="email" required></div>
|
||||
<div class="field"><label for="j-for">Joining for</label>
|
||||
<select id="j-for" name="for"><option>Myself</option><option>My son</option><option>My daughter</option><option>My brother or sister</option></select>
|
||||
</div>
|
||||
<button class="btn btn--primary" type="submit">Create your profile<svg class="icon" aria-hidden="true"><use href="#i-arrow"/></svg></button>
|
||||
<p class="form-help" aria-live="polite"></p>
|
||||
</form>
|
||||
</div>
|
||||
<div class="panel" role="tabpanel" id="panel-login" aria-labelledby="tab-login">
|
||||
<form data-endpoint="" novalidate class="panel">
|
||||
<div class="field"><label for="l-email">Email address</label><input id="l-email" name="email" type="email" autocomplete="email" inputmode="email" required></div>
|
||||
<button class="btn btn--primary" type="submit">Log in<svg class="icon" aria-hidden="true"><use href="#i-arrow"/></svg></button>
|
||||
<p class="form-help" aria-live="polite"></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="foot on-dark">
|
||||
<div class="container">
|
||||
<p class="foot__sign">Warmly, the BeingOne team.</p>
|
||||
<div class="foot__row">
|
||||
<p>For Australians and their families. <time datetime="2026-10">October 2026</time></p>
|
||||
<ul class="foot__links">
|
||||
<li><a href="#find">Discover</a></li>
|
||||
<li><a href="#verified">Checks</a></li>
|
||||
<li><a href="#privacy">Photos</a></li>
|
||||
<li><a href="#talk">Messages</a></li>
|
||||
<li><a href="#join">Join</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p data-credits>Photographs: beach by <a href="https://commons.wikimedia.org/wiki/File:Peterborough_(AU),_Port_Campbell_National_Park,_Worm_Bay_--_2019_--_0863.jpg">Dietmar Rabich</a> (CC BY-SA 4.0, Wikimedia Commons)</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>document.querySelectorAll('[data-slot] img').forEach((i) => { if (i.complete && !i.naturalWidth) i.remove(); else i.addEventListener('error', () => i.remove(), { once: true }); });</script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,309 +0,0 @@
|
||||
/* Hallmark · shape: Split Hero With Floating Search · genre: editorial · theme: custom (by request: the user's own reference screenshot) · job: sign up · idea: "a marriage site that opens like a wedding morning: warm paper, a serif that speaks plainly, and every demo a visitor can touch" · vibe: "warm cream, navy ink, quietly lively"
|
||||
* paper: oklch(98.4% 0.012 85) · accent: oklch(56% 0.15 38) · display: DM Serif Display · body: Figtree · hero: text left, the couple's photo bleeding off the right edge, a search bar floating across its foot · nav: edge-aligned · footer: letter close · imagery: photographs, user's own 1, library 1, natural · signature: the floating search bar that drives the Australia map and the scored matches
|
||||
* axes: light/roman-serif/warm · posture: restrained · contrast: pass · fold: pass · v2.0.0 */
|
||||
/* Hallmark · pre-emit critique: P4 H4 E4 S4 R3 V3 · weakest: R, the page now holds a lot of motion; cut every loop and kept only one-time entrances, hover lifts and state changes */
|
||||
/* Hallmark · waive 65 · the user's own reference screenshot pins cream paper, a high-contrast serif and a terracotta accent for BeingOne · the look is the client's brief, not a model default */
|
||||
/* Hallmark · waive 38ii · guard: true italic of the display face is loaded, font-synthesis none, one line of one h1 only · the closing line of the hero head mirrors the BeingOne reference the user supplied */
|
||||
/* Hallmark · waive 38iii · one italic closing line of the hero head, once on the page · it mirrors the BeingOne reference the user supplied */
|
||||
/* Hallmark · dismissed 59 · the path strip under the search bar is one row of five links that shows how the visitor moves through BeingOne, so it is wayfinding and not a content section */
|
||||
/* Hallmark · dismissed F7 · the pill radius sits on controls only: nav links, choice chips, map pins and their ping ring are all pressed to act */
|
||||
@import url("./tokens.css");
|
||||
@import url("./base.css");
|
||||
|
||||
:root {
|
||||
--color-navy-3: oklch(20% 0.06 255);
|
||||
--color-accent-on-dark: oklch(70% 0.14 38);
|
||||
--nav-h: 76px;
|
||||
--color-rule-on-navy: oklch(44% 0.05 255);
|
||||
}
|
||||
|
||||
/* type */
|
||||
h1, h2 { font-weight: 400; letter-spacing: var(--tracking-display); }
|
||||
h1 { max-width: none; font-size: clamp(3.25rem, 6.4vw, 6rem); line-height: 1; }
|
||||
h2 { max-width: 22ch; font-size: clamp(2.25rem, 3.6vw, 3.5rem); line-height: 1.04; }
|
||||
h3 { max-width: 40ch; font-weight: 400; font-size: var(--text-title); line-height: 1.2; letter-spacing: -0.01em; }
|
||||
.lead { font-size: var(--text-lead); line-height: 1.45; max-width: 46ch; color: var(--color-ink-2); }
|
||||
.label { font-size: var(--text-sm); font-weight: 700; color: var(--color-muted); }
|
||||
.muted { color: var(--color-muted); }
|
||||
body { font-weight: 500; }
|
||||
|
||||
/* local dark surfaces: components keep using the same tokens */
|
||||
.on-dark { --color-ink: var(--color-on-navy); --color-ink-2: var(--color-on-navy); --color-muted: var(--color-on-navy-muted); --color-paper-2: var(--color-navy-2); --color-paper-3: var(--color-navy-3); --color-rule: var(--color-rule-on-navy); --color-card: var(--color-navy-2); --color-card-ink: var(--color-on-navy); background: var(--color-navy); color: var(--color-on-navy); }
|
||||
|
||||
/* frame */
|
||||
.skip { position: absolute; left: var(--space-md); top: calc(-1 * var(--space-3xl)); z-index: var(--z-toast); padding: var(--space-xs) var(--space-md); background: var(--color-card); color: var(--color-card-ink); border-radius: var(--radius-control); font-weight: 700; }
|
||||
.skip:focus-visible { top: var(--space-md); }
|
||||
.grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--gap-columns); }
|
||||
.sec { padding-block: var(--space-3xl); }
|
||||
.sec--tall { padding-block: var(--space-4xl); }
|
||||
.band { background: var(--color-paper-2); padding-block: var(--section-gap-s); }
|
||||
.band--navy { background: var(--color-navy); color: var(--color-on-navy); padding-block: var(--section-gap-s); }
|
||||
.head { display: grid; gap: var(--space-md); align-content: start; }
|
||||
.icon { width: 24px; height: 24px; flex-shrink: 0; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.icon--sm { width: 20px; height: 20px; }
|
||||
.fill { padding: 0 0.25em; border: 1px dashed color-mix(in oklch, var(--color-muted) 70%, transparent); border-radius: 3px; color: var(--color-muted); -webkit-box-decoration-break: clone; box-decoration-break: clone; }
|
||||
|
||||
/* nav: edge-aligned */
|
||||
.nav { position: sticky; top: 0; z-index: var(--z-sticky-nav); height: var(--nav-h); background: var(--color-paper); border-bottom: var(--rule-hair) solid var(--color-rule); }
|
||||
.nav__bar { height: 100%; display: flex; align-items: center; justify-content: space-between; gap: var(--space-lg); }
|
||||
.wordmark { display: inline-flex; align-items: center; gap: var(--space-xs); font: 400 var(--text-2xl)/1 var(--font-display); letter-spacing: -0.01em; text-decoration: none; color: var(--color-ink); }
|
||||
.wordmark b { font-weight: 400; color: var(--color-accent); }
|
||||
.mark { width: 34px; height: 34px; fill: none; stroke: var(--color-accent); stroke-width: 2.5; transition: transform var(--dur-long) var(--ease-out); }
|
||||
.wordmark:hover .mark { transform: rotate(-18deg); }
|
||||
.nav__links { display: flex; align-items: center; gap: var(--space-xs); }
|
||||
.nav__links a { padding: var(--space-xs) var(--space-md); border-radius: var(--radius-pill); text-decoration: none; font-weight: 700; white-space: nowrap; transition: background-color var(--dur-short) var(--ease-out); }
|
||||
.nav__links a:hover { background: var(--color-paper-2); }
|
||||
.nav__actions { display: flex; align-items: center; gap: var(--space-md); }
|
||||
.nav__signin { white-space: nowrap; font-weight: 700; color: var(--color-accent); text-decoration: none; padding: var(--space-xs); }
|
||||
.nav__signin:hover { text-decoration: underline; text-decoration-thickness: 2px; }
|
||||
.nav__actions .nav__menu { display: none; }
|
||||
.nav__links .only-sm { display: none; }
|
||||
@media (max-width: 68rem) {
|
||||
.nav__actions .nav__menu { display: inline-flex; }
|
||||
.nav__links { position: absolute; top: var(--nav-h); left: 0; right: 0; flex-direction: column; align-items: stretch; gap: 0; padding: var(--space-md) var(--gutter); background: var(--color-paper); border-bottom: var(--rule-hair) solid var(--color-rule); display: none; }
|
||||
.nav__links.is-open { display: flex; }
|
||||
.nav__links a { padding: var(--space-md) 0; border-radius: 0; border-top: var(--rule-hair) solid var(--color-rule); }
|
||||
.nav__links a:first-child { border-top: 0; }
|
||||
}
|
||||
@media (max-width: 40rem) { .nav__signin { display: none; } .nav__links .only-sm { display: block; } .nav__actions { gap: var(--space-xs); } }
|
||||
|
||||
/* controls */
|
||||
.btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-xs); min-height: 52px; padding: 0 var(--space-lg); border: var(--rule-bar) solid var(--color-ink); border-radius: var(--radius-control); background: transparent; color: var(--color-ink); font: 700 var(--text-md)/1.25 var(--font-body); text-decoration: none; white-space: nowrap; cursor: pointer; transition: transform var(--dur-short) var(--ease-out), background-color var(--dur-short) var(--ease-out), color var(--dur-short) var(--ease-out), border-color var(--dur-short) var(--ease-out), box-shadow var(--dur-short) var(--ease-out); }
|
||||
.btn:hover { background: var(--color-paper-3); }
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn--sm { min-height: 44px; padding: 0 var(--space-md); font-size: var(--text-sm); }
|
||||
.btn--primary { background: var(--color-accent); border-color: var(--color-accent); color: var(--color-accent-ink); box-shadow: 0 14px 24px -14px color-mix(in oklab, var(--color-accent) 80%, transparent); }
|
||||
.btn--primary:hover { background: var(--color-accent-2); border-color: var(--color-accent-2); transform: translateY(-2px); }
|
||||
.btn--primary:active { transform: translateY(1px); }
|
||||
.btn .icon { width: 20px; height: 20px; transition: transform var(--dur-short) var(--ease-out); }
|
||||
.btn:hover .icon { transform: translateX(4px); }
|
||||
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; transform: none; }
|
||||
.btn[aria-busy="true"] { cursor: progress; }
|
||||
.btn[aria-pressed="true"] { background: var(--color-paper-3); box-shadow: inset 0 0 0 2px var(--color-ink); }
|
||||
.on-dark .btn--primary { color: var(--color-accent-ink); }
|
||||
.on-dark .btn--primary:hover { background: var(--color-accent-2); }
|
||||
|
||||
.choices { position: relative; display: flex; flex-wrap: wrap; gap: var(--space-xs); border: 0; margin: 0; padding: 0; min-width: 0; }
|
||||
.choices legend { padding: 0; margin-bottom: var(--space-sm); font-size: var(--text-sm); font-weight: 700; color: var(--color-muted); }
|
||||
.choices input { position: absolute; opacity: 0; width: 1px; height: 1px; }
|
||||
.choices label { display: inline-flex; align-items: center; min-height: 44px; padding: 0 var(--space-md); border: var(--rule-bar) solid var(--color-rule); border-radius: var(--radius-pill); background: var(--color-card); font-weight: 700; font-size: var(--text-sm); cursor: pointer; transition: transform var(--dur-short) var(--ease-out), background-color var(--dur-short) var(--ease-out), color var(--dur-short) var(--ease-out), border-color var(--dur-short) var(--ease-out); }
|
||||
.choices label:hover { border-color: var(--color-ink); }
|
||||
.choices label:active { transform: translateY(1px); }
|
||||
.choices input:checked + label { background: var(--color-ink); border-color: var(--color-ink); color: var(--color-paper); }
|
||||
.on-dark .choices input:checked + label { color: var(--color-navy); }
|
||||
.choices input:focus-visible + label { outline: 2px solid var(--color-focus); outline-offset: 3px; }
|
||||
.choices--stack { flex-direction: column; flex-wrap: nowrap; }
|
||||
.choices--stack label { border-radius: var(--radius-control); width: 100%; }
|
||||
|
||||
.field { display: grid; gap: var(--space-xs); }
|
||||
.field label { font-size: var(--text-sm); font-weight: 700; }
|
||||
.field input:not([type="range"]), .field select, .chat__form input { width: 100%; min-height: 52px; padding: 0 var(--space-md); border: var(--rule-bar) solid var(--color-muted); border-radius: var(--radius-control); background: var(--color-card); color: var(--color-card-ink); outline: 2px solid transparent; outline-offset: -3px; transition: border-color var(--dur-short) var(--ease-out); }
|
||||
.field input:not([type="range"]):hover, .field select:hover { border-color: var(--color-ink); }
|
||||
.field input:not([type="range"]):focus-visible, .field select:focus-visible, .chat__form input:focus-visible { outline-color: var(--color-focus); border-color: var(--color-focus); }
|
||||
.field input[aria-invalid] { border-color: var(--color-error); }
|
||||
.field input[type="range"] { width: 100%; accent-color: var(--color-accent); min-height: 32px; cursor: pointer; }
|
||||
.field output { font: 400 var(--text-xl)/1 var(--font-display); font-variant-numeric: tabular-nums; }
|
||||
.field__row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-md); }
|
||||
|
||||
/* hero: photo bleeds off the right edge, head on the left */
|
||||
.hero { position: relative; background: var(--color-paper); }
|
||||
.hero__inner { position: relative; z-index: 2; padding-block: var(--space-2xl) var(--space-xl); }
|
||||
.hero__text { display: grid; gap: var(--space-lg); justify-items: start; max-width: 46%; }
|
||||
.hero h1 .line { display: block; overflow: hidden; padding: 0 0.12em 0.12em 0; margin-bottom: -0.12em; }
|
||||
.hero h1 .line > span { display: block; animation: line-up 1000ms var(--ease-out) backwards; animation-delay: calc(var(--i) * 120ms + 120ms); }
|
||||
.hero h1 .line--it > span { font-style: italic; }
|
||||
.hero__lead { animation: rise 800ms var(--ease-out) 650ms backwards; }
|
||||
.hero__cta { animation: rise 800ms var(--ease-out) 780ms backwards; }
|
||||
.hero__photo { position: absolute; z-index: 1; top: 0; right: 0; bottom: var(--space-2xl); width: 50%; margin: 0; border-radius: var(--radius-card) 0 0 var(--radius-card); animation: curtain 1300ms var(--ease-in-out) 200ms backwards; }
|
||||
.hero__photo > img { object-position: 60% 40%; animation: settle 2200ms var(--ease-out) 200ms backwards; }
|
||||
@keyframes line-up { from { transform: translateY(110%); } }
|
||||
@keyframes rise { from { opacity: 0; transform: translateY(16px); } }
|
||||
@keyframes curtain { from { clip-path: inset(0 0 0 100% round var(--radius-card)); } to { clip-path: inset(0 0 0 0 round var(--radius-card)); } }
|
||||
@keyframes settle { from { transform: scale(1.16); } }
|
||||
@media (max-width: 60rem) {
|
||||
.hero__inner { padding-block: var(--space-2xl) var(--space-xl); }
|
||||
.hero__text { max-width: none; }
|
||||
.hero__photo { position: relative; width: 100%; aspect-ratio: 4 / 3; border-radius: 0; }
|
||||
.hero { display: flex; flex-direction: column; }
|
||||
.hero__inner { order: 1; }
|
||||
.hero__photo { order: 2; }
|
||||
.quick__wrap { order: 3; }
|
||||
}
|
||||
|
||||
/* floating search bar */
|
||||
.quick__wrap { position: relative; z-index: 5; margin-bottom: calc(-1 * var(--space-2xl)); }
|
||||
.quick { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)) auto; gap: var(--space-md); align-items: end; max-width: 1120px; margin-inline: auto; padding: var(--space-lg); background: var(--color-card); border-radius: var(--radius-card); box-shadow: var(--shadow-card); animation: rise-lg 900ms var(--ease-out) 900ms backwards; }
|
||||
.quick__field { display: grid; gap: var(--space-2xs); }
|
||||
.quick__field label { font-size: var(--text-sm); font-weight: 700; color: var(--color-muted); }
|
||||
.quick select { width: 100%; min-height: 52px; padding: 0 var(--space-md); border: var(--rule-bar) solid var(--color-muted); border-radius: var(--radius-control); background: var(--color-card); color: var(--color-card-ink); font-weight: 700; transition: border-color var(--dur-short) var(--ease-out); }
|
||||
.quick select:hover { border-color: var(--color-ink); }
|
||||
.quick select:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
|
||||
.quick__go { min-width: 160px; }
|
||||
@keyframes rise-lg { from { opacity: 0; transform: translateY(40px); } }
|
||||
@media (max-width: 60rem) { .quick__wrap { margin-bottom: 0; padding-block: var(--space-lg); } .quick { grid-template-columns: minmax(0, 1fr); } }
|
||||
|
||||
/* path: a real ol */
|
||||
.band--path { padding-block: calc(var(--space-2xl) + var(--space-md)) var(--space-2xl); }
|
||||
.path { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--gap-columns); counter-reset: step; }
|
||||
.path li { counter-increment: step; position: relative; display: grid; gap: var(--space-2xs); align-content: start; padding-top: var(--space-lg); }
|
||||
.path li::after { content: ""; position: absolute; inset: 0 0 auto 0; height: 3px; background: var(--color-ink); transform-origin: left; transition: transform 1000ms var(--ease-out); transition-delay: calc(var(--i, 0) * 120ms + 150ms); }
|
||||
.js .path li:not(.is-in)::after { transform: scaleX(0); }
|
||||
.path li::before { content: counter(step); font: 400 clamp(3.5rem, 6vw, 5.25rem)/0.95 var(--font-display); color: var(--color-accent); font-variant-numeric: lining-nums tabular-nums; }
|
||||
.path a { font: 700 var(--text-lg)/1.15 var(--font-body); text-decoration: none; }
|
||||
.path a:hover { text-decoration: underline; text-decoration-color: var(--color-accent); text-decoration-thickness: 3px; text-underline-offset: 0.25em; }
|
||||
@media (max-width: 60rem) { .path { grid-template-columns: minmax(0, 1fr); gap: var(--space-md); } .path li { grid-template-columns: 72px minmax(0, 1fr); align-items: center; } .path li::before { font-size: var(--text-3xl); } }
|
||||
|
||||
/* finder */
|
||||
.finder__top { align-items: end; margin-bottom: var(--space-2xl); }
|
||||
.finder__top h2 { grid-column: 1 / 8; }
|
||||
.finder__top .lead { grid-column: 8 / 13; }
|
||||
.finder__body { align-items: stretch; }
|
||||
.map { grid-column: 1 / 6; position: relative; aspect-ratio: 605 / 516; background: var(--color-paper-2); border-radius: var(--radius-card); }
|
||||
.map svg { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
.map path { fill: var(--color-land); stroke: var(--color-card); stroke-width: 3; stroke-linejoin: round; }
|
||||
.pin { position: absolute; left: var(--x); top: var(--y); width: 36px; height: 36px; transform: translate(-50%, -50%); padding: 0; border: 0; background: transparent; cursor: pointer; border-radius: var(--radius-pill); }
|
||||
.pin::before { content: ""; position: absolute; inset: 10px; border-radius: var(--radius-pill); background: var(--color-ink); box-shadow: 0 0 0 3px var(--color-card); transition: transform var(--dur-short) var(--ease-out), background-color var(--dur-short) var(--ease-out); }
|
||||
.js .map.is-in .pin::before { animation: pop 600ms var(--ease-out) backwards; animation-delay: calc(var(--i) * 90ms + 250ms); }
|
||||
.pin:hover::before { transform: scale(1.4); }
|
||||
.pin[aria-pressed="true"]::before { background: var(--color-accent); transform: scale(1.9); }
|
||||
.pin:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 0; }
|
||||
.ping { position: absolute; inset: 8px; border-radius: var(--radius-pill); border: 2px solid var(--color-accent); pointer-events: none; animation: ping 800ms var(--ease-out) both; }
|
||||
.map__city { position: absolute; left: var(--space-md); bottom: var(--space-md); padding: var(--space-xs) var(--space-md); border-radius: var(--radius-control); background: var(--color-card); box-shadow: var(--shadow-card); font-size: var(--text-sm); font-weight: 700; }
|
||||
@keyframes pop { from { transform: scale(0); } }
|
||||
@keyframes ping { from { opacity: 0.9; transform: scale(0.6); } to { opacity: 0; transform: scale(2.8); } }
|
||||
.controls { grid-column: 6 / 13; display: grid; gap: var(--space-xl); align-content: center; padding: var(--space-xl); background: var(--color-paper-2); border-radius: var(--radius-card); }
|
||||
.controls__ages { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--gap-columns); }
|
||||
.results { margin-top: var(--space-xl); display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--gap-columns); min-height: 300px; scroll-margin-top: var(--space-3xl); }
|
||||
.person { display: grid; gap: var(--space-md); align-content: space-between; padding: var(--space-lg); background: var(--color-card); border-radius: var(--radius-card); box-shadow: var(--shadow-card); animation: rise var(--dur-long) var(--ease-out) backwards; animation-delay: calc(var(--i, 0) * 80ms); transition: transform var(--dur-short) var(--ease-out), box-shadow var(--dur-short) var(--ease-out); }
|
||||
.person:hover { transform: translateY(-5px); box-shadow: var(--shadow-lift); }
|
||||
.person__who { display: flex; align-items: center; gap: var(--space-md); }
|
||||
.person h3 { font-size: var(--text-xl); }
|
||||
.avatar { width: 56px; height: 56px; flex-shrink: 0; display: grid; place-items: center; border-radius: var(--radius-pill); background: var(--tone, var(--tone-a)); color: var(--color-navy); font: 400 var(--text-xl)/1 var(--font-display); }
|
||||
.score { display: flex; align-items: baseline; gap: var(--space-xs); }
|
||||
.score b { font: 400 var(--text-3xl)/1 var(--font-display); font-variant-numeric: lining-nums tabular-nums; }
|
||||
.bar { height: 8px; margin-top: var(--space-xs); border-radius: var(--radius-pill); background: var(--color-paper-3); overflow: hidden; }
|
||||
.bar i { display: block; height: 100%; background: var(--color-accent); transform-origin: left; transform: scaleX(calc(var(--s, 0) / 100)); animation: grow 1100ms var(--ease-out) backwards; animation-delay: calc(var(--i, 0) * 80ms + 200ms); }
|
||||
.tags { display: flex; flex-wrap: wrap; gap: var(--space-xs); padding: 0; margin: 0; list-style: none; }
|
||||
.tags li { padding: var(--space-3xs) var(--space-sm); border-radius: var(--radius-pill); border: var(--rule-bar) solid var(--color-rule); font-size: var(--text-sm); font-weight: 700; transition: background-color var(--dur-short) var(--ease-out); }
|
||||
.tags li.is-shared { background: var(--color-paper-3); border-color: var(--color-paper-3); }
|
||||
.results__empty { grid-column: 1 / -1; align-self: center; font: 400 var(--text-2xl)/1.2 var(--font-display); max-width: 30ch; animation: rise var(--dur-long) var(--ease-out) backwards; }
|
||||
.finder__note { margin-top: var(--space-md); font-size: var(--text-sm); color: var(--color-muted); }
|
||||
@keyframes grow { from { transform: scaleX(0); } }
|
||||
@media (max-width: 60rem) {
|
||||
.finder__top h2, .finder__top .lead, .map, .controls { grid-column: 1 / -1; }
|
||||
.finder__top .lead { margin-top: var(--space-md); }
|
||||
.controls { margin-top: var(--space-lg); padding: var(--space-lg); }
|
||||
.results { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 40rem) { .results { grid-template-columns: minmax(0, 1fr); } .controls__ages { grid-template-columns: minmax(0, 1fr); } }
|
||||
|
||||
/* checks */
|
||||
.checks .head { grid-column: 1 / 6; }
|
||||
.checks__panel { grid-column: 7 / 13; background: var(--color-paper-2); border-radius: var(--radius-card); padding: var(--space-lg); display: grid; gap: var(--space-md); }
|
||||
.checks__who { display: flex; align-items: center; gap: var(--space-md); }
|
||||
.checks__who h3 { font-size: var(--text-xl); }
|
||||
.status { margin-left: auto; display: inline-flex; align-items: center; gap: var(--space-xs); padding: var(--space-xs) var(--space-md); border-radius: var(--radius-pill); background: var(--color-paper-3); font-size: var(--text-sm); font-weight: 700; }
|
||||
.status .icon { display: none; }
|
||||
.status[data-state="done"] { background: var(--color-on-navy); color: var(--color-navy); animation: settle-in 600ms var(--ease-out) both; }
|
||||
.status[data-state="done"] .icon { display: block; }
|
||||
@keyframes settle-in { from { transform: scale(0.9); opacity: 0.4; } }
|
||||
.progress { height: 6px; border-radius: var(--radius-pill); background: var(--color-paper-3); overflow: hidden; }
|
||||
.progress i { display: block; height: 100%; background: var(--color-accent-on-dark); transform-origin: left; transform: scaleX(var(--p, 0)); transition: transform 700ms var(--ease-out); }
|
||||
.rows { list-style: none; margin: 0; padding: 0; }
|
||||
.rows > li { display: grid; grid-template-columns: 24px minmax(0, 1fr) auto; gap: var(--space-md); align-items: center; padding-block: var(--space-md); border-top: var(--rule-hair) solid var(--color-rule); }
|
||||
.rows h3 { font: 700 var(--text-md)/1.3 var(--font-body); letter-spacing: 0; }
|
||||
.rows p { font-size: var(--text-sm); color: var(--color-muted); }
|
||||
.rows .tick { display: none; color: var(--color-accent-on-dark); }
|
||||
.rows .is-done .tick { display: block; animation: pop 500ms var(--ease-out) backwards; }
|
||||
.rows .is-done .idle { display: none; }
|
||||
.rows .passed { font-weight: 700; font-size: var(--text-sm); }
|
||||
.checks__foot { display: flex; justify-content: space-between; align-items: center; gap: var(--space-md); border-top: var(--rule-hair) solid var(--color-rule); padding-top: var(--space-md); }
|
||||
@media (max-width: 60rem) { .checks .head, .checks__panel { grid-column: 1 / -1; } .checks__panel { margin-top: var(--space-xl); } }
|
||||
@media (max-width: 40rem) { .rows > li { grid-template-columns: 24px minmax(0, 1fr); } .rows .btn, .rows .passed { grid-column: 2; justify-self: start; } .checks__who { flex-wrap: wrap; } }
|
||||
|
||||
/* photo privacy */
|
||||
.privacy__photo { grid-column: 1 / 8; position: relative; aspect-ratio: 4 / 3; border-radius: var(--radius-card); overflow: hidden; background: var(--color-paper-3); box-shadow: var(--shadow-card); margin: 0; }
|
||||
.privacy__photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 35% 40%; filter: saturate(1.06) contrast(1.04); }
|
||||
.privacy__photo .blur { filter: blur(28px) saturate(1.06); transform: scale(1.15); transition: opacity var(--dur-long) var(--ease-in-out); }
|
||||
.js .privacy__photo:not(.is-hidden) .blur { opacity: 0; }
|
||||
.privacy__badge { position: absolute; left: var(--space-lg); bottom: var(--space-lg); display: inline-flex; align-items: center; gap: var(--space-xs); padding: var(--space-xs) var(--space-md); border-radius: var(--radius-pill); background: var(--color-card); font-size: var(--text-sm); font-weight: 700; z-index: 2; box-shadow: var(--shadow-card); }
|
||||
.privacy__lock { position: absolute; inset: 0; display: grid; place-items: center; z-index: 2; transition: opacity var(--dur-long) var(--ease-in-out); }
|
||||
.privacy__disc { display: grid; place-items: center; width: 112px; height: 112px; border-radius: var(--radius-pill); background: var(--color-card); box-shadow: var(--shadow-card); transition: transform var(--dur-long) var(--ease-out); }
|
||||
.privacy__disc .icon { width: 44px; height: 44px; }
|
||||
.js .privacy__photo:not(.is-hidden) .privacy__lock { opacity: 0; }
|
||||
.js .privacy__photo:not(.is-hidden) .privacy__disc { transform: scale(0.7); }
|
||||
.privacy__badge .icon-open, .privacy__badge .icon-shut { display: none; }
|
||||
.privacy__photo:not(.is-hidden) .icon-open { display: block; }
|
||||
.privacy__photo.is-hidden .icon-shut { display: block; }
|
||||
.privacy__panel { grid-column: 9 / 13; display: grid; gap: var(--space-lg); align-content: center; }
|
||||
@media (max-width: 60rem) { .privacy__photo, .privacy__panel { grid-column: 1 / -1; } .privacy__panel { margin-top: var(--space-xl); } }
|
||||
|
||||
/* talk */
|
||||
.talk__top { margin-bottom: var(--space-2xl); }
|
||||
.talk__body { align-items: stretch; }
|
||||
.invite { grid-column: 1 / 6; display: grid; gap: var(--space-lg); align-content: space-between; padding: var(--space-xl); background: var(--color-card); border-radius: var(--radius-card); box-shadow: var(--shadow-card); }
|
||||
.invite__who { display: flex; align-items: center; gap: var(--space-md); }
|
||||
.invite h3 { font-size: var(--text-xl); }
|
||||
.invite__actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
|
||||
.invite__result { min-height: 1lh; font-weight: 700; }
|
||||
.chat { grid-column: 6 / 13; position: relative; display: grid; grid-template-rows: minmax(0, 1fr) auto; min-height: 380px; padding: var(--space-lg); border-radius: var(--radius-card); }
|
||||
.chat__log { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; justify-content: flex-end; gap: var(--space-xs); overflow-y: auto; }
|
||||
.chat__log li { max-width: 80%; padding: var(--space-xs) var(--space-md); border-radius: var(--radius-control); background: var(--color-paper-2); animation: rise var(--dur-short) var(--ease-out) backwards; overflow-wrap: break-word; }
|
||||
.chat__log li.me { align-self: flex-end; background: var(--color-accent); color: var(--color-accent-ink); }
|
||||
.chat__log li.note { align-self: center; max-width: 100%; background: transparent; color: var(--color-muted); font-size: var(--text-sm); font-weight: 700; }
|
||||
.chat__form { display: flex; gap: var(--space-sm); margin-top: var(--space-md); }
|
||||
.chat__form input { flex: 1; min-width: 0; }
|
||||
.chat.is-locked .chat__log, .chat.is-locked .chat__form { visibility: hidden; }
|
||||
.chat__lock { position: absolute; inset: 0; display: none; place-items: center; align-content: center; gap: var(--space-sm); text-align: center; padding: var(--space-lg); font: 400 var(--text-2xl)/1.2 var(--font-display); }
|
||||
.chat__lock .icon { width: 48px; height: 48px; margin-inline: auto; }
|
||||
.chat.is-locked .chat__lock { display: grid; }
|
||||
@media (max-width: 60rem) { .invite, .chat { grid-column: 1 / -1; } .chat { margin-top: var(--space-lg); } }
|
||||
|
||||
/* family and safety */
|
||||
.together { align-items: stretch; row-gap: var(--gap-columns); }
|
||||
.tile { display: grid; gap: var(--space-lg); align-content: start; padding: var(--space-xl); background: var(--color-paper-2); border-radius: var(--radius-card); }
|
||||
.tile--wide { grid-column: 1 / 8; }
|
||||
.tile--narrow { grid-column: 8 / 13; }
|
||||
.tile h2 { font-size: clamp(2rem, 3vw, 3rem); }
|
||||
.tile .lead { color: var(--color-ink-2); }
|
||||
.safety__who { display: flex; align-items: center; gap: var(--space-md); }
|
||||
.safety__who h3 { font-size: var(--text-xl); }
|
||||
.safety__actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
|
||||
.live { min-height: 1lh; font-weight: 700; }
|
||||
@media (max-width: 60rem) { .tile--wide, .tile--narrow { grid-column: 1 / -1; } .tile { padding: var(--space-lg); } }
|
||||
|
||||
/* questions and join */
|
||||
.join .head { grid-column: 1 / 7; align-content: start; gap: var(--space-xl); }
|
||||
.qa { margin: 0; display: grid; }
|
||||
.qa > div { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: var(--space-md); padding-block: var(--space-md); border-top: var(--rule-hair) solid var(--color-rule); }
|
||||
.qa dt { font: 700 var(--text-md)/1.3 var(--font-body); }
|
||||
.qa dd { margin: 0; color: var(--color-ink-2); }
|
||||
.card { grid-column: 8 / 13; background: var(--color-card); color: var(--color-card-ink); border-radius: var(--radius-card); box-shadow: var(--shadow-card); padding: var(--space-xl); display: grid; gap: var(--space-lg); align-content: start; }
|
||||
.tabs { display: flex; gap: var(--space-xs); border-bottom: var(--rule-bar) solid var(--color-rule); }
|
||||
.tabs button { min-height: 44px; padding: 0 var(--space-md); border: 0; border-bottom: 4px solid transparent; margin-bottom: calc(-1 * var(--space-3xs)); background: transparent; color: inherit; font-weight: 700; cursor: pointer; transition: border-color var(--dur-short) var(--ease-out); }
|
||||
.tabs button:hover { border-bottom-color: var(--color-rule); }
|
||||
.tabs button[aria-selected="true"] { border-bottom-color: var(--color-accent); }
|
||||
.form-help { min-height: 1lh; font-size: var(--text-sm); }
|
||||
.form-help.is-error { color: var(--color-error); font-weight: 700; }
|
||||
.panel { display: grid; gap: var(--space-md); }
|
||||
@media (max-width: 60rem) { .join .head, .card { grid-column: 1 / -1; } .card { margin-top: var(--space-xl); padding: var(--space-lg); } .qa > div { grid-template-columns: minmax(0, 1fr); gap: var(--space-2xs); } }
|
||||
|
||||
/* footer: letter close */
|
||||
.foot { padding-block: var(--space-3xl) var(--space-xl); }
|
||||
.foot__sign { font: 400 var(--text-display-s)/1.05 var(--font-display); letter-spacing: var(--tracking-display); max-width: 18ch; text-wrap: balance; }
|
||||
.foot__row { margin-top: var(--space-2xl); display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-lg); font-size: var(--text-sm); }
|
||||
.foot__links { display: flex; flex-wrap: wrap; gap: var(--space-lg); list-style: none; margin: 0; padding: 0; }
|
||||
.foot__links a { font-weight: 700; }
|
||||
.foot__links a:hover { text-decoration-color: var(--color-accent-on-dark); text-decoration-thickness: 3px; }
|
||||
.foot [data-credits] { margin-top: var(--space-lg); color: var(--color-muted); font-size: var(--text-xs); max-width: 80ch; }
|
||||
|
||||
/* one scroll reveal, visible without script */
|
||||
.js [data-reveal] { opacity: 0; transform: translateY(18px); transition: opacity 800ms var(--ease-out), transform 800ms var(--ease-out); transition-delay: calc(var(--i, 0) * 110ms); }
|
||||
.js [data-reveal].is-in { opacity: 1; transform: none; }
|
||||
.js .person[data-reveal], .js li[data-reveal] { transition-property: opacity, transform; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.js [data-reveal] { opacity: 1; transform: none; transition: none; }
|
||||
.hero h1 .line > span, .hero__lead, .hero__cta, .hero__photo, .hero__photo > img, .quick, .person, .bar i, .chat__log li, .ping, .status, .rows .is-done .tick, .js .map.is-in .pin::before { animation: none; }
|
||||
.bar i { transform: scaleX(calc(var(--s, 0) / 100)); }
|
||||
.js .path li:not(.is-in)::after { transform: none; }
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
:root {
|
||||
--space-3xs: 2px; --space-2xs: 4px; --space-xs: 8px; --space-sm: 12px; --space-md: 16px; --space-lg: 24px; --space-xl: 40px; --space-2xl: 64px; --space-3xl: 96px; --space-4xl: 144px;
|
||||
--text-xs: 0.8125rem; --text-sm: 0.875rem; --text-md: 1rem; --text-lg: 1.25rem; --text-xl: 1.5625rem; --text-2xl: 1.953rem; --text-3xl: 2.441rem;
|
||||
--text-display-s: clamp(2.25rem, 4vw + 0.5rem, 3.75rem); --text-display: clamp(2.75rem, 5vw + 1rem, 5.25rem);
|
||||
--text-section: clamp(2rem, 2.5vw + 1rem, 3.5rem); --text-title: clamp(1.25rem, 0.75vw + 0.875rem, 1.625rem); --text-lead: clamp(1.125rem, 0.35vw + 1rem, 1.375rem);
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); --ease-in: cubic-bezier(0.7, 0, 0.84, 0); --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
--dur-micro: 120ms; --dur-short: 220ms; --dur-long: 420ms;
|
||||
--color-error: oklch(52% 0.19 27); --color-success: oklch(50% 0.12 150);
|
||||
--container: 1440px; --gutter: 64px; --gap-columns: 32px; --section-gap-s: 120px; --section-gap: 144px; --section-gap-l: 176px;
|
||||
--z-base: 1; --z-raised: 10; --z-dropdown: 100; --z-sticky: 200; --z-sticky-nav: 300; --z-modal: 400; --z-toast: 500; --z-tooltip: 600;
|
||||
--color-paper: oklch(98.4% 0.012 85); --color-paper-2: oklch(95.6% 0.02 80); --color-paper-3: oklch(91.5% 0.03 75);
|
||||
--color-ink: oklch(25% 0.07 255); --color-ink-2: oklch(36% 0.065 255); --color-muted: oklch(46% 0.045 255);
|
||||
--color-rule: oklch(87% 0.025 80);
|
||||
--color-accent: oklch(56% 0.15 38); --color-accent-2: oklch(49% 0.15 38); --color-accent-ink: oklch(99% 0.008 85); --color-focus: oklch(58% 0.19 38);
|
||||
--color-card: oklch(99.6% 0.006 85); --color-card-ink: oklch(25% 0.07 255);
|
||||
--color-navy: oklch(25% 0.07 255); --color-navy-2: oklch(31% 0.07 255); --color-on-navy: oklch(97% 0.015 85); --color-on-navy-muted: oklch(82% 0.03 255);
|
||||
--color-land: oklch(88% 0.04 250);
|
||||
--font-display: "DM Serif Display", ui-serif, Georgia, serif; --font-body: "Figtree", ui-sans-serif, system-ui, sans-serif; --font-outlier: var(--font-display);
|
||||
--radius-card: 24px; --radius-control: 14px; --radius-pill: 999px; --rule-hair: 1px; --rule-bar: 2px;
|
||||
--shadow-card: 0 18px 40px -22px oklch(25% 0.07 255 / 0.35), 0 2px 6px oklch(25% 0.07 255 / 0.06);
|
||||
--shadow-lift: 0 26px 48px -24px oklch(25% 0.07 255 / 0.45), 0 3px 8px oklch(25% 0.07 255 / 0.08);
|
||||
--tracking-display: -0.025em;
|
||||
--tone-a: oklch(86% 0.08 20); --tone-b: oklch(92% 0.09 95); --tone-c: oklch(88% 0.07 150); --tone-d: oklch(86% 0.06 235);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Fraunces Project Authors (github.com/undercasetype/Fraunces) Fraunces-Italic[SOFT,WONK,opsz,wght].ttf: Copyright 2020 The Fraunces Project Authors (github.com/undercasetype/Fraunces)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ThinItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLight.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Light.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-LightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Regular.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Italic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Medium.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-MediumItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Bold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-BoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans) WorkSans-Italic[wght].ttf: Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,15 @@
|
||||
/* Self-hosted fonts (Latin subset), SIL Open Font License 1.1 - see OFL-*.txt */
|
||||
@font-face{font-family:"Fraunces";font-style:normal;font-weight:400;font-display:swap;src:url("fraunces-latin-400-normal.woff2") format("woff2");}
|
||||
@font-face{font-family:"Fraunces";font-style:normal;font-weight:500;font-display:swap;src:url("fraunces-latin-500-normal.woff2") format("woff2");}
|
||||
@font-face{font-family:"Fraunces";font-style:normal;font-weight:600;font-display:swap;src:url("fraunces-latin-600-normal.woff2") format("woff2");}
|
||||
@font-face{font-family:"Fraunces";font-style:normal;font-weight:700;font-display:swap;src:url("fraunces-latin-700-normal.woff2") format("woff2");}
|
||||
@font-face{font-family:"Fraunces";font-style:normal;font-weight:900;font-display:swap;src:url("fraunces-latin-900-normal.woff2") format("woff2");}
|
||||
@font-face{font-family:"Fraunces";font-style:italic;font-weight:500;font-display:swap;src:url("fraunces-latin-500-italic.woff2") format("woff2");}
|
||||
@font-face{font-family:"Fraunces";font-style:italic;font-weight:600;font-display:swap;src:url("fraunces-latin-600-italic.woff2") format("woff2");}
|
||||
@font-face{font-family:"Work Sans";font-style:normal;font-weight:400;font-display:swap;src:url("work-sans-latin-400-normal.woff2") format("woff2");}
|
||||
@font-face{font-family:"Work Sans";font-style:normal;font-weight:500;font-display:swap;src:url("work-sans-latin-500-normal.woff2") format("woff2");}
|
||||
@font-face{font-family:"Work Sans";font-style:normal;font-weight:600;font-display:swap;src:url("work-sans-latin-600-normal.woff2") format("woff2");}
|
||||
@font-face{font-family:"Work Sans";font-style:normal;font-weight:700;font-display:swap;src:url("work-sans-latin-700-normal.woff2") format("woff2");}
|
||||
@font-face{font-family:"IBM Plex Mono";font-style:normal;font-weight:400;font-display:swap;src:url("ibm-plex-mono-latin-400-normal.woff2") format("woff2");}
|
||||
@font-face{font-family:"IBM Plex Mono";font-style:normal;font-weight:500;font-display:swap;src:url("ibm-plex-mono-latin-500-normal.woff2") format("woff2");}
|
||||
@font-face{font-family:"IBM Plex Mono";font-style:normal;font-weight:600;font-display:swap;src:url("ibm-plex-mono-latin-600-normal.woff2") format("woff2");}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+32
-550
@@ -1,7 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en-AU">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">
|
||||
<title>CycleWise AU</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=Fraunces:ital,wght@0,400;0,500;0,600;0,700;0,900;1,500;1,600&family=Work+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="fonts/fonts.css">
|
||||
<style>
|
||||
:root{
|
||||
--paper:#FBF7F2;
|
||||
@@ -131,6 +135,7 @@ button{font-family:inherit;}
|
||||
.mode-panel.active{display:block;animation:rise .4s ease both;}
|
||||
@keyframes rise{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}
|
||||
@media (prefers-reduced-motion: reduce){.mode-panel.active{animation:none;}}
|
||||
@media (prefers-color-scheme: dark){.mode-btn[data-mode="pregnancy"].active{color:#1B1417;}}
|
||||
|
||||
.hero{
|
||||
display:grid;grid-template-columns:1.3fr 1fr;gap:40px;
|
||||
@@ -144,7 +149,11 @@ button{font-family:inherit;}
|
||||
}
|
||||
.hero h1{font-size:clamp(1.9rem,3.4vw,2.7rem);font-weight:600;line-height:1.08;}
|
||||
.hero p.lede{color:var(--ink-soft);font-size:1.02rem;line-height:1.6;max-width:52ch;margin-top:14px;}
|
||||
.hero-art{display:flex;justify-content:center;}
|
||||
.hero-art{display:flex;justify-content:center;position:relative;}
|
||||
.hero-art.has-3d{align-items:center;}
|
||||
.hero-art.has-3d .ring3d{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}
|
||||
.hero-art.has-3d svg{position:relative;max-width:300px;}
|
||||
.hero-art.has-3d svg path,.hero-art.has-3d svg circle{display:none;}
|
||||
.hero-art svg{width:100%;max-width:220px;height:auto;}
|
||||
|
||||
.stat-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:22px;}
|
||||
@@ -262,7 +271,7 @@ button{font-family:inherit;}
|
||||
text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;font-weight:600;}
|
||||
input[type="range"].week-slider{
|
||||
width:100%;-webkit-appearance:none;appearance:none;height:8px;border-radius:999px;
|
||||
background:linear-gradient(to right, var(--pregnancy-soft) 0%, var(--pregnancy-soft) 100%);
|
||||
background:linear-gradient(to right, var(--pregnancy) 0, var(--pregnancy) calc(11px + (100% - 22px) * var(--f,0)), var(--pregnancy-soft) calc(11px + (100% - 22px) * var(--f,0)), var(--pregnancy-soft) 100%);
|
||||
outline:none;cursor:pointer;
|
||||
}
|
||||
input[type="range"].week-slider::-webkit-slider-thumb{
|
||||
@@ -297,6 +306,7 @@ input[type="range"].week-slider::-moz-range-thumb{
|
||||
|
||||
.filmstrip{display:flex;gap:8px;overflow-x:auto;padding:4px 2px 20px;margin-top:8px;}
|
||||
.film-item{
|
||||
color:var(--ink);
|
||||
flex:none;width:64px;height:64px;border-radius:12px;border:1px solid var(--line);
|
||||
background:var(--surface);display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
cursor:pointer;font-family:"IBM Plex Mono",monospace;
|
||||
@@ -310,7 +320,8 @@ footer.note{
|
||||
font-size:0.78rem;color:var(--ink-soft);line-height:1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<header class="topbar">
|
||||
<div class="wordmark">
|
||||
@@ -324,15 +335,15 @@ footer.note{
|
||||
<span class="tag">Menstrual & Pregnancy Charting</span>
|
||||
</div>
|
||||
<nav class="mode-switch" role="tablist" aria-label="Tracking mode">
|
||||
<button class="mode-btn active" data-mode="period" role="tab" aria-selected="true">Period</button>
|
||||
<button class="mode-btn" data-mode="ovulation" role="tab" aria-selected="false">Ovulation</button>
|
||||
<button class="mode-btn" data-mode="pregnancy" role="tab" aria-selected="false">Pregnancy</button>
|
||||
<button class="mode-btn active" id="tab-period" aria-controls="mode-period" data-mode="period" role="tab" aria-selected="true">Period</button>
|
||||
<button class="mode-btn" id="tab-ovulation" aria-controls="mode-ovulation" data-mode="ovulation" role="tab" aria-selected="false">Ovulation</button>
|
||||
<button class="mode-btn" id="tab-pregnancy" aria-controls="mode-pregnancy" data-mode="pregnancy" role="tab" aria-selected="false">Pregnancy</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- ================= PERIOD ================= -->
|
||||
<section id="mode-period" class="mode-panel active">
|
||||
<section id="mode-period" class="mode-panel active" role="tabpanel" aria-labelledby="tab-period">
|
||||
<div class="hero">
|
||||
<div>
|
||||
<span class="eyebrow" style="color:var(--period)">01 — Menstrual Cycle</span>
|
||||
@@ -398,7 +409,7 @@ footer.note{
|
||||
</section>
|
||||
|
||||
<!-- ================= OVULATION ================= -->
|
||||
<section id="mode-ovulation" class="mode-panel">
|
||||
<section id="mode-ovulation" class="mode-panel" role="tabpanel" aria-labelledby="tab-ovulation">
|
||||
<div class="hero">
|
||||
<div>
|
||||
<span class="eyebrow" style="color:var(--ovulation)">02 — Fertile Window</span>
|
||||
@@ -446,7 +457,7 @@ footer.note{
|
||||
</section>
|
||||
|
||||
<!-- ================= PREGNANCY ================= -->
|
||||
<section id="mode-pregnancy" class="mode-panel">
|
||||
<section id="mode-pregnancy" class="mode-panel" role="tabpanel" aria-labelledby="tab-pregnancy">
|
||||
<div class="hero">
|
||||
<div>
|
||||
<span class="eyebrow" style="color:var(--pregnancy)">03 — Forty Weeks</span>
|
||||
@@ -476,7 +487,7 @@ footer.note{
|
||||
<span style="color:var(--pregnancy)">Trimester 2 · Weeks 14–27</span>
|
||||
<span style="color:var(--pregnancy)">Trimester 3 · Weeks 28–40</span>
|
||||
</div>
|
||||
<input type="range" class="week-slider" id="week-slider" min="1" max="40" value="20">
|
||||
<input type="range" class="week-slider" id="week-slider" aria-label="Pregnancy week, 1 to 40" min="1" max="40" value="20">
|
||||
<div class="week-ticks"><span>Wk 1</span><span>Wk 13</span><span>Wk 27</span><span>Wk 40</span></div>
|
||||
</div>
|
||||
|
||||
@@ -524,540 +535,11 @@ footer.note{
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
/* ---------------- date helpers ---------------- */
|
||||
function toDate(y,m,d){ return new Date(y,m,d); }
|
||||
function fromInput(str){
|
||||
if(!str) return null;
|
||||
var p = str.split("-");
|
||||
return new Date(+p[0], +p[1]-1, +p[2]);
|
||||
}
|
||||
function toInputVal(d){
|
||||
return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0");
|
||||
}
|
||||
function addDays(d,n){ var r=new Date(d); r.setDate(r.getDate()+n); return r; }
|
||||
function daysBetween(a,b){ // b - a, in whole days, ignoring time
|
||||
var A = new Date(a.getFullYear(),a.getMonth(),a.getDate());
|
||||
var B = new Date(b.getFullYear(),b.getMonth(),b.getDate());
|
||||
return Math.round((B-A)/86400000);
|
||||
}
|
||||
function sameDay(a,b){ return a.getFullYear()===b.getFullYear() && a.getMonth()===b.getMonth() && a.getDate()===b.getDate(); }
|
||||
function mod(n,m){ return ((n % m) + m) % m; }
|
||||
var MONTH_NAMES = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
||||
var today = new Date(); today.setHours(0,0,0,0);
|
||||
|
||||
function loadLS(key, fallback){
|
||||
try{ var v = localStorage.getItem(key); return v===null ? fallback : v; }catch(e){ return fallback; }
|
||||
}
|
||||
function saveLS(key, val){ try{ localStorage.setItem(key,val); }catch(e){} }
|
||||
|
||||
/* ---------------- mode switching ---------------- */
|
||||
var modeBtns = document.querySelectorAll(".mode-btn");
|
||||
var panels = { period: document.getElementById("mode-period"), ovulation: document.getElementById("mode-ovulation"), pregnancy: document.getElementById("mode-pregnancy") };
|
||||
modeBtns.forEach(function(btn){
|
||||
btn.addEventListener("click", function(){
|
||||
modeBtns.forEach(function(b){ b.classList.remove("active"); b.setAttribute("aria-selected","false"); });
|
||||
btn.classList.add("active"); btn.setAttribute("aria-selected","true");
|
||||
Object.keys(panels).forEach(function(k){ panels[k].classList.remove("active"); });
|
||||
panels[btn.dataset.mode].classList.add("active");
|
||||
saveLS("ca-mode", btn.dataset.mode);
|
||||
});
|
||||
});
|
||||
var savedMode = loadLS("ca-mode", null);
|
||||
if(savedMode && panels[savedMode]){
|
||||
modeBtns.forEach(function(b){ b.classList.toggle("active", b.dataset.mode===savedMode); b.setAttribute("aria-selected", b.dataset.mode===savedMode ? "true":"false"); });
|
||||
Object.keys(panels).forEach(function(k){ panels[k].classList.toggle("active", k===savedMode); });
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
PHASE + OVULATION REFERENCE DATA
|
||||
================================================================ */
|
||||
var PHASES = {
|
||||
menstrual: {
|
||||
title:"Menstrual", color:"var(--period)", soft:"var(--period-soft)",
|
||||
window:"Cycle day 1 through the end of bleeding",
|
||||
desc:"The uterine lining sheds because no pregnancy occurred last cycle. Estrogen and progesterone are both at their lowest point of the month.",
|
||||
hormone:"Estrogen & progesterone: low", energy:"Energy: lowest",
|
||||
symptoms:"Cramping, fatigue, lower back ache, mood dips",
|
||||
tip:"Iron-rich food, gentle movement, and heat on the lower abdomen ease cramping better than rest alone."
|
||||
},
|
||||
follicular: {
|
||||
title:"Follicular", color:"var(--ovulation)", soft:"var(--ovulation-soft)",
|
||||
window:"Cycle day 1 through ovulation (overlaps menstrual)",
|
||||
desc:"The pituitary releases FSH, prompting several ovarian follicles to mature. Estrogen climbs steadily as one dominant follicle takes the lead.",
|
||||
hormone:"Estrogen: rising", energy:"Energy: climbing",
|
||||
symptoms:"Clearer skin, sharper focus, rising motivation",
|
||||
tip:"This is the physiologically best window for starting new training blocks or demanding projects."
|
||||
},
|
||||
ovulatory: {
|
||||
title:"Ovulatory", color:"var(--ovulation-strong)", soft:"var(--ovulation-soft)",
|
||||
window:"Roughly one day, mid-cycle",
|
||||
desc:"A surge in luteinizing hormone (LH) triggers the dominant follicle to release its egg. This is the single most fertile day of the cycle.",
|
||||
hormone:"LH: sharp surge", energy:"Energy & libido: peak",
|
||||
symptoms:"Egg-white cervical mucus, one-sided pelvic twinge (mittelschmerz)",
|
||||
tip:"Ovulation predictor kits detect the LH surge roughly 24–36 hours before the egg is released."
|
||||
},
|
||||
luteal: {
|
||||
title:"Luteal", color:"var(--pregnancy)", soft:"var(--pregnancy-soft)",
|
||||
window:"Ovulation through the day before your next period",
|
||||
desc:"The ruptured follicle becomes the corpus luteum and secretes progesterone to prepare the uterine lining. If no implantation occurs, hormone levels fall and the cycle restarts.",
|
||||
hormone:"Progesterone: rises then falls", energy:"Energy: gradually declining",
|
||||
symptoms:"Bloating, breast tenderness, irritability, food cravings (PMS)",
|
||||
tip:"Basal body temperature stays elevated ~0.3–0.5°C through this phase — a useful way to confirm ovulation already happened."
|
||||
}
|
||||
};
|
||||
|
||||
// offset relative to ovulation day (0 = ovulation)
|
||||
var OVULATION_DAYS = {
|
||||
"-5": {fert:"Low", fertClass:"fert-low", bbt:"Baseline (pre-shift)", mucus:"Dry to sticky", note:"Sperm can survive up to 5 days, so conception is technically possible starting here."},
|
||||
"-4": {fert:"Low–moderate", fertClass:"fert-low", bbt:"Baseline", mucus:"Sticky, cloudy", note:"Fertility begins climbing as estrogen rises toward its pre-ovulation peak."},
|
||||
"-3": {fert:"Moderate", fertClass:"fert-mid", bbt:"Baseline", mucus:"Creamy, increasing in volume", note:"Cervical mucus is becoming more hospitable to sperm."},
|
||||
"-2": {fert:"High", fertClass:"fert-high", bbt:"Baseline", mucus:"Thin, stretchy, egg-white-like", note:"One of the two most fertile days of the entire cycle."},
|
||||
"-1": {fert:"Peak", fertClass:"fert-peak", bbt:"Baseline (often the day of the LH surge)", mucus:"Clear, slippery, maximum stretch", note:"Statistically the single highest-probability day for conception."},
|
||||
"0": {fert:"Peak", fertClass:"fert-peak", bbt:"Still baseline — the rise hasn't happened yet", mucus:"Egg-white, abundant", note:"The egg is released and is viable for roughly 12–24 hours. Some feel a one-sided twinge (mittelschmerz)."},
|
||||
"1": {fert:"Dropping fast", fertClass:"fert-mid", bbt:"Beginning to rise ~0.3–0.5°C", mucus:"Becoming tacky again", note:"The egg's fertile window is closing; BBT starts its post-ovulation climb."},
|
||||
"2": {fert:"Very low", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry or minimal", note:"Progesterone from the corpus luteum keeps temperature elevated through the luteal phase."},
|
||||
"3": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"If fertilization happened, the embryo is traveling toward the uterus."},
|
||||
"4": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"Too late in the cycle for a new ovulation; luteal phase symptoms may start appearing."},
|
||||
"5": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"Implantation, if it happens this cycle, typically occurs around days 6–10 after ovulation."}
|
||||
};
|
||||
var FERT_COLOR = {"fert-low":"var(--ink-soft)","fert-mid":"var(--fertile)","fert-high":"var(--fertile-strong)","fert-peak":"var(--ovulation-strong)"};
|
||||
|
||||
function phaseForDayInCycle(dic, periodLen, ovulationDic){
|
||||
if(dic < periodLen) return "menstrual";
|
||||
if(dic < ovulationDic) return "follicular";
|
||||
if(dic === ovulationDic) return "ovulatory";
|
||||
return "luteal";
|
||||
}
|
||||
function isFertile(dic, ovulationDic){
|
||||
return dic >= ovulationDic - 5 && dic <= ovulationDic;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
PREGNANCY WEEK DATA (1–40)
|
||||
================================================================ */
|
||||
var WEEKS = [
|
||||
{c:"a poppy seed (not yet fertilized)", m:"n/a", dev:"Pregnancy is dated from the first day of this period, before conception happens — your body is shedding last cycle's lining and preparing a fresh egg.", body:"This week looks and feels like an ordinary period.", tip:"If you're trying to conceive, this is a good week to start a prenatal vitamin with folic acid."},
|
||||
{c:"an egg cell", m:"~0.1 mm", dev:"Rising estrogen prompts an egg to mature; ovulation typically occurs near the end of this week, roughly two weeks before a missed period.", body:"Cervical mucus becomes clear and stretchy around ovulation — a natural fertility sign.", tip:"This is the fertile window if you're trying to conceive — see the Ovulation tab for day-by-day detail."},
|
||||
{c:"a grain of sand", m:"~0.1–0.2 mm", dev:"If sperm meets egg this week, fertilization occurs in the fallopian tube and the resulting cell begins dividing as it travels toward the uterus.", body:"No symptoms are detectable yet — even a sensitive test won't turn positive for another week or two.", tip:"Implantation, if it happens, usually occurs 6–10 days after ovulation."},
|
||||
{c:"a poppy seed", m:"~0.1 mm", dev:"The fertilized egg implants in the uterine lining and begins producing hCG, the hormone home pregnancy tests detect.", body:"Most people notice no symptoms yet; a sensitive test may already turn positive near the end of this week.", tip:"A missed period is usually the first sign — testing a few days after that gives the most reliable result."},
|
||||
{c:"a sesame seed", m:"~2 mm", dev:"The neural tube — the earliest structure of the brain and spinal cord — begins to form, along with the first blood vessels.", body:"A missed period is usually the first clear sign; fatigue and tender breasts may begin.", tip:"Avoid alcohol and confirm any medications with a clinician — this window is critical for early development."},
|
||||
{c:"a lentil", m:"~4 mm", dev:"A primitive heart tube starts beating, and the beginnings of the face, arms, and legs appear as small buds.", body:"Morning sickness and smell sensitivity often begin here, though timing varies widely.", tip:"Small, frequent, bland meals tend to sit better than three large ones if nausea has started."},
|
||||
{c:"a blueberry", m:"~1.3 cm", dev:"The brain is dividing into distinct sections, and the arm and leg buds are elongating into recognizable limbs.", body:"Bloating and mild cramping are common as the uterus makes room to grow.", tip:"This is when most people schedule their first prenatal appointment — dating scans are most accurate now."},
|
||||
{c:"a raspberry", m:"~1.6 cm", dev:"Fingers and toes are forming, still webbed; the heart now has four distinct chambers.", body:"Fatigue often peaks around this point as progesterone rises sharply.", tip:"Rest when you can — first-trimester exhaustion is real and temporary, not a sign anything is wrong."},
|
||||
{c:"a grape", m:"~2.3 cm · 4 g", dev:"The embryo is officially called a fetus starting around now; essential organs are all present in early form and the tail has disappeared.", body:"Nausea, food aversions, and a heightened sense of smell are often at their strongest.", tip:"Ginger tea, vitamin B6, and staying hydrated are commonly recommended first-line options for nausea."},
|
||||
{c:"a kumquat", m:"~3 cm · 7 g", dev:"Vital organs — kidneys, liver, brain — are functioning in a basic form, and tiny fingernail beds are forming.", body:"A visible baby bump is still unlikely, though clothes may feel tighter.", tip:"Ask your provider about first-trimester screening options available in this window."},
|
||||
{c:"a fig", m:"~4.1 cm · 10 g", dev:"The fetus can now bend its arms and legs, and tooth buds are forming beneath the gums.", body:"Round ligament twinges may start as the uterus stretches.", tip:"Gentle stretching and changing positions slowly can ease early ligament discomfort."},
|
||||
{c:"a lime", m:"~5.4 cm · 14 g", dev:"Reflexes are developing — fingers can curl, and toes can wiggle, though it can't be felt yet.", body:"The end of the first trimester is near, and miscarriage risk drops meaningfully from here.", tip:"Many people choose to share their pregnancy news around now, though the choice is entirely personal."},
|
||||
{c:"a plum", m:"~6.7 cm · 23 g", dev:"Vocal cords are forming, and unique fingerprints are being etched onto tiny fingertips.", body:"Some early nausea may start easing as the placenta takes over hormone production.", tip:"The nuchal translucency scan, if chosen, is typically done between now and week 13."},
|
||||
{c:"a passion fruit", m:"~10 cm · 45 g", dev:"Baby can open and close fists and may even suck a thumb; genitals are differentiating, though not yet visible on ultrasound.", body:"For many, first-trimester nausea and fatigue begin to lift.", tip:"Second-trimester energy often supports resuming or starting light-to-moderate exercise — check in with your provider first."},
|
||||
{c:"a peach", m:"~11.6 cm · 58 g", dev:"Vocal cords are complete and the intestines, which developed in the umbilical cord, move into the abdomen.", body:"Welcome to the second trimester — often called the most comfortable stretch of pregnancy.", tip:"An anatomy scan is usually scheduled in the coming weeks; ask your clinic when it's booked."},
|
||||
{c:"a lemon", m:"~13 cm · 70 g", dev:"Fine, downy hair called lanugo begins covering the body, and the kidneys are producing urine into the amniotic fluid.", body:"A small bump may finally become visible; appetite often improves noticeably.", tip:"Iron needs rise sharply now — leafy greens, legumes, and lean meat help meet increased demand."},
|
||||
{c:"an apple", m:"~14.2 cm · 95 g", dev:"The skeleton continues hardening and baby can sense light through closed eyelids.", body:"Skin changes like a faint linea nigra or darkened areolas may appear as pigment increases.", tip:"Daily SPF helps limit pregnancy-related skin pigmentation (melasma) from sun exposure."},
|
||||
{c:"an avocado", m:"~16 cm · 135 g", dev:"The placenta is now fully formed and functioning as baby's life-support system.", body:"First flutters of movement ('quickening') may be felt any time from now through week 20.", tip:"Movement timing varies a lot — first pregnancies and anterior placentas often mean feeling it later."},
|
||||
{c:"a pomegranate", m:"~17.7 cm · 160 g", dev:"The skeleton is converting from cartilage to true bone, and fat stores are just beginning to accumulate.", body:"The uterus is now roughly at the level of the belly button.", tip:"A pregnancy support pillow can meaningfully improve sleep as side-sleeping becomes more comfortable."},
|
||||
{c:"a bell pepper", m:"~19 cm · 190 g", dev:"Ears have reached their final position and baby may begin startling at loud external sounds.", body:"Quickening is commonly felt by now — often described as bubbles or light taps.", tip:"Try narrating your day out loud — hearing is developing and familiar voices become recognizable over time."},
|
||||
{c:"a mango", m:"~20.3 cm · 240 g", dev:"A waxy coating called vernix caseosa begins protecting the skin, and taste buds are forming.", body:"Backaches can start as posture shifts to balance the growing bump.", tip:"A supportive, flatter shoe and a pillow between the knees at night both help low back strain."},
|
||||
{c:"a banana", m:"~25.6 cm · 300 g", dev:"The detailed anatomy ultrasound is often done around now if it hasn't happened already, and sex can typically be identified on it.", body:"Movements are stronger and more frequent; the uterus is now level with the belly button.", tip:"If you want to know the sex and haven't had the anatomy scan yet, ask your clinic to schedule it soon — it's usually done weeks 18–22."},
|
||||
{c:"a carrot", m:"~27 cm · 360 g", dev:"Baby is swallowing small amounts of amniotic fluid, giving the digestive system practice.", body:"Stretch marks may start appearing on the belly, hips, or chest as skin stretches.", tip:"Moisturizing won't prevent stretch marks, but it can ease the itchiness that comes with stretching skin."},
|
||||
{c:"a papaya", m:"~28.9 cm · 430 g", dev:"Eyebrows and eyelids are fully formed, and the sense of touch is developing rapidly.", body:"Braxton Hicks — brief, painless practice contractions — may be felt occasionally from here on.", tip:"Braxton Hicks usually ease with a change of position or a glass of water; true labor contractions don't."},
|
||||
{c:"an ear of corn", m:"~32.2 cm · 600 g", dev:"The lungs begin producing surfactant, a substance essential for breathing air after birth — a key viability milestone.", body:"The linea nigra (a dark vertical line on the belly) may become more visible.", tip:"This week is often cited as the threshold of viability with intensive neonatal care, should birth happen very early."},
|
||||
{c:"a cauliflower head", m:"~33.7 cm · 660 g", dev:"Baby can now respond to your voice, and hand grip strength is developing.", body:"The glucose screening test for gestational diabetes is typically scheduled around now.", tip:"If asked to fast beforehand, follow your clinic's instructions closely for an accurate glucose result."},
|
||||
{c:"a head of lettuce", m:"~35.6 cm · 760 g", dev:"Eyes begin to open after being fused shut since early development, and hair may be visibly growing.", body:"Round ligament pain and rib discomfort can increase as space gets tighter.", tip:"A maternity support band can ease rib and ligament pressure during long periods of standing or walking."},
|
||||
{c:"a bunch of scallions", m:"~36.6 cm · 875 g", dev:"Brain tissue is developing rapidly, and baby is developing regular hiccups you may be able to feel.", body:"Third trimester begins soon — appointments typically become more frequent from here.", tip:"Hiccup-like twitches are completely normal and are the diaphragm practicing its rhythm."},
|
||||
{c:"an eggplant", m:"~37.6 cm · 1.1 kg", dev:"Eyes can blink now, and baby may cycle through active REM sleep.", body:"Welcome to the third trimester — the home stretch begins.", tip:"This is a common time to start planning a hospital bag and reviewing birth preferences."},
|
||||
{c:"a butternut squash", m:"~38.6 cm · 1.2 kg", dev:"Bones are fully formed but still soft and flexible, ready to move through the birth canal.", body:"Frequent urination often returns as the head engages lower in the pelvis.", tip:"Kick counts become a useful daily check-in — note baby's typical pattern of movement."},
|
||||
{c:"a large cabbage", m:"~39.9 cm · 1.3 kg", dev:"The brain is developing at its fastest rate yet, adding folds and surface area.", body:"Sleep can become harder to find as the bump limits comfortable positions.", tip:"A full-body pregnancy pillow supports the belly, back, and knees simultaneously."},
|
||||
{c:"a coconut", m:"~41.1 cm · 1.5 kg", dev:"All five senses are now functioning, and baby is gaining roughly half a pound a week.", body:"Colostrum, the first form of breast milk, may begin to leak slightly.", tip:"A few nursing pads in your bag can help if colostrum leaking becomes noticeable."},
|
||||
{c:"a jicama", m:"~42.4 cm · 1.7 kg", dev:"Baby is practicing breathing movements against a closed airway, strengthening the lungs.", body:"Swelling and mild carpal tunnel tingling in the hands can appear from fluid retention.", tip:"Report sudden or severe swelling to your provider promptly — it can occasionally signal preeclampsia."},
|
||||
{c:"a pineapple", m:"~43.7 cm · 1.9 kg", dev:"Bones are hardening overall, though the skull stays deliberately soft and flexible for delivery.", body:"Pelvic pressure increases as baby continues descending.", tip:"Prenatal massage or a warm bath can ease the deep pelvic ache many feel this week."},
|
||||
{c:"a cantaloupe", m:"~45 cm · 2.1 kg", dev:"The central nervous system is maturing quickly, and baby's grip is now quite strong.", body:"Braxton Hicks contractions often become more noticeable and frequent.", tip:"Time any contractions that feel different — regular, intensifying, closer together warrants a call to your provider."},
|
||||
{c:"a honeydew melon", m:"~46.2 cm · 2.4 kg", dev:"The kidneys are fully developed, and the liver is processing waste products on its own.", body:"You may feel baby 'drop' lower as it settles into position for birth.", tip:"Lightening (the baby dropping) can ease heartburn but often increases bathroom trips."},
|
||||
{c:"a head of romaine", m:"~47.4 cm · 2.6 kg", dev:"Most physical development is complete; the focus now is steady weight gain.", body:"You've just reached 'early term' — babies born from here on tend to do very well.", tip:"Pack your hospital bag if you haven't yet — early-term babies can arrive with little warning."},
|
||||
{c:"a leek", m:"~49.8 cm · 3.1 kg", dev:"Baby continues laying down fat for temperature regulation after birth, and the brain is still rapidly developing.", body:"Nesting instinct and a burst of energy are common right before labor.", tip:"Balance nesting energy with rest — labor's timing is unpredictable."},
|
||||
{c:"a mini watermelon", m:"~50.7 cm · 3.3 kg", dev:"Full term. The lungs are completing final maturation in preparation for that first breath.", body:"Increased pelvic pressure and more frequent, intense Braxton Hicks are common.", tip:"Watch for your labor signs: regular contractions, water breaking, or a bloody show."},
|
||||
{c:"a small pumpkin", m:"~51.2 cm · 3.5 kg", dev:"Baby is fully developed and simply continuing to grow, ready to meet you.", body:"If labor hasn't started, your provider will discuss monitoring or induction options.", tip:"Most babies arrive within two weeks of their due date in either direction — both are entirely normal."}
|
||||
];
|
||||
|
||||
/* ================================================================
|
||||
SVG helpers
|
||||
================================================================ */
|
||||
function polarToXY(cx,cy,r,angleDeg){
|
||||
var rad = (angleDeg-90) * Math.PI/180;
|
||||
return [cx + r*Math.cos(rad), cy + r*Math.sin(rad)];
|
||||
}
|
||||
function arcPath(cx,cy,r,startDeg,endDeg){
|
||||
if(endDeg <= startDeg) endDeg = startDeg + 0.01;
|
||||
var s = polarToXY(cx,cy,r,startDeg), e = polarToXY(cx,cy,r,endDeg);
|
||||
var large = (endDeg-startDeg) > 180 ? 1 : 0;
|
||||
return "M "+s[0].toFixed(2)+" "+s[1].toFixed(2)+" A "+r+" "+r+" 0 "+large+" 1 "+e[0].toFixed(2)+" "+e[1].toFixed(2);
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
SHARED STATE: period start / cycle length / period length
|
||||
================================================================ */
|
||||
var exampleStart = addDays(today, -10);
|
||||
var startInput = document.getElementById("p-start");
|
||||
var cycleLenInput = document.getElementById("p-cyclelen");
|
||||
var periodLenInput = document.getElementById("p-periodlen");
|
||||
|
||||
var savedStart = loadLS("ca-start", null);
|
||||
var savedCycleLen = loadLS("ca-cyclelen", null);
|
||||
var savedPeriodLen = loadLS("ca-periodlen", null);
|
||||
var usingExample = !savedStart;
|
||||
|
||||
startInput.value = savedStart || toInputVal(exampleStart);
|
||||
cycleLenInput.value = savedCycleLen || 28;
|
||||
periodLenInput.value = savedPeriodLen || 5;
|
||||
|
||||
function getCycleState(){
|
||||
return {
|
||||
start: fromInput(startInput.value) || exampleStart,
|
||||
cycleLen: Math.max(21, Math.min(35, parseInt(cycleLenInput.value,10) || 28)),
|
||||
periodLen: Math.max(2, Math.min(10, parseInt(periodLenInput.value,10) || 5))
|
||||
};
|
||||
}
|
||||
|
||||
var pCalMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
var oCalMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
var pSelectedDate = new Date(today);
|
||||
var oSelectedDate = new Date(today);
|
||||
|
||||
[startInput,cycleLenInput,periodLenInput].forEach(function(inp){
|
||||
inp.addEventListener("input", function(){
|
||||
saveLS("ca-start", startInput.value);
|
||||
saveLS("ca-cyclelen", cycleLenInput.value);
|
||||
saveLS("ca-periodlen", periodLenInput.value);
|
||||
usingExample = false;
|
||||
document.getElementById("p-example-note").style.display = "none";
|
||||
document.getElementById("pr-example-note").style.display = "none";
|
||||
renderAll();
|
||||
});
|
||||
});
|
||||
if(!usingExample) document.getElementById("p-example-note").style.display = "none";
|
||||
|
||||
/* ================================================================
|
||||
CALENDAR RENDERING (shared builder)
|
||||
================================================================ */
|
||||
function buildCalendar(gridEl, weekdayHeaderIgnored, monthDate, dayClassifier, onSelect, selectedDate){
|
||||
gridEl.innerHTML = "";
|
||||
var year = monthDate.getFullYear(), month = monthDate.getMonth();
|
||||
var firstDow = new Date(year,month,1).getDay();
|
||||
var daysInMonth = new Date(year,month+1,0).getDate();
|
||||
var totalCells = Math.ceil((firstDow+daysInMonth)/7)*7;
|
||||
for(var i=0;i<totalCells;i++){
|
||||
var dayNum = i - firstDow + 1;
|
||||
var cell = document.createElement("button");
|
||||
cell.className = "day-cell";
|
||||
if(dayNum < 1 || dayNum > daysInMonth){
|
||||
cell.classList.add("pad");
|
||||
gridEl.appendChild(cell);
|
||||
continue;
|
||||
}
|
||||
var d = new Date(year,month,dayNum);
|
||||
var info = dayClassifier(d);
|
||||
cell.innerHTML = "<span>"+dayNum+"</span>";
|
||||
if(info.period) cell.classList.add("is-period");
|
||||
if(info.fertile) cell.classList.add("is-fertile");
|
||||
if(info.ovulation) cell.classList.add("is-ovulation");
|
||||
if(sameDay(d,today)) cell.classList.add("today");
|
||||
if(selectedDate && sameDay(d,selectedDate)) cell.classList.add("selected");
|
||||
cell.setAttribute("aria-label", d.toDateString());
|
||||
cell.addEventListener("click", function(dd){ return function(){ onSelect(dd); }; }(d));
|
||||
gridEl.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
function classifyForCycle(d, state){
|
||||
var dic = mod(daysBetween(state.start, d), state.cycleLen);
|
||||
var ovDic = mod(state.cycleLen - 14, state.cycleLen);
|
||||
return {
|
||||
period: dic < state.periodLen,
|
||||
fertile: isFertile(dic, ovDic),
|
||||
ovulation: dic === ovDic,
|
||||
dic: dic, ovDic: ovDic
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------------- PERIOD tab render ---------------- */
|
||||
function renderPeriodCalendar(){
|
||||
var state = getCycleState();
|
||||
document.getElementById("p-cal-title").textContent = MONTH_NAMES[pCalMonth.getMonth()] + " " + pCalMonth.getFullYear();
|
||||
buildCalendar(document.getElementById("p-daygrid"), null, pCalMonth,
|
||||
function(d){ return classifyForCycle(d, state); },
|
||||
function(d){ pSelectedDate = d; renderPeriodDetail(); renderPeriodCalendar(); },
|
||||
pSelectedDate
|
||||
);
|
||||
}
|
||||
|
||||
function renderPeriodDetail(){
|
||||
var state = getCycleState();
|
||||
var info = classifyForCycle(pSelectedDate, state);
|
||||
var phaseKey = phaseForDayInCycle(info.dic, state.periodLen, info.ovDic);
|
||||
var phase = PHASES[phaseKey];
|
||||
var el = document.getElementById("p-detail");
|
||||
el.innerHTML =
|
||||
'<span class="d-date">'+pSelectedDate.toDateString()+'</span>'+
|
||||
'<span class="pill" style="background:'+phase.soft+';color:'+phase.color+'">'+phase.title+' phase'+(info.fertile?' · fertile window':'')+'</span>'+
|
||||
'<div class="d-phase">Cycle day '+(info.dic+1)+'</div>'+
|
||||
'<p class="d-desc">'+phase.desc+'</p>'+
|
||||
'<div class="d-grid">'+
|
||||
'<div class="d-mini"><div class="k">Hormones</div><div class="v">'+phase.hormone+'</div></div>'+
|
||||
'<div class="d-mini"><div class="k">Typical symptoms</div><div class="v" style="font-weight:500;font-size:0.82rem;">'+phase.symptoms+'</div></div>'+
|
||||
'</div>'+
|
||||
'<p class="d-desc"><strong style="color:var(--ink)">Care note — </strong>'+phase.tip+'</p>';
|
||||
}
|
||||
|
||||
function renderPeriodStats(){
|
||||
var state = getCycleState();
|
||||
var dic = mod(daysBetween(state.start, today), state.cycleLen);
|
||||
var ovDic = mod(state.cycleLen - 14, state.cycleLen);
|
||||
var daysToNextPeriod = mod(state.cycleLen - dic, state.cycleLen);
|
||||
if(daysToNextPeriod === 0 && dic !== 0) daysToNextPeriod = state.cycleLen;
|
||||
var daysToOvulation = mod(ovDic - dic, state.cycleLen);
|
||||
var phaseKey = phaseForDayInCycle(dic, state.periodLen, ovDic);
|
||||
document.getElementById("period-stats").innerHTML =
|
||||
statTile(dic+1, "Cycle day today") +
|
||||
statTile(PHASES[phaseKey].title, "Current phase") +
|
||||
statTile(dic===0 ? "Today" : (daysToNextPeriod+"d"), "Until next period") +
|
||||
statTile(dic===ovDic ? "Today" : (daysToOvulation+"d"), "Until ovulation");
|
||||
|
||||
// hero ring
|
||||
var cx=100, cy=100, r=88;
|
||||
var perStart=0, perEnd = state.periodLen/state.cycleLen*360;
|
||||
var follEnd = ovDic/state.cycleLen*360;
|
||||
var fertStart = Math.max(0,(ovDic-5))/state.cycleLen*360;
|
||||
var lutStart = (ovDic+1)/state.cycleLen*360;
|
||||
document.getElementById("phase-arc-menstrual").setAttribute("d", arcPath(cx,cy,r,perStart,perEnd));
|
||||
document.getElementById("phase-arc-follicular").setAttribute("d", arcPath(cx,cy,r,perEnd,fertStart));
|
||||
document.getElementById("phase-arc-fertile").setAttribute("d", arcPath(cx,cy,r,fertStart,follEnd+ (360/state.cycleLen)));
|
||||
document.getElementById("phase-arc-luteal").setAttribute("d", arcPath(cx,cy,r,lutStart,360));
|
||||
var todayAngle = dic/state.cycleLen*360;
|
||||
var tp = polarToXY(cx,cy,r,todayAngle);
|
||||
document.getElementById("today-marker").setAttribute("cx", tp[0]);
|
||||
document.getElementById("today-marker").setAttribute("cy", tp[1]);
|
||||
document.getElementById("cycle-day-big").textContent = dic+1;
|
||||
}
|
||||
|
||||
function statTile(num,label){
|
||||
return '<div class="stat-tile"><div class="num">'+num+'</div><div class="lbl">'+label+'</div></div>';
|
||||
}
|
||||
|
||||
document.getElementById("p-prev").addEventListener("click", function(){ pCalMonth.setMonth(pCalMonth.getMonth()-1); renderPeriodCalendar(); });
|
||||
document.getElementById("p-next").addEventListener("click", function(){ pCalMonth.setMonth(pCalMonth.getMonth()+1); renderPeriodCalendar(); });
|
||||
|
||||
function renderPhaseRefGrid(){
|
||||
var order = ["menstrual","follicular","ovulatory","luteal"];
|
||||
document.getElementById("phase-ref-grid").innerHTML = order.map(function(key,i){
|
||||
var p = PHASES[key];
|
||||
return '<div class="ref-card"><span class="r-num">0'+(i+1)+'</span><h4 style="color:'+p.color+'">'+p.title+'</h4>'+
|
||||
'<p>'+p.desc+'</p>'+
|
||||
'<div class="tag-row"><span class="tagchip">'+p.window+'</span></div></div>';
|
||||
}).join("");
|
||||
}
|
||||
|
||||
/* ---------------- OVULATION tab render ---------------- */
|
||||
var ovSelectedOffset = 0;
|
||||
function renderOvTimeline(){
|
||||
var wrap = document.getElementById("ov-timeline");
|
||||
wrap.innerHTML = "";
|
||||
for(var off=-5; off<=5; off++){
|
||||
var d = OVULATION_DAYS[String(off)];
|
||||
var btn = document.createElement("button");
|
||||
btn.className = "tl-btn" + (off===ovSelectedOffset ? " active":"");
|
||||
btn.innerHTML = '<div class="tl-day">'+(off===0?"OVUL.":(off>0?"+":"")+off)+'</div>'+
|
||||
'<div class="tl-fert" style="background:'+FERT_COLOR[d.fertClass]+'"></div>';
|
||||
btn.addEventListener("click", function(o){ return function(){ ovSelectedOffset=o; renderOvTimeline(); renderOvDetailFromOffset(); }; }(off));
|
||||
wrap.appendChild(btn);
|
||||
}
|
||||
}
|
||||
function renderOvDetailFromOffset(){
|
||||
var d = OVULATION_DAYS[String(ovSelectedOffset)];
|
||||
var el = document.getElementById("o-detail");
|
||||
var label = ovSelectedOffset===0 ? "Ovulation day" : (ovSelectedOffset>0 ? ovSelectedOffset+" day"+(ovSelectedOffset>1?"s":"")+" after ovulation" : Math.abs(ovSelectedOffset)+" day"+(ovSelectedOffset<-1?"s":"")+" before ovulation");
|
||||
el.innerHTML =
|
||||
'<span class="d-date">'+label+'</span>'+
|
||||
'<span class="pill" style="background:var(--ovulation-soft);color:'+FERT_COLOR[d.fertClass]+'">Fertility: '+d.fert+'</span>'+
|
||||
'<div class="d-phase">'+(ovSelectedOffset===0?"Egg release":(ovSelectedOffset<0?"Building fertility":"Fertility fading"))+'</div>'+
|
||||
'<div class="d-grid">'+
|
||||
'<div class="d-mini"><div class="k">Basal body temp</div><div class="v" style="font-size:0.82rem;">'+d.bbt+'</div></div>'+
|
||||
'<div class="d-mini"><div class="k">Cervical mucus</div><div class="v" style="font-size:0.82rem;">'+d.mucus+'</div></div>'+
|
||||
'</div>'+
|
||||
'<p class="d-desc">'+d.note+'</p>';
|
||||
}
|
||||
|
||||
function renderOvulationCalendar(){
|
||||
var state = getCycleState();
|
||||
document.getElementById("o-cal-title").textContent = MONTH_NAMES[oCalMonth.getMonth()] + " " + oCalMonth.getFullYear();
|
||||
buildCalendar(document.getElementById("o-daygrid"), null, oCalMonth,
|
||||
function(d){ return classifyForCycle(d, state); },
|
||||
function(d){
|
||||
oSelectedDate = d;
|
||||
var info = classifyForCycle(d, state);
|
||||
var offset = info.dic - info.ovDic;
|
||||
if(offset < -5) offset = -5; if(offset > 5) offset = 5;
|
||||
ovSelectedOffset = offset;
|
||||
renderOvTimeline(); renderOvDetailFromOffset(); renderOvulationCalendar();
|
||||
},
|
||||
oSelectedDate
|
||||
);
|
||||
}
|
||||
|
||||
function renderOvulationStats(){
|
||||
var state = getCycleState();
|
||||
var dic = mod(daysBetween(state.start, today), state.cycleLen);
|
||||
var ovDic = mod(state.cycleLen - 14, state.cycleLen);
|
||||
var daysToOvulation = mod(ovDic - dic, state.cycleLen);
|
||||
var fertileStartDic = mod(ovDic-5, state.cycleLen);
|
||||
var inFertile = isFertile(dic, ovDic);
|
||||
var daysToFertile = inFertile ? 0 : mod(fertileStartDic - dic, state.cycleLen);
|
||||
document.getElementById("ovulation-stats").innerHTML =
|
||||
statTile(inFertile ? "Yes" : "No", "In fertile window") +
|
||||
statTile(dic===ovDic ? "Today" : daysToOvulation+"d", "Until ovulation") +
|
||||
statTile(inFertile ? "Now" : daysToFertile+"d", "Fertile window") +
|
||||
statTile("6 days", "Window length");
|
||||
}
|
||||
|
||||
document.getElementById("o-prev").addEventListener("click", function(){ oCalMonth.setMonth(oCalMonth.getMonth()-1); renderOvulationCalendar(); });
|
||||
document.getElementById("o-next").addEventListener("click", function(){ oCalMonth.setMonth(oCalMonth.getMonth()+1); renderOvulationCalendar(); });
|
||||
|
||||
/* ================================================================
|
||||
PREGNANCY tab
|
||||
================================================================ */
|
||||
var lmpInput = document.getElementById("pr-lmp");
|
||||
var exampleLMP = addDays(today, -20*7 - 3);
|
||||
var savedLMP = loadLS("ca-lmp", null);
|
||||
lmpInput.value = savedLMP || toInputVal(exampleLMP);
|
||||
if(savedLMP) document.getElementById("pr-example-note").style.display = "none";
|
||||
|
||||
var slider = document.getElementById("week-slider");
|
||||
var prCalMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
|
||||
lmpInput.addEventListener("input", function(){
|
||||
saveLS("ca-lmp", lmpInput.value);
|
||||
document.getElementById("pr-example-note").style.display = "none";
|
||||
document.getElementById("p-example-note").style.display = "none";
|
||||
syncSliderToLMP();
|
||||
renderPregnancy();
|
||||
});
|
||||
|
||||
function getLMP(){ return fromInput(lmpInput.value) || exampleLMP; }
|
||||
function currentGestWeek(){
|
||||
var lmp = getLMP();
|
||||
var w = Math.floor(daysBetween(lmp,today)/7) + 1;
|
||||
return Math.max(1, Math.min(40, w));
|
||||
}
|
||||
function syncSliderToLMP(){ slider.value = currentGestWeek(); }
|
||||
syncSliderToLMP();
|
||||
|
||||
function renderPregnancyStats(){
|
||||
var lmp = getLMP();
|
||||
var due = addDays(lmp, 280);
|
||||
var week = currentGestWeek();
|
||||
var daysLeft = Math.max(0, daysBetween(today, due));
|
||||
document.getElementById("pregnancy-stats").innerHTML =
|
||||
statTile(week, "Weeks along") +
|
||||
statTile(Math.floor((week-1)/13)+1 <= 3 ? ["First","Second","Third"][Math.min(2,Math.floor((week-1)/13))] : "Third", "Trimester") +
|
||||
statTile(daysLeft, "Days to due date") +
|
||||
statTile(due.toLocaleDateString(undefined,{month:"short",day:"numeric"}), "Estimated due date");
|
||||
|
||||
var pct = (week-1)/39;
|
||||
var pathLen = 230;
|
||||
var offset = pathLen - pct*pathLen;
|
||||
var path = document.getElementById("pregnancy-progress-path");
|
||||
path.setAttribute("stroke-dashoffset", offset.toFixed(1));
|
||||
try{
|
||||
var totalLen = path.getTotalLength ? path.getTotalLength() : pathLen;
|
||||
var pt = path.getPointAtLength ? path.getPointAtLength(pct*totalLen) : null;
|
||||
if(pt) { document.getElementById("pregnancy-progress-dot").setAttribute("cx",pt.x); document.getElementById("pregnancy-progress-dot").setAttribute("cy",pt.y); }
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
function renderWeekPanel(week){
|
||||
var w = WEEKS[week-1];
|
||||
document.getElementById("wk-badge").textContent = "WEEK " + week;
|
||||
document.getElementById("wk-compare").textContent = "size of " + w.c;
|
||||
document.getElementById("wk-metric").textContent = w.m === "n/a" ? "not yet fertilized" : "≈ " + w.m;
|
||||
document.getElementById("wk-dev").textContent = w.dev;
|
||||
document.getElementById("wk-body").textContent = w.body;
|
||||
document.getElementById("wk-tip").textContent = w.tip;
|
||||
var tri = week<=13?1:(week<=27?2:3);
|
||||
var triColor = "var(--pregnancy)";
|
||||
document.getElementById("wk-blob").innerHTML =
|
||||
'<svg width="96" height="96" viewBox="0 0 96 96"><circle cx="48" cy="48" r="'+(14+week*0.9)+'" fill="'+ (tri===1?"var(--pregnancy-soft)":tri===2?"var(--pregnancy)":"var(--pregnancy-strong)") +'" opacity="0.85"/></svg>';
|
||||
document.querySelectorAll(".film-item").forEach(function(f){ f.classList.toggle("active", +f.dataset.week===week); });
|
||||
var active = document.querySelector('.film-item[data-week="'+week+'"]');
|
||||
if(active) active.scrollIntoView({inline:"center", block:"nearest", behavior:"smooth"});
|
||||
}
|
||||
|
||||
slider.addEventListener("input", function(){ renderWeekPanel(+slider.value); });
|
||||
|
||||
function buildFilmstrip(){
|
||||
var wrap = document.getElementById("week-filmstrip");
|
||||
wrap.innerHTML = "";
|
||||
for(var w=1; w<=40; w++){
|
||||
var item = document.createElement("button");
|
||||
item.className = "film-item";
|
||||
item.dataset.week = w;
|
||||
item.innerHTML = '<div class="fw">WK</div><div class="fn">'+w+'</div>';
|
||||
item.addEventListener("click", function(ww){ return function(){ slider.value=ww; renderWeekPanel(ww); }; }(w));
|
||||
wrap.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPregnancyCalendar(){
|
||||
var lmp = getLMP();
|
||||
document.getElementById("pr-cal-title").textContent = MONTH_NAMES[prCalMonth.getMonth()] + " " + prCalMonth.getFullYear();
|
||||
var grid = document.getElementById("pr-daygrid");
|
||||
grid.innerHTML = "";
|
||||
var year = prCalMonth.getFullYear(), month = prCalMonth.getMonth();
|
||||
var firstDow = new Date(year,month,1).getDay();
|
||||
var daysInMonth = new Date(year,month+1,0).getDate();
|
||||
var totalCells = Math.ceil((firstDow+daysInMonth)/7)*7;
|
||||
for(var i=0;i<totalCells;i++){
|
||||
var dayNum = i - firstDow + 1;
|
||||
var cell = document.createElement("button");
|
||||
cell.className = "day-cell";
|
||||
if(dayNum<1 || dayNum>daysInMonth){ cell.classList.add("pad"); grid.appendChild(cell); continue; }
|
||||
var d = new Date(year,month,dayNum);
|
||||
var wk = Math.floor(daysBetween(lmp,d)/7)+1;
|
||||
var inRange = wk>=1 && wk<=42;
|
||||
cell.innerHTML = "<span>"+dayNum+"</span>" + (inRange ? '<span class="dot-row" style="position:static;margin-top:2px;font-size:0.55rem;color:var(--pregnancy-strong)">W'+wk+'</span>' : "");
|
||||
if(inRange && wk<=40) cell.classList.add("is-fertile");
|
||||
if(sameDay(d,today)) cell.classList.add("today");
|
||||
cell.addEventListener("click", function(ww){ return function(){ if(ww>=1&&ww<=40){ slider.value=ww; renderWeekPanel(ww); } }; }(wk));
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
}
|
||||
document.getElementById("pr-prev").addEventListener("click", function(){ prCalMonth.setMonth(prCalMonth.getMonth()-1); renderPregnancyCalendar(); });
|
||||
document.getElementById("pr-next").addEventListener("click", function(){ prCalMonth.setMonth(prCalMonth.getMonth()+1); renderPregnancyCalendar(); });
|
||||
|
||||
function renderPregnancy(){
|
||||
renderPregnancyStats();
|
||||
renderWeekPanel(currentGestWeek());
|
||||
renderPregnancyCalendar();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
INIT
|
||||
================================================================ */
|
||||
function renderAll(){
|
||||
renderPeriodStats();
|
||||
renderPeriodCalendar();
|
||||
renderPeriodDetail();
|
||||
renderOvulationStats();
|
||||
renderOvTimeline();
|
||||
renderOvDetailFromOffset();
|
||||
renderOvulationCalendar();
|
||||
renderPregnancy();
|
||||
}
|
||||
|
||||
renderPhaseRefGrid();
|
||||
buildFilmstrip();
|
||||
slider.value = currentGestWeek();
|
||||
renderAll();
|
||||
})();
|
||||
</script>
|
||||
<script src="app.js"></script>
|
||||
<script src="vendor/gsap.min.js"></script>
|
||||
<script src="vendor/ScrollTrigger.min.js"></script>
|
||||
<script src="ring3d.js"></script>
|
||||
<script src="animations.js"></script>
|
||||
<script src="timeline.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,151 @@
|
||||
/* CycleWise AU - 3D cycle ring (Step 2).
|
||||
Draws the same four phase arcs as the flat SVG ring, in 3D.
|
||||
The page code sends the numbers via the "cycle:ring" event.
|
||||
If WebGL is missing or anything fails, the flat ring stays visible. */
|
||||
(function(){
|
||||
"use strict";
|
||||
function start(){
|
||||
try {
|
||||
if (!window.THREE) return;
|
||||
var art = document.getElementById("phase-arc-menstrual");
|
||||
art = art && art.closest(".hero-art");
|
||||
if (!art) return;
|
||||
|
||||
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
var svg = art.querySelector("svg");
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.setAttribute("aria-hidden", "true");
|
||||
canvas.className = "ring3d";
|
||||
|
||||
var renderer;
|
||||
try { renderer = new THREE.WebGLRenderer({canvas: canvas, antialias: true, alpha: true}); }
|
||||
catch(e){ return; } // no WebGL: keep the flat ring
|
||||
|
||||
var R = 1, TUBE = 0.085;
|
||||
var scene = new THREE.Scene();
|
||||
var camera = new THREE.PerspectiveCamera(30, 1, 0.1, 20);
|
||||
camera.position.set(0, 0, 5.2);
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.75));
|
||||
var sun = new THREE.DirectionalLight(0xffffff, 1.1);
|
||||
sun.position.set(2, 3, 4);
|
||||
scene.add(sun);
|
||||
|
||||
var group = new THREE.Group();
|
||||
scene.add(group);
|
||||
|
||||
function cssColor(name){
|
||||
var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
return new THREE.Color(v || "#888888");
|
||||
}
|
||||
function point(deg, r){
|
||||
var a = deg * Math.PI / 180; // 0 deg = top, clockwise
|
||||
return new THREE.Vector3(Math.sin(a) * r, Math.cos(a) * r, 0);
|
||||
}
|
||||
class ArcCurve extends THREE.Curve {
|
||||
constructor(a0, a1){ super(); this.a0 = a0; this.a1 = a1; }
|
||||
getPoint(t, target){
|
||||
return (target || new THREE.Vector3()).copy(point(this.a0 + (this.a1 - this.a0) * t, R));
|
||||
}
|
||||
}
|
||||
|
||||
var data = window.cycleRingData || null;
|
||||
var meshes = [];
|
||||
|
||||
function clear(){
|
||||
meshes.forEach(function(m){
|
||||
group.remove(m); m.geometry.dispose(); m.material.dispose();
|
||||
});
|
||||
meshes = [];
|
||||
}
|
||||
function add(mesh){ group.add(mesh); meshes.push(mesh); }
|
||||
function mat(c){ return new THREE.MeshStandardMaterial({color: c, roughness: 0.45, metalness: 0.05}); }
|
||||
|
||||
function build(){
|
||||
clear();
|
||||
if (!data) return;
|
||||
var cols = {
|
||||
menstrual: cssColor("--period"),
|
||||
follicular: cssColor("--ovulation-soft"),
|
||||
fertile: cssColor("--fertile"),
|
||||
luteal: cssColor("--pregnancy-soft"),
|
||||
ink: cssColor("--ink")
|
||||
};
|
||||
var arcs = [
|
||||
[0, data.perEnd, cols.menstrual],
|
||||
[data.perEnd, data.fertStart, cols.follicular],
|
||||
[data.fertStart, data.fertEnd, cols.fertile],
|
||||
[data.lutStart, 360, cols.luteal]
|
||||
];
|
||||
arcs.forEach(function(a){
|
||||
var end = Math.max(a[1], a[0] + 0.5);
|
||||
var tube = new THREE.Mesh(new THREE.TubeGeometry(new ArcCurve(a[0], end), 48, TUBE, 16, false), mat(a[2]));
|
||||
add(tube);
|
||||
[a[0], end].forEach(function(deg){ // round end caps
|
||||
var cap = new THREE.Mesh(new THREE.SphereGeometry(TUBE, 16, 12), mat(a[2]));
|
||||
cap.position.copy(point(deg, R));
|
||||
add(cap);
|
||||
});
|
||||
});
|
||||
var marker = new THREE.Mesh(new THREE.SphereGeometry(TUBE * 1.9, 24, 16), mat(cols.ink));
|
||||
marker.position.copy(point(data.todayAngle, R));
|
||||
marker.position.z = TUBE * 0.6;
|
||||
add(marker);
|
||||
}
|
||||
|
||||
function resize(){
|
||||
var w = svg.getBoundingClientRect().width || 220;
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
renderer.setSize(w, w, false);
|
||||
canvas.style.width = canvas.style.height = w + "px";
|
||||
}
|
||||
|
||||
var visible = false, raf = 0;
|
||||
function frame(t){
|
||||
raf = 0;
|
||||
var s = t / 1000;
|
||||
group.rotation.x = -0.55 + (reduce ? 0 : Math.sin(s * 0.6) * 0.12);
|
||||
group.rotation.y = reduce ? 0.25 : Math.sin(s * 0.4) * 0.45;
|
||||
renderer.render(scene, camera);
|
||||
if (visible && !reduce) raf = requestAnimationFrame(frame);
|
||||
}
|
||||
function kick(){ if (!raf) raf = requestAnimationFrame(frame); }
|
||||
|
||||
// Show the 3D ring; hide the flat arcs but keep the "CYCLE DAY" text.
|
||||
art.classList.add("has-3d");
|
||||
art.insertBefore(canvas, svg);
|
||||
resize();
|
||||
build();
|
||||
kick();
|
||||
|
||||
window.addEventListener("cycle:ring", function(e){ data = e.detail; build(); kick(); });
|
||||
window.addEventListener("resize", function(){ resize(); kick(); });
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function(){ build(); kick(); });
|
||||
|
||||
// Only animate while the ring is actually on screen (saves battery).
|
||||
new IntersectionObserver(function(entries){
|
||||
visible = entries[0].isIntersecting;
|
||||
if (visible) { resize(); kick(); }
|
||||
}).observe(canvas);
|
||||
} catch(err) {
|
||||
// Anything went wrong: leave the flat ring alone.
|
||||
var c = document.querySelector(".ring3d"); if (c) c.remove();
|
||||
var h = document.querySelector(".hero-art.has-3d"); if (h) h.classList.remove("has-3d");
|
||||
if (window.console) console.warn("3D ring disabled:", err && err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// three.js is big: fetch it only after the page is up, so the page shows first.
|
||||
function loadThree(){
|
||||
var tag = document.createElement("script");
|
||||
tag.src = "vendor/three.min.js";
|
||||
tag.onload = start;
|
||||
document.head.appendChild(tag);
|
||||
}
|
||||
function schedule(){
|
||||
if (window.THREE) { start(); }
|
||||
else if (window.requestIdleCallback) { requestIdleCallback(loadThree, {timeout: 2000}); }
|
||||
else { setTimeout(loadThree, 300); }
|
||||
}
|
||||
if (document.readyState === "complete") schedule();
|
||||
else window.addEventListener("load", schedule);
|
||||
})();
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/* CycleWise AU - pregnancy timeline motion (Step 3).
|
||||
- The week slider's track fills with colour up to the current week.
|
||||
It grows the first time you scroll to it, then follows every week change.
|
||||
- The baby-size picture pops a little when the week changes.
|
||||
- The 40 week buttons wave in the first time they appear.
|
||||
Cosmetic only: without GSAP the slider simply shows its normal look. */
|
||||
(function(){
|
||||
"use strict";
|
||||
var slider = document.getElementById("week-slider");
|
||||
var badge = document.getElementById("wk-badge");
|
||||
if (!slider || !badge) return;
|
||||
|
||||
var hasGsap = !!window.gsap;
|
||||
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
var animate = hasGsap && !reduce;
|
||||
|
||||
var fill = { f: 0 };
|
||||
var target = 0;
|
||||
var introDone = false;
|
||||
|
||||
function paint(){ slider.style.setProperty("--f", fill.f.toFixed(4)); }
|
||||
function currentWeek(){
|
||||
var n = parseInt((badge.textContent || "").replace(/\D/g, ""), 10);
|
||||
return isNaN(n) ? (parseInt(slider.value, 10) || 1) : Math.max(1, Math.min(40, n));
|
||||
}
|
||||
function goTo(f){
|
||||
target = f;
|
||||
if (!introDone) return; // wait until the slider is on screen
|
||||
if (!animate) { fill.f = f; paint(); return; }
|
||||
gsap.to(fill, { f: f, duration: 0.6, ease: "power2.out", overwrite: true, onUpdate: paint });
|
||||
}
|
||||
|
||||
// Follow every week change, whether from the slider, a week button or the calendar.
|
||||
var lastWeek = currentWeek();
|
||||
target = (lastWeek - 1) / 39;
|
||||
new MutationObserver(function(){
|
||||
var w = currentWeek();
|
||||
if (w === lastWeek) return;
|
||||
lastWeek = w;
|
||||
goTo((w - 1) / 39);
|
||||
var blob = document.querySelector("#wk-blob svg");
|
||||
if (animate && blob) {
|
||||
gsap.fromTo(blob, { scale: 0.8, transformOrigin: "50% 50%" },
|
||||
{ scale: 1, duration: 0.5, ease: "back.out(2)", clearProps: "transform" });
|
||||
}
|
||||
}).observe(badge, { childList: true, characterData: true, subtree: true });
|
||||
|
||||
// First time the slider is visible: grow the fill from empty up to this week.
|
||||
new IntersectionObserver(function(entries, obs){
|
||||
if (!entries[0].isIntersecting) return;
|
||||
obs.disconnect();
|
||||
introDone = true;
|
||||
if (!animate) { fill.f = target; paint(); return; }
|
||||
gsap.to(fill, { f: target, duration: 1.2, ease: "power2.out", onUpdate: paint });
|
||||
}).observe(slider);
|
||||
|
||||
// First time the week buttons are visible: wave them in.
|
||||
var strip = document.getElementById("week-filmstrip");
|
||||
if (strip && animate) {
|
||||
new IntersectionObserver(function(entries, obs){
|
||||
if (!entries[0].isIntersecting) return;
|
||||
obs.disconnect();
|
||||
gsap.from(strip.querySelectorAll(".film-item"), {
|
||||
opacity: 0, scale: 0.8, duration: 0.4, stagger: 0.02, ease: "power2.out", clearProps: "all"
|
||||
});
|
||||
}).observe(strip);
|
||||
}
|
||||
})();
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/*!
|
||||
* ScrollTrigger 3.12.5
|
||||
* https://gsap.com
|
||||
*
|
||||
* @license Copyright 2024, GreenSock. All rights reserved.
|
||||
* Subject to the terms at https://gsap.com/standard-license or for Club GSAP members, the agreement issued with that membership.
|
||||
* @author: Jack Doyle, jack@greensock.com
|
||||
*/
|
||||
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function _defineProperties(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}function r(){return Ce||"undefined"!=typeof window&&(Ce=window.gsap)&&Ce.registerPlugin&&Ce}function z(e,t){return~Le.indexOf(e)&&Le[Le.indexOf(e)+1][t]}function A(e){return!!~t.indexOf(e)}function B(e,t,r,n,o){return e.addEventListener(t,r,{passive:!1!==n,capture:!!o})}function C(e,t,r,n){return e.removeEventListener(t,r,!!n)}function F(){return De&&De.isPressed||Ie.cache++}function G(r,n){function dd(e){if(e||0===e){o&&(Se.history.scrollRestoration="manual");var t=De&&De.isPressed;e=dd.v=Math.round(e)||(De&&De.iOS?1:0),r(e),dd.cacheID=Ie.cache,t&&i("ss",e)}else(n||Ie.cache!==dd.cacheID||i("ref"))&&(dd.cacheID=Ie.cache,dd.v=r());return dd.v+dd.offset}return dd.offset=0,r&&dd}function J(e,t){return(t&&t._ctx&&t._ctx.selector||Ce.utils.toArray)(e)[0]||("string"==typeof e&&!1!==Ce.config().nullTargetWarn?console.warn("Element not found:",e):null)}function K(t,e){var r=e.s,n=e.sc;A(t)&&(t=ke.scrollingElement||Pe);var o=Ie.indexOf(t),i=n===Fe.sc?1:2;~o||(o=Ie.push(t)-1),Ie[o+i]||B(t,"scroll",F);var a=Ie[o+i],s=a||(Ie[o+i]=G(z(t,r),!0)||(A(t)?n:G(function(e){return arguments.length?t[r]=e:t[r]})));return s.target=t,a||(s.smooth="smooth"===Ce.getProperty(t,"scrollBehavior")),s}function L(e,t,o){function Cd(e,t){var r=ze();t||n<r-s?(a=i,i=e,l=s,s=r):o?i+=e:i=a+(e-a)/(r-l)*(s-l)}var i=e,a=e,s=ze(),l=s,n=t||50,c=Math.max(500,3*n);return{update:Cd,reset:function reset(){a=i=o?0:i,l=s=0},getVelocity:function getVelocity(e){var t=l,r=a,n=ze();return!e&&0!==e||e===i||Cd(e),s===l||c<n-l?0:(i+(o?r:-r))/((o?n:s)-t)*1e3}}}function M(e,t){return t&&!e._gsapAllow&&e.preventDefault(),e.changedTouches?e.changedTouches[0]:e}function N(e){var t=Math.max.apply(Math,e),r=Math.min.apply(Math,e);return Math.abs(t)>=Math.abs(r)?t:r}function O(){(Ae=Ce.core.globals().ScrollTrigger)&&Ae.core&&function _integrate(){var e=Ae.core,r=e.bridge||{},t=e._scrollers,n=e._proxies;t.push.apply(t,Ie),n.push.apply(n,Le),Ie=t,Le=n,i=function _bridge(e,t){return r[e](t)}}()}function P(e){return Ce=e||r(),!Te&&Ce&&"undefined"!=typeof document&&document.body&&(Se=window,Pe=(ke=document).documentElement,Me=ke.body,t=[Se,ke,Pe,Me],Ce.utils.clamp,Be=Ce.core.context||function(){},Oe="onpointerenter"in Me?"pointer":"mouse",Ee=k.isTouch=Se.matchMedia&&Se.matchMedia("(hover: none), (pointer: coarse)").matches?1:"ontouchstart"in Se||0<navigator.maxTouchPoints||0<navigator.msMaxTouchPoints?2:0,Re=k.eventTypes=("ontouchstart"in Pe?"touchstart,touchmove,touchcancel,touchend":"onpointerdown"in Pe?"pointerdown,pointermove,pointercancel,pointerup":"mousedown,mousemove,mouseup,mouseup").split(","),setTimeout(function(){return o=0},500),O(),Te=1),Te}var Ce,Te,Se,ke,Pe,Me,Ee,Oe,Ae,t,De,Re,Be,o=1,qe=[],Ie=[],Le=[],ze=Date.now,i=function _bridge(e,t){return t},n="scrollLeft",a="scrollTop",Ye={s:n,p:"left",p2:"Left",os:"right",os2:"Right",d:"width",d2:"Width",a:"x",sc:G(function(e){return arguments.length?Se.scrollTo(e,Fe.sc()):Se.pageXOffset||ke[n]||Pe[n]||Me[n]||0})},Fe={s:a,p:"top",p2:"Top",os:"bottom",os2:"Bottom",d:"height",d2:"Height",a:"y",op:Ye,sc:G(function(e){return arguments.length?Se.scrollTo(Ye.sc(),e):Se.pageYOffset||ke[a]||Pe[a]||Me[a]||0})};Ye.op=Fe,Ie.cache=0;var k=(Observer.prototype.init=function init(e){Te||P(Ce)||console.warn("Please gsap.registerPlugin(Observer)"),Ae||O();var o=e.tolerance,a=e.dragMinimum,t=e.type,i=e.target,r=e.lineHeight,n=e.debounce,s=e.preventDefault,l=e.onStop,c=e.onStopDelay,u=e.ignore,f=e.wheelSpeed,d=e.event,p=e.onDragStart,g=e.onDragEnd,h=e.onDrag,v=e.onPress,b=e.onRelease,m=e.onRight,y=e.onLeft,x=e.onUp,w=e.onDown,_=e.onChangeX,T=e.onChangeY,S=e.onChange,k=e.onToggleX,E=e.onToggleY,D=e.onHover,R=e.onHoverEnd,q=e.onMove,I=e.ignoreCheck,z=e.isNormalizer,Y=e.onGestureStart,H=e.onGestureEnd,X=e.onWheel,W=e.onEnable,V=e.onDisable,U=e.onClick,j=e.scrollSpeed,G=e.capture,Q=e.allowClicks,Z=e.lockAxis,$=e.onLockAxis;function cf(){return xe=ze()}function df(e,t){return(se.event=e)&&u&&~u.indexOf(e.target)||t&&he&&"touch"!==e.pointerType||I&&I(e,t)}function ff(){var e=se.deltaX=N(me),t=se.deltaY=N(ye),r=Math.abs(e)>=o,n=Math.abs(t)>=o;S&&(r||n)&&S(se,e,t,me,ye),r&&(m&&0<se.deltaX&&m(se),y&&se.deltaX<0&&y(se),_&&_(se),k&&se.deltaX<0!=le<0&&k(se),le=se.deltaX,me[0]=me[1]=me[2]=0),n&&(w&&0<se.deltaY&&w(se),x&&se.deltaY<0&&x(se),T&&T(se),E&&se.deltaY<0!=ce<0&&E(se),ce=se.deltaY,ye[0]=ye[1]=ye[2]=0),(ne||re)&&(q&&q(se),re&&(h(se),re=!1),ne=!1),ie&&!(ie=!1)&&$&&$(se),oe&&(X(se),oe=!1),ee=0}function gf(e,t,r){me[r]+=e,ye[r]+=t,se._vx.update(e),se._vy.update(t),n?ee=ee||requestAnimationFrame(ff):ff()}function hf(e,t){Z&&!ae&&(se.axis=ae=Math.abs(e)>Math.abs(t)?"xLine truncated
|
||||
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright © 2010-2023 three.js authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/*!
|
||||
* GSAP 3.12.5
|
||||
* https://gsap.com
|
||||
*
|
||||
* @license Copyright 2024, GreenSock. All rights reserved.
|
||||
* Subject to the terms at https://gsap.com/standard-license or for Club GSAP members, the agreement issued with that membership.
|
||||
* @author: Jack Doyle, jack@greensock.com
|
||||
*/
|
||||
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t){return"string"==typeof t}function s(t){return"function"==typeof t}function t(t){return"number"==typeof t}function u(t){return void 0===t}function v(t){return"object"==typeof t}function w(t){return!1!==t}function x(){return"undefined"!=typeof window}function y(t){return s(t)||r(t)}function P(t){return(i=yt(t,ot))&&ze}function Q(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function R(t,e){return!e&&console.warn(t)}function S(t,e){return t&&(ot[t]=e)&&i&&(i[t]=e)||ot}function T(){return 0}function ea(t){var e,r,i=t[0];if(v(i)||s(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=gt.length;r--&&!gt[r].targetTest(i););e=gt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Vt(t[r],e)))||t.splice(r,1);return t}function fa(t){return t._gsap||ea(Mt(t))[0]._gsap}function ga(t,e,r){return(r=t[e])&&s(r)?t[e]():u(r)&&t.getAttribute&&t.getAttribute(e)||r}function ha(t,e){return(t=t.split(",")).forEach(e)||t}function ia(t){return Math.round(1e5*t)/1e5||0}function ja(t){return Math.round(1e7*t)/1e7||0}function ka(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i}function la(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++i<r;);return i<r}function ma(){var t,e,r=dt.length,i=dt.slice(0);for(ct={},t=dt.length=0;t<r;t++)(e=i[t])&&e._lazy&&(e.render(e._lazy[0],e._lazy[1],!0)._lazy=0)}function na(t,e,r,i){dt.length&&!L&&ma(),t.render(e,r,i||L&&e<0&&(t._initted||t._startAt)),dt.length&&!L&&ma()}function oa(t){var e=parseFloat(t);return(e||0===e)&&(t+"").match(at).length<2?e:r(t)?t.trim():t}function pa(t){return t}function qa(t,e){for(var r in e)r in t||(t[r]=e[r]);return t}function ta(t,e){for(var r in e)"__proto__"!==r&&"constructor"!==r&&"prototype"!==r&&(t[r]=v(e[r])?ta(t[r]||(t[r]={}),e[r]):e[r]);return t}function ua(t,e){var r,i={};for(r in t)r in e||(i[r]=t[r]);return i}function va(t){var e=t.parent||I,r=t.keyframes?function _setKeyframeDefaults(i){return function(t,e){for(var r in e)r in t||"duration"===r&&i||"ease"===r||(t[r]=e[r])}}(Z(t.keyframes)):qa;if(w(t.inherit))for(;e;)r(t,e.vars.defaults),e=e.parent||e._dp;return t}function xa(t,e,r,i,n){void 0===r&&(r="_first"),void 0===i&&(i="_last");var a,s=t[i];if(n)for(a=e[n];s&&s[n]>a;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function ya(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function za(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Aa(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Ca(t,e,r,i){return t._startAt&&(L?t._startAt.revert(ht):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Ea(t){return t._repeat?Tt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ga(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ha(t){return t._end=ja(t._start+(t._tDur/Math.abs(t._ts||t._rts||X)||0))}function Ia(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=ja(r._time-(0<t._ts?e/t._ts:((t._dirty?t.totalDuration():t._tDur)-e)/-t._ts)),Ha(t),r._dirty||Aa(r,t)),t}function Ja(t,e){var r;if((e._time||!e._dur&&e._initted||e._start<t._time&&(e._dur||!e.add))&&(r=Ga(t.rawTime(),e),(!e._dur||Ot(0,e.totalDuration(),r)-e._tTime>X)&&e.render(r,!0)),Aa(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur<t.duration())for(r=t;r._dp;)0<=r.rawTime()&&r.totalTime(r._tTime),r=r._dp;t._zTime=-X}}function Ka(e,r,i,n){return r.parent&&za(r),r._start=ja((t(i)?i:i||e!==I?xt(e,i,r):e._time)+r._delay),r._end=ja(r._start+(r.totalDuration()/Math.abs(r.timeScale())||0)),xa(e,r,"_first","_last",e._sort?"_start":0),bt(r)||(e._recent=r),n||Ja(e,r),e._ts<0&&Ia(e,e._tTime),e}function La(t,e){return(ot.ScrollTrigger||Q("scrollTrigger",e))&&ot.ScrollTrigger.create(e,t)}function Ma(t,e,r,i,n){return Qt(t,e,n),t._initted?!r&&t._pt&&!L&&(t._dur&&!1!==t.vars.lazy||!t._dur&&t.vars.lazy)&&f!==Rt.frame?(dt.push(t),t._lazy=[n,i],1):void 0:1}function Ra(t,e,r,i){var n=t._repeat,a=ja(e)||0,s=t._tTime/t._tDur;return s&&!i&&(t._time*=a/t._dur),t._dur=a,t._tDur=n?n<0?1e10:ja(a*(n+1)+t._rDelay*n):a,0<s&&!i&&Ia(t,t._tTime=t._tDur*s),t.parent&&Ha(t),r||Aa(t.parent,t),t}function Sa(t){return t instanceof Xt?Aa(t):Ra(t,t._dur)}function Va(e,r,i){var n,aLine truncated
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
console.warn('Scripts "build/three.js" and "build/three.min.js" are deprecated with r150+, and will be removed with r160. Please use ES Modules or alternatives: https://threejs.org/docs/index.html#manual/en/introduction/Installation'),
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2010-2023 Three.js Authors
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).THREE={})}(this,(function(t){"use strict";const e="160",n=1,i=2,r=3,s=0,a=1,o=100,l=204,c=205,h=0,u=1,d=2,p=0,m=1,f=2,g=3,_=4,v=5,x=6,y="attached",M="detached",S=300,b=301,T=302,E=303,w=304,A=306,R=1e3,C=1001,L=1002,P=1003,I=1004,U=1005,D=1006,N=1007,O=1008,F=1009,B=1012,z=1013,G=1014,H=1015,V=1016,k=1017,W=1018,X=1020,j=1023,q=1026,Y=1027,Z=1029,J=1031,K=1033,$=33776,Q=33777,tt=33778,et=33779,nt=35840,it=35841,rt=35842,st=35843,at=36196,ot=37492,lt=37496,ct=37808,ht=37809,ut=37810,dt=37811,pt=37812,mt=37813,ft=37814,gt=37815,_t=37816,vt=37817,xt=37818,yt=37819,Mt=37820,St=37821,bt=36492,Tt=36494,Et=36495,wt=36284,At=36285,Rt=36286,Ct=2300,Lt=2301,Pt=2302,It=2400,Ut=2401,Dt=2402,Nt=2500,Ot=2501,Ft=3e3,Bt=3001,zt="",Gt="srgb",Ht="srgb-linear",Vt="display-p3",kt="display-p3-linear",Wt="linear",Xt="srgb",jt="rec709",qt="p3",Yt=7680,Zt=35044,Jt="300 es",Kt=1035,$t=2e3,Qt=2001;class te{addEventListener(t,e){void 0===this._listeners&&(this._listeners={});const n=this._listeners;void 0===n[t]&&(n[t]=[]),-1===n[t].indexOf(e)&&n[t].push(e)}hasEventListener(t,e){if(void 0===this._listeners)return!1;const n=this._listeners;return void 0!==n[t]&&-1!==n[t].indexOf(e)}removeEventListener(t,e){if(void 0===this._listeners)return;const n=this._listeners[t];if(void 0!==n){const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}dispatchEvent(t){if(void 0===this._listeners)return;const e=this._listeners[t.type];if(void 0!==e){t.target=this;const n=e.slice(0);for(let e=0,i=n.length;e<i;e++)n[e].call(this,t);t.target=null}}}const ee=["00","01","02","03","04","05","06","07","08","09","0a","0b","0c","0d","0e","0f","10","11","12","13","14","15","16","17","18","19","1a","1b","1c","1d","1e","1f","20","21","22","23","24","25","26","27","28","29","2a","2b","2c","2d","2e","2f","30","31","32","33","34","35","36","37","38","39","3a","3b","3c","3d","3e","3f","40","41","42","43","44","45","46","47","48","49","4a","4b","4c","4d","4e","4f","50","51","52","53","54","55","56","57","58","59","5a","5b","5c","5d","5e","5f","60","61","62","63","64","65","66","67","68","69","6a","6b","6c","6d","6e","6f","70","71","72","73","74","75","76","77","78","79","7a","7b","7c","7d","7e","7f","80","81","82","83","84","85","86","87","88","89","8a","8b","8c","8d","8e","8f","90","91","92","93","94","95","96","97","98","99","9a","9b","9c","9d","9e","9f","a0","a1","a2","a3","a4","a5","a6","a7","a8","a9","aa","ab","ac","ad","ae","af","b0","b1","b2","b3","b4","b5","b6","b7","b8","b9","ba","bb","bc","bd","be","bf","c0","c1","c2","c3","c4","c5","c6","c7","c8","c9","ca","cb","cc","cd","ce","cf","d0","d1","d2","d3","d4","d5","d6","d7","d8","d9","da","db","dc","dd","de","df","e0","e1","e2","e3","e4","e5","e6","e7","e8","e9","ea","eb","ec","ed","ee","ef","f0","f1","f2","f3","f4","f5","f6","f7","f8","f9","fa","fb","fc","fd","fe","ff"];let ne=1234567;const ie=Math.PI/180,re=180/Math.PI;function se(){const t=4294967295*Math.random()|0,e=4294967295*Math.random()|0,n=4294967295*Math.random()|0,i=4294967295*Math.random()|0;return(ee[255&t]+ee[t>>8&255]+ee[t>>16&255]+ee[t>>24&255]+"-"+ee[255&e]+ee[e>>8&255]+"-"+ee[e>>16&15|64]+ee[e>>24&255]+"-"+ee[63&n|128]+ee[n>>8&255]+"-"+ee[n>>16&255]+ee[n>>24&255]+ee[255&i]+ee[i>>8&255]+ee[i>>16&255]+ee[i>>24&255]).toLowerCase()}function ae(t,e,n){return Math.max(e,Math.min(n,t))}function oe(t,e){return(t%e+e)%e}function le(t,e,n){return(1-n)*t+n*e}function ce(t){return 0==(t&t-1)&&0!==t}function he(t){return Math.pow(2,Math.floor(Math.log(t)/Math.LN2))}function ue(t,e){switch(e.constructor){case Float32Array:return t;case Uint32Array:return t/4294967295;case Uint16Array:return t/65535;case Uint8Array:return t/255;case Int32Array:return Math.max(t/2147483647,-1);case Int16Array:return Math.max(t/32767,-1);case Int8Array:return Math.max(t/127,-1);default:throw new Error("Invalid component type.")}}function de(t,e){switch(e.constructor){case Float32Array:return t;case Uint32Array:return Math.round(4294967295*t);case Uint16Array:return Math.round(65535*t);case Uint8Array:return Math.round(255*t);case Int32Array:return Math.round(2147483647*t);case Int16Array:return Math.round(32767*t);case Int8Array:return Math.round(127*t);default:throw new Error("Invalid component type.")}}const pe={DEG2RAD:ie,RAD2DEG:re,generateUUID:se,clamp:ae,euclideanModulo:oe,mapLinear:function(t,e,n,i,r){return i+(t-e)*(r-i)/(n-e)},inverseLerp:function(t,e,n){return t!==e?(n-t)/(e-t):0},lerp:le,damp:function(t,e,n,i){return le(t,e,1-Math.exp(-n*i))},pingpong:function(t,e=1){return e-Math.abs(oe(t,2*e)-e)},smoothstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*(3-2*t)},smootherstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*t*(t*(6*t-15)+10)},randInt:function(t,e){return t+Math.floor(Math.random()*(e-t+1))},randFloat:function(t,e){return t+Math.random()*(e-t)},randFloatLine truncated
|
||||
Reference in new issue
Block a user