A red door in a bright blue sky Just enter.
Casting something?Post your casting call here.

The checker

When a casting director looks you up, this is what they find. Put a name in and see which of 72 places an actor can be found already have you, and which are still empty.

Private to you. Other members and the public never see this, it lives on your account, and the DOORS team can see it to help you.
Establishment
Audience
0of 72 ticked

Establishment counts the gatekept stuff: badges, unions, directories, awards, festivals. Audience counts the open platforms anyone can build on. Graded on the working-actor scale. This list will keep growing, got a platform to add? Tell us.

Why more than one place matters

Spotlight is one kind of portfolio. Instagram is where people background-check you: how you look, your taste, what you have been working on lately. Vimeo is where a film you made or a tape you are proud of can live. Every platform is another door someone can find you through.

These 72 places split roughly in two: establishments that are gatekept, and places you can join today.

One rule. Information about you is data, and it is your data. A casting notice, or a film you made and want people to watch: share those loudly. Personal details about yourself, your family or other people: think about that carefully.

'; const w=window.open('','_blank'); if(!w){ window.print(); return; } w.document.write(h); w.document.close(); setTimeout(function(){ try{ w.print(); }catch(e){} },400); } function renderHub(){ const el=document.getElementById('page-hub'); el.innerHTML='

Lists '+MD('#F2C21D')+'

The two that matter most, then everything else.

' +'
' +'' +'' +'
' +'
' +'' +'' +'' +'' +'' +'' +'' +'' +'' +'
' +'

Worth seeing

' +'

Watching is training too. Tick off what you have seen, star what you have not, and the rings on your profile close as you go.

' +'
' +'' +'' +'' +'' +'' +'' +'' +'' +'' +'
' +'

The shop front

' +'
' +'' +'' +'
' +'

The non-negotiable kit

' +'

The short list. Not the nice-to-haves, the things working actors reach for every single week.

' +'
' +'

Becoming an actor, honestly

' +'

There is no single way in. Some people train for three years at drama school; some build careers from self-tapes, fringe plays, student films and the internet. Both are real. Training gives you craft, community and showcase exposure, but nobody checks your certificates in an audition room.

' +'

It is subjective. Casting is taste, timing and type as much as talent, so rejection is information about fit, not a verdict on you. The actors who last treat it as a long game: they read plays, keep training, make their own work and look after their people.

' +'

Start with the practical: good headshots, a simple self-tape setup, a showreel scene or two, Spotlight when you are eligible, and the lists on this page. Then keep showing up. That is genuinely most of it.

'; } function renderResource(k){ try{ tasteTick(k); }catch(e){} const el=document.getElementById('page-'+k); if(!el) return; if(k==='auditions'){ const AU_NOTE='

Australian state companies publish real routes in: expression of interest forms and general auditions, some open to actors without agents. Each theatre page says how.

'; const startBtn=t=>''; const TW=function(n){ return ((window.TH_WORLD||{})[n])||[]; }; const areas={'London':THEATRES_LDN,'Manchester':TW('Manchester'),'Scotland':TW('Scotland'),'New York':THEATRES_NYC,'Los Angeles':THEATRES_LA,'Sydney':TW('Sydney'),'Melbourne':TW('Melbourne'),'Brisbane':TW('Brisbane'),'Perth':TW('Perth'),'Adelaide':TW('Adelaide'),'Cape Town':TW('Cape Town'),'More locations':[]}; const area=window.audArea||'London'; const areaNote={'London':'', 'New York':'

US theatres cast differently. Equity members get open calls (EPAs) listed on actorsequity.org, and Playbill and Backstage carry the rest. The theatre pages tell you who to write to.

', 'Los Angeles':'

LA stage is intimate-theatre country: small rooms where screen casting directors actually sit. Same rules, smaller stages, bigger consequences.

', 'Sydney':AU_NOTE,'Melbourne':AU_NOTE,'Brisbane':AU_NOTE,'Perth':AU_NOTE,'Adelaide':AU_NOTE, 'Cape Town':'

South Africa\'s theatre capital. Casting runs through local networks, but these are the houses to know if you are there or heading there.

', 'More locations':'

The giants of world theatre. Most cast through resident ensembles in their own language, so know them, see them when they tour, and clock the ones that genuinely cast English speakers: Dublin needs no visa at all.

'}[area]; el.innerHTML=backLink('castings','Castings')+'

Theatre Auditions

Pick your area. One page per theatre: the season, who casts it, and how to put yourself forward.

' +'
'+Object.keys(areas).map(a=>'').join('')+'
' +(areaNote||'') +(area==='More locations' ? Object.keys((window.TH_WORLD||{}).__world||{}).map(function(city){ const rows=((window.TH_WORLD||{}).__world||{})[city]||[]; return '

'+pesc(city)+'

'+rows.map(startBtn).join('')+'
'; }).join('') : '
'+areas[area].map(startBtn).join('')+'
') +'

Reaching out, with a tape

' +RC('Lead with a self-tape, not a CV','A one minute tape says more than any attachment. Phone at eye level, plain wall, daylight or one lamp, material that fits what they programme.') +'
The polite template
' +'
Subject: [Show] at [Theatre], [your name]\n\nHello [name],\n\nI saw that [show] is coming up at [theatre] in [month]. I would love to be considered for [the role, or: the company]. I am [one honest line: training, a recent credit, or your casting type].\n\nHere is a one minute self-tape of material that fits the show: [link]\nMy Spotlight: [link]\n\nI am performing in [show] at [venue] on [dates], and it would be lovely to have you along if the timing works. Thank you for your time.\n\n[Your name]\n[Phone]
' +'

' +RC('The rules of the template','Named person, never Dear Sir or Madam. One specific show, never a general plea. Three months before rehearsals is the sweet spot. And if a line does not apply to you, cut it, do not fake it.') +'
'; return; } if(k==='th-royalcourt'){ el.innerHTML=backLink('auditions','Theatre Auditions')+RC_PAGE(); return; } if(k.indexOf('th-')===0){ const worldAll=[].concat.apply([],Object.keys(window.TH_WORLD||{}).map(function(c){ const v=(window.TH_WORLD||{})[c]; return Array.isArray(v)?v:[].concat.apply([],Object.keys(v).map(function(c2){ return v[c2]; })); })); const th=THEATRES_LDN.concat(THEATRES_NYC,THEATRES_LA,worldAll).filter(t=>t.k===k)[0]; if(th){ const S=(window.TH_SEASONS||{})[k]; let body=''; if(S && (S.prods||[]).length){ body+='
This is information, not a casting call. The season below was researched from the theatre\'s own site on 3 August 2026, so you can write about specific shows early. Double-check dates on their site before writing.
' +'

The season, mapped

'; S.prods.forEach(p=>{ const src=safeUrl(p.src); body+='
'+pesc(p.t)+'
' +((p.w||p.d)?'
'+pesc(p.w||'')+(p.d?((p.w?' · ':'')+'Directed by '+pesc(p.d)):'')+'
':'') +(p.cast?'
Casting: '+pesc(p.cast)+'
':'') +(p.dates?'
'+pesc(p.dates)+'
':'') +(src?'
source ↗
':'') +'
'; }); if(S.route){ body+='

How casting works here

'+pesc(S.route)+'

'; } const em=(S.email||'').trim(); if(em && em.indexOf(' ')===-1 && em.indexOf('(')===-1 && em.indexOf('@')>0){ body+=RC('Casting email',''+pesc(em)+'. Use it the polite way: one specific show, a tape, two short paragraphs.'); } } else { body+=(th.cast?'

How casting works here

'+pesc(th.cast)+'

':'

This theatre’s page is being built Royal Court-style: the season show by show, who casts what, and when to write. Coming soon.

'); } body+=(th.cd?RC('Casting lead',th.cd+', per our Casting Directors list. Address submissions and show invitations to the casting department by name.'):'') +RL('Website','https://'+th.home,'Season, what’s on and contacts.') +RL('Casting info search','https://www.google.com/search?q='+encodeURIComponent('site:'+th.home+' casting'),'Find their casting or work-with-us page.') +RC('Keep a record','When you write to them, log who you contacted, which show and the date. The notes boxes in the Checker work well for this, or a simple notes app.'); el.innerHTML=backLink('auditions','Theatre Auditions')+RT(th.t,'',body); return; } } if(k==='castings'){ setTimeout(ccPaintFilters,0); window.ccTier=window.ccTier||'Put yourself forward'; const tierBtn=function(x){ return ''; }; el.innerHTML=backLink('home','Home')+'

Castings

' +'

Everything you can go in for, in one place.

' +'
' +'
' +['Put yourself forward','Announced shows','Everything'].map(tierBtn).join('') +'
' +'

' +'
' +'
' +'

Stage, film or short film, put it up. It appears straight away as not checked yet, and gets the blue tick once we have verified it.

' +'' +'The deadline: when submissions close (required). It archives 48 hours after this date:' +'' +'' +'' +'' +'' +'' +'' +'' +'' +'

' +'

' +'
' +'
' +'

This is information, not an audition notice. These shows have been announced but most casts have not. The move: write to the casting office or theatre about a specific show roughly three months before rehearsals, or invite them to something you are in. The Royal Court page under Theatre Auditions explains the full method.

' +RC('Start at the production page','Every entry links the show’s own website. That tells you the director, the dates and often the casting office. Primary source beats any listing site, including this one.') +RL('IMDbPro, get it or at least trial it','https://pro.imdb.com','The industry database, and honestly the best tool on this page. Paid, with a free trial. Look up any theatre, production company or casting office and you can usually find who casts what and a contact route. Every casting office named on this list has an IMDbPro search link on its card.') +RL('Spotlight','https://www.spotlight.com','If you are a member, the Contacts section lists casting directors and their offices. From your Spotlight home, open Contacts and search the office named on the entry. Non-members can still use the public site to check an office is real.') +RC('Then write like a human','Named person, two short paragraphs, one specific show, one clear ask. Log who you wrote to and when in the Checker notes.') +'
' +'

Real notices, from Instagram and the community

' +'

Things you can actually go in for: open calls posted by casting offices on their own social accounts, and notices added by actors, gathered here so you do not have to trawl Instagram for them. Every one is checked by a human before it goes up. Notices drop into the archive 48 hours after they close.

' +'
Look after yourself. No real casting ever asks for money up front, your home address, or intimate images. Auditions happen in rehearsal rooms, offices and on tape, not in flats. If any of it feels off, it is off: walk away and tell us.
' +'
' +'

Loading…

' +'

Theatre auditions: write to them cold

' +'

These are not castings. This is what is happening at the moment, and you should be aware of it. Look through these theatres, see what they have coming up, and if anything is right for you, write to them before the room opens. How to write that letter is the button at the top.

' +'
'+CASTINGS.map(c=>castingCard(c,'source')).join('')+'
' +'
'; loadCommunityCastings(); ccApplyTier(); return; } if(SESSIONS[k]){ seshStop(); const cfg=SESSIONS[k]; el.innerHTML=backLink('movement','Movement')+'

'+pesc(cfg[1])+'

'+seshPageHTML(cfg[3]); document.getElementById('sesh-go').onclick=function(){ seshStart(cfg[0],cfg[1],cfg[2]); }; return; } if(k==='cinema'){ const D=window.WS_CANON||{}; const seen=seenAll(), watch=watchAll(); window.__cinR=D.recent||[]; window.__cinU=D.upcoming||[]; const row=function(m,i,up){ const id='mv|'+m.t+'|'+m.y, sn=!!seen[id], wt=watch.indexOf(id)>=0; return '
' +'
'+pesc(m.t)+' '+pesc(m.when||String(m.y))+'' +(m.d?'
'+pesc(m.d)+'
':'')+'
' +'' +(up?'':'') +'' +'
'; }; const rec=window.__cinR; el.innerHTML=backLink('hub','Resources')+'

In cinemas, and coming

' +'

The last year of cinema worth your ticket, and everything announced through 2027. Star what excites you, tick what you have seen.

' +'
' +'

The last twelve months

' +rec.map(function(m,i){ return seen['mv|'+m.t+'|'+m.y]?'':row(m,i,false); }).join('') +(function(){ const d2=rec.filter(function(m){ return seen['mv|'+m.t+'|'+m.y]; }); return d2.length?'
The '+d2.length+' you have seen
'+d2.map(function(m){ return row(m,rec.indexOf(m),false); }).join('')+'
':''; })() +'

Announced and coming

' +window.__cinU.map(function(m,i){ return row(m,i,true); }).join('') +onlyYou() +'
'; return; } if(k==='classics'||k==='musicals'){ const D=window.WS_CANON||{}; const seen=seenAll(), watch=watchAll(); const isMus=(k==='musicals'); const secs=isMus?[['The musicals',D.musicals||[],'mu']]:[['Shakespeare',D.shakespeare||[],'sh'],['The classics',D.classics||[],'cl']]; window.__canonRows=window.__canonRows||{}; const pre=isMus?'mu|':'pl|'; const secHTML=function(sec){ const rows=sec[1]; window.__canonRows[sec[2]]=rows; const unseen=rows.filter(function(r){ return !seen[pre+r.t]; }); const done=rows.filter(function(r){ return seen[pre+r.t]; }); const row=function(r){ const i=rows.indexOf(r); const sn=!!seen[pre+r.t], rd=!!seen['plr|'+r.t], wt=watch.indexOf(pre+r.t)>=0; return '
' +'
'+pesc(r.t)+''+(r.a?' '+pesc(r.a)+'':'') +(r.d?'
'+pesc(r.d)+'
':'') +(r.status?'
'+pesc(r.status)+'
':'') +'
' +'' +'' +(isMus?'':'') +'' +'
'; }; return '

'+sec[0]+'

' +unseen.map(row).join('') +(done.length?'
The '+done.length+' you have seen
'+done.map(row).join('')+'
':''); }; el.innerHTML=backLink('hub','Resources')+'

'+(isMus?'The musicals':'Shakespeare and the classics')+'

' +'

'+(isMus?'The shows everyone has an opinion on. Tick off what you have seen, and the status tells you when you can catch it.':'The plays people talk about. Seen it, read it, or both? Go find a version somewhere, even an amateur one.')+'

' +'
' +secs.map(secHTML).join('') +onlyYou() +'
'; return; } if(k==='watchlist'){ const watch=watchAll(), seen=seenAll(); const need=watch.filter(function(id){ return !seen[id]; }); const done=watch.filter(function(id){ return seen[id]; }); window.__wlNeed=need; window.__wlDone=done; const row=function(id,i,seenRow){ const w=watchTitle(id); return '
' +''+pesc(w.kind)+'' +''+pesc(w.t)+'' +(seenRow ?'' :'') +'
'; }; el.innerHTML=backLink('profile','My profile')+'

Your watchlist

' +'

Everything you have starred, waiting to be seen. Tick things off and the rings close.

' +'
' +'
'+ringsNestHTML() +'
Green is the Top 250. Red is this watchlist. Blue is your practice streak. The number in the middle is everything you have ever ticked seen.
' +(need.length?(function(){ const films=[], shows=[]; need.forEach(function(id,i){ (watchTitle(id).kind==='Film'?films:shows).push([id,i]); }); const rows=function(list){ return list.map(function(p2){ return row(p2[0],p2[1],false); }).join(''); }; return (films.length?'

Movies watchlist ('+films.length+')

'+rows(films):'') +(shows.length?'

Plays and shows watchlist ('+shows.length+')

'+rows(shows):''); })():'

Nothing waiting. Star films on the Top 250 and shows on Worth seeing.

') +(done.length?'
The '+done.length+' you have seen
'+done.map(function(id,i){ return row(id,i,true); }).join('')+'
':'') +onlyYou() +'
'; return; } if(k==='watch-b3k9'){ try{ if(!sessionStorage.getItem('pv:__watch')){ sessionStorage.setItem('pv:__watch','1'); fetch(SBC_URL+'/rest/v1/doors_pageviews',{method:'POST',headers:Object.assign({},SBC_HEADERS,{'Content-Type':'application/json','Prefer':'return=minimal'}),body:JSON.stringify({slug:'__watch',src:(cpUid()?'member':'direct')})}).catch(function(){}); } }catch(e){} const cta=cpSession() ? '

' : '

'; el.innerHTML='
' +'

Watch this: Casting Director, Stranger Things, Walking Dead

' +'

How you should submit tapes and share tapes.

' +'
' +'' +'
This is Mariana Novak talking to actors.' +'
    ' +'
  • On her own Instagram
  • ' +'
  • Her credits include Stranger Things, Ozark and The Walking Dead
  • ' +'
  • This is the view from the other side of the table
  • ' +'
' +'Follow her
' +'
' +'
' +'

What she says, in short

' +'' +'

This is exactly what DOORS is built for

' +'

Your DOORS profile is a public page with your tapes streaming right on it. No Drive links, no downloads, no guessing: casting presses play. Ten tapes, two gigabytes of video are yours, and every tape has its own link you can drop straight into a submission email. And now you can see who has viewed your profile: every visit gets counted for you, with where it came from.

' +'

Click below and start working on your public page.

' +cta +'
'; return; } if(k==='pubtheatres'||k==='standup'||k==='galleries'||k==='amateur'){ const META={ pubtheatres:['Pub theatres','Tiny rooms above pubs where new writing starts and actors get spotted. Cheap tickets, close enough to study every choice.'], standup:['Stand-up comedy','Nobody handles an audience like a comedian. Watch timing, control and recovery, then steal all of it.'], galleries:['Art galleries','Faces, period detail and people watching. An hour in a gallery is character research, and most of these are free.'], amateur:['Amateur and fringe','Where everyone starts, and where half the industry still watches. See it, then get in it.'] }[k]; const rows=(window.WS_DATA&&window.WS_DATA[k])||[]; el.innerHTML=backLink('hub','Resources')+'

'+META[0]+'

' +'

'+META[1]+'

' +'
' +(rows.length?rows.map(function(r){ return '
'+pesc(r.t)+'' +(r.where?' '+pesc(r.where)+'':'') +(r.d?'
'+pesc(r.d)+'
':'') +(r.w?'':'') +'
'; }).join(''):'

This list is being researched now, check back soon.

') +'
'; return; } if(k==='top250'){ const list=window.TOP250||[]; const seen=seenAll(), watch=watchAll(); const seenCount=list.filter(function(m){ return seen['mv|'+m.t+'|'+m.y]; }).length; const row=function(m,i){ const id='mv|'+m.t+'|'+m.y, sn=!!seen[id], wt=watch.indexOf(id)>=0; return '
' +''+m.r+'' +'
'+pesc(m.t)+' '+m.y+'
' +'' +'' +'
'; }; el.innerHTML=backLink('worthseeing','Worth seeing')+'

IMDb Top 250 Movies

' +'

The IMDb Top 250, the films every actor ends up talking about in every greenroom. Tick off what you have seen, star what you need to see, and the red ring closes as you work through it.

' +'
' +'
' +ringHTML(seenCount,250,'#C9302E','Seen '+seenCount+' of 250') +'
'+(seenCount>=250?'All 250. Go outside.':(250-seenCount)+' to go.')+'
Starred films land on your watchlist, on your page. From the live IMDb chart, 20 Aug 2026.
' +list.map(function(m,i){ return seen['mv|'+m.t+'|'+m.y]?'':row(m,i); }).join('') +(seenCount?'
The '+seenCount+' you have seen
'+list.map(function(m,i){ return seen['mv|'+m.t+'|'+m.y]?row(m,i):''; }).join('')+'
':'') +onlyYou() +'
'; return; } if(k==='worthseeing'){ const TH=window.TH_SEASONS||{}; const names={}; try{ (typeof THEATRES_LDN!=='undefined'?THEATRES_LDN:[]).concat(typeof THEATRES_NYC!=='undefined'?THEATRES_NYC:[]).forEach(t=>{ if(t.k) names[t.k]=t.t; }); }catch(e){} let watch=[]; try{ watch=JSON.parse(localStorage.getItem('doorsWatch')||'[]'); }catch(e){} const rows=[]; Object.keys(TH).forEach(k2=>{ (TH[k2].prods||[]).forEach(p=>{ rows.push({ id:k2+'|'+p.t, th:names[k2]||k2.replace('th-','').replace(/-/g,' '), t:p.t, d:p.dates||'', w:p.w||'', src:p.src||'', on:(p.dates||'').indexOf('On now')===0 }); }); }); const seenW=seenAll(); const wlSeen=watch.filter(function(id){ return seenW[id]; }).length; const card=r=>'
' +''+pesc(r.t)+' at the '+pesc(r.th)+'' +'
'+pesc(r.d)+(r.w?' · by '+pesc(r.w):'')+'
' +'' +'' +'' +'
'; el.innerHTML=backLink('hub','Resources')+'

Worth seeing

' +'
' +'
' +ringHTML(wlSeen,watch.length||1,'#3E9B4F',watch.length?('Watchlist: '+wlSeen+' of '+watch.length+' seen'):'Watchlist: empty so far') +'
Star it here, it lands on your page.
Your watchlist lives in My profile. Tick things seen and the green ring closes.
' +'
The films every actor ends up talking about. The IMDb Top 250, tick off what you have seen.
' +RH('On now') +(rows.filter(r=>r.on&&!seenW[r.id]).map(card).join('')||'

Nothing marked as running right now.

') +RH('Coming up') +rows.filter(r=>!r.on&&!seenW[r.id]).map(card).join('') +(function(){ const sr=rows.filter(r=>seenW[r.id]); return sr.length?'
The '+sr.length+' you have seen
'+sr.map(card).join('')+'
':''; })() +'
Seen something worth seeing?

Tell everyone. The team checks it, then it goes up here.

' +'
' +'

From the theatres\' own published seasons. Dates change, the theatre\'s site is the truth.

' +onlyYou() +'
'; try{ wsPaintMember(); }catch(e){} return; } if(k==='screen'){ el.innerHTML=backLink('craft','Craft')+'

Screen

' +'
' +RH('The self tape setup') +imgslotHTML('screen-setup.png','The self tape setup, drawn') +RC('Frame','Phone at eye height, head and shoulders in shot, eye line just beside the lens. A reader stands where you look, never behind the camera.') +RC('Light','One big soft source in front of you beats everything: a window works. Overhead lights carve shadows under your eyes.') +RC('Sound','The quietest room you have, soft furnishings eat echo. Any mic beats no mic, and a reader on speakerphone is completely normal.') +RH('Kit that earns its place') +'
A tripod with a phone mount. The floor-to-ceiling ones hold height without a table stack.
' +'
A soft light, or the biggest window you have. One is enough.
' +'
A plain wall, or a paper roll if your walls are busy.
' +'
Any lav or USB mic helps, and none is still fine.
' +RH('Watch it done') +vslotHTML('self-tape-tutorial.mp4','The self tape setup, walked through') +RH('Eye lines and camera moves') +imgslotHTML('screen-diagrams.png','Camera, eye line and lighting diagrams') +RH('Practice sides') +'

Tape something nobody asked for: that is how the setup stops being scary.

' +'' +'
'; return; } if(k==='voiceproj'){ const V=(window.DOORS_DAILY&&DOORS_DAILY.voice)||{projection:[]}; const day=Math.floor(Date.now()/86400000); const proj=V.projection.length?V.projection[day%V.projection.length]:'Loading todays exercise…'; el.innerHTML=backLink('voicecraft','Voice')+'

Foundations

' +'
' +'

Everything upstairs, delivery, accents, the lot, stands on breath and support. Thirty seconds of breath, then today\'s exercise.

' +joshTop('Breath and projection','Losing your voice by the end of a rehearsal, or getting told to speak up?','That is breath and support, and it is fixable in a session or two. Josh is the DOORS voice coach. Tell him what happens and when, and he comes back with times and a quote.') +RC('Breath first, thirty seconds','In through the nose, low into the ribs, like filling a belt not a chest. Out on a long steady hiss, all the way empty. Four times. Projection rides on this, not on the throat.') +RC('Today',proj) +'

A different exercise every day, '+(V.projection.length||0)+' in the rotation and growing.

' +'

' +joshLine('Projection') +'
'; return; } if(k==='diction'){ el.innerHTML=backLink('voicecraft','Voice')+'

Delivery

' +'
' +'

Three layers, in order. Diction makes the sounds clean. Pace makes the thoughts land. Resonance makes it all carry.

' +joshTop('Diction','Not sure what is actually wrong with your diction?','Most people cannot hear their own. Josh can, in about ten minutes. Send him a voice note of you reading anything and he will tell you the two sounds to work on.') +RH('Diction') +vslotHTML('diction-workout.mp4','Diction workout') +'
' +RH('Pace, pause and rhythm') +'

Clean sounds are not the same as clear thought. This layer is the listener keeping up: it is generosity, not slowness.

' +RC('The punctuation walk','Read any speech out loud while walking, and change direction at every punctuation mark. Your body learns where thoughts actually end, and the reading stops being one long ribbon.') +RC('Hold the pause','Say one sentence, then hold the silence after it for a full three seconds. The pause is where the other person catches up. If it feels too long to you, it is landing for them.') +RC('Three speeds','Same speech three times: too slow, too fast, then the speed the thought deserves. The third pass never sounds like where you started, and that difference is the lesson.') +RC('Land one word','Every thought has one word doing the heavy lifting. Find it, lean on it a little, let the rest of the sentence be cheap. Emphasis everywhere is emphasis nowhere.') +RH('Resonance') +'

The bridge between the warm-up and the words: where the sound vibrates decides how far it carries.

' +RC('Hum to speech','Hum until your lips tingle, then open the hum straight into counting without losing the buzz. That forward buzz on the lips and cheekbones is the sound that reaches the back row without shouting.') +RC('Chest and mask','One hand on your chest for the low warmth, fingertips on your cheekbones for the forward ring. Speak a line and feel for both at once. A voice with both is warm and clear, and it costs no extra effort.') +joshLine('Delivery') +'
'; twPick(); return; } if(k==='accents'){ const V=(window.DOORS_DAILY&&DOORS_DAILY.voice)||{accents:[]}; el.innerHTML=backLink('voicecraft','Voice')+'

Accent work

' +'
' +joshTop('Accent consultation','Not sure which accents to start work on?','This is the question worth getting right, because the wrong accent is months wasted. Josh goes through your casting and your Spotlight with you and names the two or three worth your time.') +RH('American accent') +'

Filming here or taping for over there, a lot of the work asks for it. The video is on its way.

' +vslotHTML('american-accent.mp4','American accent') +RH('The glossary, so the jargon never stops you') +'

Voice teaching has its own words. Here are the ones you will actually meet, in plain English. They serve the Delivery page too.

' +RC('IPA','The International Phonetic Alphabet: one written symbol per sound, so an accent can be written down exactly. You do not need to read it fluently. Just know that when a coach writes strange symbols, they are spelling sounds, not words.') +RC('Lexical sets','Families of words that share a vowel, named by a keyword: KIT, DRESS, TRAP, BATH, GOAT. Change how one family sounds and you have changed hundreds of words at once. Accents are taught family by family.') +RC('Placement','Where an accent lives in the mouth: forward on the lips, back in the throat, up behind the nose. Find the placement first and half the sounds fall in on their own.') +RC('Resonance','Where the sound vibrates: chest, mouth, nose. Accents differ here as much as in their vowels. It is a big part of why an accent feels different before you can say why.') +RC('Rhotic or non-rhotic','Whether the r after a vowel gets pronounced. Most American accents say the r in car, most English accents do not. One switch, huge effect.') +RC('Intonation','The melody: where the pitch rises and falls across a sentence. Get the melody right and listeners forgive a stray vowel. Get it wrong and perfect vowels still sound off.') +RH('The archives') +((V.accents&&V.accents.length)?V.accents.map(a=>RL(a.name,a.url,a.d)).join(''):'

Loading…

') +joshLine('Accents') +'
'; return; } if(k==='voicecraft'){ const vBtn=(k2,t,d2)=>''; el.innerHTML=backLink('craft','Craft')+RT('Voice','The instrument you carry everywhere. This page is a programme, not a pile: it runs in order, foundations to coaching.', joshTop('Voice','Not sure where to start with your voice?','Josh is the DOORS voice coach. Tell him what you are working on and he comes back with times and a quote.') +'
Need a voice agent? Upload your voice reels to your profile, open the voiceover agents list, and draft your email: you are sending them your page with the reels already on it.' +'

Your phone is a real microphone. Record voice notes, get under a blanket for that dead studio sound, and rig a pop filter, a sock or a scarf over the phone, so your breath does not pop on the mic. As long as they can hear your voice and the range of things you can do, it works. When the work starts coming, invest in affordable kit and you are working from home.

' +'
' +vBtn('voiceproj','1. Foundations','Breath, then a different strengthening exercise every day.') +vBtn('diction','2. Delivery','Diction, then pace and pause, then resonance.') +vBtn('accents','3. Accent work','American first, the archives, and the plain-English glossary.') +'
' +RH('Your voice reel') +RC('A phone voice note is a voice reel','Record it on your phone, under a blanket: the blanket kills the echo and it sounds like a booth. Ninety seconds, a few accents, a bit of read text, a bit of you. Upload it to your tape diary like any tape and it sits on your profile for voice agents to hear.') +RH('Look after the instrument') +RC('Water, boring and true','Hydration works hours ahead: the water you drink now is your voice this evening. Sip through the day. For a voice that is already tired, steam is the fast lane: five minutes over a bowl, a hot shower counts.') +RC('Know the warning signs','Scratchy or breathy sound, effort to be heard, pitch creeping down, a voice that tires faster than usual. That is fatigue, and it is information, not weakness.') +RC('Rest beats pushing through','A tired voice wants quiet, not whispering: a forced whisper strains more than gentle talking. Give it a silent day and tick movement or text instead, the streak survives.') +RC('When to get it checked','Hoarseness that lasts past two or three weeks, pain when speaking, or a chunk of your range going missing: that is a GP conversation and maybe a specialist, not a warm-up problem. Working actors get this checked early, it is routine, not dramatic.') +RH('4. One to one coaching') +'
Josh, the DOORS voice guyA real voice coach, when you want one.' +'

Josh is the DOORS voice guy. Send him what you are working on, projection, diction, an accent, an audition piece, and he comes back with times and a quote. No account, no forms, it is just email.

' +'Email Josh' +'josh@doors.actor
' +'

'); return; } if(k==='movement'){ el.innerHTML=backLink('craft','Craft')+RT('Movement','Your body is the other half of the instrument. Warm, loose and grounded beats stiff and clever.', '

Every one of these is guided: the stretch comes up, the seconds count down, a soft beep moves you on.

' +'
' +['wakeup','stretchfull','posture','longstretch','stretchpre','nightbefore','breath'] .filter(function(k2){ return (((window.DOORS_DAILY||{})[SESSIONS[k2][0]])||[]).length; }) .map(function(k2){ return ''; }).join('') +'
' +'
' +'
' +RH('Every day, gently') +RC('Cardio, your way','Twenty minutes of anything that raises your heart rate: walk fast, dance in the kitchen, cycle. Stamina is castable.') +RC('Listen to your body','Tired is information. Sore is information. Rest is training too, and nobody books the burnt-out version of you.') +RC('Body language','Watch one stranger a day: how they stand, what their hands do when they lie. Steal it for your next character.')); return; } if(k==='news'||k.indexOf('news/')===0){ // Open to everyone. Without an account you get three swipes and one click, then the door (guestGate). const rawTab=window.__newsTab||'all'; window.__newsTab=null; const storyId=(/^s(\d+)$/.exec(rawTab)||[])[1]||null; const tab=storyId?'all':rawTab; el.innerHTML=backLink('home','Home')+'
'; const newsLimit=pwLocked()?2:0; try{ if(window.DoorsNews) DoorsNews.mount(document.getElementById('news-root'),{ tab:tab, story:storyId, limit:newsLimit, doorHTML:function(){ const em=subEmail(); return '
Keep reading with a free account.

It takes a minute and costs nothing. Members get the whole of DOORS, with 10 days free first.

'+(em?'':'')+'

'; }, onTab:function(key){ location.hash=(key==='all')?'news':('news/'+key); }, fetchAuthed:function(u,o){ o=o||{}; if(cpSession()) return doorsFetchAuthed(u,o); const h=Object.assign({apikey:SBC_KEY,Authorization:'Bearer '+SBC_KEY},o.headers||{}); return fetch(u,Object.assign({},o,{headers:h})); }, social:newsSocial(), shareStory:function(id,title){ const pretty='https://doors.actor/story/'+id; if(navigator.share){ navigator.share({title:title,url:pretty}).catch(function(){}); return; } try{ navigator.clipboard.writeText(pretty); toast('Link copied',pretty,'ok'); }catch(e){ prompt('Copy this link',pretty); } }, share:function(tab){ const pretty='https://doors.actor/news'+(tab&&tab!=='all'?'/'+tab:''); const title=(tab&&tab!=='all'?tab.replace(/_/g,' ').replace(/^./,function(c){ return c.toUpperCase(); })+' on DOORS':'Industry News on DOORS'); if(navigator.share){ navigator.share({title:title,url:pretty}).catch(function(){}); return; } try{ navigator.clipboard.writeText(pretty); toast('Link copied',pretty,'ok'); }catch(e){ prompt('Copy this link',pretty); } }, sbUrl:SBC_URL, cdData:window.CD_DATA||{}, cdHref:function(slug){ return '#casting'; }, onOutreach:function(slug,note){ try{ const a=cdTrackAll(); const tr=a[slug]||(a[slug]={}); tr.n=(tr.n?tr.n+'\n':'')+note; cdTrackSave(a); markDay('research',5); return true; }catch(e){ return false; } } }); else document.getElementById('news-root').innerHTML='

Industry news is loading. Close the tab and reopen doors.actor if this stays.

'; }catch(e){ document.getElementById('news-root').innerHTML='

Industry news could not start on this browser.

'; } return; } if(k==='lines'||k==='linelearner'){ // Reader. Members get the tool. Everyone else gets the walk-through, then the door. if(pwLocked()){ el.innerHTML=backLink('craft','Craft')+'
'; readerFunnel(1,''); return; } el.innerHTML=backLink('craft','Craft')+'
'; try{ if(window.LineLearner) LineLearner.mount(document.getElementById('ll-root'),llSyncCtx()); else document.getElementById('ll-root').innerHTML='

Reader is loading. Close the tab and reopen doors.actor if this stays.

'; }catch(e){ document.getElementById('ll-root').innerHTML='

Reader could not start on this browser.

'; } return; } if(k==='spiderman'){ const d=spdData(); const goal=spdGoal(); const last=(d.hist&&d.hist.length)?d.hist[d.hist.length-1]:null; el.innerHTML=backLink('movement','Movement') +'
' +'
Cardio · the circuit
' +'

The Spider-Man circuit

' +'

Training for Spider-Man: Brand New Day, Tom Holland did this: 5 pull-ups, 10 push-ups, 15 squats, straight through, no rest between moves. 27 rounds in 20 minutes. That is God level. Five rounds is very good. And if you cannot do 5 pull-ups yet, do 3 and press Done anyway. The clock does not judge, it just counts. Do as much as you can.

' +'
' +'
' +'
Today\'s goalrounds in 20 minutes' +''+goal+'
' +(last?'

Last time: '+last.r+' rounds · Your best: '+(d.best||0)+'

':'

First go. Five is a real score. Do as much as you can.

') +'

' +spdHistHTML(d) +'
' +'' +''; return; } if(k==='embody'){ el.innerHTML=backLink('movement','Movement')+RT('Embodying a character','Before a character has lines, they have a body. Build that first and the lines come out different.', '

Answer these on your feet, not in your head. Walk the room while you decide. One question at a time, and let each answer change how you move before you take the next.

' +RH('The questions') +RC('Where does the weight sit?','Heels, toes, hips or chest. A person led by their heels has never hurried for anyone. A person on their toes is already leaving. Pick one and walk it.') +RC('What leads?','Chin, chest, hips or knees. Watch people: the ambitious lead with the chin, the proud with the chest. Whatever leads arrives in the room first.') +RC('What is the pace?','Do they rush or linger? Where in a sentence do they speed up? Speed is fear or appetite, slowness is power or grief. Decide which one yours is.') +RC('Where is the tension held?','Jaw, shoulders, hands, stomach. Everyone holds one place. Find your character\'s and keep it while you do something ordinary, like making tea.') +RC('What do the hands do?','Pockets, hips, busy, still. Hands are where a character leaks. Decide what they do when nervous, and what they do when they think nobody is watching.') +RC('How do they meet space?','Do they take the room up or apologise for it? Do they touch furniture, walls, people? Someone who owns every room moves differently through a doorway.') +RC('Can they be still?','Some people settle, some vibrate. Find your character\'s stillness, or their failure to manage it. Stillness on camera is loud.') +'
The ten minute drill. Walk the room as yourself for one minute. Then change ONE answer, just the weight, and walk again. Add one answer at a time until all seven are living in your body. Last minute: leave the room, come back in as them, and say your first line. Notice what the body did to the voice.
' +RH('Keep stealing') +RC('One stranger a day','Watch how a real person stands, what their hands do when they lie, what leads when they walk. Steal it, file it, use it. This is the same habit as the body language note on Movement, pointed at your next role.') +'

'); return; } if(k==='text'){ const qd=dailyQuote(); el.innerHTML=backLink('craft','Craft')+RT('Text','Reading is training. A little every day and you quietly lap everyone.', (qd?'
Today’s quote. “'+pesc(qd.t)+'” '+pesc(qd.by)+'
':'') +RH('The daily reading') +readingCardHTML() +RH('Script analysis') +'

The hardest skill and the most learnable. Videos with real script breakdowns are coming. Start here meanwhile:

' +RC('First pass, no pencil','Read it once like an audience member. What did you feel, and where exactly did you feel it?') +RC('Second pass, facts only','List what is literally true: who, where, when, what happens. No opinions yet. Most bad choices come from skipping this.') +RC('Third pass, want and obstacle','Every scene: what does your character want, what is in the way, what do they do about it. Write one verb per beat.') +'

'); return; } if(k==='terms'){ el.innerHTML=backLink('home','Home')+'

Terms

' +'
' +'

What DOORS is

' +'

A website for actors: lists of casting directors, agencies and theatres, casting calls, practice tools, and a place to keep track of who you have contacted. Run from the UK, aimed at the UK for now.

' +'

What you get if you pay

' +'

Everything on DOORS is open whether you pay or not. Paying is backing the build early. The monthly price, £'+PRICE+', is the founding price: it holds for at least four months from launch, and if it ever rises we email you before anything changes. The yearly membership, £55, locks the founding price for a full year. Both start with '+TRIAL_DAYS+' days free. We are not holding anything back from people who do not pay.

' +'

Cancelling and refunds

' +'

Cancel whenever you like at your membership page and you will not be charged again. Cancel during the free trial and you are never charged at all. If you are charged by mistake, or you cancel and a payment goes through anyway, email hello@doors.actor and we refund it, no argument and no questions.

' +'

Payments are handled by Stripe. We never see or store your card details.

' +'

What we ask of you

' +'

Do not post anything false, and do not post a casting you are not involved in. Never ask an actor for money to apply for work. Anything that does either gets removed.

' +'

What we cannot promise

' +'

We do not find you work, represent you, or speak to anyone on your behalf. The lists point you at people who publish their own contact details. Casting information changes constantly, so always check the source before you act on it. We are not responsible for what happens between you and anyone you contact.

' +'

Your account and your data

' +'

You can delete your account and everything in it at any time by emailing us. Most of what you keep on DOORS, your notes, trackers, tapes and streak, is saved to your account so it follows you across devices. The public and other members never see it; the DOORS team can, and uses it only to help you. The privacy page spells out exactly what is where.

' +'

If we change these

' +'

If anything here changes in a way that affects what you pay or what you get, we email you before it takes effect.

' +'

DOORS is run by the team. Questions: hello@doors.actor. Last updated 17 August 2026.

' +'
'; return; } if(k==='privacy'){ el.innerHTML=backLink('home','Home')+'

Privacy

' +'

Short version: we hold your email address and the few profile answers you give us. Everything else stays on your own device. We do not sell anything to anybody.

' +'
' +'

What we store, and where

' +'

On our server (Supabase, in the EU): your email address and password, handled by Supabase Auth so we never see your password. If you build a profile: your name, headline, where you are based, your stage, what you want more of, and whether you are on Spotlight. If you post a casting or join the email list: what you typed in that form.

' +'

On your device only, never uploaded: your checker ticks and notes, your casting director and agent trackers, your tape diary, your characters and their answers, your streak and daily ticks, and your profile picture.

' +'

Who else sees it

' +'

Nobody buys it, ever. Three companies help run the site and hold data for us: Supabase (accounts and database), Vercel (hosting), Resend (the emails we send you). We use GoatCounter for visitor counts, which does not use cookies and does not track individuals.

' +'

If you pay

'+'

Payments run through Stripe. They handle your card, we never see or store the number. We can see that a subscription exists and the email attached to it, nothing more. Cancel any time and email us for a refund if anything goes wrong.

'+'

Emails

' +'

We email you for account things like confirming your address and resetting your password. If you asked for reminders, we email you those. Every reminder email has an unsubscribe link and we honour it immediately.

' +'

Your rights

' +'

You can ask for a copy of everything we hold on you, ask us to correct it, or ask us to delete your account and everything with it. Email privacy@doors.actor and we will do it within 30 days. To wipe the device-only data, clear your browser storage for doors.actor.

' +'

The lists

' +'

The casting director and agency listings come from public sources: those offices\' own websites, the Casting Directors\' Guild public directory, and public professional profiles. We do not publish home addresses or private numbers. If you are listed and want your entry changed or removed, email us and it goes within 48 hours, no argument.

' +'

Children

' +'

DOORS is for people aged 16 and over. If you are younger, ask a parent or guardian to set things up with you.

' +'

DOORS is run by the team. Questions: hello@doors.actor. Last updated 17 August 2026.

' +'
'; return; } if(k==='headshots'){ el.innerHTML=backLink('business','Brand strategy')+RT('What makes a good headshot?','Honest, current, and looking like you on your best normal day. That is the whole brief. Here is how you get there.', '
Drawings and animations are coming to this page. The words first, the pictures soon.
' +RH('The rules') +RC('It has to look like you','Not you five years ago, not you filtered. Casting books the person in the photo. If you walk in looking different, you have wasted the room.') +RC('Eyes do the work','A headshot is an eye contact machine. Everything else, hair, jumper, background, is there to not distract from them.') +RC('Current beats perfect','New haircut, new decade, new weight, new you: reshoot. A current honest shot beats a beautiful old one every time.') +RC('Plain, soft, simple','Soft daylight or one soft source, plain background, plain clothes with no logos. The face is the subject.') +RC('Get range without costumes','Two or three looks: open and warm, still and serious. Same person, different weather. No props, no characters.') +RH('Working with a photographer') +RC('Talk before you shoot','A good photographer asks what you get seen for and shoots towards it. If they do not ask, ask them.') +RC('Move, breathe, keep going','The good frame is usually between the posed ones. Photographers catch it when you stop performing the photo.')); return; } if(k==='taste'){ el.innerHTML=backLink('business','Brand strategy')+RT('Working on your taste','Taste is a skill, not a gift. You build it by looking, listening and watching on purpose. Animations for this page are on their way.', RH('Your eye') +RC('Films, photography, painters','Visual taste is built by looking on purpose. Watch one great film a week and ask why every shot is where it is.') +RC('Do this','Pick one photographer or painter a month. Look at twenty of their pictures. Steal one idea for your next self-tape frame.') +RH('Your ear') +RC('Music, rhythm, voices','Actors with rhythm listen differently. Dialogue is music: attack, decay, silence.') +RC('Do this','Build playlists like a director builds shot lists: one for each character you are working on. Listen before you tape.') +RH('Your feeling for people') +RC('Plays, documentaries, cafes','The nuanced thing you show without saying it comes from watching real humans, not other actors.') +RC('Do this','Sit somewhere busy for twenty minutes a week with no phone. Watch one person. Decide what they want and what they are hiding.')); return; } if(k==='postcasting'){ el.innerHTML='

Post a casting

' +'

Casting something? Put it in front of working actors, free. Stage, film, short film, student film, all welcome.

' +'
' +'
How it works. Your casting appears straight away, marked not checked yet. Once we verify it is real it gets the blue tick. Actors apply to you directly: your email or your link, we never sit in the middle. It archives itself 48 hours after your closing date.
' +'' +'' +'' +'' +'' +'' +'' +'' +'' +'' +'

' +'

' +'

By posting you confirm the casting is real and you are involved in it. No fees to apply, ever: castings that charge actors get removed.

' +'
'; return; } if(k==='books'){ const craft=[['Respect for Acting','Uta Hagen'],['A Challenge for the Actor','Uta Hagen'],['The Intent to Live','Larry Moss'],['Actions: The Actor’s Thesaurus','Caldarone & Lloyd-Williams'],['Sanford Meisner on Acting','Sanford Meisner'],['The Art of Acting','Stella Adler'],['An Actor Prepares','Constantin Stanislavski'],['Building a Character','Constantin Stanislavski'],['To the Actor','Michael Chekhov'],['The Actor and the Target','Declan Donnellan'],['The Power of the Actor','Ivana Chubbuck'],['Acting in Film','Michael Caine'],['Impro','Keith Johnstone'],['True and False','David Mamet'],['A Practical Handbook for the Actor','Melissa Bruder et al.']]; const memoir=[['A Life in Parts','Bryan Cranston'],['Greenlights','Matthew McConaughey'],['Sonny Boy','Al Pacino'],['Making It So','Patrick Stewart'],['Finding Me','Viola Davis'],['Blowing the Bloody Doors Off','Michael Caine'],['Friends, Lovers, and the Big Terrible Thing','Matthew Perry'],['The Actor’s Life: A Survival Guide','Jenna Fischer'],['Brat: An ’80s Story','Andrew McCarthy']]; const rd=booksRead(), sink=l=>l.filter(b=>!rd.includes(b[0])).concat(l.filter(b=>rd.includes(b[0]))); const shelf=rd.length?'

Your shelf: '+rd.length+' read of '+(craft.length+memoir.length)+'. Read ones sink to the bottom, so the top of each list is always your next book.

':''; el.innerHTML=backLink()+'

Books & Texts

The acting bibles, the best actor memoirs, and where to get plays. Tick off what you have read and build your shelf.

'+shelf +'

Acting craft

'+sink(craft).map(b=>RB(b[0],b[1])).join('') +'

Actor memoirs & biographies

'+sink(memoir).map(b=>RB(b[0],b[1])).join('') +'

Plays and monologue collections

' +'

Reading plays is training. The publishers, the collections and the playwrights worth starting with.

' +'

' +'
'; return; } if(k==='altskills'){ el.innerHTML=backLink('craft','Craft')+RT('Alternate skills','Casting searches filter by skills before anyone looks at a face. Every true skill on your list is a door.', '

Spotlight and every casting database let a casting director search by skill. Horse riding, fluent Spanish, drums, stage combat. If it is on your profile and true, jobs find YOU. This page is the map of what counts, grouped the way the databases group them.

' +'
The honesty rule. Claim only what you could do on camera tomorrow morning. A false yes gets found out on day one, in front of everyone, and it follows you. A true yes at a modest level is fine: say the level. "Grade 5 piano" books jobs, "piano" plus panic does not.
' +RH('The groups, the way Spotlight lists them') +RC('Music and singing','Instruments with grades if you have them, singing with your range and style. Musical theatre casts by voice type, bands in dramas cast people who actually play.') +RC('Dance and movement','Ballet, tap, jazz, street, ballroom, and stage combat with the certificate body named. Combat certs are real currency: BADC or BASSC levels get you seen for action work.') +RC('Sports and physical','Riding, swimming, martial arts, football, climbing, yoga. Say your standard: club level, county, just confident. Sports double days on set go to actors who genuinely play.') +RC('Vehicles and licences','A full manual driving licence is the single most castable practical skill in the UK. Motorbike licence, HGV, boat licence: niche, and exactly why they book. If you do not drive yet, this is the highest value skill on this page to go and get.') +RC('Languages and dialects','Fluency levels, honestly: native, fluent, conversational. A real second language opens entire markets, and accents live over on the Voice pages with Josh.') +RC('Practical trades','Bartending, cooking, medical training, sign language, juggling, magic. Screen work is full of characters doing jobs. If you have ever had one, it is a skill, list it.') +'
Train one thing. Pick the one skill that shows up most in the breakdowns you want, and give it eight honest weeks. One new true line on your profile a season beats ten vague ones. Then update it everywhere on the same day: Spotlight, your CV here, and tell your agent.
' +'

'); return; } if(k==='picks'){ const secs=['All'].concat(PICK_SECTIONS.map(function(s){ return s[0]; })); const shown=PICKS.filter(function(p){ return pickSec==='All'||p.s===pickSec; }); const blurb=(PICK_SECTIONS.find(function(s){ return s[0]===pickSec; })||[])[1]; el.innerHTML=backLink('scripts','Script Library')+'

DOORS Picks

' +'

'+PICKS.length+' things worth taping from, out in the last two years. Every title, year and lead checked before it went up here.

' +'
' +'
Try to find things you might be right for. But also, feel free to tape the part you would never be seen for. Wrong age, wrong accent, wrong everything. Nobody is casting from this page, so nobody can tell you no. An invitation, not homework.
' +'
' +secs.map(function(s){ return ''; }).join('') +'
' +(blurb?'

'+pesc(blurb)+'

':'') +shown.map(pickCard).join('') +RH('Choosing your two pages') +RC('Two pages, and they must be juicy','Two pages is the sweet spot for a tape. Not a whole scene because it is a whole scene: two pages where something actually turns. If nothing changes in the pages you picked, pick again.') +RC('The portfolio rule','Challenge yourself with material that frightens you, that is how you get better. But the tapes that go in your portfolio, the ones people see, should be work you are comfortable in and have had real feedback on. Train on the hard stuff, show the good stuff.') +RC('Then log it','Every tape you make from this page goes in the tape diary, with what you learned watching it back. That is where the actual work lives.') +'

' +'

Scripts move around and some are only out during awards season. If a link has gone, tell us. Our own scenes are coming.

' +'
'; return; } if(k==='scripts'){ el.innerHTML=backLink()+'

Script Library

Where to legally read and download screenplays and scenes, and what is worth taping right now.

' +'' +'

Free script archives

' +RL('BBC Writers Room','https://www.bbc.co.uk/writers/scripts','Official free scripts from BBC drama, comedy, radio and film.') +RL('Script Slug','https://www.scriptslug.com','Clean, searchable library of film and TV screenplay PDFs.') +RL('IMSDb','https://imsdb.com','The Internet Movie Script Database, a huge back catalogue.') +RL('SimplyScripts','https://www.simplyscripts.com','Movie, TV and radio scripts, plus unproduced screenplays.') +RL('The Script Lab','https://thescriptlab.com/screenplays/','Screenplay library plus craft articles.') +RL('Script-O-Rama','https://www.script-o-rama.com','Long-running archive of scripts and transcripts.') +'

Scenes & monologues

' +RL('Monologue Blogger','https://monologueblogger.com','Original monologues and two-person scenes to perform.') +RL('StageMilk','https://www.stagemilk.com/monologues/','Monologues and self-tape material sorted by type.') +RL('Backstage: Monologues','https://www.backstage.com/magazine/tag/monologues/','Curated monologue lists for auditions.') +'

The DOORS scene library

Coming soon: hand-picked scene PDFs for two-handers, comedy, drama and more. Got a great one? Send it in.

'; return; } if(k==='theatres'){ const th=(n,u,d)=>RL(n,'https://'+u,d); el.innerHTML=backLink()+'

Theatre List

The UK houses worth knowing: watch there, audition there, invite people there.

' +'

National & flagship

' +th('National Theatre','nationaltheatre.org.uk','South Bank, three stages, the big one.') +th('Royal Shakespeare Company','rsc.org.uk','Stratford-upon-Avon and touring.') +th('Shakespeare’s Globe','shakespearesglobe.com','Open-air classical on Bankside.') +th('Royal Court','royalcourttheatre.com','The home of new writing.') +th('The Old Vic','oldvictheatre.com','Star-led revivals and new work.') +th('Young Vic','youngvic.org','Bold revivals, big directors.') +th('Donmar Warehouse','donmarwarehouse.com','Intimate powerhouse in Covent Garden.') +th('Almeida','almeida.co.uk','Islington; transfers to the West End constantly.') +th('Bridge Theatre','bridgetheatre.co.uk','New writing at scale by Tower Bridge.') +'

New writing & contemporary

' +th('Bush Theatre','bushtheatre.co.uk','West London new-writing champion.') +th('Soho Theatre','sohotheatre.com','New plays and comedy crossover.') +th('Hampstead Theatre','hampsteadtheatre.com','New plays on two stages.') +th('Kiln Theatre','kilntheatre.com','Kilburn, international-minded new work.') +th('Theatre503','theatre503.com','Above a Battersea pub; first plays launch here.') +th('The Yard','theyardtheatre.co.uk','Hackney Wick, experimental and fearless.') +th('New Diorama','newdiorama.com','Home for ensembles and devised companies.') +'

Fringe circuit, get seen here

' +th('Southwark Playhouse','southwarkplayhouse.co.uk','Two venues, constant casting.') +th('Arcola Theatre','arcolatheatre.com','Dalston stalwart; casting directors attend.') +th('Park Theatre','parktheatre.co.uk','Finsbury Park, strong transfers.') +th('Finborough Theatre','finboroughtheatre.co.uk','Tiny room, huge reputation.') +th('Old Red Lion','oldredliontheatre.co.uk','Islington pub-theatre classic.') +th('King’s Head Theatre','kingsheadtheatre.com','Islington, now in a new space.') +th('Seven Dials Playhouse','sevendialsplayhouse.co.uk','Covent Garden studio work.') +th('Pleasance London','pleasance.co.uk','Islington; the Edinburgh pipeline.') +'

Major regional

' +th('Bristol Old Vic','bristololdvic.org.uk','Oldest working theatre in the country.') +th('Royal Exchange','royalexchange.co.uk','Manchester in the round.') +th('HOME','homemcr.org','Manchester contemporary arts centre.') +th('Leeds Playhouse','leedsplayhouse.org.uk','Major northern producing house.') +th('Sheffield Theatres','sheffieldtheatres.co.uk','Crucible and Lyceum.') +th('Birmingham Rep','birmingham-rep.co.uk','Midlands flagship.') +th('Chichester Festival Theatre','cft.org.uk','Summer season, West End transfers.') +th('Liverpool Everyman & Playhouse','everymanplayhouse.com','Merseyside producing houses.') +th('Citizens Theatre','citz.co.uk','Glasgow institution.') +th('Royal Lyceum','lyceum.org.uk','Edinburgh producing house.') +'
'; return; } if(k==='community'){ const a=COMM_ARTICLES[0]; el.innerHTML=backLink('hub','Resources')+'

For the community, from the community

' +'

Not a chat room, on purpose. A place to do things together, look after yourselves, and pass on what you had to learn the hard way.

' +'
' +'' +RH('Events') +'

DOORS events. Run by the team: readings, taping days, run club, seeing shows together, going out after. The first ones get announced here and in the email. Got a space or an idea for one? Tell the team.

' +'
' +'

From the community. Classes, shows, workshops, meetups that members want each other to know about. The team reads every one before it goes up.

' +'
' +'
' +'
Promote one
' +'' +'' +'' +'' +commIdSelect('ev-as') +'

' +'

' +'
' +RH('Looking after yourself') +'
Your instrument is you

Everything on this site, the tapes, the trackers, the emails, runs on one machine: you. Sleep, food, mates, moving your body, and saying no when a thing feels wrong are craft work, not time off it. The voice page has the care guide, movement has the daily bits, and the castings page tells you what a real casting never asks of you.

' +'
Check on each other

The industry is long waits and thin replies for everyone, including the people who look like they are flying. If someone from a class or a shoot has gone quiet, send the two line message. That habit is the community.

' +'

The articles

' +'
' +'
'+pesc(a.title)+''+pesc(a.by)+'
' +(pwLocked()?lockCard(0,'articles'):'' +'

'+pesc(a.when)+'

' +'
These are his words, as he sent them. DOORS platforms what actors pass on, it does not speak for them, and one actor\'s experience is not the rulebook. Check anything that matters against the sources linked.
' +a.body.map(function(s){ return (s[0]?'

'+pesc(s[0])+'

':'')+'

'+s[1]+'

'; }).join('') +'

Replies

' +'

Loading...

' +'' +commIdSelect('cm-as') +'

' +'

') +'
' +'
' +'
' +'' +'

Write your own

' +'

Been through something trying to become an actor? Moving countries, getting into drama school or not getting in, starting a company, quitting and coming back. Whatever it was, someone is about to go through it too. Write the thing you needed to read at the time. It goes up once the team has read it.

' +'' +'' +commIdSelect('ca-as') +'

' +'

' +'
'; commLoad(a.id); commArticlesLoad(); evLoad(); devPaintEvents(); return; } if(k==='voice'){ const tr=voTrackAll(); const done=Object.keys(tr).filter(function(n){ return (tr[n].s||0)>0; }).length; el.innerHTML=backLink('hub','Resources')+'

Voice-Over Agents

' +'

The UK voiceover agencies worth writing to, with the same tracker as the acting agents. Your voice reel is the whole application: a phone voice note under a blanket counts.

' +'
' +'
Your reels are the whole application. Voice agents do not sign you because of your email. They sign you because of the reels in it. Record on your phone under a blanket for that dead studio sound, a sock or a scarf over the mic so your breath does not pop, ninety seconds of range. Upload your voice reels to your tape diary so they sit on your profile, then write to the list below: you are sending your page with the reels already on it.
' +'
' +'

'+done+' of '+VO_LIST.length+' approached.'+(done?' Keep going.':' The first email is the hardest, send it today.')+'

' +VO_LIST.map(function(n,i){ const t=tr[n]||{}, st=t.s||0; return '
'+pesc(n)+'' +(st?' '+pesc(VO_STATUS[st])+'':'') +'' +'
' +VO_STATUS.map(function(s,si){ return si?'':''; }).join('') +'
' +'' +'
'; }).join('') +'
'; return; } if(k==='material'){ el.innerHTML=backLink('books','Books & Texts')+'

Plays and monologues

Where to actually get plays, monologue collections, and the playwrights to start with. Reading plays is training.

' +'

Where to get plays

' +RL('Nick Hern Books','https://www.nickhernbooks.co.uk','UK play publishing giant; great monologue collections too.') +RL('Concord Theatricals (Samuel French)','https://www.concordtheatricals.co.uk','Huge catalogue plus performance rights.') +RL('Faber Drama','https://www.faber.co.uk','Pinter, Stoppard, Butterworth and more.') +RL('Methuen Drama (Bloomsbury)','https://www.bloomsbury.com/uk/discover/methuen-drama/','Classic and contemporary playtexts.') +RL('Drama Online','https://www.dramaonlinelibrary.com','Digital play library; often free with a library card.') +'

Monologue & scene collections

' +RL('The Good Audition Guides (NHB)','https://www.nickhernbooks.co.uk','Curated classical and contemporary speeches.') +RL('Methuen Book of Monologues','https://www.bloomsbury.com','Multiple editions covering every age and era.') +RL('StageMilk monologue database','https://www.stagemilk.com/monologues/','Free, sorted by age, tone and gender.') +'

Playwrights to read

' +'

Contemporary: debbie tucker green, Jez Butterworth, Lucy Kirkwood, Lucy Prebble, James Graham, Simon Stephens, Arinzé Kene, Ella Hickson, duncan macmillan, Michaela Coel. Foundations: Caryl Churchill, Harold Pinter, Samuel Beckett, Tennessee Williams, Arthur Miller, Chekhov, Ibsen. Read one a week and your taste builds itself.

' +'
'; return; } if(k==='relationships'){ el.innerHTML=backLink()+'

Industry Relationships

Casting directors, agents and directors cast people they know and trust. Relationships are built slowly, through work, not networking tricks.

' +RC('Be brilliant in small rooms','Fringe shows, short films and student films are where CDs and agents first see you. Invite them personally: short email, the dates, a ticket offer, and one line on why this role.') +RC('Casting director workshops','Sessions run by The Actors’ Guild and similar let you work with CDs directly. Treat them as training, not access. One good workshop scene is remembered for years.') +RC('Spotlight events','Spotlight runs open houses, Q&As and showcases for members. Free industry contact time; go to them.') +RC('Personalised emails only','Write to named people, two short paragraphs: who you are, one credit or tape link, one clear ask. Never mass-email. Follow up months later with real news, not nudges.') +RC('Social media, used well','Follow the CDs from our Casting Directors list and engage genuinely with the work they cast. Post your own work; building a visible body of work is the whole idea.') +RC('Sets and rehearsal rooms','ADs, script supervisors and fellow actors become tomorrow’s directors and producers. Be prepared, kind and on time; it is the best networking that exists.') +RC('Track everything','Use the tick boxes and notes in the Checker, and keep a log of who you wrote to and when. That discipline is a career skill.') +'
'; return; } if(k==='profile'){ const s=cpSession(); const p=cpProfile||{}; const link=(u,t)=>{ const h=safeUrl(u); if(!h) return ''; const mail=/^mailto:/i.test(h); return ''+(mail?'Email':t)+''; }; const ig=p.instagram?('https://www.instagram.com/'+String(p.instagram).replace(/^@+/,'')+'/'):''; if(window.__profView==='public' && p.name){ el.innerHTML=backLink('home','Home') +'
' +(p.is_public?'':'
This is a preview. Only you can see it until you create your link.
') +pubProfileBodyHTML(Object.assign({},p,{tapes:(typeof tapesAll==='function'?tapesAll().filter(function(t){ return t.onProfile&&t.link; }).slice(0,10).map(function(t){ const pv=t.priv||{}; return {id:t.id,t:t.project||'Self tape',r:t.role||'',u:t.link,co:t.company||'',g:pv.g?'':(t.genre||''),ac:pv.ac?'':(t.accent||''),cd:t.office||'',n:pv.n?'':(t.notes||''),o:pv.o?'':(t.outcome||''),d:t.date||''}; }):[])}),'pv'); return; } el.innerHTML=backLink('home','Home') +'
' +'

'+pesc(p.name||'Your profile')+'

' +(p.is_public ? '' : '') +'
' +profMeterHTML() +'' +(p.headline?'

'+pesc(p.headline)+'

':'

Fill this in once. It becomes your CV, and it is saved to your account so it follows you to any device.

') +'
' +'
Who has seen your page
' +'
Your watchlist
' +'
Your favourites
' +(p.name?'
' +(p.based?''+pesc(p.based)+'':'') +(p.stage?''+pesc(p.stage)+'':'') +(p.focus?'Wants: '+pesc(p.focus)+'':'') +(p.agent?''+pesc(p.agent)+'':'Unrepresented') +''+(p.spotlight?'On Spotlight':'Not on Spotlight yet')+'' +'
':'') +((ig||p.spotlight_url||p.vimeo||p.website)?'
' +link(ig,'Instagram')+link(p.tiktok?('https://www.tiktok.com/@'+String(p.tiktok).replace(/^@+/,'')):'','TikTok')+link(p.youtube,'YouTube')+link(p.spotlight_url,'Spotlight')+link(p.vimeo,'Showreel')+link(p.website,'Website')+'
':'') +'
Your picture'+(function(){ try{ const a=avatarGet(); return a?'':''; }catch(e){ return ''; } })()+''+avatarBlockHTML()+'
' +'
Your public page '+(p.is_public?'Live':'Private')+(p.is_public?'':'')+''+profShareBlockHTML()+'
' +'
Your details'+profFormHTML()+'
' +'
Just for you'+profJfyHTML()+'
' +'
Account' +'
'+pesc((s&&s.email)||'')+'' +'
Everything here saves to your account and follows you to any device: profile, tapes, trackers, ticks, streak, the lot.
' +'
' +trackersHTML() +'

' +'' +'' +'' +'

' +'
Your membership
' +'
'; window.__skills=null; skillsPaint(); jfyLoad(); try{ paintProfMember(); }catch(e){} try{ paintProfWatch(); }catch(e){} try{ paintProfViews(); }catch(e){} try{ paintProfFavs(); }catch(e){} return; } if(k==='cv'){ const s=cpSession(); const p=cpProfile; if(!p){ el.innerHTML=backLink('profile','Profile')+'

Build your profile first, then the CV builds itself.

'; return; } const tapes=(typeof tapesAll==='function'?tapesAll().filter(function(t){ return t.onProfile&&t.link; }):[]); const photos=Array.isArray(p.photos)?p.photos.slice(0,4):[]; const skills=(Array.isArray(p.skills)?p.skills:[]).map(function(x){ return x&&x.s; }).filter(Boolean); const credits=String(p.credits||'').split(/\n+/).map(function(x){ return x.trim(); }).filter(Boolean); const links=[]; if(p.instagram) links.push('Instagram: instagram.com/'+String(p.instagram).replace(/^@+/,'')); if(p.spotlight_url) links.push('Spotlight: '+String(p.spotlight_url).replace(/^https?:\/\//,'')); if(p.imdb) links.push('IMDb: '+String(p.imdb).replace(/^https?:\/\//,'')); if(p.website) links.push(String(p.website).replace(/^https?:\/\//,'')); const pageUrl=p.is_public?('doors.actor/#p/'+(p.slug||'')):''; const line=function(l,v){ return v?'
'+l+': '+pesc(v)+'
':''; }; el.innerHTML=backLink('profile','Profile') +'
' +'
' +'

'+pesc(p.name||'')+'

' +(p.headline?'
'+pesc(p.headline)+'
':'')+'
' +'
DOORS.
' +'
' +'
' +(p.avatar_url?'':'') +'
' +((p.agent||p.agent_email||p.agent_phone) ?'
'+pesc(p.agent||'Representation')+'' +(p.agent_phone?'
Tel: '+pesc(p.agent_phone)+'
':'') +(p.agent_email?'
'+pesc(p.agent_email)+'
':'')+'
' :((s&&s.email)?'
Contact
'+pesc(s.email)+'
':'')) +line('Location',p.based) +line('Focus',p.focus) +line('Stage',p.stage) +'
Spotlight: '+(p.spotlight?'Yes':'Not yet')+'
' +'
' +(links.length?'
'+links.map(pesc).join('
')+'
':'') +'
' +'
' +(photos.length?'
'+photos.map(function(u){ return ''; }).join('')+'
':'') +(credits.length?'

Credits

' +credits.map(function(cr){ return '
'+pesc(cr)+'
'; }).join('')+'
':'') +(skills.length?'

Special skills

' +'
'+skills.map(function(sk){ return ''+pesc(sk)+''; }).join('')+'
':'') +(tapes.length&&pageUrl?'

Tapes, streaming at '+pesc(pageUrl)+'

' +tapes.slice(0,10).map(function(t){ return '
'+pesc(t.project||'Self tape')+(t.role?' · '+pesc(t.role):'')+(t.office?' · for '+pesc(t.office):'')+'
'; }).join('')+'
':'') +'
' +''+(pageUrl?pesc(pageUrl):'Made on DOORS')+'doors.actor' +'
' +'
' +'

'; try{ doorsFetchAuthed(SBC_URL+'/rest/v1/doors_private?select=meas,sizes,unions&id=eq.'+cpUid(),{}) .then(function(r){ return r.ok?r.json():[]; }) .then(function(rows){ const j=rows&&rows[0]; if(!j) return; const el2=document.getElementById('cv-jfy'); if(!el2) return; let h=''; if(j.meas) h+='
Measurements: '+pesc(j.meas)+'
'; if(j.sizes) h+='
Sizes: '+pesc(j.sizes)+'
'; if(j.unions) h+='
Unions: '+pesc(j.unions)+'
'; el2.innerHTML=h; }).catch(function(){}); }catch(e){} return; } if(k==='character'){ const all=charAll(); const names=Object.keys(all).sort(); let head=backLink('craft','Craft')+'

The 36 questions

' +onlyYou('nobody else sees your answers') +'
'; if(names.length){ head+='

Your characters

'+names.map(n=>''+pesc(n)+'×').join('')+'
'; } head+='
New character' +'' +'' +'
'; if(window.charCur && all[window.charCur]){ const c=all[window.charCur]; const a=c.answers||{}; const done=Object.keys(a).filter(x=>(a[x]||'').trim()).length; const sets=[['Set one, the shallow end',0],['Set two, getting personal',12],['Set three, the deep end',24]]; head+='

'+pesc(window.charCur)+(c.piece?' · '+pesc(c.piece):'')+'

' +'

'+done+' of 36 answered

'; sets.forEach(s=>{ head+='

'+s[0]+'

'; for(let i=s[1];i' +'
'; } }); head+='

'; } head+='

Everything saves on this device automatically. Questions adapted from the closeness study by Arthur Aron and colleagues.

'; el.innerHTML=head; return; } if(k==='tapediary'){ window.__editTape=null; window.__tapePriv={}; tapesCloudPull(function(changed){ if(changed) renderResource('tapediary'); }); try{ tapeProfilePush(); }catch(e){} const tapes=tapesAll(); const month=new Date().toISOString().slice(0,7); const thisMonth=tapes.filter(t=>(t.date||'').slice(0,7)===month).length; const booked=tapes.filter(t=>t.status==='Booked').length; let h=backLink('craft','Craft')+'

Tape diary

' +'

Every tape you log here can be dropped straight into your email to agents, link included. So fill in everything you want an agent to read.

' +onlyYou('nobody else sees what you have logged') +'
' +'

'+thisMonth+' this month'+booked+' booked

' +'
New tape' +'' +'' +'' +'' +'' +'' +'
' +'' +'' +'' +'' +'

This one shows on your public profile, so casting sees how it went. Optional.

' +''+Object.values(window.CD_DATA||{}).map(c=>c.name).sort().map(n=>'' +'' +'' +'' +'' +'' +'' +'
'+tdPrivHTML()+'
' +'' +'

' +'
How long should it be. There is no time limit here, put up what the job needs. Worth knowing though: for screen, casting watches the first thirty seconds and decides, and a two minute tape gets watched all the way through where a five minute one gets scrubbed. Theatre is different, two speeches or a classical piece with a modern one will run longer and that is normal.
' +'
Film in 1080p, not 4K. Not for our sake, for theirs. A casting director opens dozens of tapes a day, often on a laptop in a rehearsal room on bad wifi. A 4K tape takes them minutes to download and looks exactly the same as a 1080p one on the screen they are watching it on. Big files get left until later, and later is after the session.' +'
' +[['720p','120MB','Fine, and tiny'], ['1080p 30fps','260MB','What to film. Send this one'], ['1080p 60fps','400MB','No benefit for a tape'], ['4K 30fps','340MB to 700MB','Depends on your phone'], ['4K 60fps','800MB','Annoying for everyone']] .map(function(r){ return '
'+r[0]+''+r[1]+''+r[2]+'
'; }).join('') +'
' +'

The one setting. On an iPhone: Settings, then Camera, then Record Video, then 1080p at 30fps. Do it once and every tape after it is small. On Android it is in the camera app settings, under video resolution.

' +'

Sizes are for a two minute tape, from the phone makers own figures. Double them for four minutes.

' +'

'; if(tapes.length){ h+='

The diary

'+tapes.map(t=>{ const url=safeUrl(t.link); const isFile=url&&url.indexOf('/storage/v1/object/public/tapes/')>0; // The agent email format: bold title with the company in brackets, the story line under // it, then the tape. That is how this reads when it reaches an agent, so it reads that // way here too. const head=''+pesc(t.project)+(t.company?' ('+pesc(t.company)+')':'')+'' +('
'+[t.role,t.genre,t.accent?pesc(t.accent)+' accent':''].filter(Boolean).map(function(x){ return pesc(x).replace(/&/g,'&'); }).join(', ') +(t.office?(t.role||t.genre||t.accent?', ':'')+'for '+pesc(t.office):'')+'
') +(t.material?'
'+pesc(t.material)+'
':'') +(t.outcome?'
'+pesc(t.outcome)+'
':''); return '
' +head +(isFile?'
' +'' +'
'+pesc(t.project)+''+(t.role?''+pesc(t.role)+'':'')+'
' +'
':'') +'
' +tapeDueHTML(t)+(t.uploading===1?' Uploading':(t.uploading==='failed'?' Upload failed':'')) +'
'+pesc(t.date||'')+'
' +'
' +'' +'' +(!url?'
':'') +(url?'
':'') +'
'; }).join('')+'
'; } else { h+='

No tapes yet. The next one you make goes in here, and in a month you will have a record no acting class gives you.

'; } h+='

Tap the status pill to move a tape along: Taped, Sent, Callback, Booked, No reply. Saves on this device automatically.

'; el.innerHTML=h; return; } if(k==='craft'){ const cb=(k2,t,d2)=>''; el.innerHTML=backLink('home','Home')+RT('Craft '+MD('#F07FAC'),'Voice, movement, text. Five minutes counts. Do a little every day and you will smash it.', '
' +cb('lines','Reader / Run Lines','Paste a scene, pick your part, and it reads everyone else while you learn yours.') +cb('voicecraft','Voice','Projection, diction, accents. Daily.') +cb('movement','Movement','Stretch, breath, cardio, body language.') +cb('text','Text','Daily reading, script analysis, books.') +cb('screen','Screen','The self tape setup, eye lines, camera craft.') +'
' +'
' +cb('character','The 36 questions','Know your character better than the writer.') +cb('tapediary','Tape diary','Log every self-tape and what you learned.') +cb('altskills','Alternate skills','Music, dance, combat, licences. All castable.') +cb('community','Community','Events, looking after yourself, and each other.') +'
'); return; } if(k==='craft-old'){ el.innerHTML=backLink('home','Home')+RT('Craft '+MD('#F07FAC'),'The daily practice room. One hour, two halves: 30 minutes of play, 30 minutes of craft. Do the bare minimum, do it consistently.', '
Two tools are live. The 36 questions: answer them as your character and know them better than the writer does. And the tape diary: log every self-tape and what you learned watching it back.
' +'
Coming soon. Pick a pillar, pick a session, press start. A calm 30 minute timer runs and a soft chime tells you when you are done. No streaks, no guilt, no 4 hour training days.
' +RH('The five pillars') +RC('Voice','Sing along, make silly sounds, follow a voice workout. No seriousness, just sound.') +RC('Movement','Stretch, walk without your phone, move to music. Non-exercise, non-guided. Your body is the instrument.') +RC('Text','Read a scene, a play, a monologue. Write five pages about what you understand. Reading plays is training.') +RC('Screen','Self-tapes and takes on camera. Three takes back to back, no second-guessing, no perfectionism.') +RC('Stage','Monologues out loud, scene repetition, seeing shows and stealing honestly.') +'

Until the sessions land, the Books, Script Library and More Material pages under Grow are the practice cupboard.

'); return; } if(k==='feedback'){ var mailA=function(kind,label){ return ''+label+''; }; el.innerHTML=backLink('home','Home')+'

Tell the team

' +'

DOORS is built in public, from what actors say. Something broken, something missing, something annoying: this is the door for it. It gets read, every one.

' +'
' +'' +'

Plain words are perfect. If it is a bug, say what you were doing just before it went wrong.

' +'' +'

' +'

' +'

Send it opens your email app with everything filled in, so you can see exactly what goes. Your phone or computer type gets added at the bottom, because it makes bugs findable. Nothing else is collected.

' +RH('Or just email, no forms') +'

'+FB_TO+' comes straight to us. So does a reply to any DOORS email you have had.

' +'
'+mailA('Something is broken','Report a bug')+mailA('An idea','Send an idea')+mailA('A casting to add','Send a casting')+'
' +RH('What happens to it') +RC('It goes on the list','Every piece of feedback lands in a ledger with who said it, and gets answered with DONE, on the list, or parked with a reason. Nothing gets lost between a message and the build.') +RC('Fast things happen fast','Most small fixes land the same day or the same week. Big ones get told to you straight: this is coming, or this is not, and why.') +'
'; return; } if(k==='social'){ const IG_OFFICES=IG_SAFE_LIST; const igGroup=window.igGroup||'Casting offices'; const igPill=r=>''+pesc(r[0])+''; el.innerHTML=backLink('business','Brand & Value')+'

Social media

' +'
' +'

Who to follow

' +'
Follow, do not approach. A public office account is an open window, not an open door. Never pitch yourself in comments or a DM. If an office posts an open call, follow the instructions in that post exactly, and nothing else.
' +'

Accounts run for the work, not private ones. Follow them and your feed becomes the industry noticeboard: what is casting, what is announced, who is in the room. And agents look at who you follow: following the industry reads as serious. All the working people follow lots of people. Pick a kind.

' +(pwLocked()?(function(){ var head={}; IG_OFFICES.forEach(function(r){ head[r[1].toLowerCase()]=1; }); var more=Object.values(window.CD_DATA||{}).filter(function(c){ return igSafeHandle(c.ig) && !head[String(c.ig).toLowerCase()]; }); var total=IG_OFFICES.length+more.length+((window.IG_ORGS||[]).length||0); return '
'+IG_OFFICES.slice(0,3).map(igPill).join('')+'
' +lockCard(total-3,'checked industry Instagram accounts') +'

Casting offices, theatres and industry accounts in one place, every one opened and checked before it went up. The team adds more every week.

'; })():( '
' +IG_GROUPS.map(function(g){ return ''; }).join('') +'
' +(igGroup==='Casting offices' ? '
'+IG_OFFICES.map(igPill).join('')+'
' : (function(){ var rows=IG_ORGS.filter(function(r){ return r[3]===igGroup; }); return rows.length ? '
'+rows.map(igPill).join('')+'
' : '

Being checked one by one before they go up. Back shortly.

'; })()) +(function(){ if(igGroup!=='Casting offices') return ''; var head={}; IG_OFFICES.forEach(function(r){ head[r[1].toLowerCase()]=1; }); var more=Object.values(window.CD_DATA||{}) .filter(function(c){ return igSafeHandle(c.ig) && !head[String(c.ig).toLowerCase()]; }) .sort(function(a,b){ return a.nameMore offices, checked one by one' +'

Every account here has been opened and looked at before it went up. If one of them locks their account or stops posting, tell us and it comes off.

' +'
'+more.map(function(c){ return igPill([c.name,c.ig,(c.focus||'')]); }).join('')+'
'; })() +'

This list is short on purpose. Every account on it is checked as one an office runs publicly. Know one that belongs here? Send it in.

')) +'' +'

Add photos from your camera roll, several at once, then hold a square for a beat and drag it where you want it. Everything you plan stays on your own phone.

' +RH('The rules, there are only four') +RC('Casting directors do look','Not at your follower count. They look for a sense of you: face, energy, whether you are working at it. An account that shows a working actor is doing its job at 80 followers.') +RC('Never pitch in public','Follow casting directors, enjoy their posts, comment like a colleague when you have something real to say. Never ask for work in comments or DMs. It is the fastest way to be remembered wrong.') +RC('No bitterness, ever','Not about auditions, not about waiting, not about other actors. The industry reads everything and remembers tone. Celebrate people publicly, moan privately.') +RC('Post the work you want more of','Playing comedy? Post the funny. Want drama? Post the tape excerpt that aches. Your grid quietly tells people what to cast you as, so tell them on purpose.') +RH('Two kinds of account. Pick one on purpose') +'
' +'
' +RC('The portfolio','Clean and current: headshot, stills, tape moments, opening nights, classes. Updated when something real happens. This is a shop window, and quiet confidence suits it. Most working actors run this one.') +'
A real account that runs this way is being added here, so you can see it working.
' +'
' +'' +'

Neither is wrong. They are two right ways of using the same app as an actor.

' +'
'; return; } if(k==='office'){ el.innerHTML=backLink('home','Home')+'

The office

' +'

Checking you are the boss...

'; officeLoad(); return; } if(k==='gambino'){ el.innerHTML=backLink('business','Brand & Value') +'

Give it all away.

' +'
' +vslotHTML('gambino-18-slides-narrated-v2-charts-fixed.pptx (1).mp4','Give it all away.') +'

Donald Glover. Actor, writer, director, musician. Childish Gambino is the thing he is best known for, and for years he gave the music away for free. The audience that built is what opened every other door. Nine minutes.

' +'

More case studies as they get made.

' +'
'; return; } const CASE_STUDIES=[ {k:'gambino', t:'Give it all away.', w:'Donald Glover', d:'Actor, writer, director, musician. For years he gave the music away for free, and the audience that built opened every other door.', meta:'Video, nine minutes', live:true, c:'#F07FAC'}, {k:'pwb', t:'Write your own way in.', w:'Phoebe Waller-Bridge', d:'Nobody was casting her in what she wanted, so she wrote it, put it on at the Edinburgh Fringe for an hour, and the industry came to her.', meta:'Coming soon', live:false, c:'#C9302E'}, {k:'taika', t:'Be the whole crew.', w:'Taika Waititi', d:'Shorts nobody asked for, made with friends on nothing, until the work was undeniable and the budgets came to him.', meta:'Coming soon', live:false, c:'#F2C21D'} ]; function caseRailHTML(){ return '
'+CASE_STUDIES.map(function(c){ return '<'+(c.live?'button':'div')+' class="casecard'+(c.live?'':' soon')+'" style="--cc:'+c.c+'"'+(c.live?' onclick="showTab(\''+c.k+'\')"':'')+'>' +''+(c.live ? '' : 'Coming')+'' +''+pesc(c.t)+'' +''+pesc(c.w)+'' +''+pesc(c.d)+'' +''+pesc(c.meta)+'' +''; }).join('')+'
' +'

Swipe for the next one. A new story every Sunday.

'; } if(k==='business'){ const bBtn=(k2,t,d2)=>''; const bGrid=inner=>'
'+inner+'
'; el.innerHTML=backLink('home','Home')+RT('Brand & Value '+MD('#57A237'),'What the industry sees, and what your work adds up to. Your presence, your relationships, your CV.', RH('Watch the case studies') +'

Stories, because that is what you are: storytellers. Real careers, and how they actually got built.

' +caseRailHTML() +bvHTML() +'

Plan what you post: the Social media page

' +RH('Your presence') +bGrid( bBtn('checker','The Checker','72 platforms. Where you exist and where you do not.') +bBtn('profile','Your profile and CV','Answers in, CV out.') +bBtn('cv','Export your DOORS CV','The printable one-pager.')) +RH('Relationships') +'

The lists live in Resources. What you do with them lands here.

' +bGrid( bBtn('casting','Casting directors','Track who has seen you, and how often.') +bBtn('agents','Agents','Emailed, replied, meeting. Track every approach.') +bBtn('social','Social media','Plan what you post before you post it. Make it reel lives here.') +bBtn('castings','Casting calls','Verified calls plus community posts.') ) +remindersHTML() +RH('Taste') +'

Taste is a skill, not a gift. You build it by looking, listening and watching on purpose.

' +bGrid(bBtn('taste','Working on your taste','Your eye, your ear, your feeling for people.')) +RH('Opportunities to be seen') +RL('MonologueSlam (TriForce)','https://www.thetcn.com','Perform live in front of casting directors and agents. Open entry.') +RL('The Actors’ Guild','https://www.actorsguild.co.uk','Workshops where you act for real casting directors. Treat them as training, not access.') +RL('BFI Network','https://network.bfi.org.uk','Funding and development for making your own short film.') +RL('FilmFreeway','https://filmfreeway.com','Made something? Submit it to festivals. Also lives in the Checker.')); return; } if(k==='casting'){ el.innerHTML=backLink('business','Brand & Value')+cdDirectoryHTML(); cdRender(); return; } if(k==='welcomein'){ try{ localStorage.setItem('doorsWelcomeIn',String(Date.now())); }catch(e){} subOK=true; let back='craft'; try{ back=(localStorage.getItem('doorsReturnTo')||'craft').replace(/[^a-zA-Z0-9\/~.-]/g,''); }catch(e){} if(!back||back==='home'||back.indexOf('welcomein')>=0) back='craft'; try{ subCheck(function(){}); }catch(e){} el.innerHTML='
' +'

Welcome to DOORS.

' +'

You just did something good for your acting career. You backed it. All our tools, resources and lists are yours now, and we will be adding loads more. Feel free to ask us for more and we will keep building. This is just the beginning.

' +'

' +'' +'

' +'

Taking you back in a few seconds.

' +'
'; clearTimeout(window.__wiT); window.__wiT=setTimeout(function(){ if((location.hash||'').indexOf('welcomein')>=0) showTab(back); },6000); return; } if(k==='getagent'){ el.innerHTML=backLink('hub','Resources')+'

Get the agent you deserve

' +'

Answer a few quick questions and DOORS walks you to an agent one page at a time: your tapes, your page, the email, the agent list. It is a real campaign that really worked: 600 emails, three meetings, signed at one of the biggest agencies in London.

' +'
' +'
Tapes first, email second. Agents do not sign you because of your email. They sign you because of the tapes and the content in your email. Upload your self tapes to your tape diary so they sit on your profile, then draft the email here.
' +'
' +'
' +'

Start the questions again

' +'
'; try{ sessionStorage.removeItem('doorsFromGA'); }catch(e){} gaPaint(); return; } if(k==='agents'){ el.innerHTML=backLink('hub','Resources')+'

Acting agents

' +'

No agent, or want a better one? Start the walkthrough.

' +'

'+agAll().length+' UK agencies with who they represent, their contact, Instagram and IMDbPro. Track every one you approach: emailed, replied, meeting.

' +'
' +'
Take every tier with a grain of salt. The tiers below are about size, not quality. A smaller agency with great agents can be better than a giant agency with busy agents, and your relationship with the person matters more than the name on the building. Top and upper carry the famous names, mid-size and boutique are where careers actually start. The best agency is the one that gets you.
' +agFaqHTML() +'
' +'

' +'

The subject line is your top credits, or your theatre credits, or a referral from a casting director. Never your own name: your name is already on the email. And put a photo on your Gmail, agents click it.

' +'' +'
' +'

Every agency\'s public client list is in here. Type an actor, their agency comes up.

' +'
' +'

' +'
' +'

Client lists come from each agency\'s own public site. 54 more agencies are being researched, plus US and rest of world lists. Everything you track saves to your account and shows up in Brand and Value.

' +'
'; agRender(); return; } const r=window.RESOURCES && window.RESOURCES[k]; if(!r){ el.innerHTML=backLink()+'

Couldn’t load this list.

'; return; } el.innerHTML=backLink()+'

'+r.title+'

'+(r.intro||'')+'
'+r.html+'
'; } // ===== Checker ===== const PLATFORMS=ACTOR_PLATFORMS; // Stable ids. These used to be array positions, which meant adding or reordering a // platform silently reassigned everybody's saved ticks. Now every platform carries an // id built from its own name, and old index-based saves are migrated once, below. function platId(n){ return (n||'').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,''); } (function(){ const seen={}; PLATFORMS.forEach((p,i)=>{ let id=platId(p.name)||('p'+i); if(seen[id]) id=id+'-'+i; seen[id]=1; p._id=id; p._ix=i; }); })(); (function clearFalseFullHouse(){ try{ if(localStorage.getItem('doorsTickFix')==='1') return; const all=PLATFORMS.length; for(let i=0;ibyIx[p._ix]=p._id); for(let i=0;ibyIx[n]).filter(Boolean))); } localStorage.setItem('doorsTickIds','slug'); }catch(e){} })(); const TIERS=[ {key:'create', tag:'CREATE', label:'Open creation', sub:'Anyone can publish. Your work, your channel, your terms.'}, {key:'low', tag:'SUBMIT', label:'Casting sites and self-submission', sub:'Sign up, build a profile, submit yourself. The daily-graft platforms.'}, {key:'part', tag:'VERIFIED', label:'IMDb, databases and badges', sub:'Earn a database page with real credits, or apply, qualify or pay for a badge.'}, {key:'social', tag:'SOCIAL', label:'Social media', sub:'Anyone can join. No gatekeepers, just consistency.'}, {key:'gate', tag:'GATEKEPT', label:'Gatekept', sub:'Apply, get vetted or credentialed: the directories and unions of the profession.'}, {key:'develop',tag:'DEVELOP', label:'Schools, schemes and development', sub:'Apply to a scheme or school. Training routes that open doors.'}, {key:'invite', tag:'INVITE', label:'Invitation and recognition', sub:'You are chosen, you cannot apply. The long-game column.'}, ]; const INVITE_GROUPS=['Screen & stage awards','Film festivals']; // Weights. Festivals and awards used to be worth the same as a union card, and there are 39 of // them, so ten festival ticks outscored a whole working career. The gatekept things an actor is // actually judged on now carry the weight. const GRADE_W={gate:4,develop:2.5,invite:1.5,part:1.5,social:2,create:2,low:1.5}; // Casting site listings are directory entries, not an audience you built. const CASTING_SITE=/^(backstage|mandy|starnow|actors access|casting networks)$/i; function isAudience(p){ return (p.tier==='social'||p.tier==='create'||p.tier==='low') && !CASTING_SITE.test(p.name); } // Grades are a percentage of what is actually reachable, so A++ can never become impossible // arithmetic again when the list grows. // Festivals and awards are 39 of the 72 entries. Nobody is at every festival, so counting all of // them would make the ceiling imaginary and every real actor look like a failure. They are worth // real credit up to a point, and that point is INVITE_CAP. const INVITE_CAP=9; function scoreOf(has){ let e=0,a=0,inv=0; PLATFORMS.forEach(p=>{ if(!has(p)) return; const w=GRADE_W[p.tier]||0; if(p.tier==='invite'){ inv+=w; return; } if(isAudience(p)) a+=w; else e+=w; }); return {e:e+Math.min(inv,INVITE_CAP), a:a}; } let GMAX={a:0,e:0}; (function(){ const all=scoreOf(()=>true); GMAX.a=all.a; GMAX.e=all.e; })(); function gradeOf(s,max){ if(!max) return 'F'; const pct=(s/max)*100; return pct>=82?'A++':pct>=66?'A+':pct>=50?'A':pct>=34?'B':pct>=20?'C':pct>=9?'D':pct>0?'E':'F'; } function gradeColor(g){ return ({'A++':'#3fae4a','A+':'#4fa02f','A':'#6bbf3a','B':'#e0a81e','C':'#e79a2a','D':'#d63a2f','E':'#b8294a','F':'#7a7268'})[g]||'#7a7268'; } let confirmed={}, notes={}, profile=''; function keyFor(n){ return 'ac:'+(n||'(unnamed)'); } function noteKey(n){ return 'acnotes:'+(n||'(unnamed)'); } function loadTicks(n){ profile=n; confirmed={}; notes={}; let saved=null; try{ saved=localStorage.getItem(keyFor(n)); }catch(e){} if(saved===null && n){ // Start empty. A grade only means something once the person has actually been through it. // The presets below make filling it fast without the site inventing an answer for them. saveTicks(); } else { try{ (JSON.parse(saved||'[]')).forEach(id=>confirmed[id]=true); }catch(e){} } try{ notes=JSON.parse(localStorage.getItem(noteKey(n))||'{}'); }catch(e){ notes={}; } } function saveTicks(){ try{ localStorage.setItem(keyFor(profile), JSON.stringify(Object.keys(confirmed).filter(k=>confirmed[k]))); }catch(e){} } function saveNote(id,v){ v=(v||'').trim(); if(v) notes[id]=v; else delete notes[id]; try{ localStorage.setItem(noteKey(profile), JSON.stringify(notes)); }catch(e){} renderSaved(); } function hasData(n){ try{ if(JSON.parse(localStorage.getItem(keyFor(n))||'[]').length) return true; }catch(e){} try{ if(Object.keys(JSON.parse(localStorage.getItem(noteKey(n))||'{}')).length) return true; }catch(e){} return false; } function listProfiles(){ const names=new Set(); try{ for(let i=0;i{ if(k.indexOf(pre)===0){ const n=k.slice(pre.length); if(n && n!=='(unnamed)') names.add(n); } }); } }catch(e){} return [...names].sort(); } function renderSaved(){ const el=document.getElementById('savedrow'); if(!el) return; el.innerHTML=''; const names=listProfiles().filter(hasData); if(!names.length) return; const lab=document.createElement('span'); lab.className='savedlab'; lab.textContent='Saved on this device:'; el.appendChild(lab); names.forEach(n=>{ const c=document.createElement('span'); c.className='savedchip'+(n===profile?' on':''); const t=document.createElement('span'); t.textContent=n; c.appendChild(t); c.onclick=()=>{ document.getElementById('q').value=n; runCheck(); }; const d=document.createElement('b'); d.className='delp'; d.textContent='×'; d.title='delete this saved profile'; d.onclick=(e)=>{ e.stopPropagation(); if(confirm('Delete saved profile "'+n+'"? This removes the ticks and notes from this device.')){ try{ localStorage.removeItem('ac:'+n); localStorage.removeItem('acnotes:'+n); }catch(err){} if(profile===n){ document.getElementById('q').value=''; runCheck(); return; } renderSaved(); } }; c.appendChild(d); el.appendChild(c); }); } function softAccount(reason){ if(cpSession()) return; let asked=false; try{ asked=!!localStorage.getItem('doorsAskedSave'); }catch(e){} if(asked) return; try{ localStorage.setItem('doorsAskedSave','1'); }catch(e){} const t=document.getElementById('auth-title'); openAuth(null); if(t) t.innerHTML='Keep '+pesc(reason)+'.'; } function tick(id, el){ if(el.checked){ confirmed[id]=true; softAccount('your ticks'); } else delete confirmed[id]; saveTicks(); markDay('presence',5); updateDash(); const card=document.getElementById('plat-'+id); if(card) card.classList.toggle('ticked', !!el.checked); renderSaved(); } function toggleInfo(id){ const el=document.getElementById('pinfo-'+id); if(el) el.classList.toggle('open'); } function updateDash(){ let estS=0, audS=0, count=0; PLATFORMS.forEach(p=>{ if(confirmed[p._id]) count++; }); const sc=scoreOf(p=>!!confirmed[p._id]); estS=sc.e; audS=sc.a; const ge=gradeOf(estS,GMAX.e), ga=gradeOf(audS,GMAX.a); const gEl=document.getElementById('g-est'), aEl=document.getElementById('g-aud'), cEl=document.getElementById('g-count'); gEl.textContent=count?ge:'–'; gEl.style.color=count?gradeColor(ge):'var(--muted)'; aEl.textContent=count?ga:'–'; aEl.style.color=count?gradeColor(ga):'var(--muted)'; cEl.textContent=count; } function platCard(p, name){ const m=notes[p._id]||''; const links=[]; if(name) links.push('search “'+pesc(name)+'” ↗'); if(p.home) links.push(''+pesc(p.home)+' ↗'); if(p.apply) links.push('how to apply ↗'); if(p.info||p.stats) links.push(''); const metaBits=[p.year, p.city, p.desc].filter(Boolean).join(' · '); return '
' +'
'+ (TIERS.filter(t=>t.key===p.tier)[0]||{}).tag +'
' +(metaBits?'
'+metaBits+'
':'') +(links.length?'':'') +((p.info||p.stats)?'
'+pesc(p.info||'')+(p.stats?'
Numbers: '+pesc(p.stats):'')+'
':'') +'' +'
'; } function renderList(name){ // Build one complete block per tier, then paint them one at a time so the first results // appear immediately instead of waiting for all 72. const sections=[]; TIERS.forEach(t=>{ const list=PLATFORMS.filter(p=>p.tier===t.key); if(!list.length) return; let sec='

'+t.label+'

'+t.sub+'

'; if(t.key==='invite'){ INVITE_GROUPS.forEach(g=>{ const gl=list.filter(p=>p.group===g); if(!gl.length) return; sec+='

'+g+'

'+gl.map(p=>platCard(p,name)).join('')+'
'; }); const rest=list.filter(p=>INVITE_GROUPS.indexOf(p.group)===-1); if(rest.length) sec+='
'+rest.map(p=>platCard(p,name)).join('')+'
'; } else { sec+='
'+list.map(p=>platCard(p,name)).join('')+'
'; } sections.push(sec+'
'); }); const host=document.getElementById('platlist'); if(!host) return; const wrap=document.getElementById('chk-progress'), bar=document.getElementById('chk-bar'), msg=document.getElementById('chk-progress-msg'); host.innerHTML=''; if(!sections.length){ if(wrap) wrap.style.display='none'; return; } if(wrap){ wrap.style.display='block'; if(bar) bar.style.width='0%'; } // First block lands immediately so there is something to read straight away, the rest follows // on the next tick. Two steps, not seventy, so a throttled or backgrounded tab still finishes. host.insertAdjacentHTML('beforeend', sections[0]); if(bar) bar.style.width=Math.round((1/sections.length)*100)+'%'; if(msg) msg.textContent='Filling in the rest of your list'; setTimeout(function(){ if(sections.length>1) host.insertAdjacentHTML('beforeend', sections.slice(1).join('')); if(bar) bar.style.width='100%'; if(msg) msg.textContent=''; if(wrap) wrap.style.display='none'; },16); } // The eight places an actor is most often looked up on. Used by the Essentials preset. const CHK_ESSENTIALS=['spotlight-uk','imdb','instagram','equity-uk','vimeo','youtube','backstage','mandy']; function chkPreset(which){ const all=PLATFORMS; confirmed={}; if(which==='everything'){ all.forEach(p=>confirmed[p._id]=true); } else if(which==='essentials'){ all.forEach(p=>{ if(CHK_ESSENTIALS.indexOf(p._id)>=0) confirmed[p._id]=true; }); } // 'custom' clears and lets them build it up themselves saveTicks(); renderList(profile); updateDash(); renderPresets(which); } function renderPresets(active){ const el=document.getElementById('chk-presets'); if(!el) return; const opts=[['essentials','Essentials, the top 8'],['everything','Tick everything'],['custom','Clear it']]; el.innerHTML='Everything is ticked to start with. Untick anything you are not on.' +opts.map(o=>'').join(''); } function runCheck(){ const name=document.getElementById('q').value.trim(); window.__noJump=true; setTimeout(function(){ window.__noJump=false; },0); if(name){ try{ localStorage.setItem('lastChecker',name); }catch(e){} } loadTicks(name); renderList(name); updateDash(); renderSaved(); if(typeof renderPresets==='function') renderPresets(null); try{ const last=localStorage.getItem('lastChecker'); const qEl=document.getElementById('q'); if(last && qEl && !qEl.value){ qEl.value=last; setTimeout(function(){ try{ runCheck(); }catch(e){} },30); } }catch(e){} try{ if(localStorage.getItem('doorsTickWiped')==='1'){ localStorage.removeItem('doorsTickWiped'); const m=document.getElementById('chk-progress-msg'), w=document.getElementById('chk-progress'); if(m&&w){ w.style.display='block'; m.textContent='Your old score was reset. It had every platform ticked, which was our mistake, not yours. Go through it again and the grade will be real.'; } } }catch(e){} } // Card-by-card checker: swipe right if you are on it, left if you are not. let swIdx=0, swX0=null; function swipeStart(){ const q=document.getElementById('q'); if(!q.value.trim()){ q.placeholder='Type your name first, then go card by card'; q.focus(); return; } runCheck(); swIdx=0; document.getElementById('swipewrap').style.display='block'; document.getElementById('platlist').style.display='none'; swipeRender(); } function swipeEnd(){ document.getElementById('swipewrap').style.display='none'; document.getElementById('platlist').style.display=''; renderList(profile); updateDash(); } function swipeRender(){ const wrap=document.getElementById('swipecard'); if(swIdx>=PLATFORMS.length){ let count=0; PLATFORMS.forEach(p=>{ if(confirmed[p._id]) count++; }); wrap.innerHTML='

Done.

'+count+' of '+PLATFORMS.length+' ticked for '+pesc(profile||'you')+'. Your grades are live at the top.

'; return; } const p=PLATFORMS[swIdx]; const on=!!confirmed[p._id]; wrap.innerHTML='
' +''+pesc(p.home||'')+'' +'

'+pesc(p.name)+'

' +'

'+pesc(p.desc||'')+'

' +'Not sure? Check if you are on it ↗' +'
'+(swIdx+1)+' of '+PLATFORMS.length+(on?' · currently ticked':'')+'
' +'
'; const c=document.getElementById('swc'); c.addEventListener('pointerdown',function(e){ if(e.target&&e.target.closest&&e.target.closest('a')){ return; } swX0=e.clientX; c.setPointerCapture(e.pointerId); }); c.addEventListener('pointermove',function(e){ if(swX0===null) return; const x=e.clientX-swX0; c.style.transform='translateX('+x+'px) rotate('+(x/22)+'deg)'; }); c.addEventListener('pointerup',function(e){ if(swX0===null) return; const x=e.clientX-swX0; swX0=null; if(Math.abs(x)>90){ swipeAct(x>0?1:-1); } else { c.style.transition='transform .2s'; c.style.transform='none'; setTimeout(function(){ c.style.transition=''; },220); } }); } function swipeBack(){ if(SESH) return; if(swIdx<=0){ return; } swIdx--; swipeRender(); } function swipeAct(dir){ const p=PLATFORMS[swIdx]; if(!p){ return; } if(dir===1) confirmed[p._id]=true; if(dir===-1) delete confirmed[p._id]; if(dir!==0){ saveTicks(); updateDash(); } const c=document.getElementById('swc'); if(c&&dir!==0){ c.style.transition='transform .22s,opacity .22s'; c.style.transform='translateX('+(dir*130)+'%) rotate('+(dir*14)+'deg)'; c.style.opacity='0'; } setTimeout(function(){ swIdx++; swipeRender(); }, dir===0?0:170); } // Home dashboard widgets: latest castings + your presence function paintHomeWidgets(){ if(window.__justConfirmed){ const ph=document.getElementById('page-home'); if(ph && !document.getElementById('justin')){ const d=document.createElement('div'); d.id='justin'; d.className='swcard'; d.style.cssText='border:2px solid var(--accent);margin:0 0 14px'; d.innerHTML='You are in.' +'

Email confirmed, account live. Now we can get to work. Where do you want to start?

' +'
' +'' +'' +'' +'
'; ph.insertBefore(d, ph.firstChild); } } const hc=document.getElementById('hw-castings'); if(hc){ hc.innerHTML=CASTINGS.slice(0,4).map(c=>'
'+pesc(c.t)+'
'+pesc(c.co)+(c.region?' · '+pesc(c.region):'')+(c.closes?' · Closes '+pesc(c.closes):'')+'
From the theatre
').join(''); } const hq=document.getElementById('hw-quote'); if(hq){ const qd=dailyQuote(); hq.innerHTML='Today’s quote'+(qd ?'

“'+pesc(qd.t)+'”

'+pesc(qd.by)+'

' :'

The daily quotes are on their way. A different one every day, 500 of them eventually.

'); } const hs=document.getElementById('hw-streak'); if(hs){ const s=streakInfo(); const all=daysAll(); const now=new Date(); const mPrefix=now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0'); const monthN=Object.keys(all).filter(k=>k.indexOf(mPrefix)===0).length; const totalN=Object.keys(all).length; let dots=''; for(let i=27;i>=0;i--){ const d=new Date(); d.setDate(d.getDate()-i); const k=dayKey(d); dots+=''; } hs.innerHTML='Your streak' +'
'+s.streak+' day'+(s.streak===1?'':'s')+(s.big?' ★':'')+'
' +'

'+monthN+' day'+(monthN===1?'':'s')+' this month, '+totalN+' overall. They all still count.

' +'
'+dots+'
' +'

'+(s.doneToday?('Today is ticked'+(s.big?', and it is a big day':'')+'. See you tomorrow.'):'Nothing ticked today yet. Five minutes counts: one stretch, one reading, one tick.')+'

' +(s.doneToday?'':'

'); } const hp=document.getElementById('hw-presence'); if(hp){ let name=''; try{ name=localStorage.getItem('lastChecker')||''; }catch(e){} let est='–', aud='–', line='Open the checker, tick what you have, and your grades live here.'; if(name){ let ticks=[]; try{ ticks=JSON.parse(localStorage.getItem('ac:'+name)||'[]'); }catch(e){} if(ticks.length){ const set={}; ticks.forEach(id=>set[id]=1); const sc2=scoreOf(p=>!!set[p._id]); est=gradeOf(sc2.e,GMAX.e); aud=gradeOf(sc2.a,GMAX.a); line=ticks.length+' ticked as '+pesc(name)+'. Grades are live.'; } } hp.innerHTML='
' +'
'+est+'Establishment
' +'
'+aud+'Audience
' +'
'+line+' Checker →
'; } } document.getElementById('q').addEventListener('keydown',e=>{ if(e.key==='Enter') runCheck(); }); document.getElementById('g-total').textContent=PLATFORMS.length; ['pcount2','pcount3'].forEach(id=>{ const el=document.getElementById(id); if(el) el.textContent=PLATFORMS.length; }); // Email list, stored in Supabase (early_list table, insert-only for the public: // nobody can read the list from the website, only the admin in the dashboard). (function(){ const f=document.getElementById('earlyform'); if(!f) return; f.addEventListener('submit', function(e){ e.preventDefault(); const msg=document.getElementById('earlymsg'); const email=f.email.value.trim(); if(!email) return; const btn=f.querySelector('button[type=submit],button:not([type])'); if(btn) btn.disabled=true; msg.textContent='Adding you…'; fetch(SBC_URL+'/rest/v1/early_list',{method:'POST',headers:{...SBC_HEADERS,Prefer:'return=minimal'},body:JSON.stringify({email:email})}) .then(r=>{ if(btn) btn.disabled=false; // 409 means the address is already on the list, which is a success from where they sit. if(r.ok || r.status===409){ msg.textContent=(r.status===409)?'You are already on the list. Nothing more to do.':'You are on the list. No spam, just the warning and the deal.'; f.reset(); return; } return r.text().then(t=>{ if(/duplicate|already exists|unique/i.test(t)){ msg.textContent='You are already on the list. Nothing more to do.'; f.reset(); } else { msg.textContent='That did not save. Try again in a minute.'; } }); }) .catch(()=>{ if(btn) btn.disabled=false; msg.textContent='That did not save. Try again in a minute.'; }); }); })(); // ===== Accounts (Supabase Auth, email + password, confirmation emails ON) ===== // Paste the Stripe Payment Link between the quotes to switch on the founding-member button. // PATCH: put the site one-liner here. One short sentence, what DOORS is. It shows on the landing // page directly under the wordmark, above the question. Leave it empty and the slot stays hidden. const SITE_ONELINER=''; // PAYMENTS. Paste the Stripe Payment Link between the quotes and the founding section appears. // Until then the site says it is free and nothing about paying is shown anywhere. const STRIPE_LINK_LIVE='https://buy.stripe.com/eVq7sMaLp8bebv04Nl7ss0b'; const STRIPE_LINK_YEAR='https://buy.stripe.com/aFafZi8Dh9fiaqWfrZ7ss0c'; const PRICE='4.99'; const TRIAL_DAYS=10; // Sunday: change false to true and the buy buttons appear. That is the whole launch switch. const PAYMENTS_ON=true; function paying(){ return PAYMENTS_ON && !!STRIPE_LINK_LIVE; } // The wall. Members on a trial or a subscription get the site; everyone else gets the door. // The site learns who subscribed from doors_subscribers, which the office keeps in sync with // Stripe by hand until the webhook build lands. Tapping a trial button opens a 48 hour grace // window so nobody sits locked out between paying and the office syncing the list. const PAYWALL_ON=true; const OPEN_PAGES=['home','news','lines','linelearner','terms','privacy','feedback','office','castings','welcomein','hub','casting','agents','voice','classes','resources','books','scripts','theatres','relationships','craft','social','business','community','getagent','galleries','pubtheatres','standup','amateur','watch-b3k9','classics','musicals','cinema']; let subOK=null; function subEmail(){ const s=cpSession(); if(!s) return ''; let em=(s.email||(s.user&&s.user.email)||''); if(!em&&s.access_token){ try{ em=JSON.parse(atob(s.access_token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'))).email||''; }catch(e){} } return String(em).trim().toLowerCase(); } function subGrace(){ try{ const t=Number(localStorage.getItem('doorsWelcomeIn')||0); return t && (Date.now()-t) < 48*3600*1000; }catch(e){ return false; } } function subCheck(done){ const after=done; const fin=function(){ try{ paintSidePromo(); }catch(e){} try{ paintPrice(); }catch(e){} try{ paintFounding(); }catch(e){} try{ paintWelcome(); }catch(e){} try{ paintProfMember(); }catch(e){} if(after) after(); }; done=fin; if(!PAYWALL_ON){ subOK=true; if(done) done(); return; } if(subGrace()){ subOK=true; if(done) done(); return; } const em=subEmail(); if(!em){ subOK=cpSession()?null:false; if(done) done(); return; } doorsFetchAuthed(SBC_URL+'/rest/v1/doors_subscribers?select=email&email=eq.'+encodeURIComponent(em),{}) .then(function(r){ return r.ok?r.json():null; }) .then(function(rows){ subOK=rows===null?null:rows.length>0; if(done) done(); }) .catch(function(){ subOK=null; if(done) done(); }); } document.addEventListener('play',function(e){ try{ document.querySelectorAll('video').forEach(function(v){ if(v!==e.target) v.pause(); }); }catch(err){} },true); function pvLog(slug,ownerId){ try{ if(cpUid()&&ownerId&&cpUid()===ownerId) return; const k='pv:'+slug; if(sessionStorage.getItem(k)) return; sessionStorage.setItem(k,'1'); const ref=(document.referrer||'').toLowerCase(); const src=cpUid()?'member':(ref.indexOf('instagram')>=0?'instagram':(ref&&ref.indexOf('doors.actor')<0?'other':'direct')); fetch(SBC_URL+'/rest/v1/doors_pageviews',{method:'POST',headers:Object.assign({},SBC_HEADERS,{'Content-Type':'application/json','Prefer':'return=minimal'}),body:JSON.stringify({slug:slug,src:src})}).catch(function(){}); }catch(e){} } function paintProfViews(){ const el=document.getElementById('prof-views'); if(!el) return; const p=cpProfile; if(!p||!p.slug){ el.innerHTML=''; return; } const shareUrl='https://doors.actor/#p/'+p.slug; const mini=function(t){ const m=document.getElementById('pv-mini'); if(m) m.textContent=t; }; const shell=function(inner){ return '
'+inner +'
Share your page. That is the whole game.' +'
Put it in your Instagram bio if you are proud of it. Email it to people if you are proud of it. Every visit lands on this counter, and we keep the score for you.
' +'

' +'Open my page

'; }; el.innerHTML=shell('

Counting...

'); doorsFetchAuthed(SBC_URL+'/rest/v1/doors_pageviews?select=src,created_at&slug=eq.'+encodeURIComponent(p.slug)+'&order=created_at.desc&limit=1000',{}) .then(function(r){ return r.ok?r.json():null; }) .then(function(rows){ if(rows===null){ mini(''); el.innerHTML=shell('

The counter is being switched on. Views start landing here the moment it is.

'); return; } const week=Date.now()-7*86400000; const wk=rows.filter(function(r2){ return Date.parse(r2.created_at)>=week; }); const by={}; wk.forEach(function(r2){ by[r2.src]=(by[r2.src]||0)+1; }); const LBL={instagram:'From Instagram',member:'DOORS actors',direct:'Straight to your link',other:'From other sites'}; const max=Math.max.apply(null,[1].concat(Object.keys(by).map(function(k2){ return by[k2]; }))); const bars=Object.keys(by).sort(function(a,b){ return by[b]-by[a]; }).map(function(k2){ return '
'+(LBL[k2]||k2)+'' +'' +''+by[k2]+'
'; }).join(''); mini(wk.length+' view'+(wk.length===1?'':'s')+' this week'); const head=''+wk.length+' view'+(wk.length===1?'':'s')+' this week' +'
'+rows.length+(rows.length>=1000?'+':'')+' since your page went up.
'; el.innerHTML=shell(head+(wk.length?bars:'

Nothing this week yet. Share the link and watch this move.

')); }).catch(function(){}); } function paintProfMember(){ const el=document.getElementById('prof-member'); if(!el) return; const manage='

Manage my membership

' +'
Cancel, change your card, see receipts. Enter your email there and Stripe sends you a sign-in link.
'; if(subOK===true){ el.innerHTML='
You are in, at the founding price.' +'
It stays locked for you while your membership is active. Cancel or manage it any time.
' +manage+'
'; } else { el.innerHTML='
'+manage+'
'; } } function profSaveClose(){ profSave(); let n=0; const iv=setInterval(function(){ n++; const m=document.getElementById('pf-msg'); if(m&&/saved/i.test(m.textContent||'')){ const d=document.getElementById('pd-details'); if(d) d.open=false; clearInterval(iv); } if(n>10) clearInterval(iv); },600); } function ringsNestHTML(){ const seen=seenAll(), watch=watchAll(); const t250=window.TOP250||[]; const seen250=t250.filter(function(m){ return seen['mv|'+m.t+'|'+m.y]; }).length; const wlSeen=watch.filter(function(id){ return seen[id]; }).length; let sk=0; try{ sk=streakInfo().streak||0; }catch(e){} const total=Object.keys(seen).length; const ring=function(r,pct,color){ const c=2*Math.PI*r; return '' +(pct>0?'':''); }; return '' +ring(40,seen250/250,'#3E9B4F') +ring(30,wlSeen/Math.max(1,watch.length),'#C9302E') +ring(20,sk/30,'#1700FF') +''+total+''; } function paintProfWatch(){ const el=document.getElementById('prof-watch'); if(!el) return; const watch=watchAll(), seen=seenAll(); const t250=window.TOP250||[]; const seen250=t250.filter(function(m){ return seen['mv|'+m.t+'|'+m.y]; }).length; const wlSeen=watch.filter(function(id){ return seen[id]; }).length; const need=watch.length-wlSeen; let sk=0; try{ sk=streakInfo().streak||0; }catch(e){} const rg=document.getElementById('pw-rings'); if(rg) rg.innerHTML=ringsNestHTML(); el.innerHTML='
' +'

Green is the Top 250 (seen '+seen250+' of 250), red is your watchlist ('+wlSeen+' of '+watch.length+' seen), blue is your streak ('+sk+' day'+(sk===1?'':'s')+'). The number is everything you have ticked seen.

' +(need>0?'

'+need+' thing'+(need===1?'':'s')+' waiting to be seen.

':'

Nothing waiting. Star films on the Top 250 and shows on Worth seeing.

') +'' +'
'; } function paintWelcome(){ const a=document.getElementById('wc-price'), b=document.getElementById('wc-sub'); if(!a||subOK!==true) return; a.innerHTML='You are in, at the founding price.'; if(b) b.textContent='We are still making this site up, but you are already through the door, and the founding price holds for you for good.'; } function planPick(){ if(subOK===true){ toast('You are in','Your membership is live. Manage it from My profile.','ok'); return; } const em=subEmail(); if(!em){ openAuth((location.hash||'#craft').slice(1)); return; } const withMail=function(u){ return u+'?prefilled_email='+encodeURIComponent(em); }; const o=document.createElement('div'); o.id='planover'; o.style.cssText='position:fixed;inset:0;background:rgba(36,33,69,.5);z-index:95;display:flex;align-items:center;justify-content:center;padding:16px'; o.addEventListener('click',function(ev){ if(ev.target===o) o.remove(); }); o.innerHTML='
' +'' +'

Pick your door.

' +'

Both start with a free 10 day trial. Cancel in two taps.

' +'' +'£'+PRICE+' a month
The founding price, month by month
' +(STRIPE_LINK_YEAR?'' +'£55 a year
A posh latte cheaper than paying monthly
':'') +'
'; document.body.appendChild(o); } function paywallTap(){ try{ localStorage.setItem('doorsReturnTo',((location.hash||'#craft').slice(1)||'craft').replace(/[^a-zA-Z0-9\/~.-]/g,'')); }catch(e){} } function paywallRecheck(){ const m=document.getElementById('pw-msg'); if(m) m.textContent='Checking...'; subCheck(function(){ if(subOK){ renderTab((location.hash||'#home').slice(1)); } else if(m) m.textContent='Not on the list yet. If you subscribed in the last day or two, the team is syncing it, tap the trial button and you are in on the spot.'; }); } function paywallHTML(){ const em=subEmail(); const withMail=function(u){ return em?u+'?prefilled_email='+encodeURIComponent(em):u; }; return '
' +'

This part is for members.

' +'

DOORS is £4.99 a month, or £55 for the year. Every membership starts with 10 days free, cancel in two taps if it is not for you.

' +(em ? '

' +'' +'

' +'

Signed in as '+pesc(em)+'. Use the same email at the checkout and everything connects.

' +'

' +'

' : '

' +'

' +'

Already subscribed? Logging in is all it takes, the wall knows who has paid.

') +'

Shared profile and tape links always work: your page is yours to send anywhere. The wall is for the tools. Something wrong? Tell the team.

' +'
'; } // Three free looks: the first three teaser pages a non-member opens stay open on that device, // the fourth hits the wall. So people can search a casting director and browse an agents list // before deciding, and get cut off when they are hooked. function tasteList(){ try{ return JSON.parse(localStorage.getItem('doorsTaste')||'[]'); }catch(e){ return []; } } function tasteTick(k){ if(k&&k.indexOf('news/')===0) k='news'; window.__curPage=k; if(!(typeof TEASER_PAGES!=='undefined'&&TEASER_PAGES.indexOf(k)>=0)) return; if(!(typeof PAYWALL_ON!=='undefined'&&PAYWALL_ON&&subOK===false)) return; const a=tasteList(); if(a.indexOf(k)<0&&a.length<3){ a.push(k); try{ localStorage.setItem('doorsTaste',JSON.stringify(a)); }catch(e){} } } function pwLocked(){ const base=typeof PAYWALL_ON!=='undefined'&&PAYWALL_ON&&subOK===false; if(!base) return false; const p=window.__curPage||''; const teaser=(typeof TEASER_PAGES!=='undefined'&&TEASER_PAGES.indexOf(p)>=0); return !(teaser&&tasteList().indexOf(p)>=0); } function lockCard(more,noun){ const em=subEmail(); const withMail=function(u){ return em?u+'?prefilled_email='+encodeURIComponent(em):u; }; const tasted=tasteList().length>=3; return '
'+(tasted?'
That was your three free looks.
':'')+''+(more>0?more+' more '+noun+' are behind the door.':'The rest is behind the door.')+'' +'

A free 10 day trial, then £4.99 a month or £55 a year at the founding price.

' +'

' +(em ? '' : '') +'

'; } // The teaser: locked visitors see every list exists, the first three entries in full, // and the honest count of what membership opens. Patch's model, same as the castings titles. const TEASER_PAGES=['casting','agents','voice','classes','resources','books','scripts','theatres','relationships','galleries','pubtheatres','standup','amateur','classics','musicals','cinema']; const TEASER_NOUNS={casting:'casting directors',agents:'agencies',voice:'voiceover agents',classes:'classes and companies',resources:'resources',books:'books',scripts:'scripts',theatres:'theatres',relationships:'entries'}; function teaserize(el,k){ if(!el||!pwLocked()||TEASER_PAGES.indexOf(k)<0) return; const parts=el.innerHTML.split('
=g.swipes) guestShut(k); if(g.clicks>0&&!list.__gg){ list.__gg=true; list.addEventListener('click',function(ev){ if(cpSession()) return; const a=ev.target&&ev.target.closest?ev.target.closest('a[href]'):null; if(!a||!list.contains(a)) return; const u2=guestUsed(); const m2=u2[k]||(u2[k]={s:0,c:0}); if(m2.c>=g.clicks){ ev.preventDefault(); ev.stopPropagation(); guestShut(k); openAuth(k); return; } m2.c++; guestSave(u2); },true); } } window.addEventListener('scroll',function(){ const k=window.__navCur; const g=GUEST_GATES[k]; if(!g||cpSession()) return; const page=document.getElementById('page-'+k); if(!page||!page.classList.contains('active')) return; const list=page.querySelector(g.list); if(!list||list.classList.contains('ggate')) return; const top=list.getBoundingClientRect().top+window.scrollY, vh=Math.max(420,window.innerHeight); const swipes=Math.max(0,Math.floor((window.scrollY-top)/(vh*0.75))+1); const u=guestUsed(); const me=u[k]||(u[k]={s:0,c:0}); if(swipes>me.s){ me.s=swipes; guestSave(u); } if(me.s>=g.swipes) guestShut(k); },{passive:true}); // Reader's front step for people who are not members yet: what are you here for, what it does, the door. const RF_CAN=['Paste the scene, pick your part. It reads everyone else.','It waits for your line, as long as you set, line by line.','Every line on screen while you learn, then just the first word.','Loop it until it sticks.','Real voices, a robot voice, or your own recording of the other parts.','Your scenes and timings on every device.']; function readerFunnel(step,choice){ const el=document.getElementById('rf-root'); if(!el) return; const btn=function(t,c){ return ''; }; const em=subEmail(); const door=em ? '' : ''; if(step===1){ el.innerHTML='

Reader / Run Lines

A scene partner who is always free. What are you here for?

' +btn('I have got a self-tape to do','tape')+btn('I am learning lines for a project','project')+btn('Just looking','look'); return; } const lead=choice==='tape'?'Then you need a reader who is free at eleven at night.':choice==='project'?'Then you need the words in your bones, not on the page.':'Here is what it does.'; el.innerHTML='

Reader / Run Lines

'+lead+'

' +'
    '+RF_CAN.map(function(x){ return '
  • '+x+'
  • '; }).join('')+'
' +'
Members get all of it. A free 10 day trial, then £4.99 a month or £55 a year. Cancel in two taps if it is not for you.' +'

'+door+'

' +'

Back

'; } // Likes and comments on Industry News. Tables news_reactions and news_comments (supabase-news-social.sql). // Until that SQL has run the probe fails and the news page hides the feature on its own. function newsSocial(){ const anonH={apikey:SBC_KEY,Authorization:'Bearer '+SBC_KEY}; const get=function(u){ return (cpSession()?doorsFetchAuthed(u,{}):fetch(u,{headers:anonH})).then(function(r){ return r.ok?r.json():null; }).catch(function(){ return null; }); }; const member=function(){ return !!cpSession()&&!pwLocked(); }; const o={ready:window.__newsSocialReady===true}; if(window.__newsSocialReady==null){ window.__newsSocialReady=false; fetch(SBC_URL+'/rest/v1/news_reactions?select=item_id&limit=1',{headers:anonH}).then(function(r){ window.__newsSocialReady=r.ok; o.ready=r.ok; }).catch(function(){}); } o.load=function(id){ if(!window.__newsSocialReady) return Promise.resolve(null); return Promise.all([get(SBC_URL+'/rest/v1/news_reactions?select=user_id&item_id=eq.'+encodeURIComponent(id)), get(SBC_URL+'/rest/v1/news_comments?select=name,body,created_at&item_id=eq.'+encodeURIComponent(id)+'&order=created_at.asc&limit=200')]) .then(function(rs){ const likes=rs[0]||[], cs=rs[1]||[]; const me=cpUid(); return {likes:likes.length, mine:!!me&&likes.some(function(x){ return x.user_id===me; }), comments:cs, can:member(), why:cpSession()?'Members can like and comment. Start your free 10 day trial to join in.':'Log in to like and comment.'}; }); }; o.toggleLike=function(id){ if(!member()){ if(!cpSession()) openAuth('news'); else planPick(); return Promise.resolve(false); } const me=cpUid(); return doorsFetchAuthed(SBC_URL+'/rest/v1/news_reactions?select=user_id&item_id=eq.'+encodeURIComponent(id)+'&user_id=eq.'+me,{}).then(function(r){ return r.ok?r.json():[]; }).then(function(rows){ if(rows&&rows.length) return doorsFetchAuthed(SBC_URL+'/rest/v1/news_reactions?item_id=eq.'+encodeURIComponent(id)+'&user_id=eq.'+me,{method:'DELETE'}).then(function(r){ return r.ok?'off':false; }); return doorsFetchAuthed(SBC_URL+'/rest/v1/news_reactions',{method:'POST',headers:{Prefer:'return=minimal'},body:JSON.stringify({item_id:+id,user_id:me})}).then(function(r){ return r.ok?'on':false; }); }).catch(function(){ return false; }); }; o.comment=function(id,text){ if(!member()){ if(!cpSession()) openAuth('news'); else planPick(); return Promise.resolve(false); } const name=(cpProfile&&cpProfile.name)||'A member'; return doorsFetchAuthed(SBC_URL+'/rest/v1/news_comments',{method:'POST',headers:{Prefer:'return=minimal'},body:JSON.stringify({item_id:+id,user_id:cpUid(),name:name,body:String(text).slice(0,1200)})}).then(function(r){ if(r.ok){ try{ markDay('research',3); }catch(e){} } return r.ok; }).catch(function(){ return false; }); }; return o; } function paywallGate(k,el){ if(!PAYWALL_ON) return false; if(OPEN_PAGES.indexOf(k)>=0) return false; if(subOK!==false) return false; el.innerHTML=paywallHTML(); return true; } const SITE_UK=''; const STRIPE_LINK=STRIPE_LINK_LIVE; const STRIPE_PORTAL='https://billing.stripe.com/p/login/28EfZi9Hl8begPkcfN7ss00'; let authPending=null, authM='signup'; function cpSession(){ try{ const s=JSON.parse(localStorage.getItem('cpSession')||'null'); return (s&&s.access_token)?s:null; }catch(e){ return null; } } function cpSaveSession(d){ try{localStorage.setItem('doorsEver','1');}catch(e){} const s={access_token:d.access_token, refresh_token:d.refresh_token||'', expires_at:Math.floor(Date.now()/1000)+(d.expires_in||3600), email:(d.user&&d.user.email)||''}; try{ localStorage.setItem('cpSession',JSON.stringify(s)); }catch(e){} if(!s.email){ fetch(SBC_URL+'/auth/v1/user',{headers:{apikey:SBC_KEY, Authorization:'Bearer '+s.access_token}}).then(r=>r.ok?r.json():null).then(u=>{ if(u&&u.email){ s.email=u.email; try{ localStorage.setItem('cpSession',JSON.stringify(s)); }catch(e){} authPaint(); } }).catch(()=>{}); } authPaint(); } function authPaint(){ const s=cpSession(); const initial=((cpProfile&&cpProfile.name)||((s&&s.email)||'?'))[0].toUpperCase(); const av=(typeof avatarGet==='function')?avatarGet():''; const face=av?'':''; const d=document.getElementById('nav-auth'); if(d){ if(s){ d.className='mainauth avatarbtn'; d.innerHTML=face||pesc(initial); d.title='Your profile'; if(av) d.style.padding='0'; } else { d.className='mainauth'; d.textContent='Log in / Sign up'; d.title=''; d.style.padding=''; } } const m=document.getElementById('nav-auth-m'); if(m){ m.innerHTML=s?(face||pesc(initial)):'+'; m.classList.toggle('in',!!s); m.title=s?'Your profile':'Sign up'; if(s&&av) m.style.padding='0'; } const lo=document.getElementById('snav-logout'); if(lo) lo.style.display=s?'':'none'; const lom=document.getElementById('mm-logout'); if(lom) lom.style.display=s?'':'none'; paintFounding(); } function plannerGo(){ if(!cpSession()){ openAuth(null); const m=document.getElementById('auth-msg'); if(m) m.textContent='Make it reel is for DOORS members. Log in, or create a free account, and it opens.'; return; } const w=window.open('/planner/','_blank','noopener'); if(!w) location.href='/planner/'; } function authNavClick(){ if(cpSession()){ showTab('profile'); return; } openAuth(null); let ever=false; try{ ever=localStorage.getItem('doorsEver')==='1'; }catch(e){} authMode(ever?'login':'signup'); } function openAuth(target){ authPending=target||null; document.getElementById('authwall').style.display='flex'; authMode('signup'); } function closeAuth(){ document.getElementById('authwall').style.display='none'; document.getElementById('auth-msg').textContent=''; } function authMode(m){ authM=m; const em=document.getElementById('auth-email'), pw=document.getElementById('auth-pw'); em.style.display=(m==='reset')?'none':''; pw.placeholder=(m==='reset')?'new password, 8 or more characters':'password, 8 or more characters'; document.getElementById('auth-title').innerHTML=(m==='signup')?'Create your free account.':(m==='reset')?'Set a new password.':'Welcome back.'; document.getElementById('auth-go').textContent=(m==='signup')?'Create account':(m==='reset')?'Save new password':'Log in'; document.getElementById('auth-alt').innerHTML=(m==='reset') ? '' : (m==='signup') ? 'Already got an account? Log in' +' · Forgot your password?' : 'New here? Create a free account' +' · Forgot your password?'; return false; } let lastEmail=''; function authResend(){ const em=document.getElementById('auth-email'); const email=(lastEmail||(em&&em.value)||'').trim(); const msg=document.getElementById('auth-msg'); if(!email){ msg.textContent='Put your email in the box first.'; return false; } msg.textContent='Sending it again...'; fetch(SBC_URL+'/auth/v1/resend',{method:'POST',headers:SBC_HEADERS,body:JSON.stringify({type:'signup',email:email})}) .then(r=>{ msg.textContent = r.ok ? 'Sent again to '+email+'. It can take a minute, and it does hide in spam.' : 'Could not send it again just now. Wait a minute and try once more.'; }) .catch(()=>{ msg.textContent='Could not send it again just now. Wait a minute and try once more.'; }); return false; } function authForgot(){ const email=document.getElementById('auth-email').value.trim(); const msg=document.getElementById('auth-msg'); if(!email||email.indexOf('@')===-1){ msg.textContent='Type your email in the box above, then tap Forgot your password again.'; return false; } msg.textContent='One second…'; fetch(SBC_URL+'/auth/v1/recover?redirect_to='+encodeURIComponent(location.origin),{method:'POST',headers:SBC_HEADERS,body:JSON.stringify({email:email})}) .then(r=>{ if(r.ok) msg.textContent='Reset email sent to '+email+'. Open it, tap the link, and you will be asked for a new password.'; else return r.json().then(d=>{ throw (d.msg||d.error_description||'error'); }); }) .catch(e=>{ const t=String(e).toLowerCase(); msg.textContent=(t.indexOf('rate')>-1||t.indexOf('seconds')>-1)?'Too many tries in a row. Wait a minute and tap it once.':'Could not send the reset email. Check the address and try again.'; }); return false; } let authBusy=false; function authBusySet(on){ authBusy=on; const go=document.getElementById('auth-go'); if(go){ go.disabled=on; go.textContent = on ? 'One moment...' : (authM==='signup'?'Create account':authM==='reset'?'Save new password':'Log in'); } } function authSubmit(){ if(authBusy) return; const email=document.getElementById('auth-email').value.trim(), pw=document.getElementById('auth-pw').value; const msg=document.getElementById('auth-msg'); authBusySet(true); setTimeout(function(){ if(authBusy) authBusySet(false); },12000); if(authM!=='reset'){ if(!email||email.indexOf('@')===-1||pw.length<8){ msg.textContent='An email and a password of 8 or more characters, please.'; return; } msg.textContent='One second…'; } if(authM==='reset'){ if(pw.length<8){ msg.textContent='A new password of 8 or more characters, please.'; return; } const go=document.getElementById('auth-go'); go.disabled=true; msg.textContent='Saving…'; fetch(SBC_URL+'/auth/v1/user',{method:'PUT',headers:authedHeaders(),body:JSON.stringify({password:pw})}) .then(r=>r.json().then(d=>({ok:r.ok,status:r.status,d:d}))) .then(x=>{ if(!x.ok) throw {status:x.status, m:(x.d.msg||x.d.error_description||x.d.message||'unknown error')}; const em=((x.d&&x.d.email)||(cpSession()&&cpSession().email))||''; msg.textContent='Saved. Checking it works…'; const finish=txt=>{ msg.textContent=txt; setTimeout(function(){ go.disabled=false; afterAuth(); },2200); }; if(em){ fetch(SBC_URL+'/auth/v1/token?grant_type=password',{method:'POST',headers:SBC_HEADERS,body:JSON.stringify({email:em,password:pw})}) .then(r=>r.json().then(d=>({ok:r.ok,d:d}))) .then(v=>{ if(v.ok&&v.d.access_token){ cpSaveSession(v.d); finish('Verified. Your new password works and you are logged in as '+em+'.'); } else finish('Saved, and you are logged in. If the next login is fussy, use Forgot your password again.'); }) .catch(()=>finish('Saved, and you are logged in.')); } else finish('Saved, and you are logged in. Use this password from now on.'); }) .catch(e=>{ go.disabled=false; const t=String(e&&e.m||e).toLowerCase(); if(t.indexOf('different')>-1){ msg.textContent='That is the same as your old password. Pick a new one.'; } else if((e&&e.status===401)||t.indexOf('expired')>-1||t.indexOf('invalid')>-1){ msg.textContent='This reset link has expired or been replaced. Important: every new reset email kills the older ones, so tap Forgot your password once, then open only the NEWEST email.'; } else { msg.textContent='Could not save. The server said: '+String(e&&e.m||e)+'. Screenshot this and send it to us.'; } }); return; } if(authM==='signup'){ fetch(SBC_URL+'/auth/v1/signup?redirect_to='+encodeURIComponent(location.origin),{method:'POST',headers:SBC_HEADERS,body:JSON.stringify({email:email,password:pw})}) .then(r=>r.json().then(d=>({ok:r.ok,d:d}))) .then(x=>{ if(!x.ok) throw (x.d.msg||x.d.error_description||x.d.message||'error'); if(x.d.access_token){ cpSaveSession(x.d); authBusySet(false); afterAuth(); return; } // No token means confirmation is on. Try logging them straight in anyway, because // making someone type the same details twice is where people give up. return fetch(SBC_URL+'/auth/v1/token?grant_type=password',{method:'POST',headers:SBC_HEADERS,body:JSON.stringify({email:email,password:pw})}) .then(r2=>r2.json().then(d2=>({ok:r2.ok,d:d2}))) .then(y=>{ if(y.ok&&y.d.access_token){ cpSaveSession(y.d); authBusySet(false); afterAuth(); return; } lastEmail=email; authBusySet(false); msg.innerHTML='Account made. We have emailed a confirmation link to '+pesc(email)+'. Open it, then you are in.' +'
Send it again'; authMode('login'); }); }) .catch(e=>{ authBusySet(false); const t=String(e).toLowerCase(); msg.textContent=(t.indexOf('already')>-1||t.indexOf('registered')>-1)?'That email already has an account. Log in instead.':'That did not work. Check the email address and try again.'; }); } else { fetch(SBC_URL+'/auth/v1/token?grant_type=password',{method:'POST',headers:SBC_HEADERS,body:JSON.stringify({email:email,password:pw})}) .then(r=>r.json().then(d=>({ok:r.ok,d:d}))) .then(x=>{ if(!x.ok||!x.d.access_token) throw (x.d.error_description||x.d.msg||'error'); cpSaveSession(x.d); authBusySet(false); afterAuth(); }) .catch(e=>{ const t=String(e).toLowerCase(); if(t.indexOf('confirm')>-1){ lastEmail=email; msg.innerHTML='Not confirmed yet. We sent a link to '+pesc(email)+'. Open it and you are in.' +'
Send it again' +' · check spam, it hides there'; } else msg.textContent='Wrong email or password. Try again, or create a free account.'; }); } } function afterAuth(){ document.getElementById('auth-pw').value=''; closeAuth(); const t=authPending; authPending=null; ensureProfile(); devPull(); subCheck(function(){ if(t) showTab(t); else renderTab((location.hash||'#home').slice(1)); }); if(t) showTab(t); } subCheck(function(){ const k=(location.hash||'#home').slice(1); if(subOK===false && k.indexOf('p/')!==0) renderTab(k); }); devPull(function(){ try{ paintStreakStrip(); }catch(e){} }); (function(){ // session refresh on load const s=cpSession(); if(s && s.refresh_token && s.expires_at < Math.floor(Date.now()/1000)+120){ fetch(SBC_URL+'/auth/v1/token?grant_type=refresh_token',{method:'POST',headers:SBC_HEADERS,body:JSON.stringify({refresh_token:s.refresh_token})}) .then(r=>r.ok?r.json():null).then(d=>{ if(d&&d.access_token) cpSaveSession(d); else { try{ localStorage.removeItem('cpSession'); }catch(e){} authPaint(); } }).catch(()=>{}); } })(); // ===== Character study: the 36 questions, answered as your character ===== // Adapted from Aron's closeness study. Saved per character on this device. const CHAR_QS=[ 'Given the choice of anyone in the world, whom would your character want as a dinner guest?', 'Would your character like to be famous? In what way?', 'Before making a phone call, does your character ever rehearse what they are going to say? Why?', 'What would be a perfect day for your character?', 'When did your character last sing to themselves? To someone else?', 'If your character could live to 90 and keep either the mind or the body of a 30 year old for the last 60 years, which would they choose?', 'Does your character have a secret hunch about how they will die?', 'Name three things your character and the person they are closest to appear to have in common.', 'For what in their life does your character feel most grateful?', 'If your character could change anything about the way they were raised, what would it be?', 'Tell your character\'s life story in as much detail as possible, in four minutes.', 'If your character could wake up tomorrow having gained one quality or ability, what would it be?', 'If a crystal ball could tell your character the truth about themselves, their life, or the future, what would they want to know?', 'Is there something your character has dreamed of doing for a long time? Why have they not done it?', 'What is the greatest accomplishment of your character\'s life?', 'What does your character value most in a friendship?', 'What is your character\'s most treasured memory?', 'What is your character\'s most terrible memory?', 'If your character knew that in one year they would die suddenly, would they change anything about how they are living? Why?', 'What does friendship mean to your character?', 'What roles do love and affection play in your character\'s life?', 'What five things would the person closest to your character say are their best qualities?', 'How close and warm is your character\'s family? Do they feel their childhood was happier than most people\'s?', 'How does your character feel about their relationship with their mother?', 'Make three true statements starting with the word we, as your character, about them and the person opposite them in the scene.', 'Complete this sentence as your character: I wish I had someone with whom I could share...', 'If someone were going to become your character\'s close friend, what would be important for them to know?', 'What does your character like about the person opposite them, the things they would not normally say out loud?', 'What is an embarrassing moment from your character\'s life?', 'When did your character last cry in front of another person? By themselves?', 'What does your character already like about the person opposite them in the scene?', 'What, if anything, is too serious for your character to joke about?', 'If your character died this evening with no chance to speak to anyone, what would they most regret not having told someone? Why have they not told them yet?', 'Your character\'s home catches fire. After saving their people and pets, they can save one item. What is it? Why?', 'Of all the people in your character\'s family, whose death would they find most disturbing? Why?', 'What is a problem your character is carrying right now, and whose advice do they secretly want on it?', ]; function charAll(){ try{ return JSON.parse(localStorage.getItem('doorsChars')||'{}'); }catch(e){ return {}; } } function charSaveAll(o){ try{ localStorage.setItem('doorsChars',JSON.stringify(o)); }catch(e){} } window.charCur=window.charCur||''; function charCreate(){ const n=document.getElementById('ch-new').value.trim(); if(!n) return; const all=charAll(); if(!all[n]) all[n]={piece:document.getElementById('ch-piece').value.trim(), answers:{}}; charSaveAll(all); window.charCur=n; renderResource('character'); } function charOpen(n){ window.charCur=n; renderResource('character'); } function charAns(i,el){ const all=charAll(); if(!all[window.charCur]) return; all[window.charCur].answers[i]=el.value; charSaveAll(all); try{ markDay('character',5); }catch(e){} const c=document.getElementById('ch-count'); if(c){ const a=all[window.charCur].answers; c.textContent=Object.keys(a).filter(k=>(a[k]||'').trim()).length+' of 36 answered'; } } function charDelete(n){ if(!confirm('Delete '+n+' and every answer? This cannot be undone.')) return; const all=charAll(); delete all[n]; charSaveAll(all); if(window.charCur===n) window.charCur=''; renderResource('character'); } // ===== Tape diary: log every self-tape, cycle its status, learn from the watch-backs ===== const TAPE_STATUSES=['Taped','Sent','Callback','Booked','No reply']; function watchAll(){ try{ return JSON.parse(localStorage.getItem('doorsWatch')||'[]'); }catch(e){ return []; } } function seenAll(){ try{ return JSON.parse(localStorage.getItem('doorsSeen')||'{}'); }catch(e){ return {}; } } function seenToggle(id,page){ const s=seenAll(); const marking=!s[id]; if(s[id]) delete s[id]; else s[id]=new Date().toISOString().slice(0,10); try{ localStorage.setItem('doorsSeen',JSON.stringify(s)); }catch(e){} try{ devPush(); }catch(e){} const y=window.scrollY; if(page==='profile'){ try{ paintProfWatch(); }catch(e){} } else renderResource(page||'worthseeing'); window.scrollTo(0,y); if(marking) seenUndoChip(id,page); else { const x=document.getElementById('undochip'); if(x) x.remove(); } } function seenUndoChip(id,page){ try{ const old=document.getElementById('undochip'); if(old) old.remove(); if(window.__undoT) clearTimeout(window.__undoT); const w=watchTitle(id); const c=document.createElement('div'); c.id='undochip'; c.style.cssText='position:fixed;left:50%;transform:translateX(-50%);bottom:86px;z-index:96;background:var(--ink);color:#FAFAFA;border-radius:999px;padding:10px 16px;display:flex;gap:12px;align-items:center;font-size:13.5px;box-shadow:0 6px 24px rgba(36,33,69,.35);max-width:92vw'; c.innerHTML='Seen: '+pesc(w.t)+''; document.body.appendChild(c); window.__undoLast={id:id,page:page}; window.__undoT=setTimeout(function(){ const x=document.getElementById('undochip'); if(x) x.remove(); },7000); }catch(e){} } function seenUndo(){ const u=window.__undoLast; if(!u) return; window.__undoLast=null; seenToggle(u.id,u.page); } function watchToggle(id,page){ let w=watchAll(); const i=w.indexOf(id); if(i<0){ w.push(id); toast('On your watchlist','It lives on your page, in My profile. Tick it seen and the ring closes.','ok'); } else w.splice(i,1); try{ localStorage.setItem('doorsWatch',JSON.stringify(w)); }catch(e){} try{ devPush(); }catch(e){} const y=window.scrollY; if(page==='profile'){ try{ paintProfWatch(); }catch(e){} } else renderResource(page||'worthseeing'); window.scrollTo(0,y); } function t250seen(i){ const m=(window.TOP250||[])[i]; if(!m) return; seenToggle('mv|'+m.t+'|'+m.y,'top250'); } function t250watch(i){ const m=(window.TOP250||[])[i]; if(!m) return; watchToggle('mv|'+m.t+'|'+m.y,'top250'); } function cinSeen(i){ const m=(window.__cinR||[])[i]; if(!m) return; seenToggle('mv|'+m.t+'|'+m.y,'cinema'); } function cinWatch(i,up){ const m=((up?window.__cinU:window.__cinR)||[])[i]; if(!m) return; watchToggle('mv|'+m.t+'|'+m.y,'cinema'); } const FAV_KINDS=['Film','Play','Musical','Book','Show','Gallery','Other']; function paintProfFavs(){ const el=document.getElementById('prof-favs'); if(!el) return; const favs=((cpProfile||{}).prefs||{}).favs||[]; let h='

Up to five favourite things: films, plays, books, whatever made you. They show at the top of your public page. Optional, and lovely.

'; for(let i=0;i<5;i++){ const f=favs[i]||{}; h+='
' +'' +'' +'' +'
'; } h+='

'; el.innerHTML=h; } function favsSave(){ const uid=cpUid(), msg=document.getElementById('favs-msg'); if(!uid){ if(msg) msg.textContent='You need to be signed in.'; return; } const favs=[]; for(let i=0;i<5;i++){ const t=(document.getElementById('fav-t-'+i)||{}).value||''; if(!t.trim()) continue; favs.push({t:t.trim(), k:(document.getElementById('fav-k-'+i)||{}).value||'Film', y:((document.getElementById('fav-y-'+i)||{}).value||'').trim()}); } const prefs=Object.assign({},(cpProfile||{}).prefs||{},{favs:favs}); doorsFetchAuthed(SBC_URL+'/rest/v1/doors_profiles?id=eq.'+uid,{method:'PATCH',body:JSON.stringify({prefs:prefs})}) .then(function(r){ if(r.ok){ if(cpProfile) cpProfile.prefs=prefs; if(msg) msg.textContent='Saved.'; const d=document.getElementById('pd-favs'); if(d) setTimeout(function(){ d.open=false; },700); } else if(msg) msg.textContent='That did not save, try again.'; }).catch(function(){ if(msg) msg.textContent='That did not save, try again.'; }); } function favStripHTML(p){ const favs=((p||{}).prefs||{}).favs||[]; if(!favs.length) return ''; return '
My favourites
' +'
' +favs.map(function(f){ return ''+pesc(f.t)+'' +''+pesc(f.k||'')+(f.y?' · '+pesc(f.y):'')+''; }).join('') +'
'; } function cnSeen(g,i,page){ const r=((window.__canonRows||{})[g]||[])[i]; if(!r) return; seenToggle((g==='mu'?'mu|':'pl|')+r.t,page); } function cnRead(g,i,page){ const r=((window.__canonRows||{})[g]||[])[i]; if(!r) return; seenToggle('plr|'+r.t,page); } function cnWatch(g,i,page){ const r=((window.__canonRows||{})[g]||[])[i]; if(!r) return; watchToggle((g==='mu'?'mu|':'pl|')+r.t,page); } function wlSee(i){ const id=(window.__wlNeed||[])[i]; if(!id) return; seenToggle(id,'watchlist'); } function wlUnsee(i){ const id=(window.__wlDone||[])[i]; if(!id) return; seenToggle(id,'watchlist'); } function wlDrop(i){ const id=(window.__wlNeed||[])[i]; if(!id) return; watchToggle(id,'watchlist'); } function profSeen(i){ const id=(window.__profNeed||[])[i]; if(!id) return; seenToggle(id,'profile'); } function profUnwatch(i){ const id=(window.__profNeed||[])[i]; if(!id) return; watchToggle(id,'profile'); } function watchTitle(id){ const p=String(id).split('|'); if(p[0]==='mv') return {t:p[1]+(p[2]?' ('+p[2]+')':''), kind:'Film'}; if(p[0]==='ws') return {t:p.slice(2).join('|')||'A member pick', kind:'Pick'}; if(p[0]==='pl') return {t:p.slice(1).join('|'), kind:'Play'}; if(p[0]==='plr') return {t:p.slice(1).join('|')+' (read)', kind:'Play'}; if(p[0]==='mu') return {t:p.slice(1).join('|'), kind:'Musical'}; return {t:p[1]||String(id), kind:'Theatre'}; } function wsSubmit(){ const t=(document.getElementById('ws-sub-title')||{}).value||'', x=(document.getElementById('ws-sub-text')||{}).value||''; if(!t.trim()){ const m=document.getElementById('ws-sub-msg'); if(m) m.textContent='Give it a name first.'; return; } const r=commSubmit('worthsee',{title:t.trim(),text:x.trim(),as:'first',anon:false,slug:''},'ws-sub-msg'); if(r&&r.then) r.then(function(ok){ if(ok){ const a=document.getElementById('ws-sub-title'), b=document.getElementById('ws-sub-text'); if(a) a.value=''; if(b) b.value=''; } }); } function wsPaintMember(){ const box=document.getElementById('ws-member'); if(!box) return; fetch(SBC_URL+'/rest/v1/doors_submissions?select=id,payload,submitted_name,created_at&kind=eq.worthsee&status=eq.approved&order=created_at.desc',{headers:SBC_HEADERS}) .then(function(r){ return r.ok?r.json():[]; }) .then(function(rows){ if(!rows.length){ box.innerHTML=''; return; } const watch=watchAll(); window.__wsMem=rows; box.innerHTML='

Members say worth seeing

'+rows.map(function(r,i){ const p=r.payload||{}; const id='ws|'+r.id+'|'+(p.title||''); const wt=watch.indexOf(id)>=0; return '
'+pesc(p.title||'Untitled')+'' +' from '+pesc(String(r.submitted_name||'a member').split(' ')[0])+'' +(p.text?'
'+pesc(p.text)+'
':'') +'
' +'
'; }).join(''); }).catch(function(){}); } function wsMemWatch(i){ const r=(window.__wsMem||[])[i]; if(!r) return; const p=r.payload||{}; watchToggle('ws|'+r.id+'|'+(p.title||''),'worthseeing'); } function ringHTML(done,total,color,label){ const pct=total?Math.min(1,done/total):0; const c=2*Math.PI*26; return '
' +'' +(pct>0?'':'') +''+done+'' +''+label+'
'; } let officeTab='overview'; try{ officeTab=localStorage.getItem('doorsOfficeTab')||'overview'; }catch(e){} function moneyFmt(n){ n=Math.round(n*100)/100; return '£'+n.toLocaleString('en-GB',{minimumFractionDigits:2,maximumFractionDigits:2}); } function moneyWhen(d){ try{ return d.toLocaleDateString('en-GB',{day:'numeric',month:'short'})+', '+d.toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit'}); }catch(e){ return ''; } } function moneyGoal(){ try{ return Math.max(1,parseInt(localStorage.getItem('doorsSubGoal')||'20',10)); }catch(e){ return 20; } } function moneyGoalBump(n){ try{ localStorage.setItem('doorsSubGoal',String(Math.max(1,moneyGoal()+n))); }catch(e){} moneyPaintGoal(); } function moneyPaintGoal(){ var g=moneyGoal(), got=window.__moneyNew||0; var num=document.getElementById('mg-num'); if(num) num.textContent=g; var cnt=document.getElementById('mg-count'); if(cnt) cnt.innerHTML=''+got+' of '+g+' new members this month'; var bar=document.getElementById('mg-bar'); if(bar) bar.style.width=Math.min(100,got/g*100)+'%'; } function moneyMemberRows(){ var m=(window.__moneyMembers||[]).slice(), sort=window.__moneySort||'spent', now=Date.now(); if(sort==='spent') m.sort(function(a,b){ return b.spent-a.spent || a.added-b.added; }); else if(sort==='longest') m.sort(function(a,b){ return a.added-b.added; }); else m.sort(function(a,b){ return String(a.name||'').toLowerCase().localeCompare(String(b.name||'').toLowerCase()); }); var total=0, rows=m.map(function(x){ total+=x.spent; var days=Math.max(0,Math.floor((now-x.added)/864e5)); return ''+pesc(x.name)+'
'+pesc(x.email)+'
' +''+pesc(x.plan)+'' +''+days+' days' +''+moneyFmt(x.spent)+''; }).join(''); rows+='Total taken'+moneyFmt(total)+''; return rows; } function moneySort(s){ window.__moneySort=s; var tb=document.getElementById('money-tbody'); if(tb) tb.innerHTML=moneyMemberRows(); var box=document.getElementById('money-sort'); if(box) box.querySelectorAll('.fpill').forEach(function(b){ b.classList.toggle('on', b.getAttribute('data-ms')===s); }); } function moneyShowTable(){ var s=document.getElementById('money-table'); if(!s) return; s.style.display=(s.style.display==='none')?'block':'none'; if(s.style.display==='block'){ try{ s.scrollIntoView({behavior:'smooth',block:'start'}); }catch(e){} } } function officeMoneyHTML(payers, nameByEmail){ nameByEmail=nameByEmail||{}; var MONTH=4.99, YEAR=55.0, now=new Date(); var am=0,ay=0,tm=0,ty=0,comp=0,newThis=0, conv=[], trials=[], members=[]; (payers||[]).forEach(function(s){ var added=s.added_at?new Date(s.added_at):null; if(added && !isNaN(added) && added.getMonth()===now.getMonth() && added.getFullYear()===now.getFullYear()) newThis++; if(s.note==='the office'){ comp++; return; } if(s.status==='active'){ if(s.plan==='yearly') ay++; else am++; if(added && !isNaN(added)) conv.push({e:s.email, plan:s.plan||'monthly', d:new Date(added.getTime()+10*864e5)}); var fc=(added&&!isNaN(added))?added.getTime()+10*864e5:now.getTime(); var charges=1+Math.max(0,Math.floor((now.getTime()-fc)/(30*864e5))); var spent=(s.plan==='yearly')?YEAR:MONTH*charges; members.push({email:s.email, name:(nameByEmail[String(s.email||'').toLowerCase()]||s.email), plan:(s.plan||'monthly'), added:(added&&!isNaN(added)?added.getTime():now.getTime()), spent:spent}); } else if(s.status==='trialing'){ if(s.plan==='yearly') ty++; else tm++; if(added && !isNaN(added)){ var e=new Date(added.getTime()+10*864e5); trials.push({e:s.email, plan:s.plan||'monthly', end:e, days:Math.ceil((e-now)/864e5)}); } } }); window.__moneyNew=newThis; window.__moneyMembers=members; if(!window.__moneySort) window.__moneySort='spent'; var payN=am+ay, trialN=tm+ty; var mrr=am*MONTH+ay*(YEAR/12); var annual=am*MONTH*12+ay*YEAR; var vat=annual/6, exVat=annual-vat; var feeYr=am*12*(MONTH*0.015+0.20)+ay*(YEAR*0.015+0.20); var profit=exVat-feeYr, corp=profit*0.19, keep=profit-corp; var takenGross=am*MONTH+ay*YEAR; // each paying member charged once so far var tVat=takenGross/6, tExVat=takenGross-tVat; var tFee=am*(MONTH*0.015+0.20)+ay*(YEAR*0.015+0.20); var tProfit=tExVat-tFee, tCorp=tProfit*0.19, tKeep=tProfit-tCorp; var goal=moneyGoal(); var line=function(label,val,note,kind){ var minus=(kind==='minus'), keepR=(kind==='keep'); return '
' +''+label+(note?''+note+'':'')+'' +''+(minus?'− ':'')+moneyFmt(val)+'
'; }; var h=''; h+='
' +'
'+payN+'Paying now
' +'
'+trialN+'On the 10 day trial
' +'
'+moneyFmt(takenGross)+'Taken so far ▾
' +'
'+moneyFmt(mrr)+'In per month
' +'
'; // deductions on the money actually taken so far h+=RH('Money taken so far, and what is yours'); h+='

Your '+payN+' paying members, each charged once so far ('+am+' monthly at £4.99, '+ay+' yearly at £55). Here is what is really yours once the taxman and Stripe take their bit:

'; h+='
' +line('Taken so far',takenGross,'Gross, before any refunds you have given') +line('VAT (20%)',tVat,'The price already includes VAT. This fifth is HMRC’s, not yours','minus') +line('Stripe fees',tFee,'Their cut for taking the cards. UK cards 1.5% + 20p, foreign and Amex a bit more','minus') +line('Corporation tax (19%)',tCorp,'On the profit, set aside for the year end','minus') +line('You keep',tKeep,'roughly, before any other business costs','keep') +'
'; h+='
Where it is heading: '+moneyFmt(annual)+' a year' +'
If all '+payN+' stay, that is the yearly rate ('+moneyFmt(mrr)+' a month), and you would keep about '+moneyFmt(keep)+' of it on the same maths. It climbs as trials convert and new members join.
'; h+=''; h+='
Two honest notes. VAT only comes off if DOORS income sits under Patch Studio, which is VAT registered, so check that with Streets. Stripe fees shown are the UK floor; European and Amex cards cost a little more, so the real fee is slightly higher.
'; // the exact-cash caveat h+='
The exact total you have banked
The office reads your current members, so it shows the live rate above, not every past payment. Cancelled members’ old payments only live in Stripe. For the exact pound figure banked so far, open Stripe balance or all payments. Say the word and I will wire a live running total into this page.
'; // this month goal h+=RH('This month'); h+='
'+newThis+' of '+goal+' new members this month' +'goal '+goal+'
' +'
'; // conversions conv.sort(function(a,b){ return b.d-a.d; }); h+=RH('Trials that became paying'); if(conv.length){ h+='
'+conv.slice(0,12).map(function(c){ return '
'+pesc(c.e)+' ('+c.plan+')'+moneyWhen(c.d)+'
'; }).join('')+'

The moment each trial ended and the first payment went through.

'; } else h+='

No conversions yet. They start as the first trials hit day 10.

'; // trials ending trials.sort(function(a,b){ return a.days-b.days; }); h+=RH('Trials ending soon'); if(trials.length){ h+='
'+trials.map(function(t){ var col=t.days<=1?'#c53030':(t.days<=3?'#b7791f':'var(--muted)'); var when=t.days<0?'ended':(t.days===0?'today':(t.days===1?'tomorrow':'in '+t.days+' days')); return '
'+pesc(t.e)+' ('+t.plan+')'+when+'
'; }).join('')+'

When each 10 day trial ends and the card gets charged.

'; } else h+='

No trials running right now.

'; return h; } function officeEmailsHTML(ev){ ev=ev||[]; if(!ev.length) return '

No email events yet. Once the Resend webhook is live, every send, delivery, open and click lands here, per email, per person.

'; var by={}; ev.forEach(function(e){ var s=e.subject||'(no subject)'; var g=by[s]||(by[s]={subject:s,people:{},first:e.occurred_at||'',last:e.occurred_at||''}); if(e.occurred_at&&e.occurred_atg.last) g.last=e.occurred_at; var r=String(e.recipient||'').toLowerCase(); var p=g.people[r]||(g.people[r]={sent:0,delivered:0,opened:0,clicked:0,bounced:0,firstOpen:null,links:[]}); if(p[e.event]!==undefined) p[e.event]++; if(e.event==='opened'&&e.occurred_at&&(!p.firstOpen||e.occurred_at0; }).length; }; var sent=cnt('sent')||n, deliv=cnt('delivered'), opened=cnt('opened'), clicked=cnt('clicked'), bounced=cnt('bounced'); var pct=function(x){ return sent?Math.round(x/sent*100)+'%':''; }; var rows=ppl.sort(function(a,b){ var A=g.people[a],B=g.people[b]; return (B.clicked-A.clicked)||(B.opened-A.opened)||a.localeCompare(b); }).map(function(r){ var p=g.people[r]; var state=p.bounced?'bounced':(p.clicked?'clicked':(p.opened?'opened '+moneyWhen(new Date(p.firstOpen)):(p.delivered?'delivered':'sent'))); return '
'+pesc(r)+''+state+'
'; }).join(''); return '
'+pesc(g.subject)+''+n+' sent · '+opened+' opened ('+pct(opened)+') · '+clicked+' clicked' +'
'+[['Sent',sent],['Delivered',deliv],['Opened',opened+' ('+pct(opened)+')'],['Clicked',clicked+' ('+pct(clicked)+')'],['Bounced',bounced]].map(function(x){ return '
'+x[1]+''+x[0]+'
'; }).join('')+'
' +'
'+rows+'
'; }).join(''); } function officeSetTab(t){ officeTab=t; try{ localStorage.setItem('doorsOfficeTab',t); }catch(e){} document.querySelectorAll('.offsec').forEach(function(s){ s.style.display = (s.id==='offsec-'+t)?'block':'none'; }); document.querySelectorAll('#offtabs .fpill').forEach(function(b2){ b2.classList.toggle('on', b2.getAttribute('data-offtab')===t); }); } async function officeLoad(){ const b=document.getElementById('office-body'); if(!b) return; if(!cpSession()){ b.innerHTML='

The office is for the admin. Log in first.

'; openAuth(null); return; } try{ const meR=await doorsFetchAuthed(SBC_URL+'/rest/v1/doors_profiles?select=is_admin&id=eq.'+cpUid()); const me=meR.ok?(await meR.json())[0]:null; if(!me||!me.is_admin){ b.innerHTML='

This page belongs to the admin. Nothing here for anyone else.

'; return; } const [profR,castR,subR,tapeR,accR,payR,devR,emR]=await Promise.all([ doorsFetchAuthed(SBC_URL+'/rest/v1/doors_profiles?select=*&order=updated_at.desc&limit=200'), doorsFetchAuthed(SBC_URL+'/rest/v1/castings?select=*&order=created_at.desc&limit=100'), doorsFetchAuthed(SBC_URL+'/rest/v1/doors_submissions?select=*&order=created_at.desc&limit=100'), doorsFetchAuthed(SBC_URL+'/rest/v1/doors_tapediary?select=user_id,data'), doorsFetchAuthed(SBC_URL+'/rest/v1/rpc/doors_all_accounts',{method:'POST',body:'{}'}), doorsFetchAuthed(SBC_URL+'/rest/v1/doors_subscribers?select=*'), doorsFetchAuthed(SBC_URL+'/rest/v1/doors_device?select=id,data,updated_at'), doorsFetchAuthed(SBC_URL+'/rest/v1/doors_email_events?select=email_id,event,recipient,subject,link,occurred_at&order=occurred_at.desc&limit=5000').catch(function(){ return {ok:false}; }) ]); await igRevLoad(); await ckLoad(); await plLoad(); setTimeout(function(){ if(window.__igNoTable) igRevWarn(true); },50); const profs=profR.ok?await profR.json():[], casts=castR.ok?await castR.json():[], subs=subR.ok?await subR.json():[], tapes=tapeR.ok?await tapeR.json():[], accounts=accR.ok?await accR.json():[], payers=payR.ok?await payR.json():[]; const emailById={}; accounts.forEach(function(a){ if(a&&a.id) emailById[a.id]=String(a.email||'').toLowerCase(); }); const nameByEmail={}; profs.forEach(function(pr){ var em=emailById[pr.id]; if(em&&pr.name) nameByEmail[em]=pr.name; }); const subByEmail={}; payers.forEach(function(s){ subByEmail[String(s.email||'').toLowerCase()]=s; }); const devices=devR.ok?await devR.json():[]; const emails=(emR&&emR.ok)?await emR.json():[]; const deviceById={}; devices.forEach(function(dv){ deviceById[dv.id]=dv; }); const tapeBy={}; tapes.forEach(t=>{ tapeBy[t.user_id]=((t.data||{}).tapes||[]).length; }); const pend=subs.filter(x=>x.status==='pending'); const offSec=function(id,inner){ return ''; }; const T=[['overview','Overview'],['money','Money'],['emails','Emails'],['review','Waiting for review'+(pend.length?' ('+pend.length+')':'')],['castings','Castings'],['checklist','Checklist'],['instagram','Instagram'],['members','Members'],['accounts','Every account']]; b.innerHTML= '
' +T.map(function(t){ return ''; }).join('') +'' +'
' +offSec('overview', '
' +[['Accounts',accounts.length||'?'],['Profiles built',profs.length],['Castings',casts.length],['Waiting for review',pend.length],['Tape diaries synced',tapes.length]] .map(x=>'
'+x[1]+''+x[0]+'
').join('') +'
' +'

Traffic · Resend · Email logs · Database · Hosting · Payments · Trials · All subscriptions

' +'

This page was last updated '+pesc(document.lastModified)+'. If that is old, the device is running a cached copy: close the tab completely and reopen doors.actor.

') +offSec('money', officeMoneyHTML(payers, nameByEmail)) +offSec('emails', RH('Emails, who opened what')+officeEmailsHTML(emails)) +offSec('review','' +RH('Waiting for review') +(pend.length?pend.map(officeSubCard).join(''):'

Nothing waiting. This fills up once Add something goes live.

')) +offSec('castings','' +RH('Community castings') +(function(){ const open=casts.filter(function(c){ return !c.verified; }), done=casts.filter(function(c){ return c.verified; }); return (open.length?open.map(officeCastCard).join(''):'

Nothing waiting. New posts land here with a verify button.

') +(done.length?'
Archive, verified and live on the site ('+done.length+')' +done.map(function(c){ return '
'+pesc(c.title||'Untitled')+'' +'
' +'
'; }).join('')+'
':''); })()) +offSec('checklist','' +RH('The checklist') +'
' +'
' +'
' +'

' +'
' +'
' +'

' +'
' +'') +offSec('instagram','' +RH('Instagram accounts to check') +'

The old directory held '+Object.values(window.CD_DATA||{}).filter(function(c){ return c.ig; }).length+' handles and mixed office accounts with private personal ones, so they all came off the site. Open one, look at it, and put it back if it is a public account the office runs. Approvals go live for everyone straight away.

' +'' +'

' +'
' +'
' +'

') +(window.__offMem={profs:profs,tapeBy:tapeBy,emailById:emailById,subByEmail:subByEmail, accById:(function(){ const m={}; accounts.forEach(function(a){ if(a&&a.id) m[a.id]=a; }); return m; })(), tapesById:(function(){ const m={}; tapes.forEach(function(t){ m[t.user_id]=t.data||{}; }); return m; })(), subsByUid:(function(){ const m={}; subs.forEach(function(x){ if(x.submitted_by) m[x.submitted_by]=(m[x.submitted_by]||0)+1; }); return m; })(), deviceById:deviceById},'') +offSec('members','' +RH('Comps') +'

Free memberships only. Paying members arrive on their own through Stripe, nothing to do. Paste an email here, one per line, to comp someone, and the wall opens for them.

' +'' +'

' +'
Loading the list...
' +RH('The members') +'
' +[['all','Everyone'],['trial','On the trial'],['monthly','Monthly'],['yearly','Yearly'],['none','No subscription']].map(function(x){ return ''; }).join('') +'
' +'
Sort:' +[['new','Newest'],['active','Most active'],['tapes','Most tapes'],['tracked','Most tracking'],['posts','Most posts']].map(function(x){ return ''; }).join('') +'
' +'
') +offSec('accounts','' +(accounts.length?RH('Every account') +'

All '+accounts.length+' signups, newest first. No profile yet just means they have not filled it in.

' +accounts.map(a=>{ const hasProf=profs.some(p=>p.id===a.id); const sub=((window.__offMem||{}).subByEmail||{})[String(a.email||'').toLowerCase()]; return '
'+pesc(a.email||'')+(sub?' '+memGhostFlag(sub):'')+'' +'joined '+pesc((a.created_at||'').slice(0,10))+(a.last_sign_in_at?' · seen '+pesc(a.last_sign_in_at.slice(0,10)):'')+'' +''+(hasProf?'Profile built':'No profile yet')+'
'; }).join(''):'')); igRevPaint(); ckPaint(); plPaint(); ckSetTab(ckTab); officeSetTab(officeTab); subsList(); memPaint(); }catch(e){ b.innerHTML='

Could not load the office. Refresh and try again.

'; } } function memSub(p){ const M=window.__offMem||{}; const em=(M.emailById||{})[p.id]; return em?(M.subByEmail||{})[em]:null; } function subCat(s){ if(!s) return 'none'; if((s.status||'')==='trialing') return 'trial'; if((s.plan||'')==='yearly') return 'yearly'; return 'monthly'; } function memCat(p){ return subCat(memSub(p)); } function memFlagHTML(p){ const s=memSub(p); const chip=function(bg,t){ return ''+t+''; }; if(!s) return 'Free account'; let h=''; if(s.note==='the office') h+=chip('#242145','The office'); else if((s.status||'')==='trialing') h+=chip('#4b9fe8','On the trial'); else if((s.plan||'')==='yearly') h+=chip('#1e9e5a','✓ Yearly'); else h+=chip('#1e9e5a','Paying'); if((s.status||'')==='trialing'&&(s.plan||'')==='yearly') h+=chip('#1e9e5a','✓ Yearly'); if(s.discount) h+=chip('#e0a81e',pesc(String(s.discount).slice(0,24))); return h; } function memClose(){ const o=document.getElementById('memover'); if(o) o.remove(); } function memOpen(id,em){ const M=window.__offMem||{}; const p=id?((M.profs||[]).filter(function(x){ return x.id===id; })[0]||null):null; const email=(em||(id?(M.emailById||{})[id]:'')||'').toLowerCase(); const acc=id?((M.accById||{})[id]||{}):(function(){ let f={}; Object.keys(M.accById||{}).forEach(function(k){ if(String(((M.accById||{})[k]||{}).email||'').toLowerCase()===email) f=(M.accById||{})[k]; }); return f; })(); const s=email?(M.subByEmail||{})[email]:null; const uid=id||acc.id||''; const diary=uid?(((M.tapesById||{})[uid]||{}).tapes||[]):[]; const subsN=uid?((M.subsByUid||{})[uid]||0):0; const row=function(l,v){ return v?'
'+l+''+v+'
':''; }; const d=function(x){ return x?String(x).slice(0,10):''; }; memClose(); const o=document.createElement('div'); o.id='memover'; o.style.cssText='position:fixed;inset:0;background:rgba(36,33,69,.45);z-index:90;display:flex;align-items:flex-start;justify-content:center;padding:30px 14px;overflow:auto'; o.addEventListener('click',function(ev){ if(ev.target===o) memClose(); }); o.innerHTML='
' +'' +'
' +(p&&p.avatar_url?'':'
'+pesc(((p&&p.name)||email||'?').slice(0,1).toUpperCase())+'
') +'
'+pesc((p&&p.name)||email||'Unknown')+'
'+(p?memFlagHTML(p):(s?memGhostFlag(s):''))+'
' +'
' +(p&&p.headline?'

'+pesc(p.headline)+'

':'') +'
' +row('Email',pesc(email||acc.email||'')) +row('Joined',d(acc.created_at)) +row('Last seen',d(acc.last_sign_in_at)) +row('Profile updated',p?d(p.updated_at):'') +row('Subscribed since',s?d(s.added_at):'') +row('Plan',s?pesc([s.status,s.plan,s.discount].filter(Boolean).join(' · ')):'') +row('Based',p?pesc(p.based||''):'') +row('Agent',p?pesc(p.agent||''):'') +row('Instagram',p&&p.instagram?'@'+pesc(p.instagram):'') +row('Community posts',subsN?String(subsN):'') +'
' +(p&&p.is_public&&p.slug?'

Open their public page

':'') +'

The tape diary, synced ('+diary.length+')

' +(diary.length?diary.slice(0,8).map(function(t){ return '
'+pesc(t.project||'Untitled')+''+(t.office?' for '+pesc(t.office)+'':'')+''+pesc(t.date||'')+' · '+pesc(t.status||'')+'
'; }).join(''):'

Nothing synced yet.

') +(function(){ const dv=uid?((M.deviceById||{})[uid]||null):null; if(!dv||!dv.data) return '

No practice data synced yet: it flows in as they use the site.

'; let days={}; try{ days=JSON.parse(dv.data.doorsDays||'{}')||{}; }catch(e){} const dates=Object.keys(days).sort(); let streak=0; const today=new Date(); for(let i=0;i<400;i++){ const dt=new Date(today); dt.setDate(today.getDate()-i); const key=dt.toISOString().slice(0,10); if(days[key]){ streak++; } else if(i>0){ break; } } const month=new Date().toISOString().slice(0,7); const thisMonth=dates.filter(function(x){ return x.indexOf(month)===0; }).length; let chars=0; try{ chars=(JSON.parse(dv.data.doorsChars||'[]')||[]).length; }catch(e){} let cds=0; try{ cds=Object.keys(JSON.parse(dv.data.doorsCdTrack||'{}')||{}).length; }catch(e){} let ags=0; try{ ags=Object.keys(JSON.parse(dv.data.doorsAgTrack||'{}')||{}).length; }catch(e){} const ticks=Object.keys(dv.data).filter(function(k){ return k.indexOf('ac:')===0; }).length; const lastAct=dates.length?dates[dates.length-1]:''; return '

The practice, synced

' +'
' +row('Streak',streak?(streak+' day'+(streak===1?'':'s')):'0 days') +row('Active days this month',String(thisMonth)) +row('Last active day',lastAct) +row('Characters built',chars?String(chars):'') +row('Casting directors tracked',cds?String(cds):'') +row('Agencies tracked',ags?String(ags):'') +row('Checker names',ticks?String(ticks):'') +'
' +'

Synced '+pesc(String(dv.updated_at||'').slice(0,16).replace('T',' '))+'. Use it to encourage, that is what it is for.

'; })() +'
'; document.body.appendChild(o); } function memSetTab(t){ window.__memTab=t; document.querySelectorAll('#off-mem-tabs .fpill').forEach(function(b){ b.classList.toggle('on',b.getAttribute('data-mem')===t); }); memPaint(); } function memScore(p,kind){ const M=window.__offMem||{}; const dv=(M.deviceById||{})[p.id]; const data=dv&&dv.data?dv.data:{}; const J=function(k,d){ try{ return JSON.parse(data[k]||d); }catch(e){ return JSON.parse(d); } }; if(kind==='active') return Object.keys(J('doorsDays','{}')||{}).length; if(kind==='tapes') return (((M.tapesById||{})[p.id]||{}).tapes||[]).length; if(kind==='tracked') return Object.keys(J('doorsCdTrack','{}')||{}).length+Object.keys(J('doorsAgTrack','{}')||{}).length+Object.keys(data).filter(function(k){ return k.indexOf('ac:')===0; }).length; if(kind==='posts') return (M.subsByUid||{})[p.id]||0; return 0; } function memSetSort(t){ window.__memSort=t; document.querySelectorAll('[data-memsort]').forEach(function(b){ b.classList.toggle('on',b.getAttribute('data-memsort')===t); }); memPaint(); } function memPaint(){ const M=window.__offMem||{}; const grid=document.getElementById('off-mem-grid'); if(!grid) return; const t=window.__memTab||'all'; let list=(M.profs||[]).filter(function(p){ return t==='all'||memCat(p)===t; }); const srt=window.__memSort||'new'; if(srt!=='new'){ list=list.slice().sort(function(a,b){ return memScore(b,srt)-memScore(a,srt); }); } const covered={}; (M.profs||[]).forEach(function(p){ const em=(M.emailById||{})[p.id]; if(em) covered[em]=1; }); const ghosts=Object.keys(M.subByEmail||{}) .filter(function(em){ return !covered[em]; }) .map(function(em){ return M.subByEmail[em]; }) .filter(function(s){ return (t==='all'||subCat(s)===t) && s.note!=='the office'; }); const gHtml=ghosts.map(function(s){ const flag=(function(){ const p={id:'__ghost'}; window.__ghostSub=s; return memGhostFlag(s); })(); return '
@
'+pesc(s.email)+''+flag+'Paying, no profile built yet
'; }).join(''); const pHtml=list.map(function(p){ return officeMemberCard(p,M.tapeBy||{}); }).join(''); grid.innerHTML=(pHtml+gHtml)||'

Nobody in this tier yet.

'; } function memGhostFlag(s){ const chip=function(bg,t){ return ''+t+''; }; let h=''; if((s.status||'')==='trialing') h+=chip('#4b9fe8','On the trial'); else if((s.plan||'')==='yearly') h+=chip('#1e9e5a','✓ Yearly'); else h+=chip('#1e9e5a','Paying'); if((s.status||'')==='trialing'&&(s.plan||'')==='yearly') h+=chip('#1e9e5a','✓ Yearly'); if(s.discount) h+=chip('#e0a81e',pesc(String(s.discount).slice(0,24))); return h; } function officeMemberCard(p,tapeBy){ const face=p.avatar_url?'':'
'+pesc((p.name||'?').slice(0,1).toUpperCase())+'
'; const line2=[p.based,p.agent].filter(Boolean).join(' · '); const line3=[(tapeBy[p.id]?tapeBy[p.id]+' tapes':''),(p.instagram?'@'+p.instagram:'')].filter(Boolean).join(' · '); return '
'+face+''+pesc(p.name||'No name yet')+''+memFlagHTML(p)+'' +(line2?''+pesc(line2)+'':'') +(line3?''+pesc(line3)+'':'')+'
'; } function officeCastCard(c){ return '
'+pesc(c.title||'Untitled')+''+(c.company?' · '+pesc(c.company):'') +(c.submitted_by?' · from '+pesc(c.submitted_by)+'':'') +'
' +'' +'
'; } function officeSubCard(sb){ return '
'+pesc(sb.kind||'')+' from '+pesc(sb.submitted_name||'a member')+' · '+pesc((sb.created_at||'').slice(0,10)) +'
'+pesc(JSON.stringify(sb.payload||{},null,1).slice(0,500))+'
' +'
' +'
'; } function subsSave(){ const t=(document.getElementById('sub-paste')||{}).value||''; const rows=t.split(/[\n,;]+/).map(function(x){ return x.trim().toLowerCase(); }).filter(function(x){ return x.indexOf('@')>0; }) .map(function(e){ return {email:e}; }); const m=document.getElementById('sub-msg'); if(!rows.length){ if(m) m.textContent='No emails in the box.'; return; } doorsFetchAuthed(SBC_URL+'/rest/v1/doors_subscribers?on_conflict=email',{method:'POST', headers:{Prefer:'resolution=merge-duplicates'},body:JSON.stringify(rows)}) .then(function(r){ if(m) m.textContent=r.ok?(rows.length+' in. The wall opens for them on their next tap.'):'That did not save. Has the subscribers SQL run?'; if(r.ok){ const b=document.getElementById('sub-paste'); if(b) b.value=''; subsList(); } }) .catch(function(){ if(m) m.textContent='That did not save. Try again in a minute.'; }); } function subsList(){ doorsFetchAuthed(SBC_URL+'/rest/v1/doors_subscribers?select=email&order=added_at.desc',{}) .then(function(r){ return r.ok?r.json():[]; }) .then(function(rows){ const el=document.getElementById('sub-list'); if(el) el.textContent=rows.length?(rows.length+' subscribed: '+rows.map(function(x){ return x.email; }).join(', ')):'Nobody on the list yet.'; }) .catch(function(){}); } function officeVerify(id,v){ doorsFetchAuthed(SBC_URL+'/rest/v1/castings?id=eq.'+id,{method:'PATCH',body:JSON.stringify({verified:v})}).then(()=>renderResource('office')); } function officeDeleteCast(id){ if(!confirm('Remove this casting from the site for everyone?')) return; doorsFetchAuthed(SBC_URL+'/rest/v1/castings?id=eq.'+id,{method:'DELETE'}).then(()=>renderResource('office')); } function officeReview(id,st){ const note=st==='rejected'?(prompt('A short note back to them, optional')||''):''; doorsFetchAuthed(SBC_URL+'/rest/v1/doors_submissions?id=eq.'+id,{method:'PATCH',body:JSON.stringify({status:st,reviewer_note:note,reviewed_at:new Date().toISOString()})}).then(()=>renderResource('office')); } function doorsFetchAuthed(url,opts){ const go=()=>fetch(url,Object.assign({},opts||{},{headers:Object.assign({},authedHeaders(),(opts&&opts.headers)||{})})); return go().then(r=>{ if(r.status!==401) return r; return cpRefresh().then(ok=> ok? go() : r); }); } /* ---- the tape diary follows the account: local first, cloud behind it ---- */ let tapesPushTimer=null, tapesPulled=false; function tapesCloudPush(){ const uid=cpUid(); if(!uid) return; clearTimeout(tapesPushTimer); tapesPushTimer=setTimeout(function(){ const at=Date.now(); try{ localStorage.setItem('doorsTapesAt',String(at)); }catch(e){} doorsFetchAuthed(SBC_URL+'/rest/v1/doors_tapediary',{method:'POST', headers:{Prefer:'resolution=merge-duplicates'}, body:JSON.stringify({user_id:uid,data:{tapes:tapesAll(),savedAt:at},updated_at:new Date().toISOString()}) }).catch(function(){}); },800); } function tapesCloudPull(then){ const uid=cpUid(); if(!uid||tapesPulled){ if(then)then(false); return; } tapesPulled=true; doorsFetchAuthed(SBC_URL+'/rest/v1/doors_tapediary?select=data&user_id=eq.'+uid) .then(r=>r.ok?r.json():[]) .then(rows=>{ const rs=rows&&rows[0]&&rows[0].data; let localAt=0; try{ localAt=parseInt(localStorage.getItem('doorsTapesAt')||'0',10); }catch(e){} if(rs&&rs.tapes&&(rs.savedAt||0)>localAt){ try{ localStorage.setItem('doorsTapes',JSON.stringify(rs.tapes)); localStorage.setItem('doorsTapesAt',String(rs.savedAt)); }catch(e){} if(then){ then(true); return; } } if(then) then(false); }).catch(function(){ if(then)then(false); }); } function tapeUploadCount(){ return tapesAll().filter(t=>t.link&&t.link.indexOf('/storage/v1/object/public/tapes/')>=0).length; } const TAPE_SPACE=2*1024*1024*1024; function tapeBytesUsed(){ return tapesAll().reduce(function(n,t){ return n+(t.size||0); },0); } function tapeGB(b){ return (b/1073741824).toFixed(2); } function tapesAll(){ try{ return JSON.parse(localStorage.getItem('doorsTapes')||'[]'); }catch(e){ return []; } } function tapesSave(a){ try{ localStorage.setItem('doorsTapes',JSON.stringify(a)); tapesCloudPush(); if(typeof tapeProfilePush==='function') tapeProfilePush(); }catch(e){} } // Silence was the worst bug in this: you pressed the button and nothing moved, so the site // looked broken. Every refusal now shows a card you have to look at. let toastT=null; function toast(title,body,kind){ let el=document.getElementById('doorstoast'); if(!el){ el=document.createElement('div'); el.id='doorstoast'; document.body.appendChild(el); } const col = kind==='ok' ? '#3E9B4F' : kind==='bad' ? '#C9302E' : 'var(--accent)'; el.style.borderColor=col; el.innerHTML=''+pesc(title)+''+pesc(body)+''; el.classList.add('on'); clearTimeout(toastT); if(kind) toastT=setTimeout(function(){ el.classList.remove('on'); },6000); } function tapeUpload(path,f,mb,onp){ return new Promise(function(resolve,reject){ const s=cpSession(); if(!s) return reject(new Error('no session')); const x=new XMLHttpRequest(); x.open('POST',SBC_URL+'/storage/v1/object/tapes/'+path,true); x.setRequestHeader('apikey',SBC_KEY); x.setRequestHeader('Authorization','Bearer '+s.access_token); x.setRequestHeader('Content-Type',f.type||'video/mp4'); x.setRequestHeader('x-upsert','true'); x.timeout=15*60*1000; let last=0; x.upload.onprogress=function(e){ if(!e.lengthComputable) return; const p=Math.round(e.loaded/e.total*100); if(p===last) return; last=p; if(onp) onp(p); }; x.onload=function(){ (x.status>=200&&x.status<300) ? resolve() : reject(x.responseText||('HTTP '+x.status)); }; x.onerror=function(){ reject('network'); }; x.ontimeout=function(){ reject('timeout'); }; x.send(f); }); } function tapeSay(kind,title,body,extra){ const box=document.getElementById('td-say'); if(!box) return; const col = kind==='bad' ? '#C9302E' : kind==='busy' ? 'var(--accent)' : '#3E9B4F'; box.style.display='block'; box.style.borderColor=col; box.innerHTML=''+title+'' +'

'+body+'

' +(extra?'

'+extra+'

':'') +(kind==='busy'?'':'

'); try{ box.scrollIntoView({behavior:'smooth',block:'center'}); }catch(e){} } function tapeSayHide(){ const b=document.getElementById('td-say'); if(b) b.style.display='none'; } // More than one casting director per tape, because they cast in pairs. A real tappable // dropdown instead of the grey predictive-text lookalike that confused mobile. function tapeCds(){ return window.__tapeCds||(window.__tapeCds=[]); } function cdChipPaint(){ const box=document.getElementById('td-cd-chips'); if(!box) return; box.innerHTML=tapeCds().map(function(n,i){ return ''+pesc(n)+' ×'; }).join(''); } function cdChipRemove(i){ tapeCds().splice(i,1); cdChipPaint(); } function cdChipAdd(n){ n=String(n||'').trim(); if(!n) return; if(tapeCds().indexOf(n)<0) tapeCds().push(n); const inp=document.getElementById('td-office'); if(inp) inp.value=''; const d=document.getElementById('td-cd-drop'); if(d) d.style.display='none'; cdChipPaint(); } function cdSuggestPaint(q){ const d=document.getElementById('td-cd-drop'); if(!d) return; q=String(q||'').trim().toLowerCase(); if(q.length<2){ d.style.display='none'; return; } const names=Object.values(window.CD_DATA||{}).map(function(c){ return c.name; }) .filter(function(n){ return n.toLowerCase().indexOf(q)>=0; }).slice(0,6); if(!names.length){ d.style.display='none'; return; } d.style.display='block'; d.innerHTML=names.map(function(n){ return ''; }).join('') +''; } function tapeFilePicked(inp){ let x=document.getElementById('td-file-clear'); if(inp.files&&inp.files.length){ if(!x){ x=document.createElement('button'); x.id='td-file-clear'; x.className='infobtn'; x.textContent='remove the file'; x.onclick=function(){ const f=document.getElementById('td-file'); if(f) f.value=''; x.remove(); }; inp.insertAdjacentElement('afterend',x); } } else if(x) x.remove(); } function tapeAdd(){ const fEl=document.getElementById('td-file'); const f=fEl&&fEl.files&&fEl.files[0]; const msg=document.getElementById('td-msg'); if(!f){ tapeAddFinal(''); return; } const uid=cpUid(); if(!uid){ tapeSay('bad','You need to be logged in','Uploading a tape saves it to your account, so you need to be signed in. The link box below works without an account if you would rather paste a link.'); return; } if(tapeUploadCount()>=10){ tapeSay('bad','That is your ten','Ten uploaded tapes is the limit. Six strong ones is the sweet spot, so swap out one you have outgrown.','Pasting a link instead is unlimited.'); return; } if(f.size>300*1024*1024){ tapeSay('busy','Big tape, '+Math.round(f.size/1048576)+'MB. Uploading it anyway', 'Worth knowing: a casting director on rehearsal room wifi waits ages for a file this size, and 1080p instead of 4K makes the same tape a third of it. It is going up regardless.', 'On an iPhone: Settings, then Camera, then Record Video, then 1080p at 30fps.'); } if(tapeBytesUsed()+f.size>TAPE_SPACE){ tapeSay('bad','No room left','You have 2GB of space and you are using '+tapeGB(tapeBytesUsed())+'GB of it. Remove a tape you have outgrown and this one will fit.','Filming in 1080p rather than 4K takes about a third of the room.'); return; } if(f.size>150*1024*1024){ tapeSay('busy','Big file, hold on','That is '+Math.round(f.size/1048576)+'MB so it may take a minute. Leave this page open.'); } const mb=Math.round(f.size/1048576); window.__tapeSize=f.size; const path=uid+'/'+Date.now()+'-'+f.name.toLowerCase().replace(/[^a-z0-9.]+/g,'-').slice(-60); const publicUrl=SBC_URL+'/storage/v1/object/public/tapes/'+path; // The diary line lands straight away and the file goes up behind it, so nobody watches a bar. // Read the name before the form clears itself. const title=((document.getElementById('td-project')||{}).value||'').trim()||'Your tape'; const tid=tapeAddFinal(publicUrl,true); if(!tid) return; toast('Uploading '+title,'0%. It uploads in the background: leave this page, start another, it keeps going.'); tapeUpload(path,f,mb,function(p){ toast('Uploading '+title,p+'%'); }) .then(function(){ const a=tapesAll(), t=a.find(function(x){ return x.id===tid; }); if(t){ t.uploading=0; tapesSave(a); if(window.__navCur==='tapediary') renderResource('tapediary'); } toast(title,'Uploaded successfully','ok'); }) .catch(function(err){ const a=tapesAll(), t=a.find(function(x){ return x.id===tid; }); if(t){ t.uploading='failed'; tapesSave(a); if(window.__navCur==='tapediary') renderResource('tapediary'); } toast(title,'Upload failed, open the tape diary to try again','bad'); try{ console.error('tape',err); }catch(e){} }); } // Which parts of a tape the world gets to read. 1 means keep it private. Amy asked for this. const TAPE_PRIV_FIELDS=[['g','Genre'],['ac','Accent'],['o','Status and feedback'],['n','Notes']]; window.__tapePriv={}; function tdPrivHTML(){ return TAPE_PRIV_FIELDS.map(function(f){ const hid=window.__tapePriv[f[0]]; return ''; }).join(''); } function tdPrivToggle(k){ window.__tapePriv[k]=window.__tapePriv[k]?0:1; const box=document.getElementById('td-priv'); if(box) box.innerHTML=tdPrivHTML(); } function tapeAddFinal(uploadedUrl,stillGoing){ const g=id=>document.getElementById(id).value.trim(); if(!g('td-project')){ tapeSay('bad','It needs a project name','Just the show or the job, so future you can find it. Everything else is optional.'); return 0; } const a=tapesAll(); if(window.__editTape){ const t=a.find(function(x){ return x.id===window.__editTape; }); if(t){ t.date=g('td-date')||t.date; t.deadline=g('td-deadline'); t.project=g('td-project'); t.role=g('td-role'); t.office=g('td-office'); t.company=g('td-company'); t.genre=g('td-genre'); t.accent=g('td-accent'); t.outcome=g('td-outcome'); t.material=g('td-material'); t.notes=g('td-notes'); t.priv=Object.assign({},window.__tapePriv); if(uploadedUrl){ t.link=uploadedUrl; t.size=window.__tapeSize||t.size; if(stillGoing) t.uploading=1; } window.__editTape=null; window.__tapePriv={}; tapesSave(a); renderResource('tapediary'); setTimeout(function(){ tapeSay('ok','Saved','The changes are in.'); },60); return t.id; } window.__editTape=null; } const nid=Date.now(); a.unshift({ id:nid, date:g('td-date')||new Date().toISOString().slice(0,10), deadline:g('td-deadline')||'', project:g('td-project'), role:g('td-role'), office:(function(){ const typed=g('td-office'); const all=tapeCds().slice(); if(typed&&all.indexOf(typed)<0) all.push(typed); return all.join(', '); })(), company:g('td-company'), genre:g('td-genre'), accent:g('td-accent'), outcome:g('td-outcome'), link:uploadedUrl||g('td-link'), material:g('td-material'), notes:g('td-notes'), priv:Object.assign({},window.__tapePriv), status:'Taped', size:(uploadedUrl?(window.__tapeSize||0):0), uploading:(stillGoing?1:0) }); window.__tapeCds=[]; window.__tapePriv={}; tapesSave(a); renderResource('tapediary'); markDay('tape',10); if(!stillGoing) setTimeout(function(){ tapeSay('ok','Logged','It is in your diary.'); },60); return nid; } // Small until you play it, then it takes the room it deserves. function tapePlay(id){ const v=document.getElementById('tv-'+id); if(v){ tapeBig(id); v.play(); } } function tapeBig(id){ document.querySelectorAll('video[id^="tv-"]').forEach(function(v){ if(v.id!=='tv-'+id) v.pause(); }); const w=document.getElementById('tw-'+id); if(w) w.classList.add('playing'); } // Logged tapes are never locked. Edit fills the form, save updates in place, and a tape // logged without footage can have the file added later. function tapeEdit(id){ const t=tapesAll().find(function(x){ return x.id===id; }); if(!t) return; window.__editTape=id; const set=function(k,v){ const el=document.getElementById(k); if(el) el.value=v||''; }; set('td-date',t.date); set('td-deadline',t.deadline); set('td-project',t.project); set('td-role',t.role); window.__tapeCds=(t.office?String(t.office).split(/,\s*/).filter(Boolean):[]); cdChipPaint(); set('td-office',''); set('td-company',t.company); set('td-genre',t.genre); set('td-accent',t.accent); set('td-outcome',t.outcome); set('td-material',t.material); set('td-notes',t.notes); window.__tapePriv=Object.assign({},t.priv||{}); const pv=document.getElementById('td-priv'); if(pv) pv.innerHTML=tdPrivHTML(); const btn=document.querySelector('#page-tapediary .bigbtn.blue'); if(btn) btn.textContent='Save the changes'; tapeSay('busy','Editing '+(t.project||'this tape'),'Change anything above, then press Save the changes.'); window.scrollTo({top:0,behavior:'smooth'}); } function tapeAttach(id,input){ const f=input.files&&input.files[0]; if(!f) return; const t=tapesAll().find(function(x){ return x.id===id; }); if(!t) return; if(f.size>200*1024*1024){ tapeSay('bad','This tape is too big','It is '+Math.round(f.size/1048576)+'MB and the limit is 200MB. Film in 1080p rather than 4K and it lands a third of the size.'); return; } const uid=cpUid(); if(!uid){ tapeSay('bad','You need to be logged in','Log in and try again.'); return; } const path=uid+'/'+Date.now()+'-'+f.name.toLowerCase().replace(/[^a-z0-9.]+/g,'-').slice(-60); const publicUrl=SBC_URL+'/storage/v1/object/public/tapes/'+path; const a=tapesAll(), tt=a.find(function(x){ return x.id===id; }); tt.link=publicUrl; tt.size=f.size; tt.uploading=1; tapesSave(a); renderResource('tapediary'); toast('Uploading '+(tt.project||'your tape'),'0%'); tapeUpload(path,f,Math.round(f.size/1048576),function(p){ toast('Uploading '+(tt.project||'your tape'),p+'%'); }) .then(function(){ const b=tapesAll(), x=b.find(function(y){ return y.id===id; }); if(x){ x.uploading=0; tapesSave(b); if(window.__navCur==='tapediary') renderResource('tapediary'); } toast(tt.project||'Your tape','Uploaded successfully','ok'); }) .catch(function(){ const b=tapesAll(), x=b.find(function(y){ return y.id===id; }); if(x){ x.uploading='failed'; tapesSave(b); if(window.__navCur==='tapediary') renderResource('tapediary'); } toast(tt.project||'Your tape','Upload failed, try again','bad'); }); } function tapeDueHTML(t){ if(!t.deadline) return ''; const days=Math.round((new Date(t.deadline+'T12:00')-new Date())/86400000); const say=days>1?('Due in '+days+' days'):days===1?'Due tomorrow':days===0?'Due today':'The deadline was '+t.deadline; return ''+say+''; } function tapeStatus(id){ const a=tapesAll(); const t=a.find(x=>x.id===id); if(!t) return; t.status=TAPE_STATUSES[(TAPE_STATUSES.indexOf(t.status)+1)%TAPE_STATUSES.length]; tapesSave(a); renderResource('tapediary'); } function tapeNote(id,el){ const a=tapesAll(); const t=a.find(x=>x.id===id); if(t){ t.notes=el.value; tapesSave(a); } } function tapeProfileToggle(id){ const a=tapesAll(), t=a.find(function(x){ return x.id===id; }); if(!t) return; const on=!t.onProfile; if(on && a.filter(function(x){ return x.onProfile; }).length>=10){ alert('Ten is the most that go on your shared page. Take one off first.'); return; } t.onProfile=on; tapesSave(a); tapeProfilePush(); renderResource('tapediary'); } // Only the chosen ones get copied onto the profile row, which is the only bit a stranger can read. function tapeProfilePush(){ const uid=cpUid(); if(!uid) return; const chosen=tapesAll().filter(function(t){ return t.onProfile && t.link; }) .slice(0,10).map(function(t){ const pv=t.priv||{}; return {id:t.id, t:t.project||'Self tape', r:t.role||'', u:t.link, co:t.company||'', g:pv.g?'':(t.genre||''), ac:pv.ac?'':(t.accent||''), cd:t.office||'', n:pv.n?'':(t.notes||''), o:pv.o?'':(t.outcome||''), d:t.date||''}; }); doorsFetchAuthed(SBC_URL+'/rest/v1/doors_profiles?on_conflict=id',{method:'POST', headers:{Prefer:'resolution=merge-duplicates,return=representation'}, body:JSON.stringify({id:uid,tapes:chosen,updated_at:new Date().toISOString()})}) .then(function(r){ return r.ok?r.json():null; }) .then(function(rows){ if(rows&&rows[0]){ cpProfile=rows[0]; profCacheSet(cpProfile); } }).catch(function(){}); } function tapeDelete(id){ if(!confirm('Delete this tape entry? The tape itself lives wherever you saved it, this only removes the diary line.')) return; tapesSave(tapesAll().filter(x=>x.id!==id)); renderResource('tapediary'); } function tapeStatusColor(s){ return {'Taped':'var(--tint-lav)','Sent':'var(--tint-cream)','Callback':'var(--tint-mint)','Booked':'var(--accent)','No reply':'#f0f0f6'}[s]||'#f0f0f6'; } // ===== Profiles: Duolingo-style onboarding, profile page, DOORS CV ===== let cpProfile=null; function cpUid(){ const s=cpSession(); if(!s) return null; try{ const p=JSON.parse(atob(s.access_token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'))); return p.sub||null; }catch(e){ return null; } } function authedHeaders(){ const s=cpSession(); return {apikey:SBC_KEY, Authorization:'Bearer '+(s?s.access_token:SBC_KEY), 'Content-Type':'application/json'}; } // ---- Profile picture. Stored on this device as a small square jpeg. ---- function avatarGet(){ try{ return (cpProfile&&cpProfile.avatar_url)||localStorage.getItem('doorsAvatar')||''; }catch(e){ return ''; } } function avatarSet(d){ try{ d?localStorage.setItem('doorsAvatar',d):localStorage.removeItem('doorsAvatar'); }catch(e){} authPaint(); } // The crop used to happen to you: dead centre, no say in it, and a headshot with the face // off-centre came out wrong. Now you choose it. Drag to move, pinch or slide to zoom. let cropImg=null, cropZoom=1, cropX=0, cropY=0, cropDrag=null; let cropMode='avatar'; // Keep a copy of what they actually chose, shrunk, so the crop can be changed later without // asking them to find the photo again. function avatarKeepOriginal(img){ try{ const M=1400, sc=Math.min(1,M/Math.max(img.width,img.height)); const c=document.createElement('canvas'); c.width=Math.round(img.width*sc); c.height=Math.round(img.height*sc); c.getContext('2d').drawImage(img,0,0,c.width,c.height); localStorage.setItem('doorsAvatarOrig',c.toDataURL('image/jpeg',0.82)); }catch(e){} } function avatarPick(input,mode){ const f=input.files&&input.files[0]; if(!f) return; cropMode=mode||'avatar'; const msg=document.getElementById(cropMode==='avatar'?'av-msg':'ph-msg'); if(msg) msg.textContent='Opening...'; const rd=new FileReader(); rd.onload=function(ev){ const img=new Image(); img.onload=function(){ cropImg=img; cropZoom=1; cropX=0; cropY=0; if(cropMode==='avatar') avatarKeepOriginal(img); cropOpen(); if(msg) msg.textContent=''; }; img.onerror=function(){ if(msg) msg.textContent='That file did not open as an image.'; }; img.src=ev.target.result; }; rd.onerror=function(){ if(msg) msg.textContent='Could not read that file.'; }; rd.readAsDataURL(f); } function avatarRecrop(){ let d=null; try{ d=localStorage.getItem('doorsAvatarOrig'); }catch(e){} const src=d||avatarGet(); if(!src){ const m=document.getElementById('av-msg'); if(m) m.textContent='Upload a picture first.'; return; } cropMode='avatar'; const img=new Image(); img.onload=function(){ cropImg=img; cropZoom=1; cropX=0; cropY=0; cropOpen(); }; img.src=src; } function photoRecrop(i){ const list=profPhotos(cpProfile); const u=list[i]; if(!u) return; cropMode='photo'; window.__photoSlot=i; const img=new Image(); img.crossOrigin='anonymous'; img.onload=function(){ cropImg=img; cropZoom=1; cropX=0; cropY=0; cropOpen(); }; img.onerror=function(){ const m=document.getElementById('ph-msg'); if(m) m.textContent='Could not open that photo to re-crop. Remove it and add it again.'; }; img.src=u; } function cropOpen(){ const o=document.getElementById('cropper'); if(!o) return; o.style.display='flex'; const t=document.getElementById('crop-title'), s=document.getElementById('crop-sub'); const shape=document.getElementById('crop-canvas'); if(shape) shape.style.borderRadius = cropMode==='avatar' ? '50%' : '14px'; if(t) t.innerHTML = (cropMode==='avatar'?'Set your picture':'Set this photo')+'.'; if(s) s.textContent = cropMode==='avatar' ? 'Drag it about, and use the slider to zoom. What you see is what people see.' : 'Square, to sit under your profile. Drag and zoom until it is right.'; const r=document.getElementById('crop-range'); if(r) r.value=1; cropPaint(); const cv=document.getElementById('crop-canvas'); const pt=function(e){ const t=e.touches?e.touches[0]:e; return {x:t.clientX,y:t.clientY}; }; cv.onpointerdown=function(e){ cropDrag=pt(e); cv.setPointerCapture&&cv.setPointerCapture(e.pointerId); }; cv.onpointermove=function(e){ if(!cropDrag) return; const p=pt(e); cropX+=(p.x-cropDrag.x); cropY+=(p.y-cropDrag.y); cropDrag=p; cropPaint(); }; cv.onpointerup=cv.onpointercancel=function(){ cropDrag=null; }; } function cropZoomSet(v){ cropZoom=parseFloat(v); cropPaint(); } function cropPaint(){ const cv=document.getElementById('crop-canvas'); if(!cv||!cropImg) return; const S=cv.width, g=cv.getContext('2d'); const base=Math.max(S/cropImg.width,S/cropImg.height); const sc=base*cropZoom; const w=cropImg.width*sc, h=cropImg.height*sc; // Never let the picture come away from the edges of the circle. const maxX=Math.max(0,(w-S)/2), maxY=Math.max(0,(h-S)/2); cropX=Math.max(-maxX,Math.min(maxX,cropX)); cropY=Math.max(-maxY,Math.min(maxY,cropY)); g.clearRect(0,0,S,S); g.fillStyle='#242145'; g.fillRect(0,0,S,S); g.drawImage(cropImg,(S-w)/2+cropX,(S-h)/2+cropY,w,h); } function cropCancel(){ const o=document.getElementById('cropper'); if(o) o.style.display='none'; cropImg=null; } function cropUse(){ const cv=document.getElementById('crop-canvas'); if(!cv) return; if(cropMode==='photo'){ cv.toBlob(function(blob){ if(blob) profPhotoUpload(blob, window.__photoSlot); }, 'image/jpeg', 0.86); cropCancel(); return; } const msg=document.getElementById('av-msg'); let out; try{ out=cv.toDataURL('image/jpeg',0.85); }catch(e){ if(msg) msg.textContent='That image could not be read. Try one from your camera roll.'; return; } if(out.length>900000){ out=cv.toDataURL('image/jpeg',0.62); } avatarSet(out); cropCancel(); renderTab('profile'); const m2=document.getElementById('av-msg'); if(m2) m2.textContent='Saved here. Uploading to your account...'; cv.toBlob(function(blob){ if(blob) avatarUpload(blob); }, 'image/jpeg', 0.85); } // Put the headshot in the account so it follows you to any device. function avatarUpload(blob){ const uid=cpUid(); const say=t=>{ const m=document.getElementById('av-msg'); if(m) m.textContent=t; }; if(!uid){ say('Saved on this device. Log in and it will follow you everywhere.'); return; } const path=uid+'/headshot.jpg'; fetch(SBC_URL+'/storage/v1/object/headshots/'+path+'?upsert=true',{ method:'POST', headers:{apikey:SBC_KEY, Authorization:'Bearer '+(cpSession()||{}).access_token, 'Content-Type':'image/jpeg', 'x-upsert':'true'}, body:blob }) .then(r=>{ if(!r.ok) return r.text().then(t=>Promise.reject(t)); const url=SBC_URL+'/storage/v1/object/public/headshots/'+path+'?t='+Date.now(); return doorsFetchAuthed(SBC_URL+'/rest/v1/doors_profiles?on_conflict=id',{method:'POST',headers:{Prefer:'resolution=merge-duplicates,return=representation'},body:JSON.stringify({id:uid,avatar_url:url,updated_at:new Date().toISOString()})}) .then(r2=>r2.ok?r2.json():r2.text().then(t=>Promise.reject(t))) .then(rows=>{ cpProfile=(rows&&rows[0])||{...(cpProfile||{}),avatar_url:url}; profCacheSet(cpProfile); say('Saved to your account. It will be there on any device.'); }); }) .catch(err=>{ const t=String(err||''); if(/Bucket not found|404|column|PGRST204|schema cache/i.test(t)) say('Saved on this device for now. Syncing to your account is not working this minute, the team has been told automatically. Nothing you typed is lost.'); else say('Saved on this device. The upload did not go through: '+t.slice(0,90)); }); } function avatarClear(){ avatarSet(''); const uid=cpUid(); if(uid){ doorsFetchAuthed(SBC_URL+'/rest/v1/doors_profiles?id=eq.'+uid,{method:'PATCH',body:JSON.stringify({avatar_url:null})}).catch(()=>{}); if(cpProfile){ cpProfile.avatar_url=null; profCacheSet(cpProfile); } } renderTab('profile'); } // ---- The profile form. Everything here saves to your account, not just this device. ---- const PROF_FIELDS=[ {k:'name', l:'Your name', ph:'As casting should see it'}, {k:'headline', l:'One line', ph:'Actor, London, playing age 20s'}, {k:'based', l:'Based', ph:'London'}, {k:'agent', l:'Your agent', ph:'Agency, or the agent’s name. Leave empty if you are unrepresented'}, {k:'instagram', l:'Instagram', ph:'@yourhandle'}, {k:'tiktok', l:'TikTok', ph:'@yourhandle'}, {k:'youtube', l:'YouTube', ph:'Link to your channel'}, {k:'spotlight_url', l:'Spotlight link', ph:'Paste your Spotlight page link'}, {k:'vimeo', l:'Vimeo or showreel', ph:'Link to your reel'}, {k:'website', l:'Website or email', ph:'A link, or your email address'} ]; // Amy's additions: reach an actor's agent from their page, and the links agents actually check. const PROF_FIELDS2=[ {k:'agent_email', l:'Your agent\'s email', ph:'So casting can reach them straight from your page'}, {k:'agent_phone', l:'Your agent\'s phone', ph:'Optional'}, {k:'imdb', l:'IMDb link', ph:'Paste your IMDb page link'}, {k:'letterboxd', l:'Letterboxd', ph:'Optional, if film taste is part of your brand'} ]; function skillsAll(){ const s=(cpProfile&&cpProfile.skills); return Array.isArray(s)?s:[]; } window.__skills=null; function skillsCur(){ if(!window.__skills) window.__skills=skillsAll().slice(); return window.__skills; } function skillsPaint(){ const box=document.getElementById('pf-skills-chips'); if(!box) return; const tapes=tapesAll().filter(function(t){ return t.onProfile&&t.link; }); box.innerHTML=skillsCur().map(function(s,i){ return ''+pesc(s.s)+(s.tp?' · '+pesc(s.tp)+'':'') +''; }).join('')||'Nothing yet. Violin, horse riding, Afrikaans, stage combat.'; const sel=document.getElementById('pf-skill-tape'); if(sel) sel.innerHTML=(tapes.length ? ''+tapes.map(function(t){ return ''; }).join('') : ''); } function skillAdd(){ const inp=document.getElementById('pf-skill'); if(!inp||!inp.value.trim()) return; const sel=document.getElementById('pf-skill-tape'); const tid=sel&&sel.value?Number(sel.value):0; const tp=(sel&&sel.value&&sel.options[sel.selectedIndex])?sel.options[sel.selectedIndex].text:''; skillsCur().push({s:inp.value.trim(), tid:tid, tp:tp}); inp.value=''; if(sel) sel.value=''; skillsPaint(); } function skillRemove(i){ skillsCur().splice(i,1); skillsPaint(); } // The private drawer. Measurements, numbers, the things you dig for before a casting. // Its own table, own-row-only. It can never appear on a public page because a public page never asks for it. function jfyLoad(){ const uid=cpUid(); if(!uid) return; doorsFetchAuthed(SBC_URL+'/rest/v1/doors_private?select=data&id=eq.'+uid,{}) .then(function(r){ return r.ok?r.json():[]; }) .then(function(rows){ const d=(rows&&rows[0]&&rows[0].data)||{}; ['meas','sizes','passport','unions','jnotes'].forEach(function(k){ const el=document.getElementById('jfy-'+k); if(el&&!el.value) el.value=d[k]||''; }); }).catch(function(){}); } function jfySave(){ const uid=cpUid(), m=document.getElementById('jfy-msg'); if(!uid) return; const d={}; ['meas','sizes','passport','unions','jnotes'].forEach(function(k){ const el=document.getElementById('jfy-'+k); if(el) d[k]=el.value.trim(); }); if(m) m.textContent='Saving...'; doorsFetchAuthed(SBC_URL+'/rest/v1/doors_private?on_conflict=id',{method:'POST', headers:{Prefer:'resolution=merge-duplicates'}, body:JSON.stringify({id:uid,data:d,updated_at:new Date().toISOString()})}) .then(function(r){ if(m) m.textContent=r.ok?'Saved. Only you can ever see this.':'That did not save. The team may still be switching this on.'; }) .catch(function(){ if(m) m.textContent='That did not save. Try again in a minute.'; }); } function profVal(k){ const p=cpProfile||{}; return p[k]==null?'':String(p[k]); } function profFormHTML(){ return '

Your details

' +PROF_FIELDS.map(f=>'
' +'
').join('') +'
' +'

Who you are, what you are after, anything a casting director would want to know in ten seconds.

' +'
' +'

Project, then who in brackets: director, studio, network or theatre. Names are how the industry reads you.

' +'
Special skills' +'
' +'' +'' +'

' +'

A skill with a tape behind it becomes a link on your page. Proof beats a list.

' +PROF_FIELDS2.map(f=>'
' +'
').join('') +'
' +'

' +'' +'

'; } function profJfyHTML(){ return '

Only you can ever see this. The things you dig around for before a casting, in one drawer. It never appears on your page.

' +'' +'' +'' +'' +'' +'

'; } function profSave(){ const uid=cpUid(), msg=document.getElementById('pf-msg'), btn=document.getElementById('pf-save'); if(!uid){ if(msg) msg.textContent='You need to be signed in.'; return; } const body={id:uid, updated_at:new Date().toISOString()}; PROF_FIELDS.forEach(f=>{ const el=document.getElementById('pf-'+f.k); if(el) body[f.k]=el.value.trim(); }); const bioEl=document.getElementById('pf-bio'); if(bioEl) body.bio=bioEl.value.trim(); const sp=document.getElementById('pf-spotlight'); if(sp) body.spotlight=sp.checked; if(body.instagram) body.instagram=body.instagram.replace(/^@+/,'').replace(/^https?:\/\/(www\.)?instagram\.com\//i,'').replace(/\/+$/,''); if(!body.name){ if(msg) msg.textContent='Your name, at least.'; return; } const extra={id:uid, updated_at:new Date().toISOString()}; PROF_FIELDS2.forEach(f=>{ const el=document.getElementById('pf-'+f.k); if(el) extra[f.k]=el.value.trim(); }); const crEl=document.getElementById('pf-credits'); if(crEl) extra.credits=crEl.value.trim(); extra.skills=skillsCur(); if(btn) btn.disabled=true; if(msg) msg.textContent='Saving…'; doorsFetchAuthed(SBC_URL+'/rest/v1/doors_profiles?on_conflict=id',{method:'POST',headers:{Prefer:'resolution=merge-duplicates,return=representation'},body:JSON.stringify(body)}) .then(r=>r.ok?r.json():r.text().then(t=>Promise.reject(t))) .then(rows=>{ cpProfile=(rows&&rows[0])||{...(cpProfile||{}),...body}; doorsFetchAuthed(SBC_URL+'/rest/v1/doors_profiles?on_conflict=id',{method:'POST',headers:{Prefer:'resolution=merge-duplicates,return=representation'},body:JSON.stringify(extra)}) .then(function(r){ if(r.ok) return r.json(); if(msg) msg.textContent='Saved, except credits, skills and agent details: the team is still switching those on. Nothing you typed is lost, save again later.'; return null; }) .then(function(rw){ if(rw&&rw[0]){ cpProfile=rw[0]; profCacheSet(cpProfile); window.__skills=null; } }).catch(function(){}); profCacheSet(cpProfile); obMarkDone(); authPaint(); if(btn) btn.disabled=false; if(msg) msg.textContent='Saved to your account. It will be here on any device you log in on.'; }) .catch(err=>{ if(btn) btn.disabled=false; const t=String(err||''); if(/column .* does not exist|schema cache|PGRST204/i.test(t)){ if(msg) msg.textContent='Some of the newest boxes are not switched on yet. The team is on it, everything you typed is still here, save again in a bit.'; } else if(/JWT|401|expired/i.test(t)){ if(msg) msg.textContent='Your login went stale. Log in again and it will save.'; } else if(msg) msg.textContent='That did not save: '+t.slice(0,120); }); } function avatarBlockHTML(){ const a=avatarGet(); return '
Your picture' +'
' +(a?'Your headshot':'
No picture yet
') +'
' +(a?' ':'') +'

Use your headshot. You choose the crop yourself, and it is saved to your account, so it is there on any device you log in on.

' +'
'; } // ---- The shareable profile. Eight backgrounds, three extra photos, a short bio, and a link // an actor can send straight to a casting director. Off by default: nothing is public until // the person says so. ---- const PROF_THEMES=[ ['blush','#FBEDEF'],['sky','#EAF1FA'],['mint','#E9F4EE'],['butter','#FBF3E2'], ['lilac','#F0ECF8'],['clay','#F7EDE7'],['sage','#EDF1EA'],['paper','#F5F3EE'], ['rose','#F6D9DE'],['sea','#D8E8E4'],['sun','#F6E3B4'],['sky2','#D6E4F7'], ['plum','#E6DAF0'],['moss','#DCE5CE'],['coral','#F8DFD2'],['slate','#DDE1EA'], ['flamingo','#F9C6D0'],['tangerine','#FAD9B0'],['pistachio','#DFEDC0'],['lavender','#E3D9F6'], ['powder','#CFE0F5'],['lemon','#F9F0C0'],['blossom','#F3D3EA'],['stone','#E8E4DC'] ]; function profThemeHex(k){ const t=PROF_THEMES.find(function(x){ return x[0]===k; }); return t?t[1]:'#FAFAFA'; } function profSlugify(n){ return String(n||'actor').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,28)||'actor'; } function profShareUrl(p){ return location.origin+location.pathname+'#p/'+encodeURIComponent((p&&p.slug)||''); } function profPhotos(p){ const a=(p&&p.photos)||[]; return Array.isArray(a)?a:[]; } // Publishing needs a slug, and slugs collide, so try the name then the name with a number. function profPublish(on){ const uid=cpUid(), msg=document.getElementById('pub-msg'); if(!uid) return; const p=cpProfile||{}; if(on && !p.name){ if(msg) msg.textContent='Add your name first, it becomes your link.'; return; } if(msg) msg.textContent=on?'Publishing...':'Making it private...'; const tryOnce=function(slug,attempt){ const body={id:uid,is_public:on,updated_at:new Date().toISOString()}; if(on) body.slug=slug; return doorsFetchAuthed(SBC_URL+'/rest/v1/doors_profiles?on_conflict=id',{method:'POST', headers:{Prefer:'resolution=merge-duplicates,return=representation'}, body:JSON.stringify(body)}) .then(function(r){ if(r.ok) return r.json(); if(on && attempt<6) return tryOnce(profSlugify(p.name)+'-'+(attempt+2),attempt+1); return Promise.reject(0); }); }; tryOnce(p.slug||profSlugify(p.name),0) .then(function(rows){ cpProfile=(rows&&rows[0])||cpProfile; profCacheSet(cpProfile); renderTab('profile'); const m=document.getElementById('pub-msg'); if(m) m.textContent=on?'Live. Anyone with the link can see it.':'Private again.'; }) .catch(function(){ const m=document.getElementById('pub-msg'); if(m) m.textContent='That did not save. Try again in a minute.'; }); } function profShare(btn){ // One click, copied, done. No AirDrop sheet. The button itself says so. const p=cpProfile||{}; const url=profShareUrl(p); const m=document.getElementById('pub-msg'); const say=function(t){ if(m) m.textContent=t; toast('Your link','Copied. Paste it anywhere.','ok'); if(btn){ const was=btn.textContent; btn.textContent='Copied'; btn.style.background='#1e9e5a'; btn.style.borderColor='#1e9e5a'; btn.style.color='#fff'; setTimeout(function(){ btn.textContent=was; btn.style.background=''; btn.style.borderColor=''; btn.style.color=''; },2200); } }; if(navigator.clipboard){ navigator.clipboard.writeText(url).then(function(){ say('Link copied. Paste it anywhere.'); },function(){ prompt('Copy your link',url); }); } else prompt('Copy your link',url); } function profSetTheme(k){ const uid=cpUid(); if(!uid) return; cpProfile=Object.assign({},cpProfile||{},{theme:k}); profCacheSet(cpProfile); renderTab('profile'); doorsFetchAuthed(SBC_URL+'/rest/v1/doors_profiles?on_conflict=id',{method:'POST', headers:{Prefer:'resolution=merge-duplicates'}, body:JSON.stringify({id:uid,theme:k,updated_at:new Date().toISOString()})}).catch(function(){}); } // Three more photos. Straight into the public headshots bucket, under your own folder. function profPhotoPick(input){ const f=input.files&&input.files[0]; const msg=document.getElementById('ph-msg'); if(!f) return; if(!cpUid()){ if(msg) msg.textContent='Log in first.'; return; } if(profPhotos(cpProfile).length>=3){ if(msg) msg.textContent='Three is the limit. Remove one to swap it.'; return; } if(f.size>25*1024*1024){ if(msg) msg.textContent='That one is over 25MB. Anything off your camera roll is usually fine.'; return; } if(msg) msg.textContent='Reading the photo...'; const url=URL.createObjectURL(f); const img=new Image(); img.onload=function(){ try{ const max=1600; let w=img.naturalWidth, h=img.naturalHeight; if(w>max||h>max){ const s=Math.min(max/w,max/h); w=Math.round(w*s); h=Math.round(h*s); } const cv=document.createElement('canvas'); cv.width=w; cv.height=h; cv.getContext('2d').drawImage(img,0,0,w,h); URL.revokeObjectURL(url); cv.toBlob(function(b){ if(!b){ if(msg) msg.textContent='That photo did not read. Try another, or screenshot it and upload the screenshot.'; return; } profPhotoUpload(b,-1,'image/jpeg'); },'image/jpeg',0.88); }catch(e){ if(msg) msg.textContent='That photo did not read. Try another.'; } }; img.onerror=function(){ URL.revokeObjectURL(url); if(msg) msg.textContent='That photo did not read. Screenshot it and upload the screenshot, that always works.'; }; img.src=url; } // slot -1 adds a new one, 0 to 2 replaces that one function profPhotoUpload(blob,slot,mime){ const uid=cpUid(), msg=document.getElementById('ph-msg'); if(!uid) return; if(msg) msg.textContent='Uploading...'; const path=uid+'/p'+Date.now()+'.jpg'; doorsFetchAuthed(SBC_URL+'/storage/v1/object/headshots/'+path,{method:'POST', headers:{'Content-Type':mime||'image/jpeg','x-upsert':'true'},body:blob}) .then(function(r){ if(!r.ok) return Promise.reject(0); const url=SBC_URL+'/storage/v1/object/public/headshots/'+path; const list=profPhotos(cpProfile).slice(); if(slot>=0 && slot'+pesc(t)+'':''; }; const link=function(u,t){ const h=safeUrl(u); if(!h) return ''; const mail=/^mailto:/i.test(h); return ''+(mail?'Email':t)+''; }; const ig=p.instagram?('https://www.instagram.com/'+String(p.instagram).replace(/^@+/,'')+'/'):''; const photos=Array.isArray(p.photos)?p.photos:[]; const tapes=Array.isArray(p.tapes)?p.tapes:[]; return '
' +'
' +(p.avatar_url?'':'') +'

'+pesc(p.name||'Actor')+'

' +(p.headline?'

'+pesc(p.headline)+(px==='pv'?' ':'')+'

':(px==='pv'?'

':'')) +'
'+chip(p.based)+chip(p.stage)+(p.focus?chip('Wants: '+p.focus):'')+chip(p.agent||'Unrepresented')+chip(p.spotlight?'On Spotlight':'')+'
' +favStripHTML(p) +'
' +'
' +(p.bio?'

'+pesc(p.bio)+'

':'') +((ig||p.tiktok||p.youtube||p.spotlight_url||p.vimeo||p.website||p.imdb||p.letterboxd)?'
'+link(ig,'Instagram')+link(p.tiktok?('https://www.tiktok.com/@'+String(p.tiktok).replace(/^@+/,'')):'','TikTok')+link(p.youtube,'YouTube')+link(p.spotlight_url,'Spotlight')+link(p.imdb,'IMDb')+link(p.letterboxd,'Letterboxd')+link(p.vimeo,'Showreel')+link(p.website,'Website')+'
':'') +(p.credits?'

Credits

'+String(p.credits).split(/\n+/).filter(Boolean).map(function(c){ return '

'+pesc(c)+'

'; }).join(''):'') +((Array.isArray(p.skills)&&p.skills.length)?'

Special skills

' +p.skills.map(function(s){ if(!s||!s.s) return ''; let ti=-1; if(s.tid){ tapes.forEach(function(t,i){ if(ti<0&&t.id===s.tid) ti=i; }); } if(ti<0&&s.tp){ tapes.forEach(function(t,i){ if(ti<0&&(t.t||'')===s.tp) ti=i; }); } return ti>=0?''+pesc(s.s)+' ▸':''+pesc(s.s)+''; }).join('')+'
':'') +((p.agent_email||p.agent_phone)?'
Represented'+(p.agent?' by '+pesc(p.agent):'')+'. ' +(p.agent_email?'Email their agent':'') +(p.agent_phone?''+pesc(p.agent_phone)+'':'')+'
':'') +(tapes.length?(function(){ const meta=function(t){ return [t.co,t.g,t.ac,t.cd?'for '+t.cd:''].filter(Boolean).map(pesc).join(' · '); }; window.__pubTapes=tapes; window.__pubPx=px; window.__pubMainIdx=0; return '

Tapes'+(px==='pv'?' ':'')+'

' +'
' +'' +'

'+pesc(tapes[0].t||'Self tape')+(tapes[0].r?' · '+pesc(tapes[0].r):'')+''+meta(tapes[0])+'

' +'

' +'
' +(tapes.length>1?'
' +tapes.map(function(t,i){ return '
' +'' +'
'+pesc(t.t||('Tape '+(i+1)))+''+(t.r?' '+pesc(t.r)+'':'')+'
' +(meta(t)?'
'+meta(t)+'
':'') +((t.d||t.o)?'
'+(t.d?''+pesc(String(t.d).slice(0,10))+'':'')+(t.o?''+pesc(t.o)+'':'')+'
':'') +'
'; }).join('')+'
':''); })():'') +(px==='pv'?'

':'') +(photos.length?'
' +photos.map(function(u){ return ''; }).join('')+'
':'') +'
'; } const SPD_PHASES=[['Pull-ups',5,'#E23636'],['Push-ups',10,'#2B6CB0'],['Squats',15,'#7A3CE8']]; const SPD_CHEERS=['Well done. Keep going.','That is one more than most actors did today.','Good. Straight back on the bar.','Strong. Next round.','Keep swinging.','The suit is earning itself.','Do not stop now.','Lungs burning is the point.','Another one. Tom would.','You look castable already.']; function spdBeep(freq,dur,when,vol,type){ try{ window.__spdAC=window.__spdAC||new (window.AudioContext||window.webkitAudioContext)(); const ac=window.__spdAC; if(ac.state==='suspended') ac.resume(); const o=ac.createOscillator(), g=ac.createGain(); o.type=type||'triangle'; o.frequency.value=freq; const t=ac.currentTime+(when||0); g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(vol||0.22,t+0.012); g.gain.exponentialRampToValueAtTime(0.0001,t+(dur||0.08)); o.connect(g); g.connect(ac.destination); o.start(t); o.stop(t+(dur||0.08)+0.05); }catch(e){} } function spdRemind(kind,btn){ try{ const pad=function(n){ return ('0'+n).slice(-2); }; const fmt=function(dt){ return dt.getFullYear()+pad(dt.getMonth()+1)+pad(dt.getDate())+'T'+pad(dt.getHours())+pad(dt.getMinutes())+'00'; }; let start,title,mins,slug; if(kind==='circuit'){ start=new Date(Date.now()+86400000); title='The Spider-Man circuit, 20 minutes'; mins=20; slug='spiderman'; } else{ start=new Date(); start.setHours(21,0,0,0); if(start.getTime()1){ const latest=h[0].r, prev=h[1].r, first=raw[0].r, vsFirst=latest-first; trend='

' +(latest>prev?'Up '+(latest-prev)+' on last time. ':latest0?'+'+vsFirst+' since your first ever go.':vsFirst<0?'Your first go was '+(-vsFirst)+' higher. Keep at it.':'Level with your first go.') +'

'; } return '
' +'

Your history

' +'

Every attempt, newest first. The bar fills toward 27, Tom Holland\'s score.

' +trend +h.map(function(e){ const pct=Math.max(4,Math.min(100,e.r/27*100)); return '
' +''+spdNiceDate(e.d)+'' +'' +''+e.r+'' +(e.r===best&&best>0?'BEST':'') +'
'; }).join('') +'
'; } function spdGoalBump(n){ const d=spdData(); d.goal=Math.max(1,Math.min(27,(d.goal||5)+n)); spdSave(d); const g=document.getElementById('spd-goal'); if(g) g.textContent=d.goal; const gs=document.getElementById('spd-goal2'); if(gs) gs.textContent=d.goal; } function spdFmt(s){ s=Math.max(0,Math.round(s)); return Math.floor(s/60)+':'+('0'+(s%60)).slice(-2); } function spdStart(){ window.__spd={on:true,t0:Date.now(),phase:0,rounds:0,half:false}; spdBeep(523,.1,0); spdBeep(784,.16,.12); if(window.__spdTimer) clearInterval(window.__spdTimer); window.__spdTimer=setInterval(spdTick,250); spdPaintPhase(); spdTick(); const st=document.getElementById('spd-startwrap'); if(st) st.style.display='none'; const live=document.getElementById('spd-live'); if(live) live.style.display='block'; } function spdDone(){ const s=window.__spd; if(!s||!s.on) return; spdBeep(1318,.05,0,.18,'square'); s.phase++; if(s.phase>2){ s.phase=0; s.rounds++; const rc=document.getElementById('spd-rcount'); if(rc){ rc.textContent=s.rounds; rc.animate([{transform:'scale(1.5)'},{transform:'scale(1)'}],{duration:300}); } spdBeep(659,.09,.08); spdBeep(988,.14,.19); const ch=document.getElementById('spd-cheer'); if(ch){ ch.textContent=SPD_CHEERS[(s.rounds-1)%SPD_CHEERS.length]; ch.style.opacity='1'; ch.animate([{transform:'translateY(6px)',opacity:0},{transform:'translateY(0)',opacity:1}],{duration:350}); } } spdPaintPhase(); spdTick(); } function spdPaintPhase(){ const s=window.__spd; if(!s) return; const p=SPD_PHASES[s.phase]; const card=document.getElementById('spd-phase'); if(!card) return; card.style.background=p[2]; document.getElementById('spd-pnum').textContent=p[1]; document.getElementById('spd-pname').textContent=p[0]; document.getElementById('spd-pnext').textContent='then '+SPD_PHASES[(s.phase+1)%3][1]+' '+SPD_PHASES[(s.phase+1)%3][0].toLowerCase(); } function spdTick(){ const s=window.__spd; const clock=document.getElementById('spd-clock'); if(!s||!s.on||!clock){ if(window.__spdTimer&&!clock){ clearInterval(window.__spdTimer); window.__spdTimer=null; } return; } const el2=(Date.now()-s.t0)/1000, left=1200-el2; clock.textContent=spdFmt(left); const tb=document.getElementById('spd-timebar'); if(tb) tb.style.width=Math.max(0,left/1200*100)+'%'; if(left<=600&&!s.half){ s.half=true; spdBeep(880,.1,0); spdBeep(880,.1,.18); spdBeep(880,.1,.36); } const goal=spdGoal(); const rb=document.getElementById('spd-roundbar'); if(rb) rb.style.width=Math.min(100,(s.rounds+s.phase/3)/goal*100)+'%'; const prog=s.rounds+s.phase/3; const pj=document.getElementById('spd-proj'); if(pj){ if(el2>=20&&prog>0){ const proj=prog*1200/el2, r=Math.round(proj); pj.innerHTML='On this pace: '+r+' rounds by the bell'+(r>=27?' · that is Spider-Man':''); pj.style.color=proj>=goal?'#1f9d55':(proj>=goal*0.6?'#b7791f':'#c53030'); } else pj.textContent='Your pace shows after the first exercise.'; } if(left<=0) spdEnd(false); } function spdEnd(quit){ const s=window.__spd; if(!s||!s.on) return; s.on=false; if(!quit){ spdBeep(523,.14,0); spdBeep(659,.14,.15); spdBeep(784,.14,.3); spdBeep(1046,.4,.45,.26); } if(window.__spdTimer){ clearInterval(window.__spdTimer); window.__spdTimer=null; } const mins=Math.min(20,(Date.now()-s.t0)/60000); const d=spdData(); d.goal=d.goal||5; d.hist=(d.hist||[]); d.hist.push({d:new Date().toISOString().slice(0,10),r:s.rounds,m:Math.round(mins*10)/10}); if(d.hist.length>150) d.hist=d.hist.slice(-150); d.best=Math.max(d.best||0,s.rounds); spdSave(d); const goal=spdGoal(); const live=document.getElementById('spd-live'); if(live) live.style.display='none'; const res=document.getElementById('spd-result'); if(!res) return; res.style.display='block'; const hit=s.rounds>=goal; res.innerHTML='
' +'
'+(quit?'Called it at '+spdFmt((Date.now()-s.t0)/1000)+' in':'Twenty minutes, done')+'
' +'
'+s.rounds+'
' +'
round'+(s.rounds===1?'':'s')+' of your '+goal+'. '+(hit?'Goal beaten. Raise it?':'The wall is still there tomorrow.')+'
' +'
Your best: '+d.best+' · Tom Holland: 27
' +'
'+(s.rounds>=5?'That was real work. The ten minute stretch now is not optional, it is how you walk tomorrow.':'Seal it with the ten minute stretch. Muscles you surprise deserve an apology.')+'
' +'

' +'
' +'' +'' +'
' +'
'; } function pubSwap(i){ const tapes=window.__pubTapes||[]; const t=tapes[i]; if(!t||!t.u) return; const px=window.__pubPx||'pub'; window.__pubMainIdx=i; const v=document.getElementById(px+'-main'); if(!v) return; try{ document.querySelectorAll('video').forEach(function(x){ x.pause(); }); }catch(e){} v.src=t.u; v.play().catch(function(){}); const ti=document.getElementById(px+'-main-title'); if(ti) ti.innerHTML=pesc(t.t||'Self tape')+(t.r?' · '+pesc(t.r):''); const li=document.getElementById(px+'-main-line'); if(li) li.innerHTML=[t.co,t.g,t.ac,t.cd?'for '+t.cd:''].filter(Boolean).map(pesc).join(' · '); tapes.forEach(function(_,j){ const b=document.getElementById(px+'-th-'+j); if(b) b.classList.toggle('on', j===i); }); } function profEditJump(box){ window.__profView='edit'; renderTab('profile'); setTimeout(function(){ if(box==='tapediary'){ showTab('tapediary'); return; } const d=document.getElementById(box); if(d){ d.open=true; try{ d.scrollIntoView({behavior:'smooth',block:'start'}); }catch(e){} } },300); } function tapes0(p,i){ return (Array.isArray(p.tapes)?p.tapes:[])[i]||{}; } function pubTapeViewHTML(p,i){ const tapes=Array.isArray(p.tapes)?p.tapes:[]; const t=tapes[i]; if(!t||!t.u) return null; const bg=profThemeHex(p.theme||'paper'); const first=(p.name||'This actor').split(' ')[0]; const back='#p/'+encodeURIComponent(p.slug||''); const row=function(l,v,strong){ return v?'

'+l+'

'+pesc(v)+'

':''; }; return '
' +'

← Back to '+pesc(first)+'\'s profile

' +'
' +'
' +'' +'

' +'
' +'
' +'

'+pesc(t.t||'Self tape')+'

' +(t.r?'

'+pesc(t.r)+'

':'
') +row('Casting director',t.cd) +row('Production company',t.co) +row('Genre',t.g) +row('Accent',t.ac) +row('When',t.d) +row('What came from it',t.o,1) +(t.n?'

Notes

'+pesc(t.n)+'

':'') +'' +'
' +'
' +'
'; } function castingLinkCopy(id,btn){ const url=location.origin+location.pathname+'#c/'+id; const done=function(){ if(btn) btn.textContent='link copied'; setTimeout(function(){ if(btn) btn.textContent='copy a link to this casting'; },2500); }; if(navigator.clipboard){ navigator.clipboard.writeText(url).then(done,function(){ prompt('Copy this link',url); }); } else prompt('Copy this link',url); } function renderCastingPage(id){ id=String(id).replace(/[^0-9]/g,''); if(!id){ showTab('castings'); return; } let el=document.getElementById('page-castone'); if(!el){ el=document.createElement('section'); el.id='page-castone'; el.className='page'; document.getElementById('pages').appendChild(el); } document.querySelectorAll('.page').forEach(function(p){ p.classList.remove('active'); }); el.classList.add('active'); window.__navCur='castone'; el.innerHTML='

Opening the casting...

'; window.scrollTo(0,0); fetch(SBC_URL+'/rest/v1/castings?select=*&id=eq.'+id,{headers:SBC_HEADERS}) .then(function(r){ return r.ok?r.json():[]; }) .then(function(rows){ const raw=rows[0]; if(!raw||!raw.verified){ el.innerHTML=backLink('castings','Castings')+'

Nothing here.

This casting is gone, or the link is wrong. The board has the live ones.

'; return; } const c=(window.__ccNorm||function(r){return {id:r.id,roles:r.roles||null,t:r.title||'Untitled',type:r.ctype||'',co:r.company||'',venue:r.venue||'',region:r.region||'',dates:r.dates||'',dir:r.director||'',cast:r.casting_office||'',site:r.site||'',closes:r.closes_on||'',material:r.material||'',pay:r.pay||''};})(raw); window.__castN=0; el.innerHTML=backLink('castings','Castings') +'

'+pesc(c.t)+'

' +'

'+pesc([c.type,c.co,c.region].filter(Boolean).join(' · '))+'

' +'
' +castingCard(c,true) +'

' +(rolesOf(c).length?'

The roles

'+rolesOf(c).map(function(r){ return '
'+pesc(r.n||'Role')+''+((r.tp||r.age)?' '+pesc([r.tp,r.age].filter(Boolean).join(' · '))+'':'') +(r.d?'

'+pesc(r.d)+'

':'')+'
'; }).join(''):'') +'

How to tape this

' +'

The kit, non-negotiable and cheap: your phone on a tripod (a stack of books works), light from a window or a lamp in front of you, a plain wall behind you, and a reader on speakerphone if the scene has lines.

' +'

The frame: chest up, eyes just off the lens, phone landscape unless they ask for portrait. Slate only if they ask: name, height, where you are based.

' +'

The send: best take only, file named Your Name, Role, Project. Then log it in your tape diary so it works for you forever.

' +'
Warm up first, ten minutes changes the tape. Wake the voice up, then stretch and breathe. Then press record.
' +'
'; try{ document.title=(raw.title||'Casting')+' on DOORS'; }catch(e){} try{ paintStreakStrip(); }catch(e){} }) .catch(function(){ el.innerHTML=backLink('castings','Castings')+'

Could not open that casting. Try again in a minute.

'; }); } function renderPublicProfile(slug){ let tapeIdx=-1; const tm=String(slug).match(/~(\d+)$/); if(tm){ tapeIdx=parseInt(tm[1],10); slug=String(slug).replace(/~\d+$/,''); } window.__pubTapeIdx=tapeIdx; window.__pubSlug=decodeURIComponent(slug); let el=document.getElementById('page-pubprof'); if(!el){ el=document.createElement('section'); el.id='page-pubprof'; el.className='page'; document.getElementById('pages').appendChild(el); } document.querySelectorAll('.page').forEach(function(p){ p.classList.remove('active'); }); el.classList.add('active'); window.__navCur='pubprof'; const sb=document.getElementById('streakbar'); if(sb) sb.style.display='none'; el.innerHTML='

Opening...

'; document.querySelectorAll('#page-pubprof .pagesay').forEach(function(x){ x.remove(); }); window.scrollTo(0,0); fetch(SBC_URL+'/rest/v1/doors_profiles?select=*&is_public=eq.true&slug=eq.'+encodeURIComponent(decodeURIComponent(slug)),{headers:SBC_HEADERS}) .then(function(r){ return r.ok?r.json():[]; }) .then(function(rows){ const p=rows[0]; if(p){ try{ pvLog(window.__pubSlug,p.id); }catch(e){} } if(!p){ el.innerHTML='

Nothing here.

This page is private, or the link is wrong.

'; return; } const credit='
'+pesc((p.name||'This actor').split(' ')[0])+' made this on DOORS. A free site for actors: castings, the lists nobody gives you, and practice you can do today.

'; if(window.__pubTapeIdx>=0){ const solo=pubTapeViewHTML(p,window.__pubTapeIdx); if(solo){ el.innerHTML=solo+'
'+credit+'
'; try{ document.title=(tapes0(p,window.__pubTapeIdx).t||'Tape')+', '+(p.name||'Actor')+' on DOORS'; }catch(e){} return; } } el.innerHTML=pubProfileBodyHTML(p,'pt')+credit; try{ document.title=(p.name||'Actor')+' on DOORS'; }catch(e){} }) .catch(function(){ el.innerHTML='

Could not open that page. Check the link and try again.

'; }); } // Ten things that make a shared page worth sending. Red until they are done, green when they are. function profSteps(){ const p=cpProfile||{}, tapes=(typeof tapesAll==='function')?tapesAll():[]; const onProf=tapes.filter(function(t){ return t.onProfile&&t.link; }).length; return [ {t:'Add your name', ok:!!p.name, go:"document.getElementById('pf-name')&&document.getElementById('pf-name').focus()"}, {t:'Add your one line', ok:!!p.headline, go:"document.getElementById('pf-headline')&&document.getElementById('pf-headline').focus()"}, {t:'Add your profile picture or main headshot', ok:!!avatarGet(), go:"document.querySelector('.pitem input[type=file]')&&document.querySelector('.pitem input[type=file]').click()"}, {t:'Write a short bio', ok:!!p.bio, go:"document.getElementById('pf-bio')&&document.getElementById('pf-bio').focus()"}, {t:'Say where you are based', ok:!!p.based, go:"startOnboarding(cpProfile)"}, {t:'Add your Instagram, other socials and links', ok:!!(p.instagram||p.tiktok||p.youtube||p.website), go:"document.getElementById('pf-instagram')&&document.getElementById('pf-instagram').focus()"}, {t:'Upload your first tape', ok:tapes.some(function(t){ return t.link; }), go:"showTab('tapediary')"}, {t:'Choose tapes for your page',ok:onProf>0, go:"showTab('tapediary');setTimeout(function(){var d=document.getElementById('diarytop');if(d)d.scrollIntoView({behavior:'smooth'})},900)"}, {t:'Add featured photos', ok:(profPhotos(p)||[]).length>0, go:"var i=document.getElementById('pf-photo-input');if(i)i.click()"}, {t:'Create your link, then share it', ok:!!p.is_public, go:"profPublish(true)"} ]; } function profMeterHTML(){ const steps=profSteps(), done=steps.filter(function(s){ return s.ok; }).length, pct=Math.round(done/steps.length*100); const col = done===steps.length ? '#3E9B4F' : (done>=6 ? '#E0A500' : '#C9302E'); if(done===steps.length){ return '
' +'Your page is ready.' +'

Everything is filled in. Send the link to anyone.

' +'
' +((cpProfile&&cpProfile.is_public)?'' :'') +'
'; } return '
' +((done>=7&&!(cpProfile&&cpProfile.is_public))?'
Your page is ready to publish. Create your link and start sending it.

':'') +'
' +''+(done===steps.length?'Your page is ready to send.':'Your page is not ready yet.')+'' +''+done+' of '+steps.length+'' +'
' +'
' +'

'+(done===steps.length ? 'Everything is filled in. Share the link with anyone.' : 'Tap anything below to do it. And you do not have to finish: share it whenever you like, 2 of 10 is allowed.')+'

' +(done' +((cpProfile&&cpProfile.is_public)?'' :'') +'
':'') +'
' +steps.filter(function(s){ return !s.ok; }) .map(function(s,i){ return ''; }).join('') +'
'; } function profShareBlockHTML(){ const p=cpProfile||{}, on=!!p.is_public, photos=profPhotos(p); return '
Your shared page' +'

One link you can send to a casting director, an agent, anyone. It shows your picture, your bio, your photos and your details. Nothing else, and nothing private.

' +'' +'
' +PROF_THEMES.map(function(t){ return ''; }).join('') +'
' +'

Up to three, any shape, exactly as you took them. They sit under your profile and do not change your picture.

' +'
' +photos.map(function(u,i){ return ''; }).join('') +(photos.length<3?'':'') +'
' +'

' +'
Tapes: work in progress. Uploading a tape works today, so do try it, and tell us how it goes. Your best eight sit on your shared profile. Let the team know if anything is off.
' +'

' +'' +(on?'':'') +'

' +(on?'

'+pesc(profShareUrl(p))+'

':'') +'

' +'
'; } function profCacheGet(){ try{ return JSON.parse(localStorage.getItem('doorsProfile')||'null'); }catch(e){ return null; } } function profCacheSet(p){ try{ p?localStorage.setItem('doorsProfile',JSON.stringify(p)):localStorage.removeItem('doorsProfile'); }catch(e){} } function obDone(){ try{ return !!localStorage.getItem('doorsObDone'); }catch(e){ return false; } } function obMarkDone(){ try{ localStorage.setItem('doorsObDone','1'); }catch(e){} } // Returns the profile row, or the string 'unknown' when we could not check. // 'unknown' must never trigger onboarding: that is what made the wizard nag forever. function loadProfile(){ const uid=cpUid(); if(!uid) return Promise.resolve('unknown'); const cached=profCacheGet(); if(cached && !cpProfile){ cpProfile=cached; authPaint(); } return fetch(SBC_URL+'/rest/v1/doors_profiles?select=*&id=eq.'+uid,{headers:authedHeaders()}) .then(r=>{ if(r.status===401){ return cpRefresh().then(ok=>ok?loadProfile():'unknown'); } // login went stale, renew it if(!r.ok) return 'unknown'; return r.json().then(rows=>{ if(rows && rows.length){ cpProfile=rows[0]; profCacheSet(cpProfile); obMarkDone(); } authPaint(); // The profile page draws before this lands. Redraw it so it never sticks on the empty state. if((location.hash.slice(1)||'home')==='profile') renderTab('profile'); return (rows && rows.length)?cpProfile:null; }); }) .catch(()=>'unknown'); } const OB_QS=[ {k:'based', q:'Where are you based?', opts:['London','New York','Los Angeles','Somewhere else']}, {k:'stage', q:'Where are you at?', opts:['Just starting out','In training','Trained, building credits','Working actor']}, {k:'focus', q:'What do you want more of?', opts:['Stage','Screen','Commercials','Voice','All of it']}, {k:'spotlight', q:'Are you on Spotlight?', opts:['Yes','Not yet']}, ]; let obState={}, obStep=0; function startOnboarding(prefill){ obState=prefill?{...prefill}:{}; if(typeof obState.spotlight==='boolean') obState.spotlight=obState.spotlight?'Yes':'Not yet'; // saved as true/false, asked as Yes/Not yet obStep=0; document.getElementById('onboard').style.display='flex'; obRender(); } function obRender(){ const total=OB_QS.length+1; document.getElementById('ob-fill').style.width=Math.round(((obStep+1)/total)*100)+'%'; const b=document.getElementById('ob-body'); if(obStep' +q.opts.map(o=>'').join('') +'

'+(obStep+1)+' of '+total+'. You can change all of this on your profile any time. Skip for now

'; } else { b.innerHTML='

Last one. Who are you?

' +'' +'' +'

' +'

'; } } function obPick(v){ obState[OB_QS[obStep].k]=v; obStep++; obRender(); } function obFinish(){ const name=document.getElementById('ob-name').value.trim(); const msg=document.getElementById('ob-msg'); if(!name){ msg.textContent='Your name, at least. Casting needs to call you something.'; return; } const uid=cpUid(); if(!uid){ msg.textContent='You need to be signed in for this.'; return; } obState.name=name; obState.headline=document.getElementById('ob-headline').value.trim(); msg.textContent='Saving…'; const body={id:uid, name:obState.name, headline:obState.headline, based:obState.based||'', stage:obState.stage||'', focus:obState.focus||'', spotlight:obState.spotlight==='Yes', updated_at:new Date().toISOString()}; doorsFetchAuthed(SBC_URL+'/rest/v1/doors_profiles?on_conflict=id',{method:'POST',headers:{Prefer:'resolution=merge-duplicates,return=representation'},body:JSON.stringify(body)}) .then(r=>r.ok?r.json():Promise.reject(0)) .then(rows=>{ cpProfile=(rows&&rows[0])||body; profCacheSet(cpProfile); obMarkDone(); document.getElementById('onboard').style.display='none'; showTab('profile'); }) .catch(()=>{ msg.textContent='That did not save. Check you are signed in, and that the profiles table exists in Supabase.'; }); } // Once you have an account the wizard NEVER interrupts you again. It only opens when // you choose it, from the profile page or the home nudge. function ensureProfile(){ if(!cpSession()) return; loadProfile(); } // Renew a stale login. Resolves true if we now hold a working session. let cpRefreshing=null; function cpRefresh(){ const s=cpSession(); if(!s||!s.refresh_token) return Promise.resolve(false); if(cpRefreshing) return cpRefreshing; cpRefreshing=fetch(SBC_URL+'/auth/v1/token?grant_type=refresh_token',{method:'POST',headers:SBC_HEADERS,body:JSON.stringify({refresh_token:s.refresh_token})}) .then(r=>r.ok?r.json():null) .then(d=>{ cpRefreshing=null; if(d&&d.access_token){ cpSaveSession(d); return true; } cpSignedOut(); return false; }) .catch(()=>{ cpRefreshing=null; return false; }); return cpRefreshing; } function cpSignedOut(){ try{ localStorage.removeItem('cpSession'); }catch(e){} cpProfile=null; authPaint(); const t=document.getElementById('auth-title'); openAuth(null); authMode('login'); const m=document.getElementById('auth-msg'); if(m) m.textContent='You were signed out after a while away. Log back in and everything is exactly where you left it.'; } function obSkip(){ try{ localStorage.setItem('doorsObSkip',String(Date.now())); }catch(e){} document.getElementById('onboard').style.display='none'; return false; } function doLogout(){ try{ localStorage.removeItem('cpSession'); }catch(e){} cpProfile=null; try{ menuClose(); }catch(e){} authPaint(); location.hash='home'; renderTab('home'); try{ toast('Logged out','You can log back in any time from the top right.','ok'); }catch(e){} } function closeWelcome(){ try{ localStorage.setItem('acWelcomed','1'); }catch(e){} document.getElementById('welcome').style.display='none'; } (function(){ let seen=false; try{ seen=!!localStorage.getItem('acWelcomed'); }catch(e){} if(!seen) document.getElementById('welcome').style.display='flex'; })(); // If the user arrives from a confirmation email, the tokens ride in on the hash. (function(){ if(location.hash.indexOf('access_token=')>-1){ const p=new URLSearchParams(location.hash.slice(1)); if(p.get('access_token')){ cpSaveSession({access_token:p.get('access_token'), refresh_token:p.get('refresh_token')||'', expires_in:parseInt(p.get('expires_in')||'3600',10), user:{email:''}}); if(p.get('type')==='recovery') window.__pwRecovery=true; else window.__justConfirmed=true; history.replaceState(null,'',location.pathname+'#home'); } } })(); // Screen readers ignore placeholder text, and placeholders vanish once you type. Give every // input a real accessible name, including the ones drawn later by the page. function labelInputs(root){ try{ (root||document).querySelectorAll('input,textarea,select').forEach(function(el){ if(el.getAttribute('aria-label')||el.getAttribute('aria-labelledby')||el.type==='hidden') return; const t=el.getAttribute('placeholder')||el.getAttribute('title')||(el.type==='date'?'Date':'')||(el.type==='checkbox'?(el.closest('label')||{}).textContent:''); if(t) el.setAttribute('aria-label',String(t).replace(/…/g,'').trim()); }); }catch(e){} } labelInputs(); // Every link that leaves doors.actor opens in a new tab, so you never lose your place. // Done centrally because pages are rebuilt constantly and inline attributes get missed. (function(){ function fixLinks(root){ try{ (root||document).querySelectorAll('a[href]').forEach(function(a){ const h=a.getAttribute('href')||''; if(!/^https?:\/\//i.test(h)) return; // internal, hash and mailto stay put if(h.indexOf(location.host)>-1) return; a.setAttribute('target','_blank'); a.setAttribute('rel','noopener noreferrer'); }); }catch(e){} } fixLinks(); try{ new MutationObserver(function(ms){ ms.forEach(function(m){ m.addedNodes.forEach(function(n){ if(n.nodeType===1) fixLinks(n); }); }); }).observe(document.body,{childList:true,subtree:true}); }catch(e){} })(); (function(){ var t=null; function isField(el){ return el && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && el.type!=='checkbox'; } document.addEventListener('focusin', function(e){ if(isField(e.target)){ clearTimeout(t); document.body.classList.add('kbopen'); } }); document.addEventListener('focusout', function(e){ if(isField(e.target)){ clearTimeout(t); t=setTimeout(function(){ if(!isField(document.activeElement)) document.body.classList.remove('kbopen'); },120); } }); })(); try{ new MutationObserver(function(muts){ muts.forEach(function(m){ m.addedNodes.forEach(function(n){ if(n.nodeType===1) labelInputs(n); }); }); }).observe(document.body,{childList:true,subtree:true}); }catch(e){} authPaint(); try{ remNudge(); }catch(e){} runCheck(); renderTab(location.hash.slice(1)||'home'); if(window.__pwRecovery){ document.getElementById('authwall').style.display='flex'; authMode('reset'); } else if(cpSession()) ensureProfile();