Free to try ยท No credit card ยท Your stylist, always in your pocket
Look your best every singleday.
Stop guessing. No more clothes that make you look tired. Find the colours that match your unique skin, hair, and eyes in seconds.
No hidden subscriptions
Your photos analyzed privately, never stored
Your results in minutes, not days
How it works
Your personal stylist โ ready in 30 seconds
01
Check
Take a quick photo in natural light โ near a window works perfectly. Two or three photos is all you need. No appointment. No waiting.
02
Uncover
The Hued Method reads your skin tone, hair, and eyes together โ and uncovers the exact colors that make you look healthy, vibrant, and awake. Your personal color rulebook.
03
Shine
Shop with certainty and feel great in everything you wear. Open Hued any time you need it โ in a store, at the salon, or before you buy anything online. Your colours, always with you.
What's included
Everything you need to know about your colours
๐จ
Your colour season
A name for your unique combination of skin, hair, and eye color โ tells you everything about which colors work for you
๐
Hair advisor
Which shades and styles suit you
๐
Glasses advisor
Frame styles and colors that work for you
๐
Makeup matching
Every shade matched to your coloring
๐
Color combinations
Which colors pair beautifully together for you
๐๏ธ
Shopping companion
Check any colour before you buy it
โจ
Saved forever
Saved and updated as you change
Simple, honest pricing
Start free. Unlock when you're ready. No surprises.
A professional color analysis costs $300โ$500, takes weeks to book, and gives you a laminated card you leave at home. Hued gives you the same knowledge in 30 seconds โ and it's in your pocket the next time you're standing in a store, wondering.
Step 1 โ Discover your colours
The Spark
$0
Free, forever
โ Personal colour analysis
โ Your colour season + undertone
โ Your top 4 power colors
โ Colors to avoid
Most popular
Full Reveal
$39
one-time ยท yours forever
โ Complete 12-colour palette
โ Hair colour advisor + stylist script
โ Glasses advisor
โ Makeup matching
โ 3 scans included
โ Saved to your profile forever
โ 30 days of The Glow โ free
Step 2 โ Keep growing (optional)
After your Full Reveal, you get 30 days of The Glow free. If you love it, keep going โ cancel anytime.
The Glow
$12.99
per month ยท cancel anytime
โ Everything in Full Reveal
โ 5 fresh scans every month
โ Personal Style Chat
โ Shopping companion
โ Seasonal wardrobe updates
โ Palette Twins access
Save 43%
The Studio
$89/yr
per year ยท just $7.42/mo
โ Everything in The Glow
โ Save $66 vs monthly
โ Early access to new features
โ Palette Twins priority access
๐
Give Hued as a gift
A beautiful color reveal for someone you love โ fully unlocked, beautifully delivered.
What's coming next
Hued is just getting started
Your colours are just the beginning. Here's what your personal stylist will be able to do for you next.
๐ฌ
Personal Style Chat
Included with Hued ยท Coming soon
Upload a photo of any outfit, lipstick, eyeshadow, or hair color idea โ and get an honest answer based on your exact colour profile. Does this work for me? Answered in seconds.
Learn more and join waitlist โ
๐ฏ
Palette Twins
Coming soon ยท Free for all users
Discover influencers, stylists, and creators who share your exact coloring. What looks great on them will look great on you. Shop their picks, matched to your palette.
Learn more ยท Apply as a creator โ
Hued.
โจ
Start your color reveal
A few natural-light photos is all it takes. The Hued Method finds the exact colors that make you look vibrant and awake โ and the ones to avoid. Free. Takes 30 seconds. Yours forever.
๐ก
Before you take photos: wear a white, grey, or black top โ coloured clothing reflects onto skin and shifts your result. Face a window with light falling on you, not behind you. No flash.
๐ท On desktop? Select multiple photos at once by holding Cmd (Mac) or Ctrl (Windows) while clicking. For best results, use natural light photos taken on your phone.
๐ธ
Add 2โ3 photos of yourself
More photos = more accurate results
๐ Analyzed privately ยท deleted immediately ยท never stored
Why 2โ3 photos?
Lighting changes how colors look on screen. A few photos helps us give you much more accurate results โ and avoids the wrong answer that other apps are famous for.
โ๏ธ
Face the light โ window in front of you, not behind. Light behind you puts your face in shadow and changes your result.
๐
Stand facing a window โ light on your face, not your back. Take one straight-on and one slightly to the side.
๐ซ
Skip heavy filters โ they change your true colors
๐
Clean face or light makeup works best โ heavy foundation can affect accuracy
Your photo is analyzed privately and never sold or shared.
Hued.
โจ
Analyzing your colours
Reading your photos the way a colour expert would โ takes 1 to 2 minutes. Please keep this screen open.
โ
Analyzing your skin tone
โ
Checking your contrast
โ
Discovering your colour season
โ
Building your personal palette
โ
Almost done
Hued.
๐ค
โณ
Your free Glow access is running
๐
Your full profile is unlocked!
Complete palette, hair, style and chat โ all yours, saved forever.
โจ
Welcome back!
Your colours are ready.
๐
Your clothing may have affected this result
๐ก
Lighting may have affected this result
Your Hued reveal โ
Your Season
Your undertone
Your colours
Wear these near your face โ they'll make your skin glow and your eyes stand out.
Colours to avoid near your face
These fight your natural colouring and will make you look washed out or tired.
Your wardrobe neutrals
These are your foundation colours โ build your wardrobe around them.
Your outfit combinations
Combinations that always work together for your colouring.
Save your colours
Get your palette sent to your inbox โ open it any time you're shopping.
How accurate does this feel?
โ โ โ โ โ
Thank you โ your feedback helps us improve โจ
๐
Unlock your style guide
Glasses, makeup, metals and patterns โ all matched to your exact colouring. Yours forever.
One-time ยท no subscription needed ยท saved forever
Glasses
Frame colours and shapes that work with your colouring.
Makeup
Every shade matched to your undertone and season.
Foundation
Lips
Eyes
Finish
Metals & patterns
Metals
Patterns
๐
Unlock your hair guide
Salon-ready directions with exact terms for your colourist โ matched to your undertone and season.
One-time ยท no subscription needed ยท saved forever
Recommended
Avoid
What to tell your stylist
Read this out loud at your next appointment โ or show them this screen.
๐ฌ
Your personal style advisor
"Does this coat work for me?" Ask anything โ your advisor knows your exact colours and season.
Included with Full Profile ยท then part of The Glow subscription
One-time ยท no subscription needed ยท saved forever
Hair ยท Style ยท Chat ยท Saved forever ยท 30 days Glow free included
{
const pane = document.getElementById('resultTab-'+t);
const btn = document.getElementById('tab-'+t);
if (pane) pane.style.display = t===tab ? 'block' : 'none';
if (btn) {
btn.style.borderBottomColor = t===tab ? 'var(--gold)' : 'transparent';
btn.style.color = t===tab ? 'var(--ink)' : 'var(--ink-soft)';
btn.style.fontWeight = t===tab ? '500' : '400';
}
});
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// NAV AUTH BUTTON
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function updateNavAuth() {
const btn = document.getElementById('authNavBtn');
const icon = document.getElementById('authNavIcon');
if (!btn || !icon) return;
if (currentUser && currentUser.email) {
btn.style.cssText = 'cursor:pointer;width:34px;height:34px;border-radius:50%;background:var(--gold);display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:var(--ink);border:none';
icon.textContent = currentUser.email.charAt(0).toUpperCase();
} else {
btn.style.cssText = 'cursor:pointer;width:34px;height:34px;border-radius:50%;background:transparent;border:1.5px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:16px';
icon.textContent = '๐ค';
}
}
function handleNavAuth() {
if (currentUser) { showScreen('screen-settings'); if(typeof updateSettingsScreen==='function') updateSettingsScreen(); }
else { showScreen('screen-auth'); }
}
function goHome() { showScreen('screen-landing'); }
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// STYLE CHAT
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let chatMessagesUsed = 0;
let chatHistory = [];
const CHAT_LIMIT = 20;
async function sendChatMessage() {
if (!currentAnalysis) return;
const input = document.getElementById('chatInput');
if (!input || !input.value.trim()) return;
if (chatMessagesUsed >= CHAT_LIMIT) {
addChatBubble('You have used all '+CHAT_LIMIT+' style chats this month. They refresh on your billing date.', 'ai');
return;
}
const msg = input.value.trim();
input.value = '';
addChatBubble(msg, 'user');
chatMessagesUsed++;
const usageEl = document.getElementById('chatUsage');
if (usageEl) usageEl.textContent = chatMessagesUsed+' of '+CHAT_LIMIT+' chats used this month';
const typingId = 'typing-'+Date.now();
addChatBubble('...', 'ai', typingId);
try {
const session = await supabaseClient.auth.getSession();
const token = session.data.session && session.data.session.access_token;
const res = await fetch(WORKER_URL+'/style-chat', {
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+token},
body:JSON.stringify({message:msg,season:currentAnalysis.season,undertone:currentAnalysis.undertone,history:chatHistory.slice(-6)})
});
const data = await res.json();
const t = document.getElementById(typingId);
if(t) t.remove();
addChatBubble(data.reply || 'Something went wrong. Please try again.', 'ai');
chatHistory.push({role:'user',content:msg});
chatHistory.push({role:'assistant',content:data.reply||''});
} catch(e) {
const t = document.getElementById(typingId);
if(t) t.remove();
addChatBubble('Something went wrong. Please try again.', 'ai');
}
}
function addChatBubble(text, role, id) {
const msgs = document.getElementById('chatMessages');
if (!msgs) return;
const b = document.createElement('div');
const isUser = role==='user';
b.style.cssText = isUser
? 'align-self:flex-end;background:var(--ink);color:var(--cream);padding:10px 14px;border-radius:16px 16px 4px 16px;font-size:13px;line-height:1.5;max-width:80%'
: 'align-self:flex-start;background:var(--warm-white);border:1px solid var(--border);color:var(--ink);padding:10px 14px;border-radius:16px 16px 16px 4px;font-size:13px;line-height:1.5;max-width:85%';
b.textContent = text;
if(id) b.id = id;
msgs.appendChild(b);
msgs.scrollTop = msgs.scrollHeight;
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// SUPABASE INIT
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function initSupabase() {
if (SUPABASE_URL && SUPABASE_ANON_KEY && window.supabase) {
supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// Check if user is already logged in
supabaseClient.auth.getSession().then(({ data: { session } }) => {
if (session) {
currentUser = session.user;
checkSubscriptionStatus();
}
});
// Listen for auth changes
supabaseClient.auth.onAuthStateChange(async (event, session) => {
if (event === 'SIGNED_IN' && session) {
currentUser = session.user;
await checkSubscriptionStatus();
updateUIForLoggedInUser();
// After sign-in, always try to load saved profile
// This handles: email confirmation, returning users, post-payment sign-in
const currentScreen = document.querySelector('.screen.active')?.id;
const shouldRedirect = !currentScreen
|| currentScreen === 'screen-landing'
|| currentScreen === 'screen-auth'
|| currentScreen === 'screen-onboard';
if (shouldRedirect) {
// checkSubscriptionStatus already loaded analysis if saved
if (currentAnalysis) {
showWelcomeBack();
} else {
showScreen('screen-auth-welcome');
}
}
} else if (event === 'SIGNED_OUT') {
currentUser = null;
isSubscribed = false;
updateUIForLoggedInUser();
}
});
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// NAVIGATION
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function saveResultsByEmail() {
const email = document.getElementById('saveEmail')?.value?.trim();
if (!email || !email.includes('@')) {
alert('Please enter a valid email address.');
return;
}
if (!currentAnalysis) {
alert('No analysis to save yet. Please take your color analysis first.');
return;
}
const btn = document.querySelector('#emailSaveCard button');
if (btn) { btn.textContent = 'Sending...'; btn.disabled = true; }
try {
// Send results email via Worker
await fetch(WORKER_URL + '/save-results-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
season: currentAnalysis.season || 'Your Season',
power_colors: currentAnalysis.power_colors || [],
analysis_summary: currentAnalysis.analysis_summary || '',
undertone_display: currentAnalysis.undertone_display || ''
})
});
} catch (e) {
console.log('Email save error:', e);
}
// Show success โ hide the card, show confirmation
const card = document.getElementById('emailSaveCard');
if (card) {
card.innerHTML = '
โ
Colors saved!
Check your inbox โ your palette is on its way.
';
}
}
function renderCombinations() {
if (!currentAnalysis) return;
const section = document.getElementById('combinationsSection');
const locked = document.getElementById('combinationsLocked');
const grid = document.getElementById('combinationsGrid');
if (!section || !locked || !grid) return;
if (!isSubscribed) {
section.style.display = 'none';
locked.style.display = 'block';
return;
}
section.style.display = 'block';
locked.style.display = 'none';
// Build combinations from the first 4 best colours
const colors = (currentAnalysis.best_colors || []).slice(0, 6);
if (colors.length < 2) return;
grid.innerHTML = '';
// Pair colors 1+2, 1+3, 2+4 as combination suggestions
const pairs = [[0,1],[0,2],[1,3],[2,4]].filter(p => colors[p[0]] && colors[p[1]]);
pairs.forEach(function(pair) {
var c1 = colors[pair[0]], c2 = colors[pair[1]];
var row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:12px;background:var(--warm-white);border:1px solid var(--border);border-radius:12px;padding:12px 14px';
row.innerHTML = '
' +
'' +
'' +
'
' +
'
' + c1.name + ' + ' + c2.name + '
' +
'
Pair these together โ they share your warm depth
';
grid.appendChild(row);
});
}
function updateShopScreen() {
const freeView = document.getElementById('shopFreeView');
const paidView = document.getElementById('shopPaidView');
if (isSubscribed) {
if (freeView) freeView.style.display = 'none';
if (paidView) paidView.style.display = 'block';
} else {
if (freeView) freeView.style.display = 'block';
if (paidView) paidView.style.display = 'none';
}
}
function updateProfileScreen() {
if (!currentAnalysis) return;
// Update season labels
const seasonFree = document.getElementById('profileSeasonFree');
const undertone = document.getElementById('profileUndertone');
if (seasonFree) seasonFree.textContent = currentAnalysis.season || 'Your Season';
if (undertone) undertone.textContent = ((currentAnalysis.undertone || 'Warm').charAt(0).toUpperCase() + (currentAnalysis.undertone || 'Warm').slice(1)) + ' Undertone';
// Show correct view
const freeView = document.getElementById('profileFreeView');
const paidView = document.getElementById('profilePaidView');
if (isSubscribed) {
if (freeView) freeView.style.display = 'none';
if (paidView) paidView.style.display = 'block';
} else {
if (freeView) freeView.style.display = 'block';
if (paidView) paidView.style.display = 'none';
// Populate 4 free swatches
var swatchContainer = document.getElementById('profileSwatchesFree');
if (swatchContainer && currentAnalysis.best_colors) {
swatchContainer.innerHTML = '';
currentAnalysis.best_colors.slice(0, 4).forEach(function(c) {
var swatch = document.createElement('div');
swatch.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:4px';
var circle = document.createElement('div');
circle.style.cssText = 'width:40px;height:40px;border-radius:50%;background:' + c.hex + ';border:2px solid rgba(255,255,255,0.6);box-shadow:0 2px 8px rgba(0,0,0,0.1);cursor:pointer';
circle.setAttribute('data-hex', c.hex);
circle.setAttribute('data-name', c.name);
circle.setAttribute('data-desc', c.reason || '');
circle.addEventListener('click', function() { showColorDetail(this); });
var label = document.createElement('span');
label.style.cssText = 'font-size:10px;color:var(--ink-soft)';
label.textContent = c.name;
swatch.appendChild(circle);
swatch.appendChild(label);
swatchContainer.appendChild(swatch);
});
}
}
}
function showScreen(id) {
const current = document.querySelector('.screen.active');
if (current) lastScreen = current.id;
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
const screen = document.getElementById(id);
if (screen) {
screen.classList.add('active');
window.scrollTo(0, 0);
}
if (id === 'screen-profile') updateProfileScreen();
if (id === 'screen-shop') updateShopScreen();
if (id === 'screen-palette') renderCombinations();
if (id === 'screen-settings') updateSettingsScreen();
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// AUTH FUNCTIONS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function switchAuthTab(mode) {
authMode = mode;
const signupBtn = document.getElementById('tabSignup');
const loginBtn = document.getElementById('tabLogin');
const authBtn = document.getElementById('authBtn');
if (mode === 'signup') {
signupBtn.style.background = 'var(--ink)';
signupBtn.style.color = 'var(--cream)';
loginBtn.style.background = 'transparent';
loginBtn.style.color = 'var(--ink-soft)';
if (authBtn) authBtn.textContent = 'Create free account โ';
} else {
loginBtn.style.background = 'var(--ink)';
loginBtn.style.color = 'var(--cream)';
signupBtn.style.background = 'transparent';
signupBtn.style.color = 'var(--ink-soft)';
if (authBtn) authBtn.textContent = 'Log in โ';
}
}
function showAuthError(msg) {
const el = document.getElementById('authError');
if (el) { el.textContent = msg; el.style.display = 'block'; }
}
function hideAuthError() {
const el = document.getElementById('authError');
if (el) el.style.display = 'none';
}
async function handleAuth() {
if (!supabaseClient) {
showAuthError('Authentication not configured yet. Please check back soon.');
return;
}
const email = document.getElementById('authEmail').value.trim();
const password = document.getElementById('authPassword').value;
const btn = document.getElementById('authBtn');
if (!email || !email.includes('@')) { showAuthError('Please enter a valid email address.'); return; }
if (!password || password.length < 8) { showAuthError('Password must be at least 8 characters.'); return; }
hideAuthError();
btn.textContent = authMode === 'signup' ? 'Creating account...' : 'Logging in...';
btn.disabled = true;
try {
let result;
if (authMode === 'signup') {
result = await supabaseClient.auth.signUp({ email, password, options: { emailRedirectTo: 'https://hued.studio' } });
} else {
result = await supabaseClient.auth.signInWithPassword({ email, password });
}
if (result.error) {
showAuthError(result.error.message);
btn.textContent = authMode === 'signup' ? 'Create free account โ' : 'Log in โ';
btn.disabled = false;
return;
}
currentUser = result.data.user;
await applyPendingGift();
if (authMode === 'signup' && result.data.user && !result.data.session) {
// Email confirmation required
btn.textContent = 'Check your email โ';
showAuthError('We sent a confirmation link to ' + email + ' โ click it to activate your account.');
document.getElementById('authError').style.background = '#EAF3DE';
document.getElementById('authError').style.borderColor = '#639922';
document.getElementById('authError').style.color = '#3B6D11';
return;
}
// Logged in โ save analysis and check subscription
await checkSubscriptionStatus();
if (currentAnalysis) {
await saveProfile(true);
renderResults(currentAnalysis); // Re-render with correct subscription state
showScreen('screen-results');
if (isSubscribed) {
setTimeout(() => {
const banner = document.getElementById('unlockBanner');
if (banner) { banner.style.display = 'flex'; setTimeout(() => banner.style.display = 'none', 6000); }
}, 400);
}
} else {
// Load saved profile from Supabase
if (currentAnalysis) {
renderResults(currentAnalysis);
showScreen('screen-results');
} else {
showScreen('screen-auth-welcome');
}
}
} catch (err) {
showAuthError('Something went wrong. Please try again.');
btn.textContent = authMode === 'signup' ? 'Create free account โ' : 'Log in โ';
btn.disabled = false;
}
}
async function signInWithGoogle() {
if (!supabaseClient) {
showAuthError('Authentication not configured yet.');
return;
}
const { error } = await supabaseClient.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: 'https://hued.studio' }
});
if (error) showAuthError(error.message);
}
async function signOut() {
if (supabaseClient) await supabaseClient.auth.signOut();
currentUser = null;
isSubscribed = false;
currentAnalysis = null;
showScreen('screen-landing');
}
function updateUIForLoggedInUser() {
// Show/hide sign in link
const signinBtn = document.getElementById('signinBtn');
if (signinBtn) signinBtn.style.display = currentUser ? 'none' : 'inline-block';
// Update nav to show user is logged in
const navCta = document.getElementById('startBtn');
if (navCta && currentUser) {
navCta.textContent = 'My profile โ';
navCta.onclick = () => showScreen('screen-results');
}
}
async function checkSubscriptionStatus() {
if (!supabaseClient || !currentUser) return;
let { data, error } = await supabaseClient
.from('profiles')
.select('is_subscribed, subscription_plan, subscription_end, trial_ends_at, reveal_purchased, scan_count, scan_reset_date, analysis_data')
.eq('id', currentUser.id)
.single();
if (error || !data) {
await supabaseClient.from('profiles').upsert({
id: currentUser.id,
email: currentUser.email,
is_subscribed: false,
subscription_plan: 'free',
scan_count: 0
}, { onConflict: 'id' });
isSubscribed = false;
return;
}
// Check if trial has expired
if (data.subscription_plan === 'trial' && data.trial_ends_at) {
const trialExpired = new Date(data.trial_ends_at) < new Date();
if (trialExpired) {
// Trial over โ keep reveal_purchased but downgrade subscription
await supabaseClient.from('profiles').update({
is_subscribed: false,
subscription_plan: 'reveal'
}).eq('id', currentUser.id);
data.is_subscribed = false;
data.subscription_plan = 'reveal';
}
}
isSubscribed = data.is_subscribed || false;
revealPurchased = data.reveal_purchased || false;
subscriptionPlan = data.subscription_plan || 'free';
scanCount = data.scan_count || 0;
trialEndsAt = data.trial_ends_at || null;
// Load saved analysis if none in memory
if (!currentAnalysis && data.analysis_data) {
try {
currentAnalysis = JSON.parse(data.analysis_data);
renderResults(currentAnalysis);
} catch(e) {}
}
// Apply pending subscription if needed
if (!isSubscribed && !revealPurchased && currentUser.email) {
await applyPendingSubscription(currentUser.email);
}
}
async function applyPendingSubscription(email) {
try {
const session = await supabaseClient.auth.getSession();
const token = session.data.session?.access_token;
if (!token) return;
const res = await fetch(WORKER_URL + '/apply-pending-sub', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ email })
});
const data = await res.json();
if (data.applied) {
isSubscribed = true;
// Refresh UI
updateProfileScreen();
updateShopScreen();
}
} catch (e) {
console.log('Pending sub check:', e);
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// PHOTO HANDLING
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function handleFile(input) {
if (input.files && input.files.length > 0) {
Array.from(input.files).forEach(file => {
const isDupe = allPhotos.some(p => p.name === file.name && p.size === file.size);
if (!isDupe) allPhotos.push(file);
});
const count = allPhotos.length;
const zone = document.getElementById('uploadZone');
const icon = document.getElementById('uploadIcon');
const title = document.getElementById('uploadTitle');
const hint = document.getElementById('uploadHint');
if (zone) { zone.style.borderColor = 'var(--gold,#C9973A)'; zone.style.background = 'var(--gold-pale,#F5E9CC)'; }
if (icon) icon.textContent = count >= 2 ? 'โ ' : '๐ธ';
if (title) title.textContent = count >= 2 ? count + ' photos ready โ great!' : '1 photo added โ add 1 or 2 more for best results';
if (hint) hint.textContent = count >= 2 ? 'Tap Analyze when you are ready' : 'More photos = more accurate results';
}
}
function handleDrag(e) {
e.preventDefault();
const zone = document.getElementById('uploadZone');
if (zone) zone.classList.add('dragging');
}
function handleDrop(e) {
e.preventDefault();
const zone = document.getElementById('uploadZone');
if (zone) zone.classList.remove('dragging');
if (e.dataTransfer.files) handleFile({ files: e.dataTransfer.files });
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function startAnalysis() {
if (allPhotos.length === 0) {
alert('Please add at least one photo first.');
return;
}
showScreen('screen-loading');
startLoadingAnimation();
try {
// Resize and convert photos to base64
// Resizes to max 1024px to keep request size manageable
function resizeImage(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const MAX = 1024;
let w = img.width, h = img.height;
if (w > MAX || h > MAX) {
if (w > h) { h = Math.round(h * MAX / w); w = MAX; }
else { w = Math.round(w * MAX / h); h = MAX; }
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
const base64 = canvas.toDataURL('image/jpeg', 0.85).split(',')[1];
resolve({ data: base64, type: 'image/jpeg' });
};
img.onerror = reject;
img.src = e.target.result;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
const photoData = await Promise.all(allPhotos.map(resizeImage));
// Send to Worker โ Claude API
const authHeaders = { 'Content-Type': 'application/json' };
if (supabaseClient && currentUser) {
try {
const session = await supabaseClient.auth.getSession();
const token = session.data.session && session.data.session.access_token;
if (token) authHeaders['Authorization'] = 'Bearer ' + token;
} catch(e) {}
}
const response = await fetch(WORKER_URL + '/analyze', {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({ photos: photoData })
});
const result = await response.json();
if (!response.ok || result.error) {
if (result.error === 'photo_quality') {
stopLoadingAnimation();
showScreen('screen-onboard');
setTimeout(() => alert(result.message || 'Please try with clearer photos in natural light.'), 300);
return;
}
if (result.error === 'scan_limit') {
stopLoadingAnimation();
showScreen('screen-results');
const banner = document.getElementById('scanLimitBanner');
if (banner) banner.style.display = 'flex';
return;
}
// Show the actual error message for debugging
const errMsg = result.message || result.error || 'Analysis failed';
throw new Error(errMsg);
}
currentAnalysis = result.analysis;
// Render real results
renderResults(currentAnalysis);
// Auto-save if logged in
if (currentUser) {
await saveProfile(false);
}
showScreen('screen-results');
updateProfileScreen();
// Show email save card for non-logged-in users
if (!currentUser) {
const card = document.getElementById('emailSaveCard');
if (card) card.style.display = 'block';
}
// Feedback is inline at bottom of results page
} catch (err) {
console.error('Analysis error:', err);
stopLoadingAnimation();
showScreen('screen-onboard');
const msg = err && err.message ? err.message : 'Unknown error';
setTimeout(() => alert('Analysis error: ' + msg + '\n\nPlease try again with good natural light photos.'), 300);
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// RENDER REAL RESULTS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function renderResults(analysis) {
if (!analysis) return;
const showPaid = isSubscribed || revealPurchased;
const el = (id) => document.getElementById(id);
const set = (id, val) => { const e=el(id); if(e) e.textContent=val||''; };
// Season header
set('resultSeason', analysis.season);
if (el('resultTagline')) {
if (showPaid) {
el('resultTagline').textContent = analysis.analysis_summary || '';
} else {
const s = analysis.analysis_summary || '';
const stop = s.search(/[.!?]\s/);
el('resultTagline').textContent = stop > 0 ? s.substring(0,stop+1) : s;
}
}
// Power swatches in header
const sw = el('resultSwatches');
if (sw) {
const src = (analysis.power_colors&&analysis.power_colors.length) ? analysis.power_colors : (analysis.best_colors||[]).slice(0,4);
sw.innerHTML = src.slice(0,4).map(c=>'').join('');
}
// Undertone
set('undertoneDesc', analysis.undertone_display || analysis.undertone_explanation);
// Banners
const cw = el('clothingWarning');
if (cw) cw.style.display = analysis.clothing_warning ? 'flex' : 'none';
if (analysis.clothing_warning) set('clothingWarningMsg', analysis.clothing_warning);
const lw = el('lightingWarning');
if (lw) lw.style.display = (analysis.lighting_confidence==='low'||analysis.lighting_warning) ? 'flex' : 'none';
if (analysis.lighting_warning) set('lightingWarningMsg', analysis.lighting_warning);
if (subscriptionPlan==='trial' && trialEndsAt) {
const tb = el('trialBanner');
if (tb) tb.style.display = 'flex';
const days = Math.max(0, Math.ceil((new Date(trialEndsAt)-new Date())/86400000));
set('trialMsg', days+' days of free Glow access remaining ยท then $12.99/mo or $89/yr');
}
// Colours grid (free: 4 visible, 8 blurred)
const grid = el('bestColorsGrid');
if (grid) {
grid.innerHTML = '';
(analysis.best_colors||[]).slice(0,12).forEach((c,i) => {
const locked = !showPaid && i >= 4;
const wrap = document.createElement('div');
wrap.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:6px';
const circle = document.createElement('div');
circle.style.cssText = 'width:56px;height:56px;border-radius:50%;background:'+c.hex+';border:2px solid white;box-shadow:0 2px 8px rgba(0,0,0,0.12);cursor:pointer;flex-shrink:0'+(locked?';filter:blur(3px);opacity:0.5':'');
if (!locked) {
circle.setAttribute('data-hex',c.hex);
circle.setAttribute('data-name',c.name);
circle.setAttribute('data-desc',c.reason||'');
circle.addEventListener('click', function(){ showColorDetail(this); });
} else {
circle.addEventListener('click', () => saveAndSubscribe('reveal'));
}
const label = document.createElement('div');
label.style.cssText = 'font-size:10px;color:var(--ink-soft);text-align:center;line-height:1.2';
label.textContent = locked ? 'unlock' : (c.name||'');
wrap.appendChild(circle); wrap.appendChild(label);
grid.appendChild(wrap);
});
}
// Avoid colours
if (el('avoidSection')) el('avoidSection').style.display = showPaid?'block':'none';
if (showPaid && el('avoidColorsGrid') && analysis.avoid_colors) {
el('avoidColorsGrid').innerHTML = (analysis.avoid_colors||[]).slice(0,6).map(c=>
'
'
).join('');
}
// Style tab โ glasses, makeup, metals, patterns
if (showPaid && analysis.glasses_advice) {
const g=analysis.glasses_advice;
set('glassesColors',g.best_colors); set('glassesWeight',g.frame_weight?'Frame weight: '+g.frame_weight:''); set('glassesShapes',g.shapes);
}
if (showPaid && analysis.makeup_advice) {
const m=analysis.makeup_advice;
set('makeupFoundation',m.foundation); set('makeupLips',m.lips); set('makeupEyes',m.eyes); set('makeupFinish',m.finish);
}
set('metalsText', analysis.metals);
set('patternText', analysis.pattern_guidance);
// Hair tab
if (showPaid && analysis.hair_advice) {
const h=analysis.hair_advice;
set('hairRecommended',h.recommended); set('hairAvoid',h.avoid);
set('hairStylistScript', h.stylist_script ? '"'+h.stylist_script+'"' : '');
}
// Lock/unlock tabs
const lockUnlock = (lockId, contentId) => {
const l=el(lockId), c=el(contentId);
if(l) l.style.display = showPaid?'none':'block';
if(c) c.style.display = showPaid?'block':'none';
};
lockUnlock('styleLock','styleContent');
lockUnlock('hairLock','hairContent');
lockUnlock('chatLock','chatActive');
// Unlock CTA bar + email save card
if (el('unlockCTA')) el('unlockCTA').style.display = showPaid?'none':'flex';
if (el('emailSaveCard')) el('emailSaveCard').style.display = currentUser?'none':'block';
// Nav auth button
updateNavAuth();
// Update old elements that may still exist
const upgradeDesc = el('upgradeDesc');
if (upgradeDesc) upgradeDesc.textContent = 'Unlock your '+( analysis.season||'')+' profile โ complete palette, hair guide, glasses advisor, makeup matching, and Style Chat. Saved forever.';
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// SETTINGS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function updateSettingsScreen() {
// Update email display
const emailEl = document.getElementById('settingsEmail');
if (emailEl) {
if (currentUser) {
emailEl.textContent = currentUser.email || 'Your account';
} else {
emailEl.textContent = 'Not signed in';
}
}
// Update plan badge
const planBadge = document.getElementById('settingsPlanBadge');
const planName = document.getElementById('settingsPlanName');
const renewal = document.getElementById('settingsRenewal');
const freeView = document.getElementById('settingsFreeView');
const paidView = document.getElementById('settingsPaidView');
const upgradeRow = document.getElementById('upgradeToAnnualRow');
const scanEl = document.getElementById('settingsScanCount');
if (subscriptionPlan === 'trial') {
const daysLeft = trialEndsAt ? Math.max(0, Math.ceil((new Date(trialEndsAt) - new Date()) / (1000*60*60*24))) : 0;
if (planBadge) planBadge.textContent = 'Full Reveal ยท ' + daysLeft + ' days free Glow remaining';
if (planName) planName.textContent = 'Full Reveal + Free Trial';
if (renewal) renewal.textContent = 'Free Glow access ends in ' + daysLeft + ' days';
if (freeView) freeView.style.display = 'none';
if (paidView) paidView.style.display = 'block';
if (upgradeRow) upgradeRow.style.display = 'flex';
if (scanEl) scanEl.textContent = scanCount + ' of 3 reveal scans used';
} else if (subscriptionPlan === 'reveal') {
if (planBadge) planBadge.textContent = 'Full Reveal ยท Saved forever';
if (freeView) freeView.style.display = 'none';
if (paidView) paidView.style.display = 'block';
if (upgradeRow) upgradeRow.style.display = 'flex';
if (scanEl) scanEl.textContent = scanCount + ' of 3 reveal scans used';
} else if (isSubscribed && (subscriptionPlan === 'monthly' || subscriptionPlan === 'annual')) {
if (planBadge) planBadge.textContent = subscriptionPlan === 'annual' ? 'The Studio ยท Annual' : 'The Glow ยท Monthly';
if (planName) planName.textContent = subscriptionPlan === 'annual' ? 'The Studio' : 'The Glow';
if (renewal) renewal.textContent = subscriptionPlan === 'annual' ? 'Renews annually' : 'Renews monthly';
if (freeView) freeView.style.display = 'none';
if (paidView) paidView.style.display = 'block';
if (upgradeRow) upgradeRow.style.display = subscriptionPlan === 'annual' ? 'none' : 'flex';
if (scanEl) scanEl.textContent = (5 - scanCount) + ' scans remaining this month';
} else {
if (planBadge) planBadge.textContent = 'Free plan';
if (freeView) freeView.style.display = 'block';
if (paidView) paidView.style.display = 'none';
if (scanEl) scanEl.textContent = scanCount >= 1 ? '1 of 1 free scan used' : '1 free scan available';
}
// Show sign in prompt if not logged in
if (!currentUser) {
const emailEl = document.getElementById('settingsEmail');
if (emailEl) {
emailEl.onclick = () => showScreen('screen-auth');
emailEl.style.color = 'var(--gold)';
emailEl.style.cursor = 'pointer';
emailEl.textContent = 'Sign in to save your profile โ';
}
}
}
async function openManageSubscription() {
// Open Stripe customer portal
if (!currentUser) {
showScreen('screen-auth');
return;
}
try {
const session = await supabaseClient.auth.getSession();
const token = session.data.session?.access_token;
const res = await fetch(WORKER_URL + '/customer-portal', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
});
const data = await res.json();
if (data.url) {
window.location.href = data.url;
} else {
alert('To manage your subscription, email [email protected] and we will help you right away.');
}
} catch(e) {
alert('To manage your subscription or cancel, email [email protected] โ we handle all requests within 24 hours.');
}
}
async function deleteAccount() {
if (!currentUser) return;
const confirmed = confirm('Are you sure you want to delete your account? This permanently removes all your color data and cannot be undone.');
if (!confirmed) return;
const doubleConfirm = confirm('Last chance โ this will delete your colour profile, subscription data, and account permanently.');
if (!doubleConfirm) return;
try {
const session = await supabaseClient.auth.getSession();
const token = session.data.session?.access_token;
await fetch(WORKER_URL + '/delete-account', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
});
await signOut();
alert('Your account has been deleted. We are sorry to see you go.');
} catch(e) {
alert('To delete your account, email [email protected] and we will remove all your data within 48 hours.');
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// GIFTING
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let pendingGiftCode = null;
function updateGiftPreview() {
const msg = document.getElementById('giftMessage')?.value || '';
const preview = document.getElementById('giftPreviewMessage');
if (preview) preview.textContent = msg || '';
}
function showGiftError(msg) {
const el = document.getElementById('giftError');
if (el) { el.textContent = msg; el.style.display = 'block'; }
}
function hideGiftError() {
const el = document.getElementById('giftError');
if (el) el.style.display = 'none';
}
async function purchaseGift() {
hideGiftError();
const recipientName = document.getElementById('giftRecipientName')?.value?.trim();
const recipientEmail = document.getElementById('giftRecipientEmail')?.value?.trim();
const message = document.getElementById('giftMessage')?.value?.trim();
const buyerEmail = document.getElementById('giftBuyerEmail')?.value?.trim();
if (!recipientName) return showGiftError('Please enter their name.');
if (!recipientEmail || !recipientEmail.includes('@')) return showGiftError('Please enter a valid email address for them.');
if (!buyerEmail || !buyerEmail.includes('@')) return showGiftError('Please enter your email address.');
// Store gift details โ retrieved after Stripe payment completes
localStorage.setItem('hued_gift_pending', JSON.stringify({
recipientName, recipientEmail, message, buyerEmail
}));
// Go to Stripe checkout for gift payment
const btn = document.querySelector('#giftFormSection .btn-primary');
if (btn) { btn.textContent = 'Going to payment...'; btn.disabled = true; }
try {
const res = await fetch(WORKER_URL + '/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
plan: 'gift',
email: buyerEmail,
gift_details: {
recipient_name: recipientName,
recipient_email: recipientEmail,
sender_email: buyerEmail,
message: message || ''
}
})
});
const data = await res.json();
if (!res.ok || data.error) {
showGiftError(data.error || 'Payment setup failed. Please try again.');
if (btn) { btn.textContent = 'Send the gift โ'; btn.disabled = false; }
return;
}
window.location.href = data.url;
} catch (err) {
showGiftError('Something went wrong. Please check your connection and try again.');
if (btn) { btn.textContent = 'Send the gift โ'; btn.disabled = false; }
}
}
function checkPaymentReturn() {
const hash = window.location.hash;
if (hash.includes('payment=success')) {
const plan = hash.match(/plan=([a-z]+)/)?.[1] || 'monthly';
history.replaceState(null, '', window.location.pathname);
// Check if this was a gift payment
const giftPending = localStorage.getItem('hued_gift_pending');
if (plan === 'gift' && giftPending) {
localStorage.removeItem('hued_gift_pending');
const giftData = JSON.parse(giftPending);
// Now send the gift via Worker
setTimeout(async () => {
try {
const res = await fetch(WORKER_URL + '/gift-purchase', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(giftData)
});
const data = await res.json();
showScreen('screen-gift');
document.getElementById('giftFormSection').style.display = 'none';
document.getElementById('giftSuccess').style.display = 'block';
const successMsg = document.getElementById('giftSuccessMsg');
if (successMsg) successMsg.textContent = 'Payment successful! A beautiful gift is on its way to ' + giftData.recipientName + ' at ' + giftData.recipientEmail + '. Check your inbox too โ we sent you a confirmation.';
} catch(e) {
showScreen('screen-landing');
alert('Payment received! If your gift email does not arrive in 5 minutes, email [email protected] with your order details.');
}
}, 800);
return;
}
// Regular subscription payment
setTimeout(async () => {
if (currentUser) {
// Show unlocking screen while we wait for webhook
showScreen('screen-unlocking');
await pollForSubscription(plan);
} else {
// Not signed in โ show auth with green message
showScreen('screen-auth');
setTimeout(() => {
const authError = document.getElementById('authError');
if (authError) {
authError.textContent = 'Payment successful! โจ Create your account with the same email you paid with โ your subscription unlocks immediately.';
authError.style.display = 'block';
authError.style.background = '#EAF3DE';
authError.style.borderColor = '#639922';
authError.style.color = '#3B6D11';
}
}, 400);
}
}, 1000);
}
if (hash.includes('payment=cancelled')) {
history.replaceState(null, '', window.location.pathname);
}
if (hash.includes('#settings') || hash === '#settings') {
history.replaceState(null, '', window.location.pathname);
setTimeout(() => {
showScreen('screen-settings');
updateSettingsScreen();
}, 500);
}
}
function checkForGiftCode() {
// Check URL hash for gift code โ e.g. hued.studio/#gift=ABC123XY
const hash = window.location.hash;
const match = hash.match(/#gift=([A-Z0-9]{8})/i);
if (match) {
pendingGiftCode = match[1].toUpperCase();
loadGiftDetails(pendingGiftCode);
}
}
async function loadGiftDetails(code) {
try {
const res = await fetch(WORKER_URL + '/gift-check?code=' + code);
const data = await res.json();
if (!res.ok || data.error || data.redeemed) {
// Invalid or already redeemed โ show landing normally
showScreen('screen-landing');
if (data.redeemed) {
setTimeout(() => alert('This gift has already been claimed. If you think this is an error, email [email protected].'), 500);
}
return;
}
// Valid gift โ show redemption screen
const fromEl = document.getElementById('giftRedeemFrom');
const msgEl = document.getElementById('giftRedeemMessage');
if (fromEl) fromEl.textContent = data.recipient_name ? 'For ' + data.recipient_name : 'A gift for you';
if (msgEl) msgEl.textContent = data.message || 'Your colours are waiting โ enjoy your reveal.';
showScreen('screen-gift-redeem');
} catch (err) {
showScreen('screen-landing');
}
}
function claimGiftAndStart() {
// Store the gift code โ it will be applied when they create an account after analysis
if (pendingGiftCode) {
sessionStorage.setItem('hued_gift', pendingGiftCode);
}
showScreen('screen-onboard');
}
async function applyPendingGift() {
// Called after user creates account โ applies the gift to their profile
const code = sessionStorage.getItem('hued_gift');
if (!code || !currentUser) return;
try {
const session = await supabaseClient.auth.getSession();
const token = session.data.session?.access_token;
if (!token) return;
await fetch(WORKER_URL + '/gift-redeem', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({ code })
});
sessionStorage.removeItem('hued_gift');
isSubscribed = true;
} catch (err) {
console.log('Gift apply error:', err);
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// LOADING ANIMATION
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let loadingTimer = null;
function startLoadingAnimation() {
let current = 0;
const total = 5;
function advanceStep() {
if (current > 0) {
const prev = document.getElementById('lstep-' + (current - 1));
const prevInd = document.getElementById('lind-' + (current - 1));
if (prev) { prev.classList.remove('active'); prev.classList.add('done'); }
if (prevInd) prevInd.textContent = 'โ';
}
if (current < total) {
const curr = document.getElementById('lstep-' + current);
if (curr) curr.classList.add('active');
current++;
loadingTimer = setTimeout(advanceStep, current === total ? 800 : 1200);
}
}
setTimeout(advanceStep, 400);
}
function stopLoadingAnimation() {
if (loadingTimer) clearTimeout(loadingTimer);
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// SAVE PROFILE TO SUPABASE
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function saveProfile(isFirstAnalysis) {
if (!currentUser || !currentAnalysis || !supabaseClient) return;
try {
const session = await supabaseClient.auth.getSession();
const token = session.data.session?.access_token;
if (!token) return;
await fetch(WORKER_URL + '/save-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
analysis: currentAnalysis,
photo_count: allPhotos.length,
is_first_analysis: isFirstAnalysis
})
});
} catch (err) {
console.error('Save profile error:', err);
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// SAVE AND SUBSCRIBE FLOW
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function saveAndSubscribe(plan) {
plan = plan || 'reveal';
// Go straight to Stripe โ no account needed before payment
// Stripe collects their email, webhook creates/updates their profile
initiateCheckout(plan);
}
async function initiateCheckout(plan) {
const btn = document.querySelector('.btn-upgrade, .price-cta');
const originalText = btn?.textContent;
if (btn) { btn.textContent = 'Setting up payment...'; btn.disabled = true; }
try {
const email = currentUser?.email || '';
const res = await fetch(WORKER_URL + '/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan, email })
});
const data = await res.json();
if (!res.ok || data.error) {
const errMsg = data.error || 'Unknown error';
const debugInfo = data.debug ? JSON.stringify(data.debug) : '';
console.error('Checkout error:', errMsg, debugInfo);
alert('Payment error: ' + errMsg + (debugInfo ? '\n\nDebug: ' + debugInfo : ''));
if (btn) { btn.textContent = originalText; btn.disabled = false; }
return;
}
// Redirect to Stripe checkout
window.location.href = data.url;
} catch (err) {
alert('Something went wrong. Please check your connection and try again.');
if (btn) { btn.textContent = originalText; btn.disabled = false; }
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// COLOR DETAIL PANEL
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function showColorDetail(el) {
// Accept either a data element or direct values
let hex, name, desc;
if (el && el.dataset) {
hex = el.dataset.hex || '#C9973A';
name = el.dataset.name || '';
desc = el.dataset.desc || '';
} else {
return;
}
const panel = document.getElementById('colorDetail');
const swatch = document.getElementById('detailSwatch');
const nameEl = document.getElementById('detailName');
const hexEl = document.getElementById('detailHex');
const descEl = document.getElementById('detailDescEl');
const upgradeEl = document.getElementById('detailUpgrade');
if (swatch) swatch.style.background = hex;
if (nameEl) nameEl.textContent = name;
if (hexEl) hexEl.textContent = hex.toUpperCase();
if (isSubscribed) {
if (descEl) { descEl.textContent = desc; descEl.style.display = 'block'; }
if (upgradeEl) upgradeEl.style.display = 'none';
} else {
if (descEl) descEl.style.display = 'none';
if (upgradeEl) upgradeEl.style.display = 'block';
}
if (panel) panel.style.display = 'block';
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// FEEDBACK
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let selectedRating = 0;
function hoverStar(rating) {
document.querySelectorAll('.feedback-star').forEach((s, i) => {
s.textContent = i < rating ? 'โญ' : 'โ';
});
}
function resetStars() {
document.querySelectorAll('.feedback-star').forEach((s, i) => {
s.textContent = i < selectedRating ? 'โญ' : 'โ';
});
}
function selectStar(rating) {
selectedRating = rating;
resetStars();
// Show comment field after selecting a star
const wrap = document.getElementById('feedbackCommentWrap');
if (wrap) wrap.style.display = 'block';
}
async function submitFeedback(rating) {
if (!rating) return;
const comment = document.getElementById('feedbackComment')?.value || '';
try {
await fetch(WORKER_URL + '/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rating,
comment,
season_shown: currentAnalysis?.season || 'Unknown'
})
});
} catch (err) {
console.error('Feedback error:', err);
}
// Show thank you, hide the form
const wrap = document.getElementById('feedbackCommentWrap');
const thanks = document.getElementById('feedbackThanks');
const stars = document.getElementById('inlineFeedback');
if (wrap) wrap.style.display = 'none';
if (thanks) thanks.style.display = 'block';
// Hide stars after submission
document.querySelectorAll('.feedback-star').forEach(s => s.style.display = 'none');
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// WAITLIST
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function joinWaitlist(feature, emailId) {
const emailEl = document.getElementById(emailId);
const email = emailEl?.value?.trim();
if (!email || !email.includes('@')) { alert('Please enter a valid email address.'); return; }
try {
await fetch(WORKER_URL + '/waitlist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, feature, is_creator: false })
});
if (emailEl) emailEl.value = '';
alert("You are on the list! We will email you when it is ready.");
} catch (err) {
alert("You are on the list!");
}
}
function joinTwinWaitlist() { joinWaitlist('palette_twins', 'twinEmail'); }
function joinChatWaitlist() { joinWaitlist('ai_chat', 'chatEmail'); }
function applyAsCreator() {
window.location.href = 'mailto:[email protected]?subject=Palette Twin Creator Application&body=Hi! I would love to apply as a Palette Twin creator on Hued. Here is a bit about me:';
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// SHOPPING COMPANION
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function showShopResult() {
const result = document.getElementById('shopResult');
if (result) result.style.display = 'block';
window.scrollTo(0, 200);
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// DEVICE DETECTION
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function detectDevice() {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|| (navigator.maxTouchPoints > 0 && window.innerWidth < 768);
const cameraBtn = document.getElementById('cameraBtn');
const galleryBtn = document.getElementById('galleryBtn');
const flashWarning = document.getElementById('mobileFlashWarning');
const desktopNote = document.getElementById('desktopNote');
if (isMobile) {
if (cameraBtn) cameraBtn.style.display = 'inline-block';
if (galleryBtn) galleryBtn.textContent = 'Choose from library';
if (desktopNote) desktopNote.style.display = 'none';
} else {
if (cameraBtn) cameraBtn.style.display = 'none';
if (galleryBtn) galleryBtn.textContent = 'Choose photos (hold Cmd/Ctrl for multiple)';
if (desktopNote) desktopNote.style.display = 'block';
}
// Flash warning always visible โ everyone needs this reminder
if (flashWarning) flashWarning.style.display = 'flex';
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// INIT
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.addEventListener('DOMContentLoaded', () => {
initSupabase();
detectDevice();
checkForGiftCode();
checkPaymentReturn();
});
window.addEventListener('resize', detectDevice);
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
});
redPrompt = e;
});
Hued.
Shopping companion
Does this color work for you?
You're in a store. You find something you love. Before you buy it โ check it against your Hued profile. Instantly.
๐ธ
Check any colour in seconds
Point your camera at a top, lipstick, eyeshadow, nail polish, or glasses frame โ and instantly know if it works for your coloring.
Unlock with Hued โ
Shopping companion is included in your Hued subscription. $9.99/month ยท cancel anytime.
๐ธ
Take a photo
Point your camera at anything โ a top, lipstick, eyeshadow, nail polish, or glasses frame. We'll check it against your profile right away.
๐ Photo checked in seconds ยท never saved
โจ
Yes โ this is your color
Yes โ this color works really well for you. The warm, earthy tones in this shade match your profile beautifully and will make your skin and eyes look brighter.
Why it works for you
Warm undertone match
This color has warm red and earthy tones โ exactly what your profile calls for. It will work with your complexion, not against it. Safe to wear close to your face.
How to wear it
Pair it right
This color pairs well with warm camel, cream, chocolate brown, and olive green โ all colors that suit you. Try to avoid cool greys or icy whites, which can clash with your warm tones.
Works for
Everything with color
Clothing and accessories ยท Lipstick and blush ยท Eyeshadow palettes ยท Hair color swatches ยท Nail polish ยท Frame colors for glasses โ anything with color.
Your Hued profile ยท Warm Autumn ยท Golden undertone ยท Medium-high contrast
Hued.
Coming soon
Meet your Palette Twin
๐ Free for everyone โ Palette Twins will be available to all Hued users at no extra cost. Annual subscribers get first access when we launch.
Discover influencers, stylists, and creators who share your exact colour profile. Their recommendations aren't generic โ they're matched to your specific undertone, contrast level, and season. What looks good on them will look good on you.
๐จ
Find your Color Match
Love their style? We help you check your match. Find creators who share your exact coloring โ so you know their outfits, makeup, and hair colors will work for you too.
For users
Shop from people who share your colours
Every product recommended by a Palette Twin influencer has been worn by someone with your undertone and season. No more guessing if that lipstick will suit you โ if it works on your twin, it works on you.
For influencers and creators
Earn by being yourself
Get analyzed, build your capsule, and get matched with an audience that shares your exact coloring. Your recommendations reach people who are genuinely likely to love what you love. Apply to join the Hued creator network.
Be first to know when Palette Twins launches
Enter your email and we'll notify you the moment it's live.
Are you a creator or influencer?
Apply to be one of the first Palette Twin creators. Share what suits you, earn from what you love.
Hued.
Coming soon
Your personal style chat
Ask anything. Upload an outfit, a lipstick, a hair color idea โ get an honest answer tailored to your exact colour profile. Like having a personal stylist on call, any time.
๐จ
Hued Studio ยท Your personal colour advisor
This burgundy coat you uploaded? Perfect for you. The deep warm red complements your golden undertone beautifully โ and the high contrast suits your medium-high contrast level.
What about the camel version instead?
Even better, actually. Camel is one of your very best neutrals โ it sits right in the heart of your Warm Autumn palette. Get the camel one.
Demo conversation โ your personal style chat coming soon
What you'll be able to ask
Any color question, answered for you specifically
Upload a photo of an outfit, a lipstick, a nail color, an eyeshadow palette, or a hair color idea โ and get an honest answer based on your actual colour profile, not generic advice.
Available with Hued subscription
Unlimited questions for subscribers
Personal Style Chat is included with your Hued subscription โ monthly or annual. Not available on the free plan.
Get notified when Personal Style Chat launches
Enter your email and we'll let you know the moment it's ready.
Hued.
๐
Give the gift of knowing
A beautiful digital reveal โ their colors, their season, their complete style guide. One month of Hued, fully unlocked.
Hued Studio ยท A gift for you
Your colours are waiting.
One month ยท Fully unlocked ยท No credit card needed to claim
One month ยท Fully unlocked
Hair, glasses, makeup, full palette โ all theirs
$39
They receive a beautiful email instantly ยท Valid for 6 months ยท No subscription required to claim
โจ
Gift sent!
WHAT HAPPENS NEXT
They'll receive a beautiful email with their gift. When they click the link, they can start their color reveal immediately โ no credit card, no account needed to begin. Their gift unlocks the full experience after their analysis.
Hued.
๐
Someone gave you a gift
Your colours are waiting.
One month fully unlocked โ hair, glasses, makeup, complete palette
No credit card needed ยท Your gift is applied automatically
Hued.
Your account
๐
Your email
Free plan
โจ
Redo my analysis
Take new photos for updated results
โบ
๐
Sign out
โบ
Subscription
You are on the free plan
Unlock your full colour profile, hair advisor, glasses guide, makeup matching, and shopping companion.
The Glow
Renews monthly
Active
๐ณ
Manage subscription
Update payment ยท Cancel ยท Receipts
โบ
โญ
Upgrade to annual
Save 37% โ $74.99/year
โบ
Give a gift
๐
Give Hued as a gift
A color reveal for someone you love ยท $39
โบ
Support
๐
Contact us
โบ
๐
Privacy policy
โบ
๐
Terms of service
โบ
๐๏ธ
Delete my account
Permanently removes all your data
โบ
Hued Studio ยท Version 1.0 ยท Vancouver ๐
โจ
Unlocking your full profile
Your payment was received. We are activating your complete colour profile right now.
Confirming your payment...
Hued.
โจ
You are in. Let's find your colours.
Your account is ready. Take your color analysis now and your results will be saved automatically.
Takes about 2 minutes ยท Results saved to your profile
Hued.
Save your colors
Create a free account to save your colour profile forever โ and access it any time you're shopping.
or email
By continuing you agree to our Terms and Privacy Policy.
Your photos are never stored.
Hued.
Last updated April 2026
Privacy Policy
We wrote this to actually be read โ not to confuse you. Here's exactly what we do and don't do with your information.
๐ Your photos
Analyzed, not stored
When you upload a photo, it goes straight to our AI, your results are generated, and then the photo is deleted. We don't keep your photos, we don't store them anywhere, and we never use them for anything else. Simple as that.
๐ Your personal data
What we collect and why
We only collect what we need: your email address to manage your account, your color results to power your saved profile, and basic app usage to help us improve. That's the whole list. No location data, no browsing history, nothing extra.
๐ซ Third parties
We never sell your data. Ever.
Your data is never sold or shared with advertisers. Full stop. Payments go through Stripe โ they handle your card details and we never see them. Your profile is stored securely and that's it. Two trusted partners, nothing more.
โ Your rights
You are always in control
You can request a copy of all data we hold about you, update or correct your information at any time, or delete your account and all associated data completely โ no questions asked. If you are in the EU or UK, you have additional rights under GDPR including the right to data portability and the right to object to processing. Email [email protected] and we will respond within 48 hours.
๐ Where your data lives
Stored securely in Canada and the US
Hued Studio is operated from Vancouver, British Columbia, Canada. Your data is stored on servers in North America through our trusted infrastructure partners (Supabase, Cloudflare). By using Hued, you consent to this storage. You may withdraw consent at any time by deleting your account.
๐ช Cookies
We use only what we need
Hued uses only essential cookies required for the app to function โ for example, keeping you logged in. We do not use tracking cookies or advertising cookies. We do not share cookie data with any third parties.
๐ง Email
Unsubscribe any time
If you give us your email address, we will only use it to send you things you asked for โ your color results, subscription updates, or feature announcements you opted into. Every email includes an unsubscribe link. We will never send spam.
๐ก๏ธ Security
How we protect you
All data is transmitted over encrypted HTTPS. Profile data is stored in a SOC 2 compliant database. Payments are handled entirely by Stripe โ we never see or store card details. We conduct regular security reviews and keep all software up to date.
Questions? Email [email protected] โ a real person will respond within 48 hours.
Hued.
Last updated April 2026
Terms of Service
Plain English โ because you deserve to understand what you're agreeing to.
Who we are
About Hued
Hued Studio is a personal color analysis service, founded and operated by an individual sole proprietor in Vancouver, British Columbia, Canada. By using Hued, you agree to these terms.
What Hued is
Our service
Hued Studio provides personal color analysis, beauty recommendations, and styling guidance for informational purposes. Our recommendations are based on your photos and preferences. We are not licensed cosmetologists or dermatologists. Results are suggestions, not professional advice.
Billing
Subscriptions and cancellation
The free analysis requires no account. Paid subscriptions are billed monthly or annually. You may cancel at any time โ access continues until the end of your billing period. Email [email protected] with any concerns and we'll always handle them fairly.
Your photos
Limited license to analyze only
By uploading a photo, you confirm it is of yourself (or someone who has consented) and grant Hued a limited license to process that image solely for color analysis. We do not claim ownership of your photos, do not store them after analysis, and do not use them for any other purpose.
Governing law
British Columbia, Canada
These Terms are governed by the laws of British Columbia, Canada. Any disputes will be resolved under BC jurisdiction. If you are outside Canada, local laws may also apply to you โ particularly consumer protection laws in your region.
Limitation of liability
What we are responsible for
Hued Studio provides color guidance for personal use only. We are not responsible for purchasing decisions you make based on our recommendations. Our total liability to you for any claim is limited to the amount you paid us in the last 30 days. We are not liable for indirect or consequential losses.
Honesty promise
What we promise you
We will never trick you with hidden fees, fake urgency, forced reviews, or dark patterns. If something is not working, we will fix it or refund you. We are building something we are proud of โ and that means treating you the way we would want to be treated.
Questions? Email [email protected] โ a real human will respond within 48 hours.
Hued.
๐
Have a question? Let's talk.
Whether you are stuck on your colours, something did not work, or you just want to say hi โ I would love to hear from you. I usually reply in a day or two (unless I am chasing my toddler!).
Questions, feedback, something isn't working, or you just want to share your color reveal โ this is Abbie's inbox. A real person reads every message and replies within a day or two, usually sooner.