Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
528 kB · 14470 lines
HTML
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466104671046810469104701047110472104731047410475104761047710478104791048010481104821048310484104851048610487104881048910490104911049210493104941049510496104971049810499105001050110502105031050410505105061050710508105091051010511105121051310514105151051610517105181051910520105211052210523105241052510526105271052810529105301053110532105331053410535105361053710538105391054010541105421054310544105451054610547105481054910550105511055210553105541055510556105571055810559105601056110562105631056410565105661056710568105691057010571105721057310574105751057610577105781057910580105811058210583105841058510586105871058810589105901059110592105931059410595105961059710598105991060010601106021060310604106051060610607106081060910610106111061210613106141061510616106171061810619106201062110622106231062410625106261062710628106291063010631106321063310634106351063610637106381063910640106411064210643106441064510646106471064810649106501065110652106531065410655106561065710658106591066010661106621066310664106651066610667106681066910670106711067210673106741067510676106771067810679106801068110682106831068410685106861068710688106891069010691106921069310694106951069610697106981069910700107011070210703107041070510706107071070810709107101071110712107131071410715107161071710718107191072010721107221072310724107251072610727107281072910730107311073210733107341073510736107371073810739107401074110742107431074410745107461074710748107491075010751107521075310754107551075610757107581075910760107611076210763107641076510766107671076810769107701077110772107731077410775107761077710778107791078010781107821078310784107851078610787107881078910790107911079210793107941079510796107971079810799108001080110802108031080410805108061080710808108091081010811108121081310814108151081610817108181081910820108211082210823108241082510826108271082810829108301083110832108331083410835108361083710838108391084010841108421084310844108451084610847108481084910850108511085210853108541085510856108571085810859108601086110862108631086410865108661086710868108691087010871108721087310874108751087610877108781087910880108811088210883108841088510886108871088810889108901089110892108931089410895108961089710898108991090010901109021090310904109051090610907109081090910910109111091210913109141091510916109171091810919109201092110922109231092410925109261092710928109291093010931109321093310934109351093610937109381093910940109411094210943109441094510946109471094810949109501095110952109531095410955109561095710958109591096010961109621096310964109651096610967109681096910970109711097210973109741097510976109771097810979109801098110982109831098410985109861098710988109891099010991109921099310994109951099610997109981099911000110011100211003110041100511006110071100811009110101101111012110131101411015110161101711018110191102011021110221102311024110251102611027110281102911030110311103211033110341103511036110371103811039110401104111042110431104411045110461104711048110491105011051110521105311054110551105611057110581105911060110611106211063110641106511066110671106811069110701107111072110731107411075110761107711078110791108011081110821108311084110851108611087110881108911090110911109211093110941109511096110971109811099111001110111102111031110411105111061110711108111091111011111111121111311114111151111611117111181111911120111211112211123111241112511126111271112811129111301113111132111331113411135111361113711138111391114011141111421114311144111451114611147111481114911150111511115211153111541115511156111571115811159111601116111162111631116411165111661116711168111691117011171111721117311174111751117611177111781117911180111811118211183111841118511186111871118811189111901119111192111931119411195111961119711198111991120011201112021120311204112051120611207112081120911210112111121211213112141121511216112171121811219112201122111222112231122411225112261122711228112291123011231112321123311234112351123611237112381123911240112411124211243112441124511246112471124811249112501125111252112531125411255112561125711258112591126011261112621126311264112651126611267112681126911270112711127211273112741127511276112771127811279112801128111282112831128411285112861128711288112891129011291112921129311294112951129611297112981129911300113011130211303113041130511306113071130811309113101131111312113131131411315113161131711318113191132011321113221132311324113251132611327113281132911330113311133211333113341133511336113371133811339113401134111342113431134411345113461134711348113491135011351113521135311354113551135611357113581135911360113611136211363113641136511366113671136811369113701137111372113731137411375113761137711378113791138011381113821138311384113851138611387113881138911390113911139211393113941139511396113971139811399114001140111402114031140411405114061140711408114091141011411114121141311414114151141611417114181141911420114211142211423114241142511426114271142811429114301143111432114331143411435114361143711438114391144011441114421144311444114451144611447114481144911450114511145211453114541145511456114571145811459114601146111462114631146411465114661146711468114691147011471114721147311474114751147611477114781147911480114811148211483114841148511486114871148811489114901149111492114931149411495114961149711498114991150011501115021150311504115051150611507115081150911510115111151211513115141151511516115171151811519115201152111522115231152411525115261152711528115291153011531115321153311534115351153611537115381153911540115411154211543115441154511546115471154811549115501155111552115531155411555115561155711558115591156011561115621156311564115651156611567115681156911570115711157211573115741157511576115771157811579115801158111582115831158411585115861158711588115891159011591115921159311594115951159611597115981159911600116011160211603116041160511606116071160811609116101161111612116131161411615116161161711618116191162011621116221162311624116251162611627116281162911630116311163211633116341163511636116371163811639116401164111642116431164411645116461164711648116491165011651116521165311654116551165611657116581165911660116611166211663116641166511666116671166811669116701167111672116731167411675116761167711678116791168011681116821168311684116851168611687116881168911690116911169211693116941169511696116971169811699117001170111702117031170411705117061170711708117091171011711117121171311714117151171611717117181171911720117211172211723117241172511726117271172811729117301173111732117331173411735117361173711738117391174011741117421174311744117451174611747117481174911750117511175211753117541175511756117571175811759117601176111762117631176411765117661176711768117691177011771117721177311774117751177611777117781177911780117811178211783117841178511786117871178811789117901179111792117931179411795117961179711798117991180011801118021180311804118051180611807118081180911810118111181211813118141181511816118171181811819118201182111822118231182411825118261182711828118291183011831118321183311834118351183611837118381183911840118411184211843118441184511846118471184811849118501185111852118531185411855118561185711858118591186011861118621186311864118651186611867118681186911870118711187211873118741187511876118771187811879118801188111882118831188411885118861188711888118891189011891118921189311894118951189611897118981189911900119011190211903119041190511906119071190811909119101191111912119131191411915119161191711918119191192011921119221192311924119251192611927119281192911930119311193211933119341193511936119371193811939119401194111942119431194411945119461194711948119491195011951119521195311954119551195611957119581195911960119611196211963119641196511966119671196811969119701197111972119731197411975119761197711978119791198011981119821198311984119851198611987119881198911990119911199211993119941199511996119971199811999120001200112002120031200412005120061200712008120091201012011120121201312014120151201612017120181201912020120211202212023120241202512026120271202812029120301203112032120331203412035120361203712038120391204012041120421204312044120451204612047120481204912050120511205212053120541205512056120571205812059120601206112062120631206412065120661206712068120691207012071120721207312074120751207612077120781207912080120811208212083120841208512086120871208812089120901209112092120931209412095120961209712098120991210012101121021210312104121051210612107121081210912110121111211212113121141211512116121171211812119121201212112122121231212412125121261212712128121291213012131121321213312134121351213612137121381213912140121411214212143121441214512146121471214812149121501215112152121531215412155121561215712158121591216012161121621216312164121651216612167121681216912170121711217212173121741217512176121771217812179121801218112182121831218412185121861218712188121891219012191121921219312194121951219612197121981219912200122011220212203122041220512206122071220812209122101221112212122131221412215122161221712218122191222012221122221222312224122251222612227122281222912230122311223212233122341223512236122371223812239122401224112242122431224412245122461224712248122491225012251122521225312254122551225612257122581225912260122611226212263122641226512266122671226812269122701227112272122731227412275122761227712278122791228012281122821228312284122851228612287122881228912290122911229212293122941229512296122971229812299123001230112302123031230412305123061230712308123091231012311123121231312314123151231612317123181231912320123211232212323123241232512326123271232812329123301233112332123331233412335123361233712338123391234012341123421234312344123451234612347123481234912350123511235212353123541235512356123571235812359123601236112362123631236412365123661236712368123691237012371123721237312374123751237612377123781237912380123811238212383123841238512386123871238812389123901239112392123931239412395123961239712398123991240012401124021240312404124051240612407124081240912410124111241212413124141241512416124171241812419124201242112422124231242412425124261242712428124291243012431124321243312434124351243612437124381243912440124411244212443124441244512446124471244812449124501245112452124531245412455124561245712458124591246012461124621246312464124651246612467124681246912470124711247212473124741247512476124771247812479124801248112482124831248412485124861248712488124891249012491124921249312494124951249612497124981249912500125011250212503125041250512506125071250812509125101251112512125131251412515125161251712518125191252012521125221252312524125251252612527125281252912530125311253212533125341253512536125371253812539125401254112542125431254412545125461254712548125491255012551125521255312554125551255612557125581255912560125611256212563125641256512566125671256812569125701257112572125731257412575125761257712578125791258012581125821258312584125851258612587125881258912590125911259212593125941259512596125971259812599126001260112602126031260412605126061260712608126091261012611126121261312614126151261612617126181261912620126211262212623126241262512626126271262812629126301263112632126331263412635126361263712638126391264012641126421264312644126451264612647126481264912650126511265212653126541265512656126571265812659126601266112662126631266412665126661266712668126691267012671126721267312674126751267612677126781267912680126811268212683126841268512686126871268812689126901269112692126931269412695126961269712698126991270012701127021270312704127051270612707127081270912710127111271212713127141271512716127171271812719127201272112722127231272412725127261272712728127291273012731127321273312734127351273612737127381273912740127411274212743127441274512746127471274812749127501275112752127531275412755127561275712758127591276012761127621276312764127651276612767127681276912770127711277212773127741277512776127771277812779127801278112782127831278412785127861278712788127891279012791127921279312794127951279612797127981279912800128011280212803128041280512806128071280812809128101281112812128131281412815128161281712818128191282012821128221282312824128251282612827128281282912830128311283212833128341283512836128371283812839128401284112842128431284412845128461284712848128491285012851128521285312854128551285612857128581285912860128611286212863128641286512866128671286812869128701287112872128731287412875128761287712878128791288012881128821288312884128851288612887128881288912890128911289212893128941289512896128971289812899129001290112902129031290412905129061290712908129091291012911129121291312914129151291612917129181291912920129211292212923129241292512926129271292812929129301293112932129331293412935129361293712938129391294012941129421294312944129451294612947129481294912950129511295212953129541295512956129571295812959129601296112962129631296412965129661296712968129691297012971129721297312974129751297612977129781297912980129811298212983129841298512986129871298812989129901299112992129931299412995129961299712998129991300013001130021300313004130051300613007130081300913010130111301213013130141301513016130171301813019130201302113022130231302413025130261302713028130291303013031130321303313034130351303613037130381303913040130411304213043130441304513046130471304813049130501305113052130531305413055130561305713058130591306013061130621306313064130651306613067130681306913070130711307213073130741307513076130771307813079130801308113082130831308413085130861308713088130891309013091130921309313094130951309613097130981309913100131011310213103131041310513106131071310813109131101311113112131131311413115131161311713118131191312013121131221312313124131251312613127131281312913130131311313213133131341313513136131371313813139131401314113142131431314413145131461314713148131491315013151131521315313154131551315613157131581315913160131611316213163131641316513166131671316813169131701317113172131731317413175131761317713178131791318013181131821318313184131851318613187131881318913190131911319213193131941319513196131971319813199132001320113202132031320413205132061320713208132091321013211132121321313214132151321613217132181321913220132211322213223132241322513226132271322813229132301323113232132331323413235132361323713238132391324013241132421324313244132451324613247132481324913250132511325213253132541325513256132571325813259132601326113262132631326413265132661326713268132691327013271132721327313274132751327613277132781327913280132811328213283132841328513286132871328813289132901329113292132931329413295132961329713298132991330013301133021330313304133051330613307133081330913310133111331213313133141331513316133171331813319133201332113322133231332413325133261332713328133291333013331133321333313334133351333613337133381333913340133411334213343133441334513346133471334813349133501335113352133531335413355133561335713358133591336013361133621336313364133651336613367133681336913370133711337213373133741337513376133771337813379133801338113382133831338413385133861338713388133891339013391133921339313394133951339613397133981339913400134011340213403134041340513406134071340813409134101341113412134131341413415134161341713418134191342013421134221342313424134251342613427134281342913430134311343213433134341343513436134371343813439134401344113442134431344413445134461344713448134491345013451134521345313454134551345613457134581345913460134611346213463134641346513466134671346813469134701347113472134731347413475134761347713478134791348013481134821348313484134851348613487134881348913490134911349213493134941349513496134971349813499135001350113502135031350413505135061350713508135091351013511135121351313514135151351613517135181351913520135211352213523135241352513526135271352813529135301353113532135331353413535135361353713538135391354013541135421354313544135451354613547135481354913550135511355213553135541355513556135571355813559135601356113562135631356413565135661356713568135691357013571135721357313574135751357613577135781357913580135811358213583135841358513586135871358813589135901359113592135931359413595135961359713598135991360013601136021360313604136051360613607136081360913610136111361213613136141361513616136171361813619136201362113622136231362413625136261362713628136291363013631136321363313634136351363613637136381363913640136411364213643136441364513646136471364813649136501365113652136531365413655136561365713658136591366013661136621366313664136651366613667136681366913670136711367213673136741367513676136771367813679136801368113682136831368413685136861368713688136891369013691136921369313694136951369613697136981369913700137011370213703137041370513706137071370813709137101371113712137131371413715137161371713718137191372013721137221372313724137251372613727137281372913730137311373213733137341373513736137371373813739137401374113742137431374413745137461374713748137491375013751137521375313754137551375613757137581375913760137611376213763137641376513766137671376813769137701377113772137731377413775137761377713778137791378013781137821378313784137851378613787137881378913790137911379213793137941379513796137971379813799138001380113802138031380413805138061380713808138091381013811138121381313814138151381613817138181381913820138211382213823138241382513826138271382813829138301383113832138331383413835138361383713838138391384013841138421384313844138451384613847138481384913850138511385213853138541385513856138571385813859138601386113862138631386413865138661386713868138691387013871138721387313874138751387613877138781387913880138811388213883138841388513886138871388813889138901389113892138931389413895138961389713898138991390013901139021390313904139051390613907139081390913910139111391213913139141391513916139171391813919139201392113922139231392413925139261392713928139291393013931139321393313934139351393613937139381393913940139411394213943139441394513946139471394813949139501395113952139531395413955139561395713958139591396013961139621396313964139651396613967139681396913970139711397213973139741397513976139771397813979139801398113982139831398413985139861398713988139891399013991139921399313994139951399613997139981399914000140011400214003140041400514006140071400814009140101401114012140131401414015140161401714018140191402014021140221402314024140251402614027140281402914030140311403214033140341403514036140371403814039140401404114042140431404414045140461404714048140491405014051140521405314054140551405614057140581405914060140611406214063140641406514066140671406814069140701407114072140731407414075140761407714078140791408014081140821408314084140851408614087140881408914090140911409214093140941409514096140971409814099141001410114102141031410414105141061410714108141091411014111141121411314114141151411614117141181411914120141211412214123141241412514126141271412814129141301413114132141331413414135141361413714138141391414014141141421414314144141451414614147141481414914150141511415214153141541415514156141571415814159141601416114162141631416414165141661416714168141691417014171141721417314174141751417614177141781417914180141811418214183141841418514186141871418814189141901419114192141931419414195141961419714198141991420014201142021420314204142051420614207142081420914210142111421214213142141421514216142171421814219142201422114222142231422414225142261422714228142291423014231142321423314234142351423614237142381423914240142411424214243142441424514246142471424814249142501425114252142531425414255142561425714258142591426014261142621426314264142651426614267142681426914270142711427214273142741427514276142771427814279142801428114282142831428414285142861428714288142891429014291142921429314294142951429614297142981429914300143011430214303143041430514306143071430814309143101431114312143131431414315143161431714318143191432014321143221432314324143251432614327143281432914330143311433214333143341433514336143371433814339143401434114342143431434414345143461434714348143491435014351143521435314354143551435614357143581435914360143611436214363143641436514366143671436814369143701437114372143731437414375143761437714378143791438014381143821438314384143851438614387143881438914390143911439214393143941439514396143971439814399144001440114402144031440414405144061440714408144091441014411144121441314414144151441614417144181441914420144211442214423144241442514426144271442814429144301443114432144331443414435144361443714438144391444014441144421444314444144451444614447144481444914450144511445214453144541445514456144571445814459144601446114462144631446414465144661446714468144691447014471<!DOCTYPE html><html><head> <title>Give · Aesthetic Computer</title> <!-- SVG filter for image sharpening (unsharp mask) --> <svg width="0" height="0" style="position:absolute"> <filter id="sharpen"> <feConvolveMatrix order="3" preserveAlpha="true" kernelMatrix="0 -0.5 0 -0.5 3 -0.5 0 -0.5 0" /> </filter> </svg> <meta name="description" content="Aesthetic Computer" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- Social sharing meta tags --> <meta property="og:title" content="Give · Aesthetic Computer" /> <meta property="og:description" content="Aesthetic Computer" /> <meta property="og:image" content="https://assets.aesthetic.computer/give-og.jpg" /> <meta property="og:image:width" content="1200" /> <meta property="og:image:height" content="630" /> <meta property="og:url" content="https://give.aesthetic.computer" /> <meta property="og:type" content="website" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="Give · Aesthetic Computer" /> <meta name="twitter:description" content="Aesthetic Computer" /> <meta name="twitter:image" content="https://assets.aesthetic.computer/give-og.jpg" /> <meta name="twitter:site" content="@aestheticco_mp" /> <!-- Fonts --> <link rel="stylesheet" href="https://aesthetic.computer/type/webfonts/ywft-processing-regular.css"> <link rel="stylesheet" href="https://aesthetic.computer/type/webfonts/berkeley-mono-variable.css"> <!-- Flag icons for language selector --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" /> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Comic+Relief:wght@400;700&display=swap" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script> <script src="https://cdn.auth0.com/js/auth0-spa-js/2.0/auth0-spa-js.production.js"></script> <!-- TODO: Give Page Improvements (2026.01.03) ========================================= [x] 1. Shop module: Show multiple images per product before switching to next product - Cycle through all product images with a faster interval (2.5s) - Only switch to next product after showing all images - Added image counter dots indicator [x] 2. Shop module: Product descriptions - support newlines and proper spacing - Added white-space: pre-line for newline support - Increased line-height to 1.6 and margin - Increased truncation limit to 150 chars [x] 3. SOLD badges: Change from pink to red/white in red box - Background: #d32f2f (red) - Text: white - Style: bold badge with padding and border-radius [x] 4. KidLisp panel links: Change from aesthetic.computer/$code to kidlisp.com/$code - Updated pieceUrl generation - Updated QR code URL generation All tasks completed! 🎉 --> <style> /* Dark mode (default) */ :root { --bg: #1a1a2e; --text: #e8e8e8; --dim: #888; --pink: #ff6b9d; --cyan: #4ecdc4; --gold: #ffd93d; --green: #6bcb77; --box-bg: rgba(255,255,255,0.03); --box-border: rgba(255,255,255,0.1); --mono: 'Berkeley Mono Variable', 'Menlo', monospace; /* Light mode specific overrides */ --shadow-soft: 0 2px 8px rgba(0,0,0,0.15); --shadow-medium: 0 4px 16px rgba(0,0,0,0.2); --input-bg: rgba(255,255,255,0.05); --slider-track: linear-gradient(90deg, #ff6b6b, #feca57, #48dbfb, #ff9ff3, #54a0ff, #5f27cd, #00d2d3); }
/* Light mode theme */ body.light-mode { --bg: #f5f5f5; --text: #1a1a2e; --dim: #666; --pink: rgb(205, 92, 155); --cyan: #0891b2; --gold: #d97706; --green: #059669; --box-bg: rgba(0,0,0,0.03); --box-border: rgba(0,0,0,0.12); --shadow-soft: 0 2px 8px rgba(0,0,0,0.08); --shadow-medium: 0 4px 16px rgba(0,0,0,0.12); --input-bg: white; --slider-track: linear-gradient(90deg, #f87171, #fbbf24, #22d3ee, #f472b6, #60a5fa, #8b5cf6, #06b6d4); }
* { box-sizing: border-box; }
html { overflow-x: hidden; scrollbar-gutter: stable; /* Reserve space for scrollbar on right only */ }
body { font-family: var(--mono); background: var(--bg); color: var(--text); margin: 0; padding: 2em; min-height: 100vh; font-size: 14px; line-height: 1.6; overflow-x: hidden; -webkit-text-size-adjust: 100%; /* Prevent Safari from inflating fonts */ text-size-adjust: 100%; user-select: none; -webkit-user-select: none; }
/* Allow text selection on captions, links, and copyable content */ .module-caption, .link-block a, a[href], .shop-product-desc, .kidlisp-desc, .prose, .invest-content, input, textarea { user-select: text; -webkit-user-select: text; }
/* Top bar with logo and language chooser */ .top-bar { position: fixed; top: 0; left: 0; width: 100%; max-width: 100vw; display: flex; justify-content: space-between; align-items: center; padding: 10px 16px; z-index: 1000; background: linear-gradient(to bottom, var(--bg) 0%, var(--bg) 60%, transparent 100%); padding-bottom: 20px; }
/* Simple gift label */ .logo { font-size: 18px; cursor: pointer; user-select: none; }
.logo:hover { opacity: 0.8; }
.logo-give { font-family: var(--mono); font-weight: 500; color: var(--text); }
.logo-ac { font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 1.15em; color: var(--pink); }
.logo-dot { color: var(--cyan); }
.logo-sep { color: var(--dim); margin: 0 0.3em; font-family: var(--mono); }
.logo-year { font-family: 'YWFTProcessing-Regular', sans-serif; color: var(--pink); font-size: 1.15em; margin-left: 0.3em; }
main { max-width: 1200px; margin: 0 auto; padding-top: 2.5em; overflow: visible; }
/* Grid layout for wider screens */ .content-grid { display: grid; grid-template-columns: 1fr; max-width: 100%; gap: 1em; }
@media (min-width: 700px) { .content-grid { grid-template-columns: repeat(2, 1fr); } .content-grid .full-width { grid-column: 1 / -1; } }
@media (min-width: 1000px) { .content-grid { grid-template-columns: repeat(3, 1fr); } .content-grid .span-2 { grid-column: span 2; } }
h1 { font-family: 'YWFTProcessing-Regular', sans-serif; font-weight: normal; font-size: 1.8em; margin: 0 0 0.5em 0; color: var(--text); grid-column: 1 / -1; }
h1::before { content: "❄ "; opacity: 0.6; }
h1::after { content: " ✦"; opacity: 0.6; }
h1 .bounce-dot { display: inline-block; font-weight: 900; font-size: 1.1em; color: var(--pink); animation: dotBounce 1.2s ease-in-out 3; }
@keyframes dotBounce { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-6px) scale(1.2); } }
.holiday-banner { font-family: var(--mono); font-size: 0.75em; color: var(--dim); padding: 0.6em; background: var(--box-bg); border: 1px solid var(--box-border); border-radius: 3px; text-align: center; grid-column: 1 / -1; }
.holiday-banner span { color: var(--gold); }
/* Prose blocks - language-switchable */ .prose { background: var(--box-bg); border: 1px solid var(--box-border); border-radius: 4px; padding: 1em 1.2em; font-size: 0.9em; line-height: 1.7; }
.prose.full-width { grid-column: 1 / -1; }
/* Why Give + Bills transparency bridge */ .why-give-section { grid-column: 1 / -1; background: var(--box-bg); border: 1px solid var(--box-border); border-left: 3px solid var(--pink); border-radius: 6px; padding: 0.9em 1.1em; font-size: 0.9em; line-height: 1.7; }
.why-give-title { font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 1.1em; color: var(--pink); margin-bottom: 0.3em; letter-spacing: 0.02em; }
.why-give-section .why-link { color: var(--gold); font-weight: 700; text-decoration: none; border-bottom: 1px dotted currentColor; }
.why-give-section .why-link:hover { text-decoration: underline; }
/* Language content visibility - All non-English hidden by default */ /* Exclude dropdown options from language hiding */ [data-lang="da"]:not(.lang-option):not(.curr-option), [data-lang="zh"]:not(.lang-option):not(.curr-option), [data-lang="de"]:not(.lang-option):not(.curr-option), [data-lang="es"]:not(.lang-option):not(.curr-option) { display: none !important; } [data-lang="en"]:not(.lang-option):not(.curr-option) { display: block; } /* When Danish is selected */ body.lang-da [data-lang="da"]:not(.lang-option):not(.curr-option) { display: block !important; } body.lang-da [data-lang="en"]:not(.lang-option):not(.curr-option), body.lang-da [data-lang="zh"]:not(.lang-option):not(.curr-option), body.lang-da [data-lang="de"]:not(.lang-option):not(.curr-option), body.lang-da [data-lang="es"]:not(.lang-option):not(.curr-option) { display: none !important; } /* When Chinese is selected */ body.lang-zh [data-lang="zh"]:not(.lang-option):not(.curr-option) { display: block !important; } body.lang-zh [data-lang="en"]:not(.lang-option):not(.curr-option), body.lang-zh [data-lang="da"]:not(.lang-option):not(.curr-option), body.lang-zh [data-lang="de"]:not(.lang-option):not(.curr-option), body.lang-zh [data-lang="es"]:not(.lang-option):not(.curr-option) { display: none !important; } /* When German is selected */ body.lang-de [data-lang="de"]:not(.lang-option):not(.curr-option) { display: block !important; } body.lang-de [data-lang="en"]:not(.lang-option):not(.curr-option), body.lang-de [data-lang="da"]:not(.lang-option):not(.curr-option), body.lang-de [data-lang="zh"]:not(.lang-option):not(.curr-option), body.lang-de [data-lang="es"]:not(.lang-option):not(.curr-option) { display: none !important; }
/* When Spanish is selected */ body.lang-es [data-lang="es"]:not(.lang-option):not(.curr-option) { display: block !important; } body.lang-es [data-lang="en"]:not(.lang-option):not(.curr-option), body.lang-es [data-lang="da"]:not(.lang-option):not(.curr-option), body.lang-es [data-lang="zh"]:not(.lang-option):not(.curr-option), body.lang-es [data-lang="de"]:not(.lang-option):not(.curr-option) { display: none !important; }
/* Link description spans */ .link-desc { display: inline; } .link-desc[data-lang="da"], .link-desc[data-lang="zh"], .link-desc[data-lang="de"], .link-desc[data-lang="es"] { display: none !important; } body.lang-da .link-desc[data-lang="da"] { display: inline !important; } body.lang-da .link-desc[data-lang="en"], body.lang-da .link-desc[data-lang="zh"], body.lang-da .link-desc[data-lang="de"], body.lang-da .link-desc[data-lang="es"] { display: none !important; } body.lang-zh .link-desc[data-lang="zh"] { display: inline !important; } body.lang-zh .link-desc[data-lang="en"], body.lang-zh .link-desc[data-lang="da"], body.lang-zh .link-desc[data-lang="de"], body.lang-zh .link-desc[data-lang="es"] { display: none !important; } body.lang-de .link-desc[data-lang="de"] { display: inline !important; } body.lang-de .link-desc[data-lang="en"], body.lang-de .link-desc[data-lang="da"], body.lang-de .link-desc[data-lang="zh"], body.lang-de .link-desc[data-lang="es"] { display: none !important; } body.lang-es .link-desc[data-lang="es"] { display: inline !important; } body.lang-es .link-desc[data-lang="en"], body.lang-es .link-desc[data-lang="da"], body.lang-es .link-desc[data-lang="zh"], body.lang-es .link-desc[data-lang="de"] { display: none !important; }
/* Feature module list items */ .feature-module li[data-lang="da"], .feature-module li[data-lang="zh"], .feature-module li[data-lang="de"], .feature-module li[data-lang="es"] { display: none !important; } body.lang-da .feature-module li[data-lang="da"] { display: block !important; } body.lang-da .feature-module li[data-lang="en"] { display: none !important; } body.lang-zh .feature-module li[data-lang="zh"] { display: block !important; } body.lang-zh .feature-module li[data-lang="en"] { display: none !important; } body.lang-de .feature-module li[data-lang="de"] { display: block !important; } body.lang-de .feature-module li[data-lang="en"] { display: none !important; } body.lang-es .feature-module li[data-lang="es"] { display: block !important; } body.lang-es .feature-module li[data-lang="en"] { display: none !important; }
a { color: var(--cyan); text-decoration: none; } a:hover { text-decoration: underline; }
a.handle-link { color: var(--pink); }
code { background: var(--box-bg); padding: 0.15em 0.4em; border-radius: 2px; color: var(--pink); }
code a { color: var(--pink); }
/* Stats Section */ .stats-section { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.6em; grid-column: 1 / -1; }
@media (min-width: 800px) { .stats-section { grid-template-columns: repeat(6, 1fr); } }
.stat-item { text-align: center; padding: 0.6em 0.4em; background: var(--stat-bg, var(--box-bg)); border: 1px solid var(--box-border); border-radius: 4px; display: flex; flex-direction: column; justify-content: center; align-items: center; overflow: hidden; position: relative; user-select: none; min-height: 60px; }
#stat-handles { --stat-bg: rgba(100, 200, 255, 0.08); } #stat-paintings { --stat-bg: rgba(255, 180, 100, 0.08); } #stat-moods { --stat-bg: rgba(255, 120, 180, 0.08); } #stat-kidlisp { --stat-bg: rgba(180, 100, 255, 0.08); } #stat-commands { --stat-bg: rgba(100, 255, 150, 0.08); } #stat-messages { --stat-bg: rgba(255, 220, 100, 0.08); }
.stat-value { font-family: 'YWFTProcessing-Regular', var(--mono); font-size: 1.8em; color: var(--gold); position: relative; z-index: 2; text-shadow: 0 2px 6px rgba(0, 0, 0, 0.8), 0 1px 2px rgba(0, 0, 0, 0.9); }
.stat-label { font-size: 0.9em; font-weight: 600; color: var(--dim); margin-top: 0.2em; position: relative; z-index: 2; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.7); }
.stat-sigil { color: var(--cyan); margin-right: 0.1em; }
/* Floating items in stat boxes */ .stat-floaters { position: absolute; inset: 0; overflow: hidden; z-index: 1; pointer-events: none; }
.stat-floater { position: absolute; display: flex; flex-direction: column; align-items: center; gap: 2px; animation: floatUp var(--float-duration, 12s) linear forwards; opacity: 0.35; }
.stat-floater.float-horizontal { animation: floatLeft var(--float-duration, 12s) linear forwards; }
.stat-floater.float-horizontal-right { animation: floatRight var(--float-duration, 12s) linear forwards; }
.stat-floater img { object-fit: contain; border-radius: 2px; image-rendering: pixelated; }
.stat-floater .floater-code { font-family: var(--mono); font-size: 0.5em; color: var(--dim); white-space: nowrap; }
@keyframes floatUp { 0% { bottom: -20px; } 100% { bottom: 120%; } }
@keyframes floatLeft { 0% { right: -50px; } 100% { right: 120%; } }
@keyframes floatRight { 0% { left: -80px; } 100% { left: 120%; } }
/* Gives Feed */ .recent-gives { grid-column: 1 / -1; background: var(--box-bg); border: 1px solid var(--box-border); border-radius: 8px; padding: 1em; overflow: hidden; }
/* Gives Ticker - horizontal scrolling marquee */ .gives-ticker { background: linear-gradient(180deg, rgba(0, 40, 20, 0.4) 0%, rgba(0, 20, 10, 0.5) 100%); border: 1px solid rgba(100, 255, 150, 0.2); border-bottom: none; border-radius: 8px 8px 0 0; margin-bottom: 0; position: relative; height: 2.4em; overflow: hidden; cursor: grab; user-select: none; }
.gives-ticker.dragging { cursor: grabbing; }
.gives-ticker.dragging .gives-ticker-track { animation: none !important; }
.gives-ticker-track { position: absolute; top: 0; left: 0; display: flex; align-items: center; height: 100%; white-space: nowrap; animation: ticker-scroll 30s linear infinite; }
/* Pause animation when not visible */ .gives-ticker.paused .gives-ticker-track { animation-play-state: paused; }
@keyframes ticker-scroll { 0% { transform: translateX(0); } 100% { transform: translateX(-33.333%); } } @keyframes tickerScroll { 0% { transform: translateX(0%); } 100% { transform: translateX(-50%); } }
.gives-ticker-item { display: inline-flex; align-items: center; padding: 0 1.5em; font-size: 0.85em; border-right: 1px solid rgba(100, 255, 150, 0.15); gap: 0.35em; }
.gives-ticker-item .got { color: var(--cyan); font-weight: 500; }
.gives-ticker-item .amount { color: var(--green); font-weight: bold; }
.gives-ticker-item .time { color: var(--dim); font-style: italic; }
.gives-ticker-item .note { color: var(--gold); }
.gives-ticker-item .sep { color: var(--dim); }
/* Monthly/recurring gives - yellow theme */ .gives-ticker-item.monthly { border-right-color: rgba(255, 215, 100, 0.2); }
.gives-ticker-item.monthly .got { color: var(--gold); }
.gives-ticker-item.monthly .amount { color: var(--gold); }
.gives-ticker-item.monthly .time { color: var(--gold); opacity: 0.7; }
.gives-ticker-empty { display: flex; align-items: center; justify-content: center; height: 100%; min-height: 2.4em; color: var(--dim); font-size: 0.9em; } /* CTA as ticker item (scrolling) */ .gives-ticker-cta-item { color: var(--cyan); padding: 0 2em !important; white-space: nowrap; } .gives-ticker-cta-item .pink-handle { color: var(--pink); text-decoration: none; padding: 0 0.3em; } .gives-ticker-cta-item .pink-handle:hover { text-decoration: underline; }
.gives-ticker-empty a { color: var(--gold); text-decoration: none; margin-left: 0.3em; }
.gives-ticker-empty a:hover { text-decoration: underline; }
/* Hide old gives styles */ .recent-gives { display: none; }
.recent-gives-header { display: none; }
.gives-list { display: none; }
.give-item { display: none; }
.gives-loading { text-align: center; color: var(--dim); font-size: 0.85em; padding: 1em; }
/* Unified module loading spinner */ .module-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 0.5em; color: rgba(255, 255, 255, 0.4); font-family: var(--mono); font-size: 0.8em; }
.module-loading::before { content: ""; width: 24px; height: 24px; border: 2px solid rgba(255, 255, 255, 0.1); border-top-color: rgba(255, 255, 255, 0.4); border-radius: 50%; animation: moduleSpinner 0.8s linear infinite; }
@keyframes moduleSpinner { to { transform: rotate(360deg); } }
.module-loading.hidden { display: none; }
/* Module caption - positioned at bottom of content boxes */ .module-caption { position: absolute; bottom: 0; left: 0; right: 0; padding: 0.8em 1em; font-size: 0.75em; color: var(--dim); background: inherit; z-index: 2; }
.module-caption .highlight { color: var(--yellow); font-family: var(--mono); }
.cmd-highlight { color: var(--pink); font-family: var(--mono); }
/* Fiat Currency Section */ .fiat-section { background: transparent; border: none; border-radius: 0; padding: 0; }
.fiat-section.full-width { grid-column: 1 / -1; }
.fiat-header { display: none; }
.fiat-header h2 { font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 1em; margin: 0; color: var(--cyan); }
/* Language and Currency selectors */ .selector-group { display: flex; align-items: center; gap: 12px; }
/* Language dropdown (compact, like kidlisp.com) */ .lang-selector { position: relative; display: flex; align-items: center; gap: 4px; padding: 6px 10px; cursor: pointer; font-size: 12px; user-select: none; border-radius: 4px; transition: background 0.15s; }
.lang-selector:hover { background: var(--box-bg); }
.lang-selector .lang-flag { font-size: 14px; } .lang-selector .lang-text { font-size: 12px; color: var(--dim); font-weight: 500; } .lang-selector .lang-arrow { font-size: 8px; color: var(--dim); opacity: 0.6; margin-left: 2px; }
.lang-dropdown { position: absolute; top: 100%; left: 0; background: var(--bg); border: 1px solid var(--box-border); border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 100; min-width: 100px; margin-top: 2px; display: none; }
.lang-selector.open .lang-dropdown { display: block; }
.lang-option { padding: 8px 12px; cursor: pointer; font-size: 12px; color: var(--text); transition: all 0.15s; }
.lang-option:hover { background: rgba(232, 74, 138, 0.1); color: var(--pink); }
.lang-option:first-child { border-radius: 4px 4px 0 0; } .lang-option:last-child { border-radius: 0 0 4px 4px; }
/* Currency link selector */ .currency-links { display: flex; align-items: center; gap: 16px; margin-right: calc(max(0px, (100vw - 1200px) / 2)); }
.curr-link { font-size: 13px; color: var(--dim); cursor: pointer; transition: color 0.15s; text-decoration: none; font-weight: 500; }
.curr-link:hover { color: var(--text); }
.curr-link.active { color: var(--pink); }
.curr-link .curr-flag { margin-right: 4px; }
/* Different active colors per currency */ .curr-link.active[data-curr="usd"] { color: var(--green); } .curr-link.active[data-curr="dkk"] { color: var(--cyan); } .curr-link.active[data-curr="crypto"] { color: #a78bfa; } .curr-link.active[data-curr="paypal"] { color: #0070ba; } .curr-link.active[data-curr="liberapay"] { color: #f6c915; }
@media (max-width: 600px) { body { padding: 1em; padding-right: calc(1em + 10px); font-size: 13px; } .top-bar { padding: 6px 10px; background: linear-gradient(to bottom, rgba(5, 15, 20, 0.95) 0%, rgba(5, 15, 20, 0.85) 50%, transparent 100%); flex-wrap: nowrap; padding-bottom: 16px; } .logo { font-size: 13px; white-space: nowrap; } /* Hide "aesthetic.computer" on very small screens, just show "give · AC '26" */ .logo-ac { display: none; } .logo-year { display: none; } .logo::after { content: "AC '26"; font-family: 'YWFTProcessing-Regular', sans-serif; color: var(--pink); font-size: 1.15em; } main { padding-top: 4em; } .gives-ticker { height: 2.1em; } .gives-ticker-item { font-size: 0.75em; padding: 0 1em; } .currency-links { gap: 10px; } .curr-link { font-size: 12px; } /* Show labels on mobile too */ .lang-selector .lang-text { display: none; } }
/* Hide old dropdown styles */ .dropdown-selector { display: none !important; } border: 1px solid var(--box-border); border-radius: 4px; display: none; min-width: 100px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 100; }
.dropdown-selector.open .dropdown-menu { display: block; }
.dropdown-option { display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; font-size: 12px; color: var(--text); }
.dropdown-option:hover { background: var(--box-bg); }
.dropdown-option.active { color: var(--pink); font-weight: bold; }
.currency-pickers { width: 100%; max-width: 100%; overflow: hidden; }
.currency-picker { padding: 0; max-width: 100%; overflow: hidden; }
.currency-picker h3 { font-family: var(--mono); font-size: 0.85em; margin: 0 0 0.8em 0; color: var(--dim); }
.gift-widget { display: flex; flex-direction: column; gap: 0; }
.gift-visual { position: relative; display: flex; justify-content: center; align-items: center; padding: 0; /* No padding - slideshow fills completely */ background: linear-gradient(135deg, rgba(205, 92, 155, 0.1) 0%, rgba(0, 255, 255, 0.05) 100%); border: 1px solid var(--box-border); border-top: none; border-bottom: none; border-radius: 0; /* Safari mobile fix for border-radius */ -webkit-mask-image: -webkit-radial-gradient(white, black); isolation: isolate; overflow: hidden; /* Maintain aspect ratio when page narrows */ aspect-ratio: 21 / 9; min-height: 0; /* Loading state - starts blurred */ filter: blur(8px) brightness(0.7); transition: filter 0.8s ease-out; }
.gift-visual.loaded { filter: blur(0) brightness(1); }
/* Ticker wrapper with superscript stats above */ .ticker-stats-row { position: relative; margin-bottom: 0; }
.ticker-center { width: 100%; }
.ticker-subscripts { display: flex; justify-content: space-between; gap: 1em; margin-bottom: 0.4em; padding-left: 0.25em; padding-right: 0.25em; font-family: var(--mono); font-size: 0.85em; }
.ticker-stat-badge { display: flex; align-items: center; gap: 0.3em; opacity: 0; transition: opacity 0.4s ease; }
.ticker-stat-badge.visible { opacity: 1; }
.ticker-stat-badge.gives-badge .ticker-stat-value { color: var(--green); }
.ticker-stat-badge.monthly-badge .ticker-stat-value { color: var(--gold); }
.ticker-stat-value { font-weight: bold; }
.ticker-stat-label { color: var(--dim); text-transform: lowercase; }
.ticker-stat-amount { display: inline; font-size: 1em; color: var(--dim); font-weight: normal; }
.ticker-stat-amount .got { color: var(--cyan); font-weight: 500; }
.ticker-stat-amount .amount-value { color: var(--green); font-weight: bold; }
.ticker-stat-amount .count-value { color: var(--cyan); font-weight: 500; }
.ticker-stat-amount .so-far { color: var(--dim); font-weight: normal; }
.gift-logo-wrap { display: none; /* Rendered on canvas instead */ }
.gift-logo { /* Fill the wrap container */ width: 100%; height: auto; aspect-ratio: 1; opacity: 1; filter: drop-shadow(0 6px 20px rgba(0, 0, 0, 0.7)) drop-shadow(0 3px 8px rgba(0, 0, 0, 0.5)) brightness(1.2); transition: opacity 0.4s ease-out, transform 0.6s cubic-bezier(0.4, 0, 0.2, 1), filter 0.4s ease-out; transform-origin: bottom left; animation: logo-white-blink 3s ease-in-out infinite; display: block; }
@keyframes logo-white-blink { 0%, 85%, 100% { filter: drop-shadow(0 6px 20px rgba(0, 0, 0, 0.7)) drop-shadow(0 3px 8px rgba(0, 0, 0, 0.5)) brightness(1.2); } 90%, 95% { filter: drop-shadow(0 6px 20px rgba(0, 0, 0, 0.7)) drop-shadow(0 3px 8px rgba(0, 0, 0, 0.5)) brightness(2) drop-shadow(0 0 25px rgba(255,255,255,0.8)); } }
.gift-widget:hover .gift-logo:not(.intensity-1):not(.intensity-2):not(.intensity-3):not(.intensity-4) { opacity: 1; transform: scale(1.05); }
.gift-amount { position: absolute; /* Bottom-left of logo */ bottom: -5%; left: -10%; font-family: 'YWFTProcessing-Regular', var(--mono); font-size: clamp(1.2em, 2.2vw, 2.5em); color: #5a9a5a; background: linear-gradient(135deg, #1a2a1a 0%, #0a1a0a 100%); border: 2px solid #ff0080; border-radius: 4px; padding: 0.25em 0.45em; box-shadow: 0 2px 6px rgba(0,0,0,0.3); white-space: nowrap; transition: background 0.4s ease-out, border 0.4s ease-out; }
.gift-amount .wiggle-char { display: inline-block; transition: color 0.1s; } /* DKK space - uses margin instead of font space character (YWFT renders space as +) */ .gift-amount .wiggle-char.dkk-space { width: 0.5em; }
/* Logo intensity animations - shrinks like zooming out, maintains tight shadow, adds colored moving shadows */ .gift-logo.intensity-1 { animation: logo-pulse-1 2s ease-in-out infinite, logo-shadow-1 3s ease-in-out infinite; }
.gift-logo.intensity-2 { animation: logo-pulse-2 1.5s ease-in-out infinite, logo-shadow-2 2s ease-in-out infinite; }
.gift-logo.intensity-3 { animation: logo-pulse-3 1s ease-in-out infinite, logo-shadow-3 1.5s linear infinite; }
.gift-logo.intensity-4 { animation: logo-shake 0.3s ease-in-out infinite, logo-shadow-4 0.8s linear infinite; }
/* Intensity 1: Brightest, lightest shadow */ @keyframes logo-pulse-1 { 0%, 100% { opacity: 1; } 50% { opacity: 1; filter: brightness(1.3); } } @keyframes logo-shadow-1 { 0%, 100% { filter: drop-shadow(0 4px 8px rgba(0,0,0,0.5)) brightness(1.2); } 50% { filter: drop-shadow(0 4px 12px rgba(0,0,0,0.4)) brightness(1.3); } }
/* Intensity 2: Bright with medium shadow */ @keyframes logo-pulse-2 { 0%, 100% { opacity: 1; } 50% { opacity: 1; filter: brightness(1.15); } } @keyframes logo-shadow-2 { 0%, 100% { filter: drop-shadow(0 5px 12px rgba(0,0,0,0.5)) drop-shadow(0 0 15px rgba(255,215,0,0.3)) brightness(1.1); } 50% { filter: drop-shadow(0 5px 12px rgba(0,0,0,0.5)) drop-shadow(0 0 20px rgba(0,229,255,0.3)) brightness(1.15); } }
/* Intensity 3: Darker with heavier shadows */ @keyframes logo-pulse-3 { 0%, 100% { opacity: 0.95; } 50% { opacity: 1; } } @keyframes logo-shadow-3 { 0% { filter: drop-shadow(0 6px 15px rgba(0,0,0,0.6)) drop-shadow(0 0 20px #ff6b6b) brightness(1); } 33% { filter: drop-shadow(0 6px 15px rgba(0,0,0,0.6)) drop-shadow(0 0 20px #4ecdc4) brightness(1); } 66% { filter: drop-shadow(0 6px 15px rgba(0,0,0,0.6)) drop-shadow(0 0 20px #ffd93d) brightness(1); } 100% { filter: drop-shadow(0 6px 15px rgba(0,0,0,0.6)) drop-shadow(0 0 20px #ff6b6b) brightness(1); } }
/* Intensity 4: Super bright pink electric saturated */ @keyframes logo-shadow-4 { 0% { filter: drop-shadow(0 0 25px #ff0080) drop-shadow(0 0 40px #ff00ff) brightness(1.4) saturate(1.5); } 25% { filter: drop-shadow(0 0 30px #ff00ff) drop-shadow(0 0 40px #00e5ff) brightness(1.5) saturate(1.6); } 50% { filter: drop-shadow(0 0 25px #00e5ff) drop-shadow(0 0 40px #ffd700) brightness(1.4) saturate(1.5); } 75% { filter: drop-shadow(0 0 30px #ffd700) drop-shadow(0 0 40px #ff0080) brightness(1.5) saturate(1.6); } 100% { filter: drop-shadow(0 0 25px #ff0080) drop-shadow(0 0 40px #ff00ff) brightness(1.4) saturate(1.5); } }
@keyframes logo-shake { 0%, 100% { transform: translate(0, 0); } 20% { transform: translate(-4px, 3px); } 40% { transform: translate(4px, -3px); } 60% { transform: translate(-3px, -4px); } 80% { transform: translate(3px, 4px); } }
/* Vegas intensity levels for amount */ @keyframes vegas-pulse { 0%, 100% { transform: scale(1); filter: brightness(1); } 50% { transform: scale(1.05); filter: brightness(1.2); } }
@keyframes vegas-glow { 0%, 100% { box-shadow: 0 0 12px rgba(255,215,0,0.6); } 50% { box-shadow: 0 0 20px rgba(255,215,0,0.8); } }
@keyframes vegas-rainbow { 0% { border-color: #ff6b6b; box-shadow: 0 0 15px #ff6b6b; } 16% { border-color: #ffd93d; box-shadow: 0 0 15px #ffd93d; } 33% { border-color: #6bcb77; box-shadow: 0 0 15px #6bcb77; } 50% { border-color: #4ecdc4; box-shadow: 0 0 15px #4ecdc4; } 66% { border-color: #9b59b6; box-shadow: 0 0 15px #9b59b6; } 83% { border-color: #ff6b9d; box-shadow: 0 0 15px #ff6b9d; } 100% { border-color: #ff6b6b; box-shadow: 0 0 15px #ff6b6b; } }
@keyframes vegas-shake { 0%, 100% { transform: translate(-0.5em, -50%) scale(1.6); } 10%, 30%, 50%, 70%, 90% { transform: translate(calc(-0.5em - 2px), -50%) scale(1.6); } 20%, 40%, 60%, 80% { transform: translate(calc(-0.5em + 2px), -50%) scale(1.6); } }
.gift-amount.intensity-1 { top: 15%; right: -25%; transform: translateY(-50%); color: #fff; background: linear-gradient(135deg, #1a4a2a 0%, #0a2a1a 100%); border-color: #3a7a4a; }
.gift-amount.intensity-2 { top: 15%; right: -25%; transform: translateY(-50%); color: #ffd700; background: linear-gradient(135deg, #4a3a1a 0%, #3a2a0a 100%); border-color: #8a7a3a; }
.gift-amount.intensity-3 { top: 15%; right: -25%; transform: translateY(-50%); color: #fff; background: linear-gradient(135deg, #5a2a2a 0%, #4a1a1a 100%); border-color: #ff6b6b; }
.gift-amount.intensity-4 { top: 10%; right: -30%; transform: translateY(-50%); color: #fff; font-weight: bold; text-shadow: 0 0 10px #ffd700; background: linear-gradient(135deg, #6a1a3a 0%, #4a0a2a 100%); border-color: #ff0080; }
@keyframes price-blink-insane { 0% { background: linear-gradient(135deg, #ff0000 0%, #000 100%); color: #fff; text-shadow: 0 0 20px #fff, 0 0 40px #fff; font-weight: 900; } 25% { background: linear-gradient(135deg, #fff 0%, #ff0080 100%); color: #000; text-shadow: 0 0 15px #ff0080, 0 0 30px #ff0080; font-weight: 900; } 50% { background: linear-gradient(135deg, #000 0%, #ff0000 100%); color: #ff0; text-shadow: 0 0 25px #ff0, 0 0 50px #ff0; font-weight: 900; } 75% { background: linear-gradient(135deg, #ff0080 0%, #00e5ff 100%); color: #fff; text-shadow: 0 0 20px #00e5ff, 0 0 40px #ff0080; font-weight: 900; } 100% { background: linear-gradient(135deg, #ff0000 0%, #000 100%); color: #fff; text-shadow: 0 0 20px #fff, 0 0 40px #fff; font-weight: 900; } }
@keyframes price-blink { 0%, 100% { background: linear-gradient(135deg, #6a1a3a 0%, #4a0a2a 100%); } 33% { background: linear-gradient(135deg, #ff0080 0%, #6a1a3a 100%); } 66% { background: linear-gradient(135deg, #fff 0%, #ff0080 100%); } }
@keyframes vegas-shake-mild { 0%, 100% { transform: translateY(-50%) rotate(0deg); } 25% { transform: translateY(calc(-50% - 1px)) rotate(0.5deg); } 50% { transform: translateY(calc(-50% + 1px)) rotate(-0.5deg); } 75% { transform: translateY(calc(-50% + 1px)) rotate(0.3deg); } }
@keyframes vegas-shake-wild { 0%, 100% { transform: translateY(-50%) rotate(0deg); } 10% { transform: translate(3px, calc(-50% - 2px)) rotate(2deg); } 20% { transform: translate(-4px, calc(-50% + 3px)) rotate(-3deg); } 30% { transform: translate(2px, calc(-50% - 4px)) rotate(2deg); } 40% { transform: translate(-3px, calc(-50% + 2px)) rotate(-2deg); } 50% { transform: translate(4px, calc(-50% + 3px)) rotate(3deg); } 60% { transform: translate(-2px, calc(-50% - 3px)) rotate(-1deg); } 70% { transform: translate(3px, calc(-50% + 4px)) rotate(2deg); } 80% { transform: translate(-4px, calc(-50% - 2px)) rotate(-3deg); } 90% { transform: translate(2px, calc(-50% + 2px)) rotate(1deg); } }
@keyframes vegas-rainbow-fast { 0% { border-color: #ff6b6b; box-shadow: 0 0 20px #ff6b6b; } 14% { border-color: #ffd93d; box-shadow: 0 0 20px #ffd93d; } 28% { border-color: #6bcb77; box-shadow: 0 0 20px #6bcb77; } 42% { border-color: #4ecdc4; box-shadow: 0 0 20px #4ecdc4; } 57% { border-color: #9b59b6; box-shadow: 0 0 20px #9b59b6; } 71% { border-color: #ff6b9d; box-shadow: 0 0 20px #ff6b9d; } 85% { border-color: #00e5ff; box-shadow: 0 0 20px #00e5ff; } 100% { border-color: #ff6b6b; box-shadow: 0 0 20px #ff6b6b; } }
/* Flames effect for max monthly ($500/mo) */ .gift-amount.flames::before { content: '🔥'; position: absolute; top: -1.2em; left: 0; font-size: 0.8em; }
.gift-amount.flames::after { content: '🔥'; position: absolute; top: -1.1em; right: 0; font-size: 0.7em; }
@keyframes flame-dance { 0% { transform: translateY(0) scale(1); opacity: 0.9; } 100% { transform: translateY(-3px) scale(1.1); opacity: 1; } }
/* Lightning effect for max one-time ($2048) */ .gift-amount.lightning { border-color: #00ffff; box-shadow: 0 0 10px rgba(0, 255, 255, 0.3); }
@keyframes lightning-flash { 0%, 89%, 100% { box-shadow: 0 2px 8px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.1); } 90%, 92% { box-shadow: 0 0 30px #fff, 0 0 60px #00ffff, 0 0 90px #fff, inset 0 0 20px rgba(255,255,255,0.5); } 94%, 96% { box-shadow: 0 2px 8px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.1); } 97%, 98% { box-shadow: 0 0 40px #fff, 0 0 80px #ffff00, 0 0 120px #fff, inset 0 0 30px rgba(255,255,255,0.8); } }
.gift-amount.lightning.flames::before { content: '⚡'; font-size: 1em; top: -1.3em; }
.gift-amount.lightning.flames::after { content: '⚡'; font-size: 0.9em; top: -1.2em; }
@keyframes lightning-bolt { 0%, 100% { opacity: 0.7; transform: translateY(0) rotate(-5deg); } 50% { opacity: 1; transform: translateY(-2px) rotate(5deg); } }
/* Power of 2 special color effects */ .gift-amount.pow2-1 { color: #88ff88; text-shadow: 0 0 8px #00ff00; } .gift-amount.pow2-2 { color: #00ffcc; text-shadow: 0 0 8px #00ffcc; } .gift-amount.pow2-4 { color: #00ccff; text-shadow: 0 0 10px #00ccff; } .gift-amount.pow2-8 { color: #6699ff; text-shadow: 0 0 10px #6699ff; } .gift-amount.pow2-16 { color: #9966ff; text-shadow: 0 0 12px #9966ff; } .gift-amount.pow2-32 { color: #cc66ff; text-shadow: 0 0 12px #cc66ff; } .gift-amount.pow2-64 { color: #ff66cc; text-shadow: 0 0 14px #ff66cc; } .gift-amount.pow2-128 { color: #ff6699; text-shadow: 0 0 14px #ff6699; } .gift-amount.pow2-256 { color: #ff9933; text-shadow: 0 0 16px #ff9933; } .gift-amount.pow2-512 { color: #ffcc00; text-shadow: 0 0 16px #ffcc00; } .gift-amount.pow2-1024 { color: #ffff00; text-shadow: 0 0 20px #ffff00, 0 0 30px #ffaa00; } .gift-amount.pow2-2048 { color: #ffffff; text-shadow: 0 0 20px #fff, 0 0 40px #ff00ff, 0 0 60px #00ffff; animation: pow2-max 0.5s linear infinite; } @keyframes pow2-max { 0%, 100% { text-shadow: 0 0 20px #fff, 0 0 40px #ff00ff, 0 0 60px #00ffff; } 25% { text-shadow: 0 0 20px #fff, 0 0 40px #00ffff, 0 0 60px #ffff00; } 50% { text-shadow: 0 0 20px #fff, 0 0 40px #ffff00, 0 0 60px #ff00ff; } 75% { text-shadow: 0 0 20px #fff, 0 0 40px #ff00ff, 0 0 60px #00ff00; } }
/* Currency conversion display - to right of price */ .gift-conversion { position: absolute; top: -0.5em; right: -7em; font-family: 'YWFTProcessing-Regular', var(--mono); font-size: 1.1em; color: var(--cyan); white-space: nowrap; }
.gift-conversion:empty { display: none; }
.gift-conversion span { margin: 0; }
.gift-controls { display: flex; flex-direction: column; gap: 0; position: relative; overflow: visible; }
.gift-controls input[type="range"] { -webkit-appearance: none; width: 100%; height: 8px; background: linear-gradient(90deg, #ff6b6b, #feca57, #48dbfb, #ff9ff3, #54a0ff, #5f27cd, #00d2d3); border-radius: 0; outline: none; cursor: pointer; margin: 0; padding: 0 20px; box-sizing: content-box; display: block; position: relative; z-index: 1000; overflow: visible; }
.gift-controls input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 26px; height: 26px; background: var(--green); border-radius: 50%; cursor: grab; border: 2px solid rgba(0,0,0,0.35); box-shadow: 0 2px 8px rgba(0,0,0,0.5), inset 0 2px 4px rgba(255,255,255,0.3); transition: background 0.2s ease, transform 0.1s ease, box-shadow 0.1s ease; transform: scale(1.6); transform-origin: center center; position: relative; z-index: 1001; }
.gift-controls input[type="range"]::-webkit-slider-thumb:hover { box-shadow: 0 3px 10px rgba(0,0,0,0.6), inset 0 2px 4px rgba(255,255,255,0.3); transform: scale(1.7); }
.gift-controls input[type="range"]::-webkit-slider-thumb:active { cursor: grabbing; transform: scale(1.8); box-shadow: 0 4px 12px rgba(0,0,0,0.7), inset 0 2px 4px rgba(255,255,255,0.3); }
.gift-controls input[type="range"]::-moz-range-thumb { width: 26px; height: 26px; background: var(--green); border-radius: 50%; cursor: grab; border: 2px solid rgba(0,0,0,0.35); box-shadow: 0 2px 8px rgba(0,0,0,0.5), inset 0 2px 4px rgba(255,255,255,0.3); transition: background 0.2s ease, transform 0.1s ease, box-shadow 0.1s ease; transform: scale(1.6); transform-origin: center center; position: relative; z-index: 1001; }
.gift-controls input[type="range"]::-moz-range-thumb:hover { box-shadow: 0 3px 10px rgba(0,0,0,0.6), inset 0 2px 4px rgba(255,255,255,0.3); transform: scale(1.5); }
.gift-controls input[type="range"]::-moz-range-thumb:active { cursor: grabbing; transform: scale(1.1); box-shadow: 0 4px 12px rgba(0,0,0,0.7), inset 0 2px 4px rgba(255,255,255,0.3); }
/* Gold thumb for monthly mode */ .gift-widget.monthly-mode input[type="range"]::-webkit-slider-thumb { background: var(--gold); }
.gift-widget.monthly-mode input[type="range"]::-moz-range-thumb { background: var(--gold); }
@keyframes thumbPulseDefault { 0%, 100% { transform: scale(1); box-shadow: 0 2px 6px rgba(0,0,0,0.3), 0 0 0 rgba(78, 205, 196, 0); } 50% { transform: scale(1.15); box-shadow: 0 2px 8px rgba(0,0,0,0.4), 0 0 12px rgba(78, 205, 196, 0.5); } }
/* Slider intensity states */ .gift-controls input[type="range"].intensity-1 { background: linear-gradient(90deg, var(--cyan) 0%, #3dbda8 100%); } .gift-controls input[type="range"].intensity-1::-webkit-slider-thumb { background: #4ecdc4; box-shadow: 0 0 8px rgba(78, 205, 196, 0.5); } .gift-controls input[type="range"].intensity-1::-moz-range-thumb { background: #4ecdc4; box-shadow: 0 0 8px rgba(78, 205, 196, 0.5); }
.gift-controls input[type="range"].intensity-2 { background: linear-gradient(90deg, #4ecdc4 0%, #6bde8a 50%, #a8e063 100%); } .gift-controls input[type="range"].intensity-2::-webkit-slider-thumb { background: #7de88a; transform: scale(1.1); box-shadow: 0 0 12px rgba(125, 232, 138, 0.6); } .gift-controls input[type="range"].intensity-2::-moz-range-thumb { background: #7de88a; transform: scale(1.1); box-shadow: 0 0 12px rgba(125, 232, 138, 0.6); }
.gift-controls input[type="range"].intensity-3 { background: linear-gradient(90deg, #6bde8a 0%, #ffd93d 50%, #ffb347 100%); } .gift-controls input[type="range"].intensity-3::-webkit-slider-thumb { background: #ffc857; transform: scale(1.2); box-shadow: 0 0 16px rgba(255, 200, 87, 0.7); } .gift-controls input[type="range"].intensity-3::-moz-range-thumb { background: #ffc857; transform: scale(1.2); box-shadow: 0 0 16px rgba(255, 200, 87, 0.7); }
.gift-controls input[type="range"].intensity-4 { background: linear-gradient(90deg, #ffb347 0%, #ff6b6b 50%, #ff4757 100%); animation: sliderPulse 0.5s ease-in-out infinite alternate; } .gift-controls input[type="range"].intensity-4::-webkit-slider-thumb { background: linear-gradient(135deg, #ff6b6b 0%, #ff4757 100%); transform: scale(1.35); box-shadow: 0 0 20px rgba(255, 71, 87, 0.8), 0 0 40px rgba(255, 107, 107, 0.4); animation: thumbGlow 0.3s ease-in-out infinite alternate; } .gift-controls input[type="range"].intensity-4::-moz-range-thumb { background: linear-gradient(135deg, #ff6b6b 0%, #ff4757 100%); transform: scale(1.35); box-shadow: 0 0 20px rgba(255, 71, 87, 0.8), 0 0 40px rgba(255, 107, 107, 0.4); animation: thumbGlow 0.3s ease-in-out infinite alternate; }
@keyframes sliderPulse { from { filter: brightness(1); } to { filter: brightness(1.15); } }
@keyframes thumbGlow { from { box-shadow: 0 0 20px rgba(255, 71, 87, 0.8), 0 0 40px rgba(255, 107, 107, 0.4); } to { box-shadow: 0 0 25px rgba(255, 71, 87, 1), 0 0 50px rgba(255, 107, 107, 0.6); } }
/* Gift mode checkbox (monthly repeat) */ .gift-monthly-check { display: flex; align-items: center; gap: 8px; padding: 0.6em 0 0.3em; font-size: 0.85em; color: var(--gold); cursor: pointer; user-select: none; }
.gift-monthly-check input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--gold); cursor: pointer; /* Dark checkbox appearance */ appearance: none; -webkit-appearance: none; background: rgba(0, 0, 0, 0.4); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 3px; position: relative; }
.gift-monthly-check input[type="checkbox"]:checked { background: var(--gold); border-color: var(--gold); }
.gift-monthly-check input[type="checkbox"]:checked::after { content: '✓'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #000; font-size: 12px; font-weight: bold; }
.gift-monthly-check:hover { color: var(--text); }
.gift-monthly-check.checked { color: var(--gold); }
/* Monthly mode styling */ .gift-amount { position: absolute; }
/* Tax deductible note and monthly checkbox area */ .gift-monthly-row { display: flex; align-items: center; justify-content: space-between; width: 100%; margin-top: 0.4em; }
/* Container for monthly checkbox + cancel link */ .gift-monthly-stack { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; gap: 0.6em; }
/* Subscribe as user indicator */ .subscribe-as { font-size: 0.65em; color: var(--dim); opacity: 0.7; padding-left: 0.1em; } .subscribe-as-handle { color: var(--gold); }
/* Login prompt modal (native dialog) */ .login-modal { background: #0a0a0f; border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 4px; padding: 1.5em 2em; max-width: 360px; text-align: center; color: var(--text); font-family: var(--mono); box-shadow: 0 0 0 1px rgba(205, 92, 155, 0.3), 0 8px 32px rgba(0, 0, 0, 0.5); }
.login-modal::backdrop { background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(4px); }
/* Login button blink animation */ .footer-login-btn.blinking { animation: login-btn-blink 0.5s ease-in-out 4; }
@keyframes login-btn-blink { 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 215, 0, 0); } 50% { box-shadow: 0 0 15px 5px rgba(255, 215, 0, 0.7); } }
.login-modal-icon { font-size: 2em; margin-bottom: 0.4em; filter: grayscale(0.2); }
.login-modal-title { font-size: 1em; font-weight: 600; margin-bottom: 0.6em; color: var(--pink); font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.05em; }
.login-modal-text { font-size: 0.85em; color: rgba(255, 255, 255, 0.6); margin-bottom: 1.4em; line-height: 1.5; font-family: var(--mono); }
.login-modal-buttons { display: flex; gap: 0.6em; justify-content: center; }
.login-modal-btn { padding: 0.5em 1.2em; border-radius: 3px; font-size: 0.85em; cursor: pointer; transition: all 0.15s ease; border: 1px solid transparent; font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.03em; }
.login-modal-btn.primary { background: var(--gold); color: #000; font-weight: 600; border-color: var(--gold); }
.login-modal-btn.primary:hover { background: #f5d34a; border-color: #f5d34a; transform: translateY(-1px); }
.login-modal-btn.secondary { background: transparent; color: rgba(255, 255, 255, 0.5); border: 1px solid rgba(255, 255, 255, 0.2); }
.login-modal-btn.secondary:hover { background: rgba(255, 255, 255, 0.05); color: rgba(255, 255, 255, 0.8); border-color: rgba(255, 255, 255, 0.3); }
.cancel-subscription-link { font-size: 0.85em; color: var(--dim); opacity: 0.5; text-decoration: none; align-self: center; padding-top: 0.35em; margin-left: 0.3em; }
.cancel-subscription-link:hover { opacity: 1; text-decoration: underline; }
/* Cancel link styling - below Monthly */ .gift-monthly-stack .cancel-subscription-link::before { content: none; }
.tax-note { font-size: 0.85em; color: var(--dim); text-align: right; align-self: flex-start; margin-top: 0.8em; }
.tax-note .faded { opacity: 0.5; }
.tax-note a { color: var(--gold); text-decoration: none; }
.tax-note a:hover { text-decoration: underline; }
/* Bottom row: monthly checkbox left, fiat note right */ .gift-bottom-row { display: flex; justify-content: space-between; align-items: center; padding: 0.8em 0 0; }
.gift-bottom-row .gift-monthly-check { padding: 0; }
.gift-bottom-row .fiat-note { font-size: 0.7em; color: var(--dim); text-align: right; margin: 0; }
.gift-btn { font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 1.4em; padding: 0.8em 1.5em; background: linear-gradient(180deg, var(--green) 0%, #3a8a4a 100%); color: white; border: 2px solid #5ab06a; border-radius: 0 0 8px 8px; cursor: pointer; width: 100%; transition: all 0.15s; font-weight: normal; letter-spacing: 0.02em; box-shadow: 0 4px 12px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.2); text-shadow: 0 1px 2px rgba(0,0,0,0.3); margin-top: 0; } .gift-btn .price { font-weight: bold; }
.gift-btn:hover { background: linear-gradient(180deg, var(--pink) 0%, #c83a7a 100%); border-color: #ff8ab0; transform: translateY(-1px); box-shadow: 0 6px 16px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.2); }
.gift-btn:active { transform: translateY(1px); box-shadow: 0 2px 8px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.1); }
.gift-btn:disabled { opacity: 0.7; cursor: wait; }
/* Yellow button when monthly mode is active */ .gift-widget.monthly-mode .gift-btn { background: linear-gradient(180deg, var(--gold) 0%, #c9a830 100%); border-color: #ffe066; color: #1a1a2e; text-shadow: 0 1px 0 rgba(255,255,255,0.3); } .gift-widget.monthly-mode .gift-btn:hover { background: linear-gradient(180deg, #ffe066 0%, var(--gold) 100%); border-color: #fff099; } /* Yellow/gold price display when monthly mode is active */ .gift-widget.monthly-mode .gift-amount { color: var(--gold); border-color: var(--gold); background: linear-gradient(135deg, #2a2a1a 0%, #1a1a0a 100%); }
/* Crypto controls - match fiat layout */ .crypto-controls { display: flex; flex-direction: column; gap: 0; padding: 1em; background: var(--box-bg); border: 1px solid var(--box-border); border-top: none; border-radius: 0 0 8px 8px; } /* Compact crypto header with tiny logo */ .crypto-compact .gift-visual { display: none; } .crypto-header { display: flex; align-items: center; gap: 0.5em; padding: 0.6em 1em; background: linear-gradient(135deg, rgba(106, 74, 138, 0.2) 0%, rgba(0, 255, 255, 0.05) 100%); border: 1px solid var(--box-border); border-radius: 0; } .crypto-logo-tiny { width: 24px; height: 24px; } .crypto-title { font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 1em; color: #a78bfa; } .crypto-subtitle { font-family: var(--mono); font-size: 0.8em; color: var(--dim); margin-left: 0.8em; } .crypto-compact .crypto-controls { border-radius: 0 0 8px 8px; } /* Stacked crypto addresses for all-in-one view */ .crypto-controls.crypto-all { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; padding: 0; background: transparent; border: none; } .crypto-controls.crypto-all .crypto-address { padding: 1.2em 0.8em; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; border: none; border-radius: 0; min-height: 160px; } .crypto-controls.crypto-all .crypto-address:first-child { border-radius: 0 0 0 8px; } .crypto-controls.crypto-all .crypto-address:last-child { border-radius: 0 0 8px 0; } /* XTZ - Purple */ .crypto-controls.crypto-all .crypto-address[data-crypto="xtz"] { background: linear-gradient(180deg, rgba(138, 74, 255, 0.3) 0%, rgba(100, 50, 180, 0.4) 100%); } /* ETH - Blue */ .crypto-controls.crypto-all .crypto-address[data-crypto="eth"] { background: linear-gradient(180deg, rgba(74, 138, 255, 0.3) 0%, rgba(50, 100, 180, 0.4) 100%); } /* BTC - Orange */ .crypto-controls.crypto-all .crypto-address[data-crypto="btc"] { background: linear-gradient(180deg, rgba(255, 160, 50, 0.3) 0%, rgba(200, 120, 30, 0.4) 100%); } .crypto-controls.crypto-all .crypto-label { margin-bottom: 0.3em; } .crypto-controls.crypto-all .crypto-symbol { font-size: 2.8em; line-height: 1; } .crypto-controls.crypto-all .crypto-symbol .crypto-icon { width: 48px; height: 48px; vertical-align: middle; filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); } .crypto-controls.crypto-all .crypto-name { font-size: 0.75em; opacity: 0.7; } .crypto-controls.crypto-all .crypto-balance { font-size: 1.1em; margin-bottom: 0.3em; } .crypto-controls.crypto-all .crypto-addr { font-size: 0.8em; padding: 0.5em 0.6em; background: rgba(0, 0, 0, 0.3); border: none; } .crypto-controls.crypto-all .crypto-copy-hint { font-size: 0.65em; margin-top: 0.4em; opacity: 0.6; } @media (max-width: 600px) { .crypto-controls.crypto-all { grid-template-columns: 1fr; max-width: 100%; } .crypto-controls.crypto-all .crypto-address:first-child { border-radius: 0; } .crypto-controls.crypto-all .crypto-address:last-child { border-radius: 0 0 8px 8px; } .crypto-controls.crypto-all .crypto-address { min-height: 120px; padding: 0.8em 0.5em; } .crypto-controls.crypto-all .crypto-addr { font-size: 0.7em; word-break: break-all; } .crypto-header { flex-wrap: wrap; } .crypto-subtitle { display: none; } }
.crypto-address { background: linear-gradient(135deg, rgba(100, 255, 150, 0.08) 0%, rgba(0, 255, 255, 0.05) 100%); border: 2px solid rgba(100, 255, 150, 0.3); padding: 1.2em 1.4em; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; position: relative; overflow: hidden; }
.crypto-address:hover { border-color: var(--green); background: linear-gradient(135deg, rgba(100, 255, 150, 0.15) 0%, rgba(0, 255, 255, 0.1) 100%); transform: scale(1.01); }
.crypto-address:active { transform: scale(0.99); }
.crypto-address.copied { background: var(--green); border-color: var(--green); }
.crypto-address.copied::before { animation: none; }
.crypto-address.copied .crypto-label, .crypto-address.copied .crypto-addr, .crypto-address.copied .crypto-copy-hint, .crypto-address.copied .crypto-balance { color: var(--bg); }
.crypto-label { display: flex; flex-direction: column; align-items: center; font-family: var(--mono); font-weight: 600; color: var(--cyan); margin-bottom: 0.4em; text-align: center; }
.crypto-label .crypto-symbol { font-size: 2.2em; line-height: 1; margin-bottom: 0.1em; }
.crypto-label .crypto-name { font-size: 0.9em; opacity: 0.8; }
.crypto-balance { display: block; font-family: var(--mono); font-size: 1.1em; font-weight: 600; color: var(--gold); text-align: center; margin-bottom: 0.6em; min-height: 1.4em; }
.crypto-balance .loading { color: var(--dim); font-size: 0.9em; }
.crypto-balance .usd-equiv { font-size: 0.8em; color: var(--dim); font-weight: normal; }
.crypto-addr { display: block; font-family: var(--mono); font-size: 0.95em; letter-spacing: 0.03em; color: #00ff88; text-align: center; padding: 0.8em 0.6em; background: rgba(0, 20, 10, 0.8); border-radius: 4px; user-select: all; cursor: pointer; text-shadow: 0 0 8px #00ff8855; border: 1px solid #00ff8833; }
.crypto-addr .addr-dots { color: #00aa66; letter-spacing: 0.1em; }
.crypto-addr:hover { background: rgba(0, 40, 20, 0.9); color: #00ffaa; text-shadow: 0 0 12px #00ffaa88; }
.crypto-copy-hint { display: block; font-size: 0.85em; color: var(--gold); margin-top: 0.6em; text-align: center; font-weight: 500; }
.crypto-address:hover .crypto-copy-hint { color: var(--green); }
.crypto-funds-header { text-align: center; padding: 0.6em 1em; background: linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, rgba(100, 255, 150, 0.05) 100%); border-bottom: 1px solid var(--box-border); font-size: 0.85em; color: var(--dim); }
.crypto-funds-header strong { color: var(--gold); }
.crypto-amount { background: linear-gradient(135deg, #3a2a5a 0%, #2a1a4a 100%); border-color: #6a4a8a; }
.crypto-usd { position: absolute; bottom: 0.5em; right: 0.5em; font-family: var(--mono); font-size: 0.85em; color: var(--gold); opacity: 0.9; }
.gift-widget.crypto .gift-visual { background: linear-gradient(135deg, rgba(106, 74, 138, 0.2) 0%, rgba(0, 255, 255, 0.05) 100%); }
/* PayPal styles - similar to crypto compact layout */ .paypal-compact .gift-visual { display: none; }
.paypal-header { display: flex; align-items: center; gap: 0.5em; padding: 0.6em 1em; background: linear-gradient(135deg, rgba(0, 112, 186, 0.25) 0%, rgba(0, 48, 135, 0.15) 100%); border: 1px solid var(--box-border); border-radius: 0; }
.paypal-logo-tiny { width: 24px; height: 24px; }
.paypal-title { font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 1em; color: #0070ba; }
.paypal-subtitle { font-family: var(--mono); font-size: 0.8em; color: var(--dim); margin-left: 0.8em; }
.paypal-controls { padding: 2em 4em; background: linear-gradient(180deg, rgba(0, 112, 186, 0.1) 0%, rgba(0, 48, 135, 0.15) 100%); border: 1px solid var(--box-border); border-top: none; border-radius: 0 0 8px 8px; display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 3em; width: 100%; box-sizing: border-box; overflow: hidden; }
/* PayPal wrapper for centering on wide screens */ .paypal-compact { display: flex; flex-direction: column; align-items: stretch; }
.paypal-compact .paypal-header { width: 100%; border-radius: 0; }
@media (max-width: 480px) { .paypal-controls { flex-direction: column; padding: 0.8em; gap: 0.8em; max-width: 100%; } .paypal-qr-wrap { padding: 0.6em; } .paypal-qr { width: 100px; height: 100px; } .paypal-actions { width: 100%; max-width: 100%; } .paypal-link-btn { width: 100%; box-sizing: border-box; } .paypal-header { flex-wrap: wrap; } .paypal-subtitle { display: none; } .paypal-email { flex-wrap: wrap; justify-content: center; } .paypal-email-addr { font-size: 0.85em; word-break: break-all; } }
.paypal-qr-wrap { flex-shrink: 0; display: flex; align-items: center; justify-content: center; padding: 0.4em; background: #fff; border-radius: 6px; box-shadow: inset 0 2px 8px rgba(0,0,0,0.15); overflow: hidden; }
.paypal-qr { width: 160px; height: 160px; display: block; transform: scale(1.15); }
.paypal-actions { display: flex; flex-direction: column; gap: 1em; min-width: 280px; }
.paypal-link-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.6em; padding: 0.9em 1.8em; background: linear-gradient(135deg, #0070ba 0%, #003087 100%); color: white; text-decoration: none; border-radius: 24px; font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 1.1em; font-weight: 600; transition: all 0.2s ease; box-shadow: 0 3px 10px rgba(0, 112, 186, 0.3); }
.paypal-link-btn:hover { background: linear-gradient(135deg, #0080d0 0%, #0040a0 100%); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0, 112, 186, 0.4); }
.paypal-link-btn:active { transform: translateY(0); }
.paypal-btn-icon { display: flex; align-items: center; }
.paypal-btn-icon svg { width: 24px; height: 24px; }
.paypal-email { display: flex; flex-direction: row; align-items: center; gap: 0.6em; padding: 0.8em 1.2em; background: rgba(0, 0, 0, 0.2); border-radius: 8px; cursor: pointer; transition: all 0.2s ease; }
.paypal-email:hover { background: rgba(0, 112, 186, 0.15); }
.paypal-email.copied { background: var(--green); }
.paypal-email.copied .paypal-email-label, .paypal-email.copied .paypal-email-addr, .paypal-email.copied .paypal-copy-hint { color: var(--bg); }
.paypal-email-label { font-size: 0.75em; color: var(--dim); white-space: nowrap; }
.paypal-email-addr { font-family: var(--mono); font-size: 0.85em; color: #0070ba; padding: 0.2em 0.5em; background: rgba(0, 112, 186, 0.1); border-radius: 3px; border: 1px solid rgba(0, 112, 186, 0.3); }
.paypal-copy-hint { font-size: 0.7em; color: var(--gold); opacity: 0.8; }
.paypal-email:hover .paypal-copy-hint { color: var(--green); opacity: 1; }
/* Liberapay section */ .liberapay-compact .gift-visual { display: none; }
.liberapay-header { display: flex; align-items: center; gap: 0.5em; padding: 0.6em 1em; background: linear-gradient(135deg, rgba(246, 201, 21, 0.25) 0%, rgba(200, 160, 0, 0.15) 100%); border: 1px solid var(--box-border); border-radius: 0; }
.liberapay-logo-tiny { width: 24px; height: 24px; }
.liberapay-title { font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 1em; color: #f6c915; }
.liberapay-subtitle { font-family: var(--mono); font-size: 0.8em; color: var(--dim); margin-left: 0.8em; }
.liberapay-controls { padding: 2em 4em; background: linear-gradient(180deg, rgba(246, 201, 21, 0.1) 0%, rgba(200, 160, 0, 0.15) 100%); border: 1px solid var(--box-border); border-top: none; border-radius: 0 0 8px 8px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1.5em; width: 100%; box-sizing: border-box; }
.liberapay-compact { display: flex; flex-direction: column; align-items: stretch; }
.liberapay-compact .liberapay-header { width: 100%; border-radius: 0; }
.liberapay-link-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.6em; padding: 0.9em 1.8em; background: linear-gradient(135deg, #f6c915 0%, #d4a800 100%); color: #1a1a2e; text-decoration: none; border-radius: 24px; font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 1.1em; font-weight: 600; transition: all 0.2s ease; box-shadow: 0 3px 10px rgba(246, 201, 21, 0.3); }
.liberapay-link-btn:hover { background: linear-gradient(135deg, #ffd630 0%, #e0b500 100%); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(246, 201, 21, 0.4); }
.liberapay-link-btn:active { transform: translateY(0); }
.liberapay-desc { font-size: 0.85em; color: var(--dim); text-align: center; max-width: 360px; }
@media (max-width: 480px) { .liberapay-controls { padding: 1.2em 0.8em; } .liberapay-subtitle { display: none; } }
.crypto-slider { -webkit-appearance: none; width: 100%; height: 12px; background: linear-gradient(90deg, var(--pink) 0%, #6a4a8a 100%); border-radius: 0; outline: none; cursor: pointer; margin: 0; border-left: 1px solid var(--box-border); border-right: 1px solid var(--box-border); }
.crypto-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 24px; background: var(--text); border-radius: 50%; cursor: grab; box-shadow: 0 2px 6px rgba(0,0,0,0.3); }
.crypto-slider::-moz-range-thumb { width: 24px; height: 24px; background: var(--text); border-radius: 50%; cursor: grab; border: none; box-shadow: 0 2px 6px rgba(0,0,0,0.3); }
.crypto-buttons { display: flex; gap: 0; }
.crypto-send { flex: 1; border-radius: 0 0 8px 8px; background: linear-gradient(135deg, #6a4a8a 0%, #4a2a6a 100%); }
.crypto-send:hover { background: linear-gradient(135deg, #8a6aaa 0%, #6a4a8a 100%); }
small { display: block; font-size: 0.7em; color: var(--dim); text-align: center; grid-column: 1 / -1; margin: 0.5em 0; }
hr { border: none; border-top: 1px dashed var(--box-border); grid-column: 1 / -1; margin: 0.5em 0; }
.digital-currency { background: var(--box-bg); border: 1px solid var(--box-border); border-radius: 4px; padding: 1em 1.2em; }
.digital-currency h3 { font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 0.9em; margin: 0 0 0.8em 0; color: var(--cyan); }
.crypto-row { display: flex; gap: 1em; flex-wrap: wrap; }
.crypto-item { flex: 1; min-width: 140px; }
.link-block { background: var(--box-bg); border: 1px solid var(--box-border); border-radius: 4px; padding: 0; display: flex; flex-direction: column; aspect-ratio: 1; overflow: hidden; position: relative; }
.link-block strong { position: sticky; top: 0; left: 0; background: inherit; padding: 0.8em 1em; color: var(--pink); font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 0.9em; z-index: 1; }
.link-block a { color: var(--cyan); font-family: var(--mono); font-size: 0.9em; padding: 0 1em; flex: 1; overflow: auto; }
.link-block .link-desc { position: sticky; bottom: 0; left: 0; background: inherit; padding: 0.8em 1em; font-size: 0.75em; color: var(--dim); z-index: 1; }
/* Distinct link-block colors */ .link-block.desktop-section { background: linear-gradient(135deg, #2d1b4e 0%, #1a1a2e 100%); border-color: #3d2b5e; } .link-block.desktop-section strong, .link-block.desktop-section .link-desc { background: transparent; }
/* Notepat - musical purple/blue */ .link-block.notepat-section { background: linear-gradient(135deg, #1a1a3e 0%, #2a1a4e 100%); border-color: #3a2a5e; } .link-block.notepat-section strong { color: #a090ff; } .link-block.notepat-section strong, .link-block.notepat-section .link-desc { background: transparent; }
/* Invest - announcement ticker style */ .invest-section { background: linear-gradient(135deg, rgba(10, 26, 18, 0.85) 0%, rgba(13, 32, 24, 0.9) 100%), url('https://assets.aesthetic.computer/jeffreys/jpg/IMG_2658.jpg'); background-size: cover, 200%; background-position: center; border: 1px solid rgba(78, 205, 196, 0.3); border-radius: 8px; padding: 1.2em 1.5em; overflow: visible; position: relative; aspect-ratio: 1; }
.invest-header { display: flex; align-items: center; gap: 0.4em; margin-bottom: 0.8em; font-family: var(--mono); font-size: 0.9em; color: #4ecdc4; letter-spacing: 0.05em; }
.invest-content { font-family: 'Berkeley Mono Variable', var(--mono); font-size: 0.9em; line-height: 1.7; color: rgba(255, 255, 255, 0.9); }
.invest-content p { margin: 0 0 1em 0; }
.invest-content p:last-child { margin-bottom: 0; }
.invest-content a { color: #4ecdc4; text-decoration: none; border-bottom: 1px solid rgba(78, 205, 196, 0.4); transition: border-color 0.2s, color 0.2s; }
.invest-content a:hover { color: #6eeee6; border-bottom-color: #6eeee6; }
.invest-content a.handle-link { color: var(--pink); border-bottom: none; }
.invest-content a.handle-link:hover { color: #ff90d0; }
.invest-content .demo-link { /* No special styling - just inherit normal link style */ }
.invest-section.blinking { animation: invest-blink 0.6s ease-in-out 3; }
@keyframes invest-blink { 0%, 100% { box-shadow: 0 0 0 0 rgba(78, 205, 196, 0); } 25%, 75% { box-shadow: 0 0 20px 4px rgba(78, 205, 196, 0.6); } 50% { box-shadow: 0 0 30px 8px rgba(78, 205, 196, 0.8); } }
/* Invest suggestion styling */ .invest-suggestion { font-size: 0.8em; color: var(--gold); padding: 0.6em 0.8em; margin-top: 0.5em; background: rgba(78, 205, 196, 0.15); border: 1px solid rgba(78, 205, 196, 0.3); border-radius: 4px; text-align: center; }
.invest-suggestion a { color: #4ecdc4; font-weight: bold; text-decoration: underline; }
.invest-suggestion a:hover { color: #6eeee6; }
/* Mug easter egg in invest section - fully responsive */ .invest-mug { position: absolute; bottom: 5%; right: 5%; display: flex; flex-direction: column; align-items: center; gap: 0; text-decoration: none; opacity: 1; transition: transform 0.3s; width: 30%; max-width: 140px; } .invest-mug:hover { transform: scale(1.02); text-decoration: none !important; } .invest-mug-icon { font-size: 2.5em; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); } .invest-mug-image { width: 100%; height: auto; aspect-ratio: 1; object-fit: contain; filter: drop-shadow(0 3px 12px rgba(0,0,0,0.6)) brightness(1.1); display: none; /* Hidden until loaded */ } .invest-mug-caption { font-family: var(--mono); font-size: clamp(0.45em, 2.5vw, 0.85em); letter-spacing: 0.02em; white-space: normal; text-align: center; line-height: 1.3; position: relative; margin-top: -8%; z-index: 1; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9), 0 0 6px rgba(0, 0, 0, 0.7); max-width: 100%; } .invest-mug-caption .mug-color { color: rgba(255, 255, 255, 1); } .invest-mug-caption .mug-of { color: rgba(200, 200, 200, 0.9); } .invest-mug-caption .mug-code { color: rgba(150, 200, 255, 1); } .invest-mug-caption .mug-via { color: rgba(255, 200, 100, 1); display: block; } /* Character-level syntax highlighting */ .invest-mug-caption .char-hash { color: #00bcd4; } /* cyan for # */ .invest-mug-caption .char-dollar { color: #ffd700; } /* gold for $ */ .invest-mug-caption .char-code { color: #87ceeb; } /* light blue for code chars */ .invest-mug-caption .char-via { color: #00e5ff; } /* cyan for via identifier */
/* Container query for smaller invest boxes (e.g., 315px) */ .invest-section { container-type: inline-size; container-name: invest; } @container invest (max-width: 350px) { .invest-mug { width: 26%; max-width: 100px; bottom: 4%; right: 4%; } .invest-mug-caption { font-size: 0.65em; margin-top: -7%; } }
/* Hacker Section - ghost-style minimal cards */ .hacker-section { grid-column: 1 / -1; display: flex; justify-content: center; gap: 1em; padding: 1em 0; margin-top: 0.5em; }
.hacker-card { flex: 0 1 auto; max-width: 420px; background: transparent; border: none; border-radius: 4px; padding: 1em 1.2em; font-family: var(--mono); font-size: 1.1em; color: rgba(255, 255, 255, 0.5); transition: border-color 0.2s, color 0.2s; }
.hacker-card:hover { border-color: rgba(205, 92, 155, 0.4); color: rgba(255, 255, 255, 0.7); }
.hacker-card a { color: rgba(205, 92, 155, 0.7); text-decoration: none; }
.hacker-card a:hover { color: rgb(205, 92, 155); text-decoration: none; }
.hacker-card-title { display: flex; flex-direction: column; align-items: center; gap: 0.2em; font-size: 0.9em; color: rgba(255, 255, 255, 0.4); margin-bottom: 0.5em; letter-spacing: 0.05em; text-align: center; text-decoration: none; }
.hacker-card-title:hover { color: rgba(255, 255, 255, 0.55); text-decoration: none !important; }
.at-line-1, .at-line-2 { display: block; white-space: nowrap; }
.at-gray { color: rgba(255, 255, 255, 0.4); }
.hacker-card-title:hover { color: rgba(255, 255, 255, 0.55); text-decoration: none; }
.hacker-card a { color: rgba(205, 92, 155, 0.8); text-decoration: none; }
.hacker-card-title a:hover { color: rgb(205, 92, 155); text-decoration: underline; }
.at-icon { width: 1.4em; height: 1.4em; vertical-align: -0.3em; margin-right: 0.3em; margin-left: 0.15em; }
.hacker-card-title a { font-size: 1.1em; }
.ac-name { color: inherit; }
.ac-dot { color: var(--pink); }
.at-blue { color: #0085FF; }
/* AT Protocol handle cycling animation */ .at-handle-prefix { display: inline-block; min-width: 3ch; text-align: right; }
.at-handle-prefix .at-char { display: inline-block; animation: atCharColor 1.8s steps(1) infinite; }
.at-handle-prefix .at-char-gray { display: inline-block; animation: atCharGray 1.8s steps(1) infinite; }
@keyframes atCharColor { 0% { color: rgb(205, 92, 155); } 16.6% { color: rgb(255, 120, 100); } 33.3% { color: rgb(255, 200, 100); } 50% { color: rgb(150, 230, 150); } 66.6% { color: rgb(100, 180, 255); } 83.3% { color: rgb(180, 130, 230); } 100% { color: rgb(205, 92, 155); } }
@keyframes atCharGray { 0% { color: rgba(255, 255, 255, 0.3); } 16.6% { color: rgba(255, 255, 255, 0.45); } 33.3% { color: rgba(255, 255, 255, 0.35); } 50% { color: rgba(255, 255, 255, 0.5); } 66.6% { color: rgba(255, 255, 255, 0.4); } 83.3% { color: rgba(255, 255, 255, 0.3); } 100% { color: rgba(255, 255, 255, 0.3); } }
.hacker-card-title code { background: rgba(205, 92, 155, 0.15); color: rgba(205, 92, 155, 0.8); padding: 0.1em 0.4em; border-radius: 2px; font-size: 1.1em; }
.hacker-card-body { line-height: 1.5; }
.hacker-card-body code { background: rgba(255, 255, 255, 0.05); padding: 0.1em 0.3em; border-radius: 2px; color: rgba(255, 255, 255, 0.6); }
.at-table { width: 100%; border-collapse: collapse; font-size: 0.9em; }
.at-table td { padding: 0.25em 0; vertical-align: top; }
.at-table td:first-child { color: rgba(255, 255, 255, 0.4); width: 5.5em; padding-right: 0.5em; }
.at-table td:last-child { color: rgba(255, 255, 255, 0.7); }
.at-table a { color: var(--pink); }
.at-table code { font-size: 0.95em; }
/* Apps Flip Card - Desktop/Mobile swivel */ .apps-flip-container { aspect-ratio: 1; position: relative; perspective: 1200px; border-radius: 8px; }
/* Glow removed - now only on tapes section */
@keyframes glowRotate { to { --glow-angle: 360deg; } }
@property --glow-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
.apps-flip-face .module-caption { position: absolute; bottom: 3px; /* Above progress bar */ left: 0; right: 0; background: #000; color: rgba(255,255,255,0.5); padding: 0.8em; }
.apps-flip-card { position: absolute; inset: 0; transform-style: preserve-3d; transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1); }
.apps-flip-card.tilt-left { transform: rotateY(-8deg); }
.apps-flip-card.tilt-right { transform: rotateY(8deg); }
.apps-flip-card.flipped { transform: rotateY(180deg); }
.apps-flip-card.flipped.tilt-left { transform: rotateY(188deg); }
.apps-flip-card.flipped.tilt-right { transform: rotateY(172deg); }
.apps-flip-face { position: absolute; inset: 0; border-radius: 8px; overflow: hidden; display: flex; flex-direction: column; transition: opacity 0.1s ease, filter 0.1s ease; /* Safari mobile fix for border-radius */ -webkit-mask-image: -webkit-radial-gradient(white, black); isolation: isolate; }
.apps-flip-front { z-index: 1; opacity: 1; filter: none; }
.apps-flip-back { z-index: 3; transform: rotateY(180deg); opacity: 0.15; filter: blur(2px); pointer-events: none; }
.apps-flip-card.flipped .apps-flip-front { z-index: 3; opacity: 0.15; filter: blur(2px); pointer-events: none; }
.apps-flip-card.flipped .apps-flip-back { z-index: 1; opacity: 1; filter: none; pointer-events: auto; }
.apps-flip-trigger { cursor: pointer; user-select: none; }
.apps-flip-trigger:hover { opacity: 0.85; }
/* Desktop face - grayscale techy */ .desktop-face { background: linear-gradient(135deg, #1e1e1e 0%, #121212 100%); border: 1px solid #333; padding: 0.5em; padding-bottom: 3.5em; /* Space for absolute caption */ }
.desktop-face-header { display: flex; align-items: center; gap: 0.4em; padding-bottom: 0.4em; border-bottom: 1px solid rgba(255, 255, 255, 0.1); margin-bottom: 0.4em; overflow: hidden; cursor: pointer; }
.face-label { font-size: 0.85em; color: #aaa; font-family: monospace; text-transform: lowercase; letter-spacing: 0.05em; flex-shrink: 0; }
.desktop-face-header .version-badge { font-size: 0.65em; color: #6a6; background: rgba(100, 170, 100, 0.15); padding: 0.15em 0.5em; border-radius: 3px; margin-left: auto; font-family: monospace; } .desktop-header-ticker { flex: 1; overflow: hidden; white-space: nowrap; margin-left: 0.5em; } .desktop-header-ticker-content { display: inline-block; animation: tickerScroll 60s linear infinite; font-size: 0.6em; color: #666; } .desktop-header-ticker-content span { margin-right: 2em; } .desktop-face-header:hover .desktop-header-ticker-content { animation-play-state: paused; }
.desktop-platforms-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px; flex: 1; }
.desktop-platform-btn { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 0.6em 0.3em; border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 6px; text-decoration: none; transition: all 0.2s ease; }
.desktop-platform-btn.plat-mac { background: rgba(150, 150, 150, 0.25); }
.desktop-platform-btn.plat-win { background: rgba(0, 120, 212, 0.25); }
.desktop-platform-btn.plat-linux { background: rgba(245, 196, 0, 0.2); }
.desktop-platform-btn:hover { border-color: rgba(255, 255, 255, 0.4); transform: translateY(-2px); }
.desktop-platform-btn.plat-mac:hover { background: rgba(180, 180, 180, 0.35); }
.desktop-platform-btn.plat-win:hover { background: rgba(0, 120, 212, 0.4); }
.desktop-platform-btn.plat-linux:hover { background: rgba(245, 196, 0, 0.35); }
.desktop-platform-btn .plat-icon { width: 52px; height: 52px; margin-bottom: 0.2em; display: flex; align-items: center; justify-content: center; }
.desktop-platform-btn .plat-icon svg { width: 100%; height: 100%; fill: currentColor; }
.desktop-platform-btn .plat-name { font-size: 1.05em; color: #fff; font-weight: bold; }
.desktop-platform-btn .plat-arch { font-size: 0.7em; color: #aaa; font-family: var(--mono); }
.desktop-platform-btn:hover .plat-name { color: #fff; }
.desktop-changelog-ticker { background: rgba(0, 0, 0, 0.3); border-radius: 4px; padding: 0.3em 0.5em; margin-top: 0.4em; border: 1px solid rgba(255, 255, 255, 0.05); overflow: hidden; white-space: nowrap; }
.desktop-changelog-ticker-content { display: inline-block; animation: tickerScroll 60s linear infinite; font-size: 0.65em; color: var(--dim); }
.desktop-changelog-ticker-content span { margin-right: 2em; }
@keyframes tickerScroll { 0% { transform: translateX(0%); } 100% { transform: translateX(-50%); } }
.desktop-changelog-ticker:hover .desktop-changelog-ticker-content { animation-play-state: paused; }
.desktop-tools-row { display: flex; gap: 0.5em; margin-top: 0.5em; flex: 1; }
.desktop-vscode-link { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.3em; background: rgba(40, 40, 40, 0.95); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 4px; padding: 0.5em; text-decoration: none; transition: all 0.2s ease; flex: 1; }
.desktop-vscode-link:hover { border-color: rgba(255, 255, 255, 0.35); background: rgba(60, 60, 60, 0.95); transform: translateY(-1px); }
.desktop-vscode-link .vscode-icon-img { width: 56px; height: 56px; border-radius: 4px; }
.desktop-vscode-link .vscode-icon { font-size: 2.8em; }
.desktop-vscode-link .vscode-label { color: #ccc; font-weight: 600; font-size: 1em; }
.desktop-vscode-link .vscode-ver { color: #8c8; font-size: 0.75em; font-family: monospace; }
.desktop-ableton-link { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.3em; background: rgba(40, 40, 40, 0.95); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 4px; padding: 0.5em; text-decoration: none; transition: all 0.2s ease; flex: 1; }
.desktop-ableton-link:hover { border-color: rgba(255, 255, 255, 0.35); background: rgba(60, 60, 60, 0.95); transform: translateY(-1px); }
.desktop-ableton-link .ableton-icon { width: 56px; height: 56px; color: #fff; }
.desktop-ableton-link .ableton-label { color: #ccc; font-weight: 600; font-size: 1em; }
.desktop-ableton-link .ableton-tag { color: #aaa; font-size: 0.7em; font-family: monospace; background: rgba(255, 255, 255, 0.12); padding: 0.15em 0.4em; border-radius: 3px; }
/* Mobile face - grayscale techy */ .mobile-face { background: linear-gradient(135deg, #181818 0%, #101010 100%); border: 1px solid #2a2a2a; padding: 1em; padding-bottom: 3.5em; /* Space for absolute caption */ display: flex; flex-direction: column; }
.mobile-face-header { display: flex; align-items: center; gap: 0.5em; padding-bottom: 0.8em; border-bottom: 1px solid rgba(255, 255, 255, 0.08); margin-bottom: 0.8em; cursor: pointer; }
.mobile-face-content { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap: 0.8em; }
.mobile-app-icon { width: 90px; height: 90px; border-radius: 18px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5); }
.mobile-app-icon-fallback { width: 90px; height: 90px; border-radius: 18px; background: linear-gradient(135deg, #444 0%, #222 100%); display: flex; align-items: center; justify-content: center; font-size: 2.4em; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5); }
.mobile-app-title { font-size: 1.15em; color: #aaa; font-weight: 500; text-align: center; display: flex; align-items: center; justify-content: center; line-height: 1; }
.app-bounce-dot { display: inline-flex; align-items: center; justify-content: center; color: var(--pink); font-weight: 900; animation: appDotBounce 1.5s ease-in-out infinite; margin: 0 -0.02em; position: relative; top: 0.02em; }
@keyframes appDotBounce { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-3px) scale(1.2); } }
.mobile-store-link { display: inline-flex; align-items: center; gap: 0.5em; padding: 0.6em 1.2em; background: rgba(30, 30, 30, 0.9); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 8px; color: #ccc; text-decoration: none; font-size: 0.85em; transition: all 0.2s ease; }
.mobile-store-link:hover { background: rgba(50, 50, 50, 0.9); border-color: rgba(255, 255, 255, 0.3); transform: scale(1.02); color: #fff; }
.mobile-store-link .store-icon { font-size: 1.2em; }
/* VS Code Extension - Simple link block */ .vscode-section { background: linear-gradient(135deg, rgba(0, 122, 204, 0.15) 0%, rgba(0, 80, 150, 0.1) 100%); border: 1px solid rgba(0, 122, 204, 0.3); }
.vscode-section strong { color: #0078d4; }
.vscode-section .link-desc { background: transparent; }
.vscode-features-row { display: flex; flex-wrap: wrap; gap: 0.4em; margin-top: 0.6em; }
.vscode-feature-tag { font-size: 0.7em; padding: 0.2em 0.5em; background: rgba(0, 122, 204, 0.15); border: 1px solid rgba(0, 122, 204, 0.25); border-radius: 3px; color: var(--text); }
.vscode-install-link { display: inline-block; margin-top: 0.6em; padding: 0.4em 0.8em; background: #0078d4; border-radius: 4px; color: #fff; text-decoration: none; font-size: 0.8em; transition: background 0.2s ease; }
.vscode-install-link:hover { background: #1a8fe8; }
.feature-module { background: var(--box-bg); border: 1px solid var(--box-border); border-radius: 4px; padding: 1em 1.2em; }
.feature-module h3 { font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 0.95em; margin: 0 0 0.7em 0; color: var(--cyan); }
.feature-module ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5em; }
.feature-module li { font-size: 0.85em; line-height: 1.4; padding-left: 0.5em; border-left: 2px solid var(--box-border); }
.feature-module li strong { color: var(--pink); }
.feature-module li a { color: inherit; text-decoration: none; }
.feature-module li a:hover { text-decoration: underline; }
.feature-module li a strong { color: var(--pink); }
.addr { font-size: 0.85em; background: var(--box-bg); padding: 0.3em 0.5em; border-radius: 2px; display: inline-flex; align-items: center; gap: 0.5em; margin-top: 0.2em; }
.addr button { font-family: var(--mono); font-size: 0.75em; padding: 0.2em 0.4em; background: transparent; border: 1px solid var(--dim); color: var(--dim); border-radius: 2px; cursor: pointer; }
.addr button:hover { border-color: var(--cyan); color: var(--cyan); }
/* Shop Section - clean white background like shop.aesthetic.computer */ .shop-section { background: #000; border: none; border-radius: 8px; padding: 0; color: #1a1a1a; overflow: hidden; display: flex; flex-direction: column; position: relative; aspect-ratio: 1; /* Safari mobile fix for border-radius */ -webkit-mask-image: -webkit-radial-gradient(white, black); isolation: isolate; z-index: 2; /* Above other modules so their shadows don't cover it */ }
.shop-header { position: absolute; top: 0; left: 0; right: 0; z-index: 3; background: transparent; padding: 0.8em 1em 2em 1em; text-align: left; }
.shop-title { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 1.3em; font-weight: 600; color: #fff; text-decoration: none; text-shadow: 0 1px 2px rgba(0,0,0,0.6); }
.shop-title:hover { color: var(--pink); }
.shop-section .module-caption { background: #000; color: rgba(255,255,255,0.5); font-family: var(--mono); z-index: 3; position: relative; }
.shop-products { position: relative; width: 100%; aspect-ratio: 1; overflow: hidden; }
/* Dual-layer container for crossfade transitions */ .shop-product-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; transition: opacity 0.6s ease-in-out, transform 0.6s ease-in-out; }
.shop-product-layer.layer-back { z-index: 0; }
.shop-product-layer.layer-front { z-index: 1; }
.shop-product-layer.entering { opacity: 0; transform: scale(1.02); }
.shop-product-layer.visible { opacity: 1; transform: scale(1); }
.shop-product-layer.exiting { opacity: 0; transform: scale(0.98); }
.shop-product { display: block; width: 100%; height: 100%; text-decoration: none; color: #fff; position: relative; }
.shop-product-img-wrap { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; z-index: 0; /* Keep below overlay and price */ }
.shop-product-img-wrap::after { content: ""; position: absolute; inset: 0; background: radial-gradient(ellipse at center, transparent 40%, rgba(0, 0, 0, 0.5) 100%); pointer-events: none; z-index: 1; }
.shop-product img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; transform: scale(1.15); animation: kenBurns 15s ease-in-out infinite alternate; transition: opacity 0.4s ease; }
.shop-product img.shop-img-back { z-index: 0; }
.shop-product img.shop-img-front { z-index: 0; }
@keyframes kenBurns { 0% { transform: scale(1.15) translate(0, 0); } 25% { transform: scale(1.25) translate(-3%, -2%); } 50% { transform: scale(1.2) translate(2%, -3%); } 75% { transform: scale(1.3) translate(-2%, 2%); } 100% { transform: scale(1.2) translate(2%, -1%); } }
.shop-product-overlay { position: absolute; left: 0; right: 0; bottom: 0; padding: 0.75em; padding-right: 5em; text-align: left; z-index: 2; /* Above the darkening ::after layer */ }
.shop-product-title-row { margin-bottom: 0.25em; }
.shop-product-title { font-family: 'YWFTProcessing-Regular', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 1.8em; font-weight: 500; color: #fff; text-shadow: 2px 2px 0 #000; line-height: 1.2; margin-right: 0.4em; }
.shop-product-vendor { font-family: var(--mono); font-size: 0.85em; color: #ff6b9d !important; text-shadow: 1px 1px 0 #000; display: block; margin-top: 0.2em; }
.shop-product-vendor a, .shop-product-vendor a:link, .shop-product-vendor a:visited, .shop-product-vendor a:active { color: #ff6b9d !important; text-decoration: none; }
.shop-product-vendor a:hover { text-decoration: underline; }
.shop-product-info { display: flex; flex-direction: row; align-items: center; justify-content: space-between; gap: 1em; }
.shop-product-desc { font-family: var(--mono); font-size: 0.8em; color: rgba(255, 255, 255, 0.9); line-height: 1.5; text-shadow: 1px 1px 0 #000; max-height: 3.6em; overflow: hidden; -webkit-mask-image: linear-gradient(180deg, #000 70%, transparent 100%); mask-image: linear-gradient(180deg, #000 70%, transparent 100%); margin-top: 0.3em; }
.shop-product-desc-inner { animation: descScroll 12s ease-in-out infinite; animation-delay: 2s; }
@keyframes descScroll { 0%, 20% { transform: translateY(0); } 80%, 100% { transform: translateY(calc(-100% + 3.6em)); } }
.shop-product-price { position: absolute; top: 0.3em; right: 0.6em; z-index: 100; font-family: 'YWFTProcessing-Regular', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 2.4em; color: #00ff66; font-weight: 500; text-shadow: 3px 3px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 0 0 12px rgba(0, 0, 0, 0.9); white-space: nowrap; }
.shop-product-sold { position: absolute; top: 0.5em; right: 0.5em; z-index: 10; background: #d32f2f; color: #fff; font-size: 1em; font-weight: 700; text-transform: uppercase; padding: 0.25em 0.6em; border-radius: 3px; white-space: nowrap; }
@keyframes shopFadeIn { to { opacity: 1; } }
/* Shop auto-advance progress bar - inside panel */ .shop-auto-progress { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: rgba(255, 255, 255, 0.08); z-index: 15; overflow: hidden; }
.shop-auto-progress-bar { height: 100%; width: 0%; background: rgba(255, 255, 255, 0.4); transition: width 0.05s linear; }
/* AT Proto Section - decentralized identity */ .at-section { background: rgb(20, 18, 24); border-radius: 8px; overflow: hidden; display: flex; flex-direction: column; position: relative; aspect-ratio: 1; box-shadow: 0 0 0 1px rgb(205, 92, 155), 0 4px 20px rgba(205, 92, 155, 0.2); -webkit-mask-image: -webkit-radial-gradient(white, black); isolation: isolate; font-family: var(--mono); }
.at-header { padding: 0.5em 0.7em; text-decoration: none; background: transparent; display: flex; align-items: center; justify-content: space-between; }
.at-header strong { font-family: var(--mono); font-size: 0.85em; color: rgb(205, 92, 155); font-weight: 600; }
.at-header:hover strong { color: #fff; }
.at-record-display { flex: 1; position: relative; overflow: hidden; }
.at-record { position: absolute; inset: 0; display: flex; flex-direction: column; opacity: 0; transition: opacity 0.4s ease; }
.at-record.active { opacity: 1; }
/* Center content - letterboxed painting or large mood */ .at-record-center { flex: 1; display: flex; align-items: center; justify-content: center; padding: 0.3em 0.8em; min-height: 0; overflow: hidden; }
/* Handle and type below content */ .at-record-meta { display: flex; flex-direction: column; align-items: center; padding: 0 0.6em 0.1em; text-align: center; margin-top: -0.3em; }
.at-record-handle { color: rgb(205, 92, 155); font-weight: 700; font-size: 0.85em; letter-spacing: -0.01em; }
.at-record-type { color: rgba(255, 255, 255, 0.6); text-transform: uppercase; font-size: 0.55em; letter-spacing: 0.1em; margin-top: 0.05em; }
/* Painting: letterboxed with Ken Burns animation */ .at-record-thumb-wrap { position: relative; width: 80%; max-height: 90%; aspect-ratio: 16/10; border-radius: 4px; overflow: hidden; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.6), 0 2px 8px rgba(0, 0, 0, 0.4), inset 0 0 0 1px rgba(255, 255, 255, 0.08); background: #000; }
.at-record-thumb { width: 100%; height: 100%; object-fit: cover; animation: atKenBurns 12s ease-in-out infinite alternate; }
@keyframes atKenBurns { 0% { transform: scale(1) translate(0, 0); } 100% { transform: scale(1.12) translate(-2%, -1%); } }
.at-record-mood { font-size: 1.4em; text-align: center; color: rgba(255, 255, 255, 0.95); line-height: 1.35; padding: 0.3em 0.5em; font-family: var(--sans); max-height: 100%; overflow: hidden; font-weight: 500; }
/* Bottom: AT data proof */ .at-record-bottom { display: flex; flex-direction: column; gap: 0.15em; padding: 0.3em 0.6em 0.4em; font-size: 0.5em; text-align: center; opacity: 0.7; }
.at-record-uri { color: rgba(205, 92, 155, 0.8); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: var(--mono); }
.at-record-collection { color: rgba(255, 255, 255, 0.5); font-family: var(--mono); letter-spacing: 0.02em; }
/* Progress bar - aligned with caption top */ .at-progress { position: absolute; bottom: 2.35em; left: 0; right: 0; height: 2px; background: rgba(205, 92, 155, 0.2); z-index: 3; }
.at-progress-bar { height: 100%; background: rgb(205, 92, 155); width: 0%; transition: width 0.05s linear; }
.at-section .module-caption { position: absolute; bottom: 0; left: 0; right: 0; padding: 0.8em 1em; font-size: 0.75em; color: rgba(205, 92, 155, 0.8); background: rgb(40, 35, 50); z-index: 2; }
/* TV Section - letterbox video feed */ .tv-section { background: #000; border-radius: 8px; overflow: hidden; display: flex; flex-direction: column; position: relative; aspect-ratio: 1; /* VHS chromatic aberration shadow - subtle */ box-shadow: -2px 0 0 #ff0080, 2px 0 0 #00e5ff, -3px 2px 0 rgba(255, 0, 128, 0.4), 3px 2px 0 rgba(0, 229, 255, 0.4), 0 6px 20px rgba(0, 0, 0, 0.6); /* Safari mobile fix for border-radius */ -webkit-mask-image: -webkit-radial-gradient(white, black); isolation: isolate; }
.tv-header { position: absolute; top: 0; left: 0; right: 0; z-index: 10; padding: 0.6em 0.8em; text-decoration: none; cursor: pointer; background: transparent; display: flex; flex-direction: column; gap: 2px; }
.tv-header:hover { text-decoration: none; }
.tv-header-row { display: flex; flex-direction: column; align-items: flex-start; gap: 0.08em; }
/* Individual code animations - current slides in from bottom, others shift up */ @keyframes tvCodeEnter { from { opacity: 0; transform: translateY(1.2em); } to { opacity: 1; transform: translateY(0); } }
@keyframes tvCodeShiftUp { from { transform: translateY(1.2em); } to { transform: translateY(0); } }
/* Make upcoming codes look tappable */ .tv-header .tv-upcoming-code { cursor: pointer; text-decoration: none !important; transition: color 0.15s, transform 0.15s; padding: 0.05em 0; display: block; animation: tvCodeShiftUp 0.35s cubic-bezier(0.22, 1, 0.36, 1); }
.tv-header .tv-upcoming-code:hover, .tv-header .tv-upcoming-code:active { color: rgba(255,255,255,0.85); transform: translateX(4px); }
.tv-header .tv-upcoming-code:active { transform: translateX(4px) scale(0.95); }
/* Decreasing sizes for upcoming codes */ .tv-header .tv-upcoming-code:nth-child(1) { font-size: 0.9em; opacity: 0.55; } .tv-header .tv-upcoming-code:nth-child(2) { font-size: 0.75em; opacity: 0.4; } .tv-header .tv-upcoming-code:nth-child(3) { font-size: 0.6em; opacity: 0.3; }
.tv-header .tv-code { font-family: 'YWFTProcessing-Regular', var(--mono), sans-serif; font-size: 1.4em; color: #fff; font-weight: 500; letter-spacing: 0.02em; text-shadow: 2px 2px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 0 0 8px rgba(0, 0, 0, 0.9); animation: tvCodeEnter 0.35s cubic-bezier(0.22, 1, 0.36, 1); }
.tv-header .tv-upcoming { font-family: 'YWFTProcessing-Regular', var(--mono), sans-serif; font-size: 1.1em; color: rgba(255, 255, 255, 0.5); font-weight: 400; letter-spacing: 0.02em; display: flex; flex-direction: column; gap: 0.05em; text-shadow: 1px 1px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 0 0 6px rgba(0, 0, 0, 0.8); }
.tv-header .tv-timestamp { color: rgba(255,255,255,0.6); font-family: var(--mono); font-size: 0.7em; font-weight: 400; }
.tv-header:hover .tv-code { color: var(--pink); } /* Mute button */ .tv-mute-btn { position: absolute; top: 8px; right: 8px; width: 28px; height: 28px; border-radius: 4px; background: transparent; border: none; cursor: pointer; z-index: 20; display: flex; align-items: center; justify-content: center; transition: transform 0.2s; } .tv-mute-btn:hover { transform: scale(1.1); } .tv-mute-btn svg { width: 22px; height: 22px; fill: rgba(255,255,255,0.9); filter: drop-shadow(1px 1px 0 #000) drop-shadow(-1px -1px 0 #000) drop-shadow(1px -1px 0 #000) drop-shadow(-1px 1px 0 #000); } .tv-mute-btn.muted svg .sound-wave { display: none; }
.tv-section .module-caption { position: absolute; bottom: 0; left: 0; right: 0; z-index: 5; background: rgb(40, 40, 50); color: rgba(255, 255, 255, 0.7); padding: 0.8em 1em; font-size: 0.75em; }
.tv-player { position: absolute; inset: 0; bottom: 2.4em; /* Leave space for caption */ background: #000; display: flex; align-items: center; justify-content: center; }
.tv-player video { max-width: 100%; max-height: 100%; object-fit: contain; background: #000; }
.tv-no-signal { color: rgba(255,255,255,0.3); font-size: 0.8em; font-family: var(--mono); }
.tv-progress { position: absolute; bottom: 0; /* At bottom of tv-player, which ends at top of caption */ left: 0; right: 0; height: 32px; background: linear-gradient(0deg, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.4) 60%, transparent 100%); z-index: 12; display: flex; flex-direction: column; justify-content: flex-end; }
.tv-progress-bar { position: absolute; bottom: 0; left: 0; height: 3px; background: #e53935; width: 0%; }
.tv-progress { cursor: pointer; }
.tv-progress:hover .tv-progress-bar { height: 5px; }
.tv-timer { position: absolute; bottom: 8px; right: 10px; font-family: var(--mono); font-size: 0.85em; color: rgba(255,255,255,0.9); text-shadow: 0 1px 3px rgba(0,0,0,0.7); z-index: 13; cursor: pointer; }
.tv-timer:hover { color: var(--pink); }
.tv-play-indicator { position: absolute; bottom: 14px; left: 10px; width: 24px; height: 24px; border-radius: 4px; background: transparent; display: flex; align-items: center; justify-content: center; z-index: 15; pointer-events: none; opacity: 0.9; transition: opacity 0.2s ease; }
.tv-play-indicator::after { /* Pause icon (two bars) - shown while playing */ content: ''; display: block; width: 12px; height: 14px; background: linear-gradient(to right, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.95) 35%, transparent 35%, transparent 65%, rgba(255,255,255,0.95) 65%, rgba(255,255,255,0.95) 100%); filter: drop-shadow(0 1px 2px rgba(0,0,0,0.4)); }
.tv-play-indicator.paused { opacity: 1; }
.tv-play-indicator.paused::after { /* Play icon (triangle) - shown when paused */ background: none; width: 0; height: 0; border-style: solid; border-width: 6px 0 6px 10px; border-color: transparent transparent transparent rgba(255,255,255,0.95); margin-left: 2px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.4)); }
.tv-slide:hover .tv-play-indicator { opacity: 1; }
.tv-slide:hover .tv-play-indicator.paused { opacity: 1; }
/* Tape slide animation - vertical swipe like TikTok */ .tv-slide { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; background: #000; /* iOS Safari fixes */ -webkit-transform: translateZ(0); transform: translateZ(0); -webkit-backface-visibility: hidden; backface-visibility: hidden; }
/* Big centered play/pause button - appears on tap */ .tv-big-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.8); width: 80px; height: 80px; border-radius: 50%; background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; z-index: 25; pointer-events: none; opacity: 0; transition: opacity 0.2s ease, transform 0.2s ease; -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
.tv-big-play.visible { opacity: 1; transform: translate(-50%, -50%) scale(1); }
.tv-big-play::after { content: ''; display: block; width: 0; height: 0; border-style: solid; border-width: 15px 0 15px 26px; border-color: transparent transparent transparent rgba(255, 255, 255, 0.95); margin-left: 6px; }
.tv-big-play.paused::after { /* Pause icon - two bars */ border: none; width: 26px; height: 30px; background: linear-gradient(to right, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.95) 35%, transparent 35%, transparent 65%, rgba(255,255,255,0.95) 65%, rgba(255,255,255,0.95) 100%); margin-left: 0; }
.tv-slide video { max-width: 100%; max-height: 100%; object-fit: contain; background: #000; /* VHS color bleeding effect */ filter: saturate(1.1) contrast(1.05); position: relative; z-index: 2; }
/* Blurred background glow video - fills letterbox with soft color */ .tv-bg-glow { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(1.8); width: 100%; height: 100%; object-fit: cover; filter: blur(30px) saturate(1.8) brightness(0.8); z-index: 0; pointer-events: none; opacity: 1; }
/* TV section wrapper for backdrop glow effect */ .tv-section-wrap { position: relative; isolation: isolate; padding-left: 8px; /* Allow chromatic effect to bleed left */ margin-left: -8px; }
/* Colorful video blur backdrop behind the panel - like a glowing drop shadow */ .tv-backdrop-glow { position: absolute; top: 10px; left: 8px; right: 0; bottom: -10px; border-radius: 12px; overflow: hidden; filter: blur(20px) saturate(1.3) brightness(0.6); opacity: 0.6; z-index: -1; pointer-events: none; }
.tv-backdrop-glow video { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(2); width: 100%; height: 100%; object-fit: cover; }
/* VHS scanlines overlay - scrolling */ .tv-slide::before { content: ''; position: absolute; inset: -100% 0; height: 300%; background: repeating-linear-gradient( 0deg, transparent 0px, transparent 2px, rgba(0, 0, 0, 0.2) 2px, rgba(0, 0, 0, 0.2) 4px ); pointer-events: none; z-index: 5; animation: scanlineScroll 8s linear infinite; } @keyframes scanlineScroll { 0% { transform: translateY(0); } 100% { transform: translateY(33.33%); } }
/* VHS noise overlay */ .tv-slide::after { content: ''; position: absolute; inset: 0; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); opacity: 0.04; pointer-events: none; z-index: 6; mix-blend-mode: overlay; }
.tv-slide.entering { animation: tvSlideIn 0.4s ease-out forwards; z-index: 3; }
.tv-slide.exiting { animation: tvSlideOut 0.4s ease-out forwards; z-index: 2; }
@keyframes tvSlideIn { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes tvSlideOut { from { transform: translateY(0); opacity: 1; } to { transform: translateY(-100%); opacity: 0; } }
/* Flip Card System - Desktop App Style (ghost overlay approach) */ .chat-flip-container { aspect-ratio: 1; position: relative; perspective: 1200px; border-radius: 8px; }
/* Glow removed - now only on tapes section */
/* The flipping card - this rotates in 3D */ .chat-flip-card { position: absolute; inset: 0; transform-style: preserve-3d; transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1); }
/* Hover tilt effect - subtle swivel preview */ .chat-flip-card.tilt-left { transform: rotateY(-8deg); }
.chat-flip-card.tilt-right { transform: rotateY(8deg); }
.chat-flip-card.flipped { transform: rotateY(180deg); }
/* Tilt when flipped (reversed direction since we're looking at the back) */ .chat-flip-card.flipped.tilt-left { transform: rotateY(188deg); }
.chat-flip-card.flipped.tilt-right { transform: rotateY(172deg); }
/* Both faces positioned together */ .chat-flip-face { position: absolute; inset: 0; border-radius: 8px; overflow: hidden; display: flex; flex-direction: column; transition: opacity 0.1s ease, filter 0.1s ease; /* Safari mobile fix for border-radius */ -webkit-mask-image: -webkit-radial-gradient(white, black); isolation: isolate; }
/* Front face - laer-klokken - active when NOT flipped */ .chat-flip-front { z-index: 1; opacity: 1; filter: none; }
/* Back face - chat-system - ghost ON TOP when NOT flipped */ .chat-flip-back { z-index: 3; transform: rotateY(180deg); /* Shows mirrored when viewed from front */ opacity: 0.15; filter: blur(2px); pointer-events: none; }
/* When FLIPPED: front becomes ghost, back becomes active */ .chat-flip-card.flipped .chat-flip-front { z-index: 3; opacity: 0.15; filter: blur(2px); pointer-events: none; }
.chat-flip-card.flipped .chat-flip-back { z-index: 1; opacity: 1; filter: none; pointer-events: auto; }
/* Clickable areas for flip */ .chat-flip-trigger { cursor: pointer; user-select: none; }
.chat-flip-trigger:hover { opacity: 0.85; }
/* Embedded progress bar inside flip faces */ .flip-face-progress { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: rgba(0, 0, 0, 0.3); overflow: hidden; z-index: 10; }
.flip-face-progress-bar { height: 100%; width: 0%; background: var(--gold); transition: width 0.1s linear; }
/* Chat flip - gold for laer-klokken, purple for chat */ .chat-preview .flip-face-progress-bar { background: var(--gold); } .chat-preview-system .flip-face-progress-bar { background: #8844ff; } /* Apps flip - silvery for both faces */ .desktop-face .flip-face-progress-bar, .mobile-face .flip-face-progress-bar { background: linear-gradient(90deg, #666 0%, #888 50%, #aaa 100%); }
/* Chat Rolodex - laer-klokken warm gold theme */ .chat-preview { background: linear-gradient(135deg, #2a2010 0%, #1a1508 100%); border: 1px solid #3d3520; border-radius: 8px; overflow: hidden; aspect-ratio: 1; display: flex; flex-direction: column; position: relative; scrollbar-width: none; -ms-overflow-style: none; /* Safari mobile fix for border-radius */ -webkit-mask-image: -webkit-radial-gradient(white, black); isolation: isolate; } .chat-preview::-webkit-scrollbar { display: none; }
.chat-preview-header { position: sticky; top: 0; z-index: 1; padding: 0.8em 1em; text-decoration: none; background: linear-gradient(135deg, #2a2010 0%, #1a1508 100%); }
.chat-preview-header strong { color: var(--gold); font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 1.05em; }
.chat-preview-header:hover strong { color: #ffe066; }
.chat-preview .module-caption { background: linear-gradient(135deg, #2a2010 0%, #1a1508 100%); color: #a08050; }
.chat-messages { flex: 1; overflow: hidden; background: #111; pointer-events: none; scrollbar-width: none; -ms-overflow-style: none; }
.chat-messages::-webkit-scrollbar { display: none; }
.chat-msg { padding: 0.5em 1em; border-bottom: 1px solid rgba(255,255,255,0.05); animation: chatFadeIn 0.3s ease; }
.chat-msg:last-child { border-bottom: none; }
@keyframes chatFadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } }
.chat-msg-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.2em; }
.chat-handle { font-family: var(--mono); font-size: 0.75em; color: var(--cyan); }
.chat-handle:hover { color: var(--pink); }
.chat-time { font-family: var(--mono); font-size: 0.6em; color: var(--dim); }
.chat-text { font-size: 0.8em; color: var(--text); line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.chat-text .chat-link-url { color: var(--cyan); text-decoration: none; }
.chat-text .chat-link-url:hover { text-decoration: underline; }
.chat-text .chat-link-handle { color: var(--pink); text-decoration: none; }
.chat-text .chat-link-handle:hover { text-decoration: underline; }
.chat-text .chat-link-prompt { color: inherit; text-decoration: none; font-family: var(--mono); }
.chat-text .chat-link-prompt:hover { text-decoration: none; opacity: 0.8; }
.chat-text .chat-link-painting { color: #95E1D3; text-decoration: none; }
.chat-text .chat-link-painting:hover { text-decoration: underline; }
/* Painting thumbnail previews in chat */ .chat-painting-thumbs { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.chat-painting-thumb { width: 48px; height: 48px; border-radius: 4px; overflow: hidden; background: #1a1a1a; position: relative; }
.chat-painting-thumb a { display: block; width: 100%; height: 100%; }
.chat-painting-thumb img { width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity 0.3s ease; }
.chat-painting-thumb img.loaded { opacity: 1; }
.chat-painting-thumb:hover { outline: 2px solid var(--cyan); }
.chat-painting-thumb .thumb-code { position: absolute; bottom: 0; left: 0; right: 0; font-family: var(--mono); font-size: 0.5em; color: #fff; background: rgba(0,0,0,0.7); padding: 1px 3px; text-align: center; }
/* Chat System (back face) - purple theme (lighter) */ .chat-preview-system { background: linear-gradient(135deg, #252540 0%, #1a1230 100%); border: 1px solid rgba(160, 100, 255, 0.35); }
.chat-system-header { position: sticky; top: 0; z-index: 1; padding: 0.8em 1em; text-decoration: none; background: linear-gradient(135deg, #252540 0%, #1a1230 100%); }
.chat-system-header strong { color: rgba(180, 140, 255, 0.95); font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 1.05em; }
.chat-system-header:hover strong { color: rgba(210, 170, 255, 1); }
.chat-preview-system .module-caption { background: linear-gradient(135deg, #252540 0%, #1a1230 100%); color: rgba(160, 120, 255, 0.8); }
.chat-preview-system .chat-messages { background: rgba(20, 18, 35, 0.9); }
.chat-preview-system .chat-handle { color: rgba(180, 140, 255, 0.95); }
.chat-preview-system .chat-handle:hover { color: var(--pink); }
/* KidLisp Preview - Full-bleed animated webp like shop */ .kidlisp-preview { background: #0d0d1a; border: none; border-radius: 8px; overflow: hidden; aspect-ratio: 1; display: flex; flex-direction: column; position: relative; /* Safari mobile fix for border-radius */ -webkit-mask-image: -webkit-radial-gradient(white, black); isolation: isolate; /* Enable transform for bounce animation */ transform-origin: center center; transition: transform 0.1s ease; }
/* Bounce press animation on slide change */ .kidlisp-preview.bounce-press { animation: kidlispBouncePress 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes kidlispBouncePress { 0% { transform: scale(1) rotateX(0deg) rotateY(0deg); } 15% { transform: scale(0.96) rotateX(2deg) rotateY(-1deg); } 30% { transform: scale(0.97) rotateX(-1deg) rotateY(1deg); } 50% { transform: scale(1.02) rotateX(0deg) rotateY(0deg); } 70% { transform: scale(0.99) rotateX(0.5deg) rotateY(-0.5deg); } 100% { transform: scale(1) rotateX(0deg) rotateY(0deg); } }
.kidlisp-preview-header:hover .kidlisp-logo { filter: brightness(1.2); }
.kidlisp-preview .module-caption { background: linear-gradient(135deg, rgba(20,10,30,0.85) 0%, rgba(10,5,20,0.9) 100%); color: rgba(200,180,255,0.9); z-index: 10; }
/* KidLisp auto-advance progress bar */ .kidlisp-auto-progress { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: rgba(255, 255, 255, 0.15); z-index: 15; overflow: hidden; }
.kidlisp-auto-progress-bar { height: 100%; width: 0%; background: #AA96DA; /* No transition - smooth continuous updates from requestAnimationFrame */ }
.kidlisp-preview-header { position: absolute; top: 0; left: 0; right: 0; padding: 0.8em 1em; display: flex; align-items: center; justify-content: space-between; gap: 0.5em; text-decoration: none; z-index: 10; background: linear-gradient(135deg, rgba(20,10,30,0.85) 0%, rgba(10,5,20,0.8) 100%); }
.kidlisp-preview-header:hover { text-decoration: none; }
.kidlisp-preview-header:hover .kidlisp-logo { text-decoration: none; }
/* Header code display - shows current $code */ .kidlisp-header-code-wrap { display: flex; align-items: center; gap: 0.4em; flex: 1; min-width: 0; overflow: hidden; }
.kidlisp-header-sep { color: var(--dim); font-size: 0.9em; opacity: 0.6; }
.kidlisp-header-code { font-family: 'Comic Relief', 'Comic Sans MS', cursive; font-size: 1em; white-space: nowrap; letter-spacing: -0.03em; }
.kidlisp-header-code .code-char { display: inline-block; }
.kidlisp-header-code .code-dollar { color: #32cd32; /* limegreen - brighter */ }
.kidlisp-header-code .code-name { color: #90EE90; /* lightgreen - softer */ }
/* Stale code chars blast off and fade - POOF! */ .kidlisp-header-code.transitioning .code-char { animation: kidlispCharBlastOff 0.4s ease-out forwards; }
/* Stagger the blast-off for each character */ .kidlisp-header-code.transitioning .code-char:nth-child(1) { animation-delay: 0ms; } .kidlisp-header-code.transitioning .code-char:nth-child(2) { animation-delay: 30ms; } .kidlisp-header-code.transitioning .code-char:nth-child(3) { animation-delay: 50ms; } .kidlisp-header-code.transitioning .code-char:nth-child(4) { animation-delay: 70ms; } .kidlisp-header-code.transitioning .code-char:nth-child(5) { animation-delay: 90ms; } .kidlisp-header-code.transitioning .code-char:nth-child(6) { animation-delay: 110ms; } .kidlisp-header-code.transitioning .code-char:nth-child(7) { animation-delay: 130ms; } .kidlisp-header-code.transitioning .code-char:nth-child(8) { animation-delay: 150ms; } .kidlisp-header-code.transitioning .code-char:nth-child(9) { animation-delay: 170ms; } .kidlisp-header-code.transitioning .code-char:nth-child(10) { animation-delay: 190ms; }
/* Randomize rotation direction per character */ .kidlisp-header-code.transitioning .code-char:nth-child(odd) { --blast-rotate: 15deg; } .kidlisp-header-code.transitioning .code-char:nth-child(even) { --blast-rotate: -15deg; } .kidlisp-header-code.transitioning .code-char:nth-child(3n) { --blast-rotate: 25deg; } .kidlisp-header-code.transitioning .code-char:nth-child(3n+1) { --blast-rotate: -20deg; }
/* Green play button indicator (like kidlisp.com editor) */ .kidlisp-play-indicator { width: 24px; height: 24px; background: rgb(76, 175, 80); border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.2); flex-shrink: 0; transition: all 0.2s ease; }
.kidlisp-play-indicator::before { content: ''; width: 0; height: 0; border-left: 8px solid white; border-top: 5px solid transparent; border-bottom: 5px solid transparent; margin-left: 2px; }
.kidlisp-preview-header:hover .kidlisp-play-indicator { background: rgb(102, 187, 106); transform: scale(1.1); box-shadow: 0 3px 6px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.3); }
/* Single rotating KidLisp preview carousel */ .kidlisp-carousel { position: absolute; inset: 0; overflow: hidden; }
.kidlisp-slide { position: absolute; inset: 0; opacity: 0; transform: translateX(30px); transition: opacity 0.5s ease, transform 0.5s ease; pointer-events: none; }
.kidlisp-slide.active { opacity: 1; transform: translateX(0); pointer-events: auto; }
.kidlisp-slide.exiting { opacity: 0; transform: translateX(-30px); }
/* Hide all slide content until webp is validated and ready */ .kidlisp-slide .kidlisp-slide-code, .kidlisp-slide .kidlisp-slide-qr-wrap, .kidlisp-slide .kidlisp-slide-plays, .kidlisp-slide .kidlisp-slide-credit, .kidlisp-slide .module-loading { opacity: 0; transition: opacity 0.4s ease; }
/* Show content only when slide is marked ready (webp validated) */ .kidlisp-slide.ready .kidlisp-slide-code, .kidlisp-slide.ready .kidlisp-slide-qr-wrap, .kidlisp-slide.ready .kidlisp-slide-plays, .kidlisp-slide.ready .kidlisp-slide-credit { opacity: 1; }
/* Show loading spinner while validating (but not other content) */ .kidlisp-slide.validating .module-loading { opacity: 1; }
.kidlisp-slide a { display: block; width: 100%; height: 100%; position: absolute; inset: 0; }
.kidlisp-slide img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; opacity: 0; transition: opacity 0.5s ease; transform: scale(1.1); }
.kidlisp-slide img.loaded { opacity: 0.5; }
.kidlisp-slide:hover img.loaded { opacity: 0.7; }
.kidlisp-slide-code { position: absolute; top: 4em; left: 0.2em; right: 1em; bottom: 6.5em; padding: 0.4em 0.6em; font-family: var(--mono); font-size: 0.85em; line-height: 1.4; color: #fff; overflow: hidden; text-shadow: 0 1px 2px rgba(0,0,0,0.9); white-space: pre-wrap; word-break: break-word; pointer-events: none; z-index: 2; -webkit-mask-image: linear-gradient(to bottom, black 0%, black 85%, transparent 100%); mask-image: linear-gradient(to bottom, black 0%, black 85%, transparent 100%); }
.kidlisp-slide-code-inner { /* For scrolling long code */ }
.kidlisp-slide-code-inner.scrolling { animation: codeScroll var(--scroll-duration, 10s) ease-in-out infinite; animation-delay: 2s; }
@keyframes codeScroll { 0%, 15% { transform: translateY(0); } 85%, 100% { transform: translateY(var(--scroll-distance, 0)); } }
.kidlisp-slide-credit { position: absolute; bottom: 4em; left: 1em; font-family: var(--mono); font-size: 0.8em; text-shadow: 0 1px 3px rgba(0,0,0,1), 0 0 8px rgba(0,0,0,0.9), 0 0 2px rgba(0,0,0,1); z-index: 5; }
.kidlisp-slide-credit .credit-handle { color: var(--pink); }
.kidlisp-slide-credit .credit-date { color: rgba(255,255,255,0.7); }
.kidlisp-slide-plays { position: absolute; bottom: 5.6em; left: 1em; font-family: var(--mono); font-size: 0.8em; color: var(--gold); text-shadow: 0 1px 3px rgba(0,0,0,1), 0 0 8px rgba(0,0,0,0.9), 0 0 2px rgba(0,0,0,1); z-index: 5; }
.kidlisp-slide-qr-wrap { position: absolute; bottom: 3.2em; right: 1em; display: flex; flex-direction: column; align-items: flex-end; z-index: 5; }
.kidlisp-slide-label { font-family: var(--mono); font-size: 0.85em; color: #fff; background: #000; padding: 0.1em 0.3em; }
/* Code character animation for slide transitions */ .kidlisp-slide-label .code-char { display: inline-block; }
/* Stale code chars blast off and fade - POOF! (from kidlisp.com) */ .kidlisp-slide-label.transitioning .code-char { animation: kidlispCharBlastOff 0.4s ease-out forwards; }
/* Stagger the blast-off for each character */ .kidlisp-slide-label.transitioning .code-char:nth-child(1) { animation-delay: 0ms; } .kidlisp-slide-label.transitioning .code-char:nth-child(2) { animation-delay: 30ms; } .kidlisp-slide-label.transitioning .code-char:nth-child(3) { animation-delay: 50ms; } .kidlisp-slide-label.transitioning .code-char:nth-child(4) { animation-delay: 70ms; } .kidlisp-slide-label.transitioning .code-char:nth-child(5) { animation-delay: 90ms; } .kidlisp-slide-label.transitioning .code-char:nth-child(6) { animation-delay: 110ms; } .kidlisp-slide-label.transitioning .code-char:nth-child(7) { animation-delay: 130ms; } .kidlisp-slide-label.transitioning .code-char:nth-child(8) { animation-delay: 150ms; } .kidlisp-slide-label.transitioning .code-char:nth-child(9) { animation-delay: 170ms; } .kidlisp-slide-label.transitioning .code-char:nth-child(10) { animation-delay: 190ms; }
@keyframes kidlispCharBlastOff { 0% { opacity: 1; transform: translateY(0) scale(1) rotate(0deg); } 50% { opacity: 0.6; transform: translateY(-8px) scale(1.2) rotate(var(--blast-rotate, 10deg)); } 100% { opacity: 0; transform: translateY(-20px) scale(0.3) rotate(var(--blast-rotate, 15deg)); } }
/* Randomize rotation direction per character */ .kidlisp-slide-label.transitioning .code-char:nth-child(odd) { --blast-rotate: 15deg; } .kidlisp-slide-label.transitioning .code-char:nth-child(even) { --blast-rotate: -15deg; } .kidlisp-slide-label.transitioning .code-char:nth-child(3n) { --blast-rotate: 25deg; } .kidlisp-slide-label.transitioning .code-char:nth-child(3n+1) { --blast-rotate: -20deg; }
.kidlisp-slide-qr { background: #fff; padding: 2px; line-height: 0; }
.kidlisp-slide-qr img { display: block; image-rendering: pixelated; }
/* KidLisp syntax highlighting - matches kidlisp.com */ .kidlisp-slide-code .hl-comment { color: #6272a4; font-style: italic; } .kidlisp-slide-code .hl-string { color: orange; } .kidlisp-slide-code .hl-number { color: lime; } .kidlisp-slide-code .hl-keyword { color: pink; } .kidlisp-slide-code .hl-api { color: cyan; } .kidlisp-slide-code .hl-color { /* inline style sets actual color */ } .kidlisp-slide-code .hl-timing { color: #ffb86c; font-weight: bold; } .kidlisp-slide-code .hl-paren { color: #888; } .kidlisp-slide-code .hl-fade { font-weight: bold; } .kidlisp-slide-code .hl-code-ref { color: limegreen; font-weight: bold; } /* $code references */ .kidlisp-slide-code .hl-code-id { color: lime; } /* identifier part of $code */ .kidlisp-slide-code .hl-paint-ref { color: magenta; font-weight: bold; } /* # symbol */ .kidlisp-slide-code .hl-paint-id { color: orange; } /* identifier part of #hashtag */ .kidlisp-slide-code .hl-fade-sep { color: mediumseagreen; } /* - separator in fade */ .kidlisp-slide-code .hl-fade-dir { color: cyan; } /* direction in fade */ .kidlisp-slide-code .hl-fade-colon { color: lime; } /* : in fade */
/* Rainbow text animation - per character cycling */ @keyframes rainbowCycle { 0% { color: red; } 14% { color: orange; } 28% { color: yellow; } 42% { color: lime; } 57% { color: cyan; } 71% { color: blue; } 85% { color: magenta; } 100% { color: red; } }
.hl-rainbow { animation: rainbowCycle 2s linear infinite; font-weight: bold; }
/* Stagger rainbow animation for each character */ .hl-rainbow-0 { animation-delay: 0s; } .hl-rainbow-1 { animation-delay: -0.28s; } .hl-rainbow-2 { animation-delay: -0.56s; } .hl-rainbow-3 { animation-delay: -0.84s; } .hl-rainbow-4 { animation-delay: -1.12s; } .hl-rainbow-5 { animation-delay: -1.40s; } .hl-rainbow-6 { animation-delay: -1.68s; }
/* Zebra text animation - alternating black/white */ @keyframes zebraCycle { 0%, 49% { color: white; } 50%, 100% { color: #333; } }
.hl-zebra { animation: zebraCycle 1s steps(1) infinite; font-weight: bold; }
.hl-zebra-0 { animation-delay: 0s; } .hl-zebra-1 { animation-delay: -0.5s; }
/* Timing blink animation - uses CSS custom property for duration */ @keyframes hl-timing-blink { 0%, 90% { opacity: 1; } 95% { opacity: 0.4; color: lime; } 100% { opacity: 1; } }
.hl-timing-active { animation: hl-timing-blink var(--timing-duration, 1s) ease-in-out infinite; }
#pals { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.2em; grid-column: 1 / -1; color: rgba(255,255,255,0.3); font-family: var(--mono); font-size: 1em; text-decoration: none; cursor: pointer; transition: color 0.2s, filter 0.2s; padding: 0 0 1em 0; margin-top: -1em; }
.pals-logo-container { position: relative; display: inline-block; }
.pals-logo { width: 128px; height: auto; filter: grayscale(100%) opacity(0.3); transition: filter 0.3s, opacity 0.3s; }
.pals-logo-pink { position: absolute; top: 0; left: 0; width: 128px; height: auto; opacity: 0; transition: opacity 0.3s; filter: hue-rotate(-30deg) saturate(1.5) brightness(1.2) drop-shadow(0 0 8px rgba(255, 100, 200, 0.8)) drop-shadow(0 0 16px rgba(255, 100, 200, 0.5)); }
@keyframes pals-energy { 0%, 100% { transform: rotate(0deg) scale(1); filter: drop-shadow(0 0 8px rgba(255, 100, 200, 0.8)) drop-shadow(0 0 16px rgba(255, 100, 200, 0.5)); } 10% { transform: rotate(-4deg) scale(1.02); } 20% { transform: rotate(3deg) scale(0.98); } 30% { transform: rotate(-3deg) scale(1.03); } 40% { transform: rotate(4deg) scale(0.97); } 50% { transform: rotate(-2deg) scale(1.02); filter: drop-shadow(0 0 12px rgba(255, 100, 200, 1)) drop-shadow(0 0 24px rgba(255, 100, 200, 0.7)); } 60% { transform: rotate(3deg) scale(0.99); } 70% { transform: rotate(-4deg) scale(1.01); } 80% { transform: rotate(2deg) scale(1.03); } 90% { transform: rotate(-3deg) scale(0.98); } }
@keyframes pals-shake { 0%, 100% { transform: rotate(0deg); } 20% { transform: rotate(-3deg); } 40% { transform: rotate(3deg); } 60% { transform: rotate(-2deg); } 80% { transform: rotate(2deg); } }
#pals span { order: 2; }
#pals:hover { color: rgba(255,255,255,0.8); }
#pals:hover .pals-logo { filter: grayscale(100%) opacity(0.15); }
#pals:hover .pals-logo-pink { opacity: 1; animation: pals-energy 0.6s ease-in-out infinite; }
.page-footer { grid-column: 1 / -1; text-align: center; padding: 3.5em 0 0.5em 0; font-family: var(--mono); font-size: 0.85em; color: rgba(255,255,255,0.25); }
.page-footer a { color: rgba(255,255,255,0.4); text-decoration: none; }
.page-footer a:hover { color: rgba(255,255,255,0.7); }
/* Footer auth section */ .footer-auth { display: flex; justify-content: center; align-items: center; gap: 12px; margin-bottom: 0.5em; }
.footer-auth-buttons { display: flex; gap: 16px; }
.footer-login-btn { padding: 10px 20px; background: rgba(30, 50, 120, 0.95); color: white; border: 1px solid white; border-radius: 0; font-size: 14px; font-weight: normal; cursor: pointer; transition: all 0.15s; font-family: var(--mono); box-shadow: 0 0 10px rgba(80, 120, 255, 0.25), 0 0 20px rgba(80, 120, 255, 0.1); }
.footer-login-btn:hover { background: rgba(40, 60, 140, 0.95); box-shadow: 0 0 15px rgba(100, 150, 255, 0.4), 0 0 30px rgba(100, 150, 255, 0.2); }
.footer-signup-btn { padding: 10px 20px; background: rgba(30, 90, 50, 0.95); color: white; border: 1px solid white; border-radius: 0; font-size: 14px; font-weight: normal; cursor: pointer; transition: all 0.15s; font-family: var(--mono); box-shadow: 0 0 10px rgba(80, 200, 120, 0.25), 0 0 20px rgba(80, 200, 120, 0.1); }
.footer-signup-btn:hover { background: rgba(40, 110, 60, 0.95); box-shadow: 0 0 15px rgba(100, 255, 150, 0.4), 0 0 30px rgba(100, 255, 150, 0.2); }
.footer-user-menu { display: flex; align-items: center; gap: 4px; padding: 10px 20px; background: black; border: 1px solid white; border-radius: 0; cursor: pointer; transition: all 0.15s; font-family: var(--mono); text-decoration: none; }
.footer-user-menu:hover { background: rgb(40, 40, 40); }
.footer-user-handle { font-size: 14px; color: white; font-weight: normal; font-family: var(--mono); }
.footer-user-stack { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.footer-logout-link { display: block; margin-top: 6px; font-size: 10px; color: rgba(255, 255, 255, 0.3); text-decoration: none; font-family: var(--mono); }
.footer-logout-link:hover { color: rgba(255, 255, 255, 0.6); }
/* Mobile adjustments */ @media (max-width: 699px) { .stats-section { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 400px) { .stats-section { grid-template-columns: repeat(2, 1fr); } .stat-item { min-width: unset; } }
/* Thank You Mode */ .thanks-section { display: none; grid-column: 1 / -1; text-align: center; padding: 2em 1em; grid-row: 2; /* After ticker */ }
body.thanks-mode .thanks-section { display: block; }
body.thanks-mode .fiat-section { grid-row: 1; /* Ticker first */ }
body.thanks-mode .fiat-section .currency-picker { display: none !important; }
body.thanks-mode .fiat-section .currency-pickers { display: none !important; }
body.thanks-mode .currency-links { display: none !important; }
.thanks-canvas-wrap { display: inline-block; margin-bottom: 1.5em; }
.thanks-canvas { width: 400px; height: 400px; border-radius: 12px; border: 4px solid var(--pink); box-shadow: 0 0 20px rgba(255, 107, 157, 0.4), 0 0 40px rgba(255, 107, 157, 0.2), 0 8px 32px rgba(0, 0, 0, 0.3); animation: celebrateSwivel 8s ease-in-out infinite; transition: transform 0.3s ease, box-shadow 0.3s ease; }
.thanks-canvas:hover { transform: scale(1.02) rotate(1deg); box-shadow: 0 0 30px rgba(255, 107, 157, 0.6), 0 0 60px rgba(255, 107, 157, 0.3), 0 12px 48px rgba(0, 0, 0, 0.4); }
@keyframes celebrateSwivel { 0%, 100% { transform: rotate(-1deg) scale(1); } 25% { transform: rotate(0.5deg) scale(1.01); } 50% { transform: rotate(1deg) scale(1); } 75% { transform: rotate(-0.5deg) scale(1.01); } }
.thanks-message {
font-family: 'YWFTProcessing-Regular', sans-serif; font-size: 1.4em; color: var(--text); max-width: 500px; margin: 0 auto 1.5em auto; line-height: 1.5; }
.thanks-amount { color: var(--green); font-weight: bold; }
.thanks-actions { display: flex; gap: 1em; justify-content: center; flex-wrap: wrap; }
.thanks-again { display: inline-block; padding: 0.8em 1.5em; background: var(--box-bg); border: 1px solid var(--box-border); border-radius: 6px; color: var(--cyan); text-decoration: none; transition: all 0.15s; font-size: 0.95em; }
.thanks-again:hover { background: var(--cyan); color: var(--bg); text-decoration: none; }
.thanks-share { display: inline-block; padding: 0.8em 1.5em; background: #000; border: 1px solid #333; border-radius: 6px; color: #fff; font-family: var(--mono); font-size: 0.95em; cursor: pointer; transition: all 0.15s; }
.thanks-share:hover { background: #1da1f2; border-color: #1da1f2; }
/* Thanks page mobile responsive */ @media (max-width: 768px) { .thanks-message { font-size: 1.2em; max-width: 90%; padding: 0 1em; } .thanks-actions { gap: 0.8em; } .thanks-again, .thanks-share { padding: 0.7em 1.2em; font-size: 0.9em; } }
@media (max-width: 480px) { .thanks-message { font-size: 1em; max-width: 95%; padding: 0 0.5em; } .thanks-actions { flex-direction: column; gap: 0.6em; align-items: center; } .thanks-again, .thanks-share { padding: 0.6em 1em; font-size: 0.85em; width: 100%; max-width: 250px; text-align: center; } }
/* Ken Burns Canvas Slideshow - Nostalgic Photo Background */ .jeffreys-slideshow { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; border-radius: 0; z-index: 0; }
.jeffreys-canvas { position: absolute; /* Center the oversized canvas */ top: 50%; left: 50%; transform: translate(-50%, -50%); width: calc(100% + 20px); height: calc(100% + 20px); /* Sharp and crisp with slight darkening */ filter: url(#sharpen) saturate(0.95) brightness(0.75); image-rendering: -webkit-optimize-contrast; /* Better scaling on iOS */ image-rendering: crisp-edges; z-index: 4; /* Above floating messages */ }
/* Subtle color tint */ .jeffreys-slideshow::before { content: ''; position: absolute; inset: 0; background: linear-gradient( 135deg, rgba(148, 0, 211, 0.15) 0%, rgba(205, 92, 155, 0.18) 50%, rgba(138, 43, 226, 0.12) 100% ); mix-blend-mode: overlay; pointer-events: none; z-index: 5; }
/* Subtle vignette */ .jeffreys-slideshow::after { content: ''; position: absolute; inset: 0; background: radial-gradient(ellipse at center, transparent 40%, rgba(26, 26, 46, 0.35) 100%); pointer-events: none; z-index: 6; }
/* Make sure gift-logo-wrap stays above slideshow */ .gift-logo-wrap { position: relative; z-index: 4; }
/* Logo with stronger dark shadow on slideshow background */ .gift-visual:has(.jeffreys-slideshow) .gift-logo { filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.8)) drop-shadow(0 4px 10px rgba(0, 0, 0, 0.6)) drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4)); }
/* Floating give notes on jeffreys slideshow - alphabet soup particle system */ .floating-give { position: absolute; font-family: var(--mono), "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Android Emoji", "EmojiSymbols", sans-serif; font-size: 1.1em; color: #fff; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9); pointer-events: none; white-space: nowrap; max-width: 55%; overflow: visible; opacity: 0; animation: floatAmbient 30s ease-in-out forwards; transform-origin: center center; letter-spacing: 0.1em; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; z-index: 10; /* Above canvas (z-index: 4) */ }
.floating-give.left-side { left: 5%; text-align: left; }
.floating-give.right-side { right: 15%; text-align: right; }
.floating-give.center-side { left: 45%; transform: translateX(-50%); text-align: center; }
.floating-give .char { display: inline-block; animation: charSoup var(--char-duration) ease-in-out infinite; animation-delay: var(--char-delay); transform-origin: center bottom; position: relative; z-index: var(--char-z, 0); }
.floating-give .got { color: var(--cyan); margin-right: 0.3em; }
.floating-give .amount { color: var(--green); font-weight: bold; margin-right: 0.3em; font-size: 1em; letter-spacing: 0.08em; }
.floating-give .amount .char { animation: charSoupBig var(--char-duration) ease-in-out infinite; }
.floating-give.monthly .amount { color: var(--gold); }
.floating-give .note { color: var(--gold); font-size: 1.05em; letter-spacing: 0.08em; animation: noteColorPulse 3s ease-in-out infinite; }
@keyframes noteColorPulse { 0%, 100% { color: var(--gold); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9); } 33% { color: #fff; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9), 0 0 8px rgba(255, 215, 61, 0.4); } 66% { color: var(--cyan); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9); } }
.floating-give .time { margin-right: 0.3em; color: #fff; font-size: 0.85em; letter-spacing: 0.06em; margin-left: 0.3em; }
@keyframes floatAmbient { 0% { opacity: 0; transform: translateY(0) translateX(var(--drift-start, 0px)); } 5% { opacity: 0.9; } 15% { transform: translateY(-50px) translateX(calc(var(--drift-start, 0px) + 25px)); } 30% { transform: translateY(-120px) translateX(calc(var(--drift-start, 0px) - 35px)); } 45% { transform: translateY(-200px) translateX(calc(var(--drift-start, 0px) + 40px)); } 60% { transform: translateY(-300px) translateX(calc(var(--drift-start, 0px) - 30px)); } 75% { transform: translateY(-420px) translateX(calc(var(--drift-start, 0px) + 20px)); } 90% { opacity: 0.9; } 100% { opacity: 0; transform: translateY(-550px) translateX(var(--drift-end, 0px)); } }
/* Alphabet soup - gentle letter sway */ @keyframes charSoup { 0%, 100% { transform: translateY(0) rotate(0deg); } 25% { transform: translateY(-2px) translateX(1px) rotate(1.5deg); } 50% { transform: translateY(-1px) translateX(-1px) rotate(-1deg); } 75% { transform: translateY(-2px) translateX(0.5px) rotate(1deg); } }
/* Bigger swing for amount letters */ @keyframes charSoupBig { 0%, 100% { transform: translateY(0) rotate(var(--rot-base, 0deg)) scale(1); } 15% { transform: translateY(calc(var(--swing-y, -8px) * 1.5)) translateX(calc(var(--swing-x, 3px) * 1.3)) rotate(calc(var(--rot-base, 0deg) + var(--rot-swing, 4deg) * 1.2)) scale(1.02); } 35% { transform: translateY(calc(var(--swing-y, -8px) * 0.9)) translateX(calc(var(--swing-x, 3px) * -1.1)) rotate(calc(var(--rot-base, 0deg) - var(--rot-swing, 4deg) * 0.9)) scale(0.98); } 55% { transform: translateY(calc(var(--swing-y, -8px) * 1.3)) translateX(calc(var(--swing-x, 3px) * 0.8)) rotate(calc(var(--rot-base, 0deg) + var(--rot-swing, 4deg) * 0.7)) scale(1.01); } 75% { transform: translateY(calc(var(--swing-y, -8px) * 0.6)) translateX(calc(var(--swing-x, 3px) * -0.5)) rotate(calc(var(--rot-base, 0deg) - var(--rot-swing, 4deg) * 0.4)) scale(0.99); } }
/* Responsive floating gives - smaller on mobile */ @media (max-width: 768px) { .floating-give { font-size: 0.85em; letter-spacing: 0.05em; } .floating-give .amount { font-size: 0.95em; } .floating-give .note { font-size: 0.9em; } }
@media (max-width: 480px) { .floating-give { font-size: 0.75em; letter-spacing: 0.02em; max-width: 70%; } .floating-give .amount { font-size: 0.85em; margin-right: 0.2em; } .floating-give .note { font-size: 0.8em; } }
/* ================================================ LIGHT MODE THEME OVERRIDES Based on kidlisp.com and at.aesthetic.computer ================================================ */
/* Light mode: Top bar */ body.light-mode .top-bar { background: linear-gradient(to bottom, var(--bg) 0%, var(--bg) 70%, transparent 100%); }
/* Light mode: Logo adjustments */ body.light-mode .logo-give { color: var(--text); } body.light-mode .logo-sep { color: var(--dim); }
/* Light mode: Prose/content boxes */ body.light-mode .prose { background: white; border-color: var(--box-border); box-shadow: var(--shadow-soft); }
/* Light mode: Ticker */ body.light-mode .gives-ticker { background: linear-gradient(180deg, rgba(0, 100, 50, 0.08) 0%, rgba(0, 50, 25, 0.12) 100%); border-color: rgba(0, 150, 100, 0.25); } body.light-mode .gives-ticker-item { border-right-color: rgba(0, 150, 100, 0.15); }
/* Light mode: Gift widget visual */ body.light-mode .gift-visual { background: linear-gradient(135deg, rgba(205, 92, 155, 0.08) 0%, rgba(0, 180, 180, 0.05) 100%); border-color: var(--box-border); }
/* Light mode: Slider/range */ body.light-mode .gift-controls input[type="range"] { background: var(--slider-track); }
/* Light mode: Gift amount display */ body.light-mode .gift-amount { background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); border-color: var(--green); color: #1b5e20; } body.light-mode .gift-widget.monthly-mode .gift-amount { background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%); border-color: var(--gold); color: #e65100; }
/* Light mode: Gift button */ body.light-mode .gift-btn { background: linear-gradient(180deg, var(--green) 0%, #047857 100%); border-color: #10b981; box-shadow: var(--shadow-medium); } body.light-mode .gift-btn:hover { background: linear-gradient(180deg, var(--pink) 0%, rgb(175, 72, 130) 100%); border-color: rgb(225, 112, 175); } body.light-mode .gift-widget.monthly-mode .gift-btn { background: linear-gradient(180deg, var(--gold) 0%, #b45309 100%); border-color: #f59e0b; color: white; }
/* Light mode: Stat items */ body.light-mode .stat-item { background: white; border-color: var(--box-border); box-shadow: var(--shadow-soft); } body.light-mode #stat-handles { background: rgba(59, 130, 246, 0.08); } body.light-mode #stat-paintings { background: rgba(249, 115, 22, 0.08); } body.light-mode #stat-moods { background: rgba(236, 72, 153, 0.08); } body.light-mode #stat-kidlisp { background: rgba(139, 92, 246, 0.08); } body.light-mode #stat-commands { background: rgba(34, 197, 94, 0.08); } body.light-mode #stat-messages { background: rgba(234, 179, 8, 0.08); } body.light-mode .stat-value { text-shadow: none; } body.light-mode .stat-label { text-shadow: none; }
/* Light mode: Module boxes (shop, kidlisp, etc.) */ body.light-mode .shop-module, body.light-mode .kidlisp-module, body.light-mode .apps-flip-face, body.light-mode .hacker-card, body.light-mode .feature-module { background: white; border-color: var(--box-border); box-shadow: var(--shadow-soft); }
/* Light mode: Desktop face (VSCode/Ableton) */ body.light-mode .desktop-face { background: linear-gradient(135deg, #f8f8f8 0%, #eeeeee 100%); border-color: var(--box-border); } body.light-mode .desktop-face-header { border-bottom-color: var(--box-border); } body.light-mode .face-label { color: var(--dim); } body.light-mode .desktop-vscode-link, body.light-mode .desktop-ableton-link { background: rgba(0, 0, 0, 0.04); border-color: var(--box-border); } body.light-mode .desktop-vscode-link:hover, body.light-mode .desktop-ableton-link:hover { background: rgba(0, 0, 0, 0.08); border-color: var(--dim); } body.light-mode .desktop-vscode-link .vscode-label, body.light-mode .desktop-ableton-link .ableton-label { color: var(--text); } body.light-mode .desktop-ableton-link .ableton-tag { background: rgba(0, 0, 0, 0.08); color: var(--dim); } body.light-mode .desktop-header-ticker-content { color: var(--dim); }
/* Light mode: Mobile face */ body.light-mode .mobile-face { background: linear-gradient(135deg, #f8f8f8 0%, #eeeeee 100%); border-color: var(--box-border); } body.light-mode .mobile-face-header { border-bottom-color: var(--box-border); } body.light-mode .phone-frame { background: #333; }
/* Light mode: Links */ body.light-mode a { color: var(--cyan); } body.light-mode a.handle-link { color: var(--pink); } body.light-mode code { background: rgba(205, 92, 155, 0.1); color: var(--pink); }
/* Light mode: Crypto section */ body.light-mode .crypto-header { background: linear-gradient(135deg, rgba(139, 92, 246, 0.12) 0%, rgba(0, 180, 180, 0.06) 100%); } body.light-mode .crypto-address { background: linear-gradient(135deg, rgba(34, 197, 94, 0.08) 0%, rgba(0, 180, 180, 0.05) 100%); border-color: rgba(34, 197, 94, 0.3); } body.light-mode .crypto-address:hover { border-color: var(--green); background: linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(0, 180, 180, 0.1) 100%); } body.light-mode .crypto-addr { background: rgba(0, 50, 30, 0.9); color: #00ff88; } body.light-mode .crypto-controls.crypto-all .crypto-address[data-crypto="xtz"] { background: linear-gradient(180deg, rgba(139, 92, 246, 0.15) 0%, rgba(109, 62, 206, 0.2) 100%); } body.light-mode .crypto-controls.crypto-all .crypto-address[data-crypto="eth"] { background: linear-gradient(180deg, rgba(59, 130, 246, 0.15) 0%, rgba(37, 99, 235, 0.2) 100%); } body.light-mode .crypto-controls.crypto-all .crypto-address[data-crypto="btc"] { background: linear-gradient(180deg, rgba(249, 115, 22, 0.15) 0%, rgba(234, 88, 12, 0.2) 100%); }
/* Light mode: PayPal section */ body.light-mode .paypal-header { background: linear-gradient(135deg, rgba(0, 112, 186, 0.12) 0%, rgba(0, 48, 135, 0.08) 100%); } body.light-mode .paypal-controls { background: linear-gradient(180deg, rgba(0, 112, 186, 0.05) 0%, rgba(0, 48, 135, 0.08) 100%); }
/* Light mode: Currency links */ body.light-mode .curr-link { color: var(--dim); } body.light-mode .curr-link:hover { color: var(--text); } body.light-mode .curr-link.active[data-curr="usd"] { color: var(--green); } body.light-mode .curr-link.active[data-curr="dkk"] { color: var(--cyan); } body.light-mode .curr-link.active[data-curr="crypto"] { color: #7c3aed; } body.light-mode .curr-link.active[data-curr="paypal"] { color: #0070ba; } body.light-mode .curr-link.active[data-curr="liberapay"] { color: #d4a800; }
/* Light mode: Liberapay section */ body.light-mode .liberapay-header { background: linear-gradient(135deg, rgba(246, 201, 21, 0.12) 0%, rgba(200, 160, 0, 0.08) 100%); }
body.light-mode .liberapay-controls { background: linear-gradient(180deg, rgba(246, 201, 21, 0.05) 0%, rgba(200, 160, 0, 0.08) 100%); }
body.light-mode .liberapay-title { color: #d4a800; }
/* Light mode: Language dropdown */ body.light-mode .lang-dropdown { background: white; border-color: var(--box-border); box-shadow: var(--shadow-medium); } body.light-mode .lang-option:hover { background: rgba(205, 92, 155, 0.1); }
/* Light mode: Holiday banner */ body.light-mode .holiday-banner { background: rgba(205, 92, 155, 0.08); border-color: rgba(205, 92, 155, 0.2); }
/* Light mode: Login modal */ body.light-mode .login-modal { background: #fafafa; border: 1px solid rgba(0, 0, 0, 0.15); box-shadow: 0 0 0 1px rgba(205, 92, 155, 0.2), 0 8px 32px rgba(0, 0, 0, 0.15); } body.light-mode .login-modal::backdrop { background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(4px); } body.light-mode .login-modal-title { color: var(--pink); } body.light-mode .login-modal-text { color: rgba(0, 0, 0, 0.6); } body.light-mode .login-modal-btn.secondary { color: rgba(0, 0, 0, 0.5); border-color: rgba(0, 0, 0, 0.15); } body.light-mode .login-modal-btn.secondary:hover { background: rgba(0, 0, 0, 0.05); color: rgba(0, 0, 0, 0.8); border-color: rgba(0, 0, 0, 0.25); }
/* Light mode: Thanks section */ body.light-mode .thanks-section { background: white; }
/* Light mode: Footer - keep original button styles */ body.light-mode .give-footer { background: linear-gradient(to top, var(--bg) 0%, transparent 100%); } /* Light mode: Add darker shadow behind footer buttons for visibility */ body.light-mode .footer-login-btn { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 10px rgba(80, 120, 255, 0.25); } body.light-mode .footer-signup-btn { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 10px rgba(80, 200, 120, 0.25); }
/* Light mode: Monthly checkbox */ body.light-mode .gift-monthly-check input[type="checkbox"] { background: white; border-color: var(--box-border); } body.light-mode .gift-monthly-check input[type="checkbox"]:checked { background: var(--gold); border-color: var(--gold); }
/* Light mode: Shop products */ body.light-mode .shop-product { background: #fafafa; border-color: var(--box-border); } body.light-mode .shop-product-img { background: #f0f0f0; }
/* Light mode: KidLisp panels */ body.light-mode .kidlisp-panel { background: white; border-color: var(--box-border); } body.light-mode .kidlisp-preview { background: #f8f6ff; border: 1px solid rgba(139, 92, 246, 0.2); } body.light-mode .kidlisp-preview .module-caption { background: #fffacd !important; color: var(--text); border-bottom: 3px solid rgba(139, 92, 246, 0.3); } body.light-mode .kidlisp-preview-header { background: #fffacd; border-bottom: 1px solid rgba(0, 0, 0, 0.1); } body.light-mode .kidlisp-header-code .code-dollar { color: #22c55e; text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3); } body.light-mode .kidlisp-header-code .code-name { color: #15803d; text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2); } body.light-mode .kidlisp-logo, body.light-mode .baby-colors { text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25); } /* Light mode: KidLisp slide - brighter handles/hits, full opacity images */ body.light-mode .kidlisp-slide-credit .credit-handle { color: #d946ef; text-shadow: 0 1px 3px rgba(0,0,0,0.8), 0 0 6px rgba(0,0,0,0.6); } body.light-mode .kidlisp-slide-plays { color: #f59e0b; text-shadow: 0 1px 3px rgba(0,0,0,0.8), 0 0 6px rgba(0,0,0,0.6); } body.light-mode .kidlisp-slide img.loaded { opacity: 0.85; } body.light-mode .kidlisp-slide:hover img.loaded { opacity: 1; }
/* Light mode: Invest section */ body.light-mode .invest-section { background: linear-gradient(135deg, rgba(240, 253, 244, 0.95) 0%, rgba(236, 253, 245, 0.98) 100%), url('https://assets.aesthetic.computer/jeffreys/jpg/IMG_2658.jpg'); background-size: cover, 200%; background-position: center; border-color: rgba(16, 185, 129, 0.3); box-shadow: var(--shadow-soft); } body.light-mode .invest-header { color: var(--cyan); } body.light-mode .invest-content { color: var(--text); } body.light-mode .invest-content a { color: var(--cyan); border-bottom-color: rgba(8, 145, 178, 0.4); } body.light-mode .invest-content a:hover { color: #0e7490; border-bottom-color: #0e7490; } body.light-mode .invest-suggestion { background: rgba(16, 185, 129, 0.1); border-color: rgba(16, 185, 129, 0.3); color: var(--text); } body.light-mode .invest-suggestion a { color: var(--cyan); }
/* Light mode: Module captions (all types) */ body.light-mode .module-caption { background: rgba(255, 255, 255, 0.95) !important; color: var(--text); } body.light-mode .module-caption .highlight { color: var(--gold); } body.light-mode .cmd-highlight { color: var(--pink); }
/* Light mode: Chat preview boxes */ body.light-mode .chat-preview { background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); border-color: rgba(217, 119, 6, 0.25); box-shadow: var(--shadow-soft); } body.light-mode .chat-preview-header { background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); } body.light-mode .chat-preview-header strong { color: var(--gold); } body.light-mode .chat-preview .module-caption { background: #fffacd !important; color: var(--text); } body.light-mode .chat-messages { background: rgba(255, 255, 255, 0.9); } body.light-mode .chat-preview .chat-msg { color: var(--text); } body.light-mode .chat-preview .chat-time { color: var(--dim); } body.light-mode .chat-preview .chat-handle { color: var(--pink); } /* Light mode: Chat system (purple theme) */ body.light-mode .chat-preview-system { background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%); border-color: rgba(139, 92, 246, 0.25); box-shadow: var(--shadow-soft); } body.light-mode .chat-system-header { background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%); } body.light-mode .chat-system-header strong { color: #7c3aed; } body.light-mode .chat-preview-system .module-caption { background: #ede9fe !important; color: var(--text); } body.light-mode .chat-preview-system .chat-messages { background: rgba(255, 255, 255, 0.9); } body.light-mode .chat-preview-system .chat-handle { color: #7c3aed; }
/* Light mode: Hacker card / AT Proto section - keep transparent, no shadow */ body.light-mode .hacker-card { background: transparent; border: none; box-shadow: none; color: var(--text); } body.light-mode .hacker-card:hover { color: var(--text); box-shadow: none; } body.light-mode .hacker-card-title { color: var(--text); } body.light-mode .hacker-card-body { color: var(--text); } body.light-mode .hacker-card-body code { background: rgba(0, 0, 0, 0.06); color: var(--dim); } body.light-mode .hacker-card a { color: var(--pink); } body.light-mode .at-gray { color: var(--dim); } /* Light mode: AT handle scramble animation - gray instead of white */ body.light-mode .at-handle-prefix .at-char-gray { animation: atCharGrayLight 1.8s steps(1) infinite; } @keyframes atCharGrayLight { 0% { color: rgba(0, 0, 0, 0.35); } 16.6% { color: rgba(0, 0, 0, 0.5); } 33.3% { color: rgba(0, 0, 0, 0.4); } 50% { color: rgba(0, 0, 0, 0.55); } 66.6% { color: rgba(0, 0, 0, 0.45); } 83.3% { color: rgba(0, 0, 0, 0.35); } 100% { color: rgba(0, 0, 0, 0.35); } } body.light-mode .hacker-section { box-shadow: none; } body.light-mode .at-table td:first-child { color: var(--dim); } body.light-mode .at-table td:last-child { color: var(--text); } /* Light mode: Back to top / pals link */ body.light-mode #pals { color: var(--dim); } body.light-mode #pals:hover { color: var(--text); }
/* Light mode: Link blocks */ body.light-mode .link-block { background: white; border-color: var(--box-border); } body.light-mode .link-block a { color: var(--cyan); } body.light-mode .link-block .link-desc { color: var(--dim); }
/* Light mode: Feature modules */ body.light-mode .feature-module { background: white; border-color: var(--box-border); box-shadow: var(--shadow-soft); } body.light-mode .feature-module li { color: var(--text); }
/* Light mode: Apps flip card faces */ body.light-mode .apps-flip-face .module-caption { background: rgba(255, 255, 255, 0.95); color: var(--text); }
/* Light mode: Shop module - thicker border */ body.light-mode .shop-module { border: 3px solid rgba(205, 92, 155, 0.6); box-shadow: var(--shadow-medium); } /* Shop bottom caption - light background with black text, with bottom border */ body.light-mode .shop-section .module-caption { background: rgba(255, 255, 255, 0.95); color: rgba(0, 0, 0, 0.6); border-bottom: 3px solid rgba(205, 92, 155, 0.6); } /* Shop title (top left "shop") - keep white with shadow */ body.light-mode .shop-title { color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.6), 0 0 8px rgba(0,0,0,0.4); } body.light-mode .shop-title:hover { color: var(--pink); } /* Shop progress bar - light gray track with pink progress for light mode */ body.light-mode .shop-auto-progress { background: rgba(200, 200, 200, 0.3); } body.light-mode .shop-auto-progress-bar { background: rgba(205, 92, 155, 0.7); }
/* Light mode: Tapes/TV section */ body.light-mode .tapes-section { background: white; border-color: var(--box-border); } body.light-mode .tapes-section .module-caption { background: rgba(255, 255, 255, 0.95); color: var(--text); } /* Light mode: TV section with light letterbox */ body.light-mode .tv-section { background: #e8e8e8; box-shadow: -2px 0 0 rgba(205, 92, 155, 0.5), 2px 0 0 rgba(8, 145, 178, 0.5), 0 4px 12px rgba(0, 0, 0, 0.15); } body.light-mode .tv-section .module-caption { background: #e8e8e8; color: var(--text); } body.light-mode .tv-player { background: #e8e8e8; }
/* Light mode: Floating gives - no background, just readable text */ body.light-mode .floating-give { background: transparent; border: none; color: var(--text); text-shadow: 0 1px 3px rgba(255, 255, 255, 0.9), 0 0 8px rgba(255, 255, 255, 0.7); box-shadow: none; } body.light-mode .floating-give .amount { color: var(--green); } body.light-mode .floating-give .note { color: var(--gold); }
/* Light mode: Scrollbar - ensure visible */ body.light-mode { scrollbar-width: auto; -ms-overflow-style: auto; } body.light-mode::-webkit-scrollbar { display: block; width: 10px; } body.light-mode::-webkit-scrollbar-track { background: var(--bg); } body.light-mode::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 5px; } body.light-mode::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.35); }
/* Light mode: Input fields */ body.light-mode input[type="text"], body.light-mode input[type="email"], body.light-mode textarea { background: white; border-color: var(--box-border); color: var(--text); } body.light-mode input[type="text"]:focus, body.light-mode input[type="email"]:focus, body.light-mode textarea:focus { border-color: var(--pink); outline: none; }
/* Light mode: Platform download buttons - black text */ body.light-mode .desktop-platform-btn .plat-name { color: #111; } body.light-mode .desktop-platform-btn .plat-icon svg { fill: #111; } body.light-mode .desktop-platform-btn .plat-arch { color: #555; } body.light-mode .desktop-platform-btn:hover .plat-name { color: #000; } /* Light mode: Ableton link - black text/icon */ body.light-mode .desktop-ableton-link .ableton-icon { color: #111; } body.light-mode .desktop-ableton-link .ableton-label { color: #111; } /* Light mode: Thanks section */ body.light-mode .thanks-section { background: var(--bg); } body.light-mode .thanks-canvas { border-color: var(--pink); box-shadow: 0 0 20px rgba(205, 92, 155, 0.3), 0 0 40px rgba(205, 92, 155, 0.15), 0 8px 32px rgba(0, 0, 0, 0.15); } body.light-mode .thanks-canvas:hover { box-shadow: 0 0 30px rgba(205, 92, 155, 0.5), 0 0 60px rgba(205, 92, 155, 0.25), 0 12px 48px rgba(0, 0, 0, 0.2); } body.light-mode .thanks-message { color: var(--text); } body.light-mode .thanks-amount { color: var(--green); } body.light-mode .thanks-again { background: white; border-color: var(--box-border); color: var(--cyan); } body.light-mode .thanks-again:hover { background: var(--cyan); color: white; } body.light-mode .thanks-share { background: white; border: 1px solid var(--box-border); color: var(--text); } body.light-mode .thanks-share:hover { background: #1da1f2; border-color: #1da1f2; color: white; }
/* Light mode: Respect user's system preference */ @media (prefers-color-scheme: light) { body { --bg: #f5f5f5; --text: #1a1a2e; --dim: #666; --pink: rgb(205, 92, 155); --cyan: #0891b2; --gold: #d97706; --green: #059669; --box-bg: rgba(0,0,0,0.03); --box-border: rgba(0,0,0,0.12); --shadow-soft: 0 2px 8px rgba(0,0,0,0.08); --shadow-medium: 0 4px 16px rgba(0,0,0,0.12); --input-bg: white; --slider-track: linear-gradient(90deg, #f87171, #fbbf24, #22d3ee, #f472b6, #60a5fa, #8b5cf6, #06b6d4); } } </style> <!-- TEMP: simplified give page (2026-06-04) — remove this block to restore --> <style id="temp-simplify-give"> /* hide the stats + content-preview modules under the give bar (jeffrey hero canvas + invest are intentionally kept/restored) */ .stats-section, .kidlisp-preview, .chat-flip-container, .notepat-section, .tv-section-wrap, .apps-flip-container, .shop-section, .invest-section, .hacker-section { display: none !important; } </style> <!-- /TEMP --></head>
<body> <div class="top-bar"> <div style="display: flex; align-items: center; gap: 8px;"> <div class="logo" onclick="window.location.href='https://aesthetic.computer'"><span class="logo-give">give</span><span class="logo-sep">—</span><span class="logo-ac">Aesthetic<span class="logo-dot">.</span>Computer</span><span class="logo-year">'26</span></div> <div class="lang-selector" id="langSelector"> <span class="lang-flag fi fi-us" id="lang-flag"></span> <span class="lang-text" id="lang-text">English</span> <span class="lang-arrow">▼</span> <div class="lang-dropdown" id="lang-dropdown"> <div class="lang-option" data-lang="en" data-flag="us"><span class="fi fi-us"></span> English</div> <div class="lang-option" data-lang="da" data-flag="dk"><span class="fi fi-dk"></span> Dansk</div> <div class="lang-option" data-lang="de" data-flag="de"><span class="fi fi-de"></span> Deutsch</div> <div class="lang-option" data-lang="es" data-flag="es"><span class="fi fi-es"></span> Español</div> <div class="lang-option" data-lang="zh" data-flag="cn"><span class="fi fi-cn"></span> 中文</div> </div> </div> </div> <div class="currency-links" id="currSelector"> <span class="curr-link active" data-curr="usd"><span class="curr-flag">💵</span><span class="curr-text">USD</span></span> <span class="curr-link" data-curr="dkk"><span class="curr-flag">💶</span><span class="curr-text">DKK</span></span> <span class="curr-link" data-curr="crypto"><span class="curr-flag">🔗</span><span class="curr-text">CRYPTO</span></span> <span class="curr-link" data-curr="paypal"><span class="curr-flag">🅿️</span><span class="curr-text">PAYPAL</span></span> <span class="curr-link" data-curr="liberapay"><span class="curr-flag">🪙</span><span class="curr-text">LIBERAPAY</span></span> </div> </div>
<main> <div class="content-grid"> <!-- Thank You Section (shown when ?thanks=1) --> <div class="thanks-section"> <div class="thanks-canvas-wrap"> <canvas class="thanks-canvas" id="thanksCanvas" width="560" height="560"></canvas> </div> <p class="thanks-message" data-lang="en"> TY for keeping systems online and free to use! </p> <p class="thanks-message" data-lang="da"> Tak for at give <span class="thanks-amount" id="thanksAmountDa">$25</span> og holde Aesthetic.Computer online og gratis at bruge. </p> <p class="thanks-message" data-lang="de"> Danke für Ihre Spende von <span class="thanks-amount" id="thanksAmountDe">$25</span>. Damit bleibt Aesthetic.Computer online und kostenlos. </p> <p class="thanks-message" data-lang="es"> Gracias por donar <span class="thanks-amount" id="thanksAmountEs">$25</span> y mantener Aesthetic.Computer en línea y gratuito. </p> <p class="thanks-message" data-lang="zh"> 感谢您捐赠<span class="thanks-amount" id="thanksAmountZh">$25</span>,让Aesthetic.Computer保持在线并免费使用。 </p> <div class="thanks-actions"> <a href="./" class="thanks-again" data-lang="en">Give again?</a> <a href="./" class="thanks-again" data-lang="da">Giv igen?</a> <a href="./" class="thanks-again" data-lang="de">Nochmal spenden?</a> <a href="./" class="thanks-again" data-lang="es">¿Donar de nuevo?</a> <a href="./" class="thanks-again" data-lang="zh">再次捐赠?</a> <button class="thanks-share" id="shareBtn" data-lang="en">Share</button> <button class="thanks-share" id="shareBtnDa" data-lang="da">Del</button> <button class="thanks-share" id="shareBtnDe" data-lang="de">Teilen</button> <button class="thanks-share" id="shareBtnEs" data-lang="es">Compartir</button> <button class="thanks-share" id="shareBtnZh" data-lang="zh">分享</button> </div> </div>
<!-- Gives Ticker - cycles through recent gives --> <div class="fiat-section full-width"> <div class="ticker-stats-row"> <div class="ticker-subscripts"> <div class="ticker-stat-badge gives-badge" id="giveStatsGives"> <span class="ticker-stat-value">0</span> <span class="ticker-stat-label">gives</span> <span class="ticker-stat-amount" id="giveStatsAmount"></span> </div> <div class="ticker-stat-badge monthly-badge" id="giveStatsSubs"> <span class="ticker-stat-value">0</span> <span class="ticker-stat-label">monthly</span> </div> </div> <div class="ticker-center"> <div class="gives-ticker" id="givesTicker"> <div class="gives-ticker-empty">Loading gives...</div> </div> </div> </div> <div class="currency-pickers"> <div class="currency-picker" data-for="usd"> <div class="gift-widget" data-currency="usd"> <div class="gift-visual"> <div class="jeffreys-slideshow" id="jeffreysSlideshow"></div> <div class="gift-logo-wrap"> <img src="https://aesthetic.computer/purple-pals.svg" class="gift-logo" alt=""> <div class="gift-amount">$128</div> </div> </div> <div class="gift-controls"> <input type="range" min="1" max="2048" step="1" value="128"> <button class="gift-btn" data-currency="usd">Give $128</button> <div class="invest-suggestion" style="display: none;"> <span data-lang="en">At this level, consider <a href="#invest" class="invest-link">investing</a> for equity!</span> <span data-lang="da">På dette niveau, overvej at <a href="#invest" class="invest-link">investere</a> for ejerandel!</span> <span data-lang="de">Bei diesem Betrag, erwägen Sie zu <a href="#invest" class="invest-link">investieren</a> für Eigenkapital!</span> <span data-lang="es">¡A este nivel, considere <a href="#invest" class="invest-link">invertir</a> para obtener participación!</span> <span data-lang="zh">在此金额下,考虑<a href="#invest" class="invest-link">投资</a>获取股权!</span> </div> <div class="gift-monthly-row"> <div class="gift-monthly-stack"> <label class="gift-monthly-check"> <input type="checkbox" class="monthly-checkbox"> <span data-lang="en">Monthly</span> <span data-lang="da">Månedligt</span> <span data-lang="de">Monatlich</span> <span data-lang="es">Mensual</span> <span data-lang="zh">每月</span> </label> <a href="#" class="cancel-subscription-link"> <span data-lang="en">Cancel?</span> <span data-lang="da">Annuller?</span> <span data-lang="de">Kündigen?</span> <span data-lang="es">¿Cancelar?</span> <span data-lang="zh">取消?</span> </a> </div> <span class="tax-note"> <span data-lang="en">Not tax deductible. <a href="#invest" class="invest-link">Investor?</a></span> <span data-lang="da">Ikke fradragsberettiget. <a href="#invest" class="invest-link">Investor?</a></span> <span data-lang="de">Nicht steuerlich absetzbar. <a href="#invest" class="invest-link">Investor?</a></span> <span data-lang="es">No deducible de impuestos. <a href="#invest" class="invest-link">¿Inversor?</a></span> <span data-lang="zh">不可抵税。<a href="#invest" class="invest-link">投资者?</a></span> </span> </div> </div> </div> </div> <div class="currency-picker" data-for="dkk" style="display: none;"> <div class="gift-widget" data-currency="dkk"> <div class="gift-visual"> <div class="gift-logo-wrap"> <img src="https://aesthetic.computer/purple-pals.svg" class="gift-logo" alt=""> <div class="gift-amount">512 kr</div> <div class="gift-conversion" id="dkkConversion"></div> </div> </div> <div class="gift-controls"> <input type="range" min="1" max="17500" step="1" value="512"> <button class="gift-btn" data-currency="dkk">Giv 512 kr</button> <div class="gift-monthly-row"> <div class="gift-monthly-stack"> <label class="gift-monthly-check"> <input type="checkbox" class="monthly-checkbox"> <span data-lang="en">Monthly</span> <span data-lang="da">Månedligt</span> <span data-lang="de">Monatlich</span> <span data-lang="es">Mensual</span> <span data-lang="zh">每月</span> </label> <a href="#" class="cancel-subscription-link"> <span data-lang="en">Cancel?</span> <span data-lang="da">Annuller?</span> <span data-lang="de">Kündigen?</span> <span data-lang="es">¿Cancelar?</span> <span data-lang="zh">取消?</span> </a> </div> <span class="tax-note"> <span data-lang="en">Not tax deductible. <a href="#invest" class="invest-link">Investor?</a></span> <span data-lang="da">Ikke fradragsberettiget. <a href="#invest" class="invest-link">Investor?</a></span> <span data-lang="de">Nicht steuerlich absetzbar. <a href="#invest" class="invest-link">Investor?</a></span> <span data-lang="es">No deducible de impuestos. <a href="#invest" class="invest-link">¿Inversor?</a></span> <span data-lang="zh">不可抵税。<a href="#invest" class="invest-link">投资者?</a></span> </span> </div> </div> </div> </div> <div class="currency-picker" data-for="crypto" style="display: none;"> <div class="gift-widget crypto crypto-compact" data-currency="crypto"> <div class="crypto-header"> <img src="https://aesthetic.computer/purple-pals.svg" class="crypto-logo-tiny" alt=""> <span class="crypto-title">🔗 CRYPTO</span> <span class="crypto-subtitle">🐷 Piggy Banks for Rainy Days</span> </div> <div class="crypto-controls crypto-all"> <div class="crypto-address copyable" data-copy="tz1gkf8EexComFBJvjtT1zdsisdah791KwBE" data-crypto="xtz"> <span class="crypto-label"><span class="crypto-symbol"><img src="https://raw.githubusercontent.com/ErikThiart/cryptocurrency-icons/master/64/tezos.png" alt="XTZ" class="crypto-icon"></span><span class="crypto-name">aesthetic.tez</span></span> <span class="crypto-balance" id="balance-xtz"><span class="loading">loading...</span></span> <code class="crypto-addr">tz1gkf8E<span class="addr-dots">···</span>1KwBE</code> <span class="crypto-copy-hint">→ copy</span> </div> <div class="crypto-address copyable" data-copy="0x1D64E3eFa983D945cBFe29Ad5b3C8ABB53Aef023" data-crypto="eth"> <span class="crypto-label"><span class="crypto-symbol"><img src="https://raw.githubusercontent.com/ErikThiart/cryptocurrency-icons/master/64/ethereum.png" alt="ETH" class="crypto-icon"></span><span class="crypto-name">4esthetic.eth</span></span> <span class="crypto-balance" id="balance-eth"><span class="loading">loading...</span></span> <code class="crypto-addr">0x1D64E3<span class="addr-dots">···</span>ef023</code> <span class="crypto-copy-hint">→ copy</span> </div> <div class="crypto-address copyable" data-copy="bc1q699gnqr92gvgv62nla82typpj3wls5a8xf59as" data-crypto="btc"> <span class="crypto-label"><span class="crypto-symbol"><img src="https://raw.githubusercontent.com/ErikThiart/cryptocurrency-icons/master/64/bitcoin.png" alt="BTC" class="crypto-icon"></span><span class="crypto-name">bitcoin</span></span> <span class="crypto-balance" id="balance-btc"><span class="loading">loading...</span></span> <code class="crypto-addr">bc1q699g<span class="addr-dots">···</span>f59as</code> <span class="crypto-copy-hint">→ copy</span> </div> </div> </div> </div> <div class="currency-picker" data-for="paypal" style="display: none;"> <div class="gift-widget paypal paypal-compact" data-currency="paypal"> <div class="paypal-header"> <img src="https://aesthetic.computer/purple-pals.svg" class="paypal-logo-tiny" alt=""> <span class="paypal-title">🅿️ PAYPAL</span> <span class="paypal-subtitle">Pay Aesthetic.Computer on PayPal</span> </div> <div class="paypal-controls"> <div class="paypal-qr-wrap"> <img src="https://assets.aesthetic.computer/images/paypal-qrcode.png" alt="PayPal QR Code" class="paypal-qr"> </div> <div class="paypal-actions"> <a href="https://www.paypal.com/qrcodes/managed/93f544e1-c016-4747-a63c-b1ee2ead0292?utm_source=payandgetpaid" target="_blank" rel="noopener" class="paypal-link-btn"> <span class="paypal-btn-text"> <span data-lang="en">Give via PayPal</span> <span data-lang="da">Giv via PayPal</span> <span data-lang="de">Geben via PayPal</span> <span data-lang="es">Dar via PayPal</span> <span data-lang="zh">通过PayPal捐赠</span> </span> </a> <div class="paypal-email copyable" data-copy="mail@aesthetic.computer"> <span class="paypal-email-label"> <span data-lang="en">or:</span> <span data-lang="da">eller:</span> <span data-lang="de">oder:</span> <span data-lang="es">o:</span> <span data-lang="zh">或:</span> </span> <code class="paypal-email-addr">mail@aesthetic.computer</code> <span class="paypal-copy-hint">→ copy</span> </div> </div> </div> </div> </div> <div class="currency-picker" data-for="liberapay" style="display: none;"> <div class="gift-widget liberapay liberapay-compact" data-currency="liberapay"> <div class="liberapay-header"> <img src="https://aesthetic.computer/purple-pals.svg" class="liberapay-logo-tiny" alt=""> <span class="liberapay-title">🪙 LIBERAPAY</span> <span class="liberapay-subtitle">Recurring donations on Liberapay</span> </div> <div class="liberapay-controls"> <a href="https://liberapay.com/aesthetic.computer/donate" target="_blank" rel="noopener" class="liberapay-link-btn"> <span data-lang="en">Give on Liberapay</span> <span data-lang="da">Giv via Liberapay</span> <span data-lang="de">Geben via Liberapay</span> <span data-lang="es">Dar via Liberapay</span> <span data-lang="zh">通过Liberapay捐赠</span> </a> <span class="liberapay-desc"> <span data-lang="en">open source platform for recurring donations — no fees taken by Liberapay</span> <span data-lang="da">open source platform for tilbagevendende donationer — ingen gebyrer fra Liberapay</span> <span data-lang="de">Open-Source-Plattform für wiederkehrende Spenden — keine Gebühren von Liberapay</span> <span data-lang="es">plataforma de código abierto para donaciones recurrentes — sin comisiones de Liberapay</span> <span data-lang="zh">开源定期捐赠平台 — Liberapay不收取任何费用</span> </span> </div> </div> </div> </div> </div>
<div class="prose full-width" data-lang="en"> <a href="https://aesthetic.computer">Aesthetic<span style="color: var(--pink);">.</span>Computer</a> is a social software network supported by the full-time programming practice of <a href="https://aesthetic.computer/@jeffrey" class="handle-link">@jeffrey</a> who has authored new media interfaces for 15 years. He lives in Echo Park with <a href="https://aesthetic.computer/@fifi" class="handle-link">@fifi</a> in a house full of musicians and regularly does show and tell at the <a href="https://nelacomputer.club" target="_blank">NELA Computer Club</a> in Chinatown. </div> <div class="prose full-width" data-lang="da"> <a href="https://aesthetic.computer">Aesthetic<span style="color: var(--pink);">.</span>Computer</a> er et socialt softwarenetværk understøttet af fuldtids ingeniørpraksis af <a href="https://aesthetic.computer/@jeffrey" class="handle-link">@jeffrey</a>, som har bygget eksperimentelle software-instrumenter i over et årti. Han bor i Echo Park med <a href="https://aesthetic.computer/@fifi" class="handle-link">@fifi</a> i et hus fuldt af musikere, og demonstrerer regelmæssigt softwaren ved <a href="https://nelacomputer.club" target="_blank">NELA Computer Club</a> i Chinatown. </div> <div class="prose full-width" data-lang="de"> <a href="https://aesthetic.computer">Aesthetic<span style="color: var(--pink);">.</span>Computer</a> ist ein soziales Software-Netzwerk, unterstützt durch die Vollzeit-Ingenieurpraxis von <a href="https://aesthetic.computer/@jeffrey" class="handle-link">@jeffrey</a>, der seit über einem Jahrzehnt experimentelle Software-Instrumente baut. Er lebt in Echo Park mit <a href="https://aesthetic.computer/@fifi" class="handle-link">@fifi</a> in einem Haus voller Musiker und präsentiert regelmäßig beim <a href="https://nelacomputer.club" target="_blank">NELA Computer Club</a> in Chinatown. </div> <div class="prose full-width" data-lang="es"> <a href="https://aesthetic.computer">Aesthetic<span style="color: var(--pink);">.</span>Computer</a> es una red de software social apoyada por la práctica de ingeniería a tiempo completo de <a href="https://aesthetic.computer/@jeffrey" class="handle-link">@jeffrey</a>, quien ha estado creando instrumentos de software experimentales durante más de una década. Vive en Echo Park con <a href="https://aesthetic.computer/@fifi" class="handle-link">@fifi</a> en una casa llena de músicos, y regularmente hace demostraciones en el <a href="https://nelacomputer.club" target="_blank">NELA Computer Club</a> en Chinatown. </div> <div class="prose full-width" data-lang="zh"> <a href="https://aesthetic.computer">Aesthetic<span style="color: var(--pink);">.</span>Computer</a> 是一个社交软件网络,由 <a href="https://aesthetic.computer/@jeffrey" class="handle-link">@jeffrey</a> 的全职工程实践所支持,他已经创建实验性软件工具超过十年。他与 <a href="https://aesthetic.computer/@fifi" class="handle-link">@fifi</a> 住在 Echo Park 一个满是音乐人的房子里,并定期在 Chinatown 的 <a href="https://nelacomputer.club" target="_blank">NELA Computer Club</a> 展示。 </div>
<div class="prose full-width" data-lang="da"> 🇩🇰 <a href="https://en.wikipedia.org/wiki/Goodiepal" target="_blank"><strong>Goodiepal</strong></a> og <a href="https://prompt.ac/laer-klokken" target="_blank" class="handle-link">laer-klokken</a>-fællesskabet har brugt softwaren hver dag til at dyrke fællesskab i Københavns IRL musik-, kunst- og kulturscene. </div>
<div class="why-give-section full-width"> <div class="why-give-title" data-lang="en">Why give?</div> <div class="why-give-title" data-lang="da">Hvorfor give?</div> <div class="why-give-title" data-lang="de">Warum geben?</div> <div class="why-give-title" data-lang="es">¿Por qué dar?</div> <div class="why-give-title" data-lang="zh">为什么捐赠?</div>
<div data-lang="en">Because we publish transparent costs. See <a class="why-link" href="https://bills.aesthetic.computer" target="_blank">Bills</a> to track where support goes.</div> <div data-lang="da">Fordi vi viser gennemsigtige omkostninger. Se <a class="why-link" href="https://bills.aesthetic.computer" target="_blank">Bills</a> for at følge hvor støtten går hen.</div> <div data-lang="de">Weil wir unsere Kosten transparent veröffentlichen. Unter <a class="why-link" href="https://bills.aesthetic.computer" target="_blank">Bills</a> sehen Sie, wohin die Unterstützung geht.</div> <div data-lang="es">Porque publicamos costos transparentes. En <a class="why-link" href="https://bills.aesthetic.computer" target="_blank">Bills</a> puedes ver a dónde va el apoyo.</div> <div data-lang="zh">因为我们公开透明地展示成本。查看 <a class="why-link" href="https://bills.aesthetic.computer" target="_blank">Bills</a>,了解支持资金的去向。</div> </div>
<div class="stats-section"> <div class="stat-item" id="stat-handles"> <div class="stat-floaters"></div> <div class="stat-value">—</div> <div class="stat-label"> <span data-lang="en"><span class="stat-sigil">@</span>handles</span> <span data-lang="da"><span class="stat-sigil">@</span>brugernavne</span> <span data-lang="de"><span class="stat-sigil">@</span>Benutzer</span> <span data-lang="es"><span class="stat-sigil">@</span>usuarios</span> <span data-lang="zh"><span class="stat-sigil">@</span>用户名</span> </div> </div> <div class="stat-item" id="stat-paintings"> <div class="stat-floaters"></div> <div class="stat-value">—</div> <div class="stat-label"> <span data-lang="en"><span class="stat-sigil">#</span>paintings</span> <span data-lang="da"><span class="stat-sigil">#</span>malerier</span> <span data-lang="de"><span class="stat-sigil">#</span>Bilder</span> <span data-lang="es"><span class="stat-sigil">#</span>pinturas</span> <span data-lang="zh"><span class="stat-sigil">#</span>绘画</span> </div> </div> <div class="stat-item" id="stat-moods"> <div class="stat-floaters"></div> <div class="stat-value">—</div> <div class="stat-label"> <span data-lang="en">moods</span> <span data-lang="da">stemninger</span> <span data-lang="de">Stimmungen</span> <span data-lang="es">estados</span> <span data-lang="zh">心情</span> </div> </div> <div class="stat-item" id="stat-kidlisp"> <div class="stat-floaters"></div> <div class="stat-value">—</div> <div class="stat-label"> <span data-lang="en"><span class="stat-sigil">$</span>kidlisps</span> <span data-lang="da"><span class="stat-sigil">$</span>kidlisps</span> <span data-lang="de"><span class="stat-sigil">$</span>kidlisps</span> <span data-lang="es"><span class="stat-sigil">$</span>kidlisps</span> <span data-lang="zh"><span class="stat-sigil">$</span>kidlisps</span> </div> </div> <div class="stat-item" id="stat-commands"> <div class="stat-floaters"></div> <div class="stat-value">—</div> <div class="stat-label"> <span data-lang="en">commands</span> <span data-lang="da">kommandoer</span> <span data-lang="de">Befehle</span> <span data-lang="es">comandos</span> <span data-lang="zh">命令</span> </div> </div> <div class="stat-item" id="stat-messages"> <div class="stat-floaters"></div> <div class="stat-value">—</div> <div class="stat-label"> <span data-lang="en">chats</span> <span data-lang="da">chats</span> <span data-lang="de">Chats</span> <span data-lang="es">chats</span> <span data-lang="zh">聊天</span> </div> </div> </div>
<div class="kidlisp-preview"> <a href="https://kidlisp.com" class="kidlisp-preview-header" id="kidlispHeaderLink" target="_blank"> <div class="kidlisp-header-code-wrap"> <strong class="kidlisp-logo" style="font-family: 'Comic Relief', 'Comic Sans MS', cursive; font-size: 1.1em;"><span style="color:#FF6B6B">K</span><span style="color:#4ECDC4">i</span><span style="color:#FFE66D">d</span><span style="color:#95E1D3">L</span><span style="color:#F38181">i</span><span style="color:#AA96DA">s</span><span style="color:#70D6FF">p</span><span style="color:var(--dim)">.</span><span style="color:#FF6B6B">c</span><span style="color:#9370DB">o</span><span style="color:#90EE90">m</span></strong> <span class="kidlisp-header-sep">/</span> <span class="kidlisp-header-code" id="kidlispHeaderCode"></span> </div> <span class="kidlisp-play-indicator" title="Open Editor"></span> </a> <div class="kidlisp-carousel" id="kidlispCarousel"> <div class="module-loading" id="kidlispLoading">loading</div> </div> <!-- Auto-advance progress bar --> <div class="kidlisp-auto-progress"> <div class="kidlisp-auto-progress-bar" id="kidlispAutoProgress"></div> </div> <div class="module-caption"> <span data-lang="en">A new programming language for gen beta</span> <span data-lang="da">Et nyt programmeringssprog for gen beta</span> <span data-lang="de">Eine neue Programmiersprache für Gen Beta</span> <span data-lang="es">Un nuevo lenguaje de programación para gen beta</span> <span data-lang="zh">为gen beta设计的新编程语言</span> </div> </div>
<div class="chat-flip-container"> <div class="chat-flip-card" id="chatFlipCard"> <!-- Front: laer-klokken (active when not flipped) --> <div class="chat-flip-face chat-flip-front chat-preview"> <div class="chat-preview-header chat-flip-trigger" onclick="toggleChatFlip()"> <strong>🕐 laer-klokken</strong> </div> <div class="chat-messages" id="chatMessages"> <div class="chat-msg"><div class="chat-text" style="color: var(--dim);">Loading...</div></div> </div> <div class="module-caption chat-flip-trigger" onclick="toggleChatFlip()"> <span data-lang="en"><a href="https://aesthetic.computer/@prutti" target="_blank" style="color: var(--pink);">@prutti</a>'s weekly <a href="https://prompt.ac/r8dio" target="_blank" style="color: var(--pink);">r8Dio</a> show LIVE from CPH</span> <span data-lang="da"><a href="https://aesthetic.computer/@prutti" target="_blank" style="color: var(--pink);">@prutti</a>s ugentlige <a href="https://prompt.ac/r8dio" target="_blank" style="color: var(--pink);">r8Dio</a> show LIVE fra KBH</span> <span data-lang="de"><a href="https://aesthetic.computer/@prutti" target="_blank" style="color: var(--pink);">@prutti</a>s wöchentliche <a href="https://prompt.ac/r8dio" target="_blank" style="color: var(--pink);">r8Dio</a> Show LIVE aus KPH</span> <span data-lang="es"><a href="https://aesthetic.computer/@prutti" target="_blank" style="color: var(--pink);">@prutti</a> show semanal de <a href="https://prompt.ac/r8dio" target="_blank" style="color: var(--pink);">r8Dio</a> EN VIVO desde CPH</span> <span data-lang="zh"><a href="https://aesthetic.computer/@prutti" target="_blank" style="color: var(--pink);">@prutti</a>的每周<a href="https://prompt.ac/r8dio" target="_blank" style="color: var(--pink);">r8Dio</a>节目从哥本哈根直播</span> </div> <div class="flip-face-progress"> <div class="flip-face-progress-bar" id="chatFlipProgress"></div> </div> </div> <!-- Back: chat-system (ghost overlay when not flipped, active when flipped) --> <div class="chat-flip-face chat-flip-back chat-preview-system"> <div class="chat-system-header chat-flip-trigger" onclick="toggleChatFlip()"> <strong>💬 chat</strong> </div> <div class="chat-messages" id="chatSystemMessages"> <div class="chat-msg"><div class="chat-text" style="color: var(--dim);">Loading...</div></div> </div> <div class="module-caption chat-flip-trigger" onclick="toggleChatFlip()"> <span data-lang="en">Get a <a href="https://prompt.ac/get-handle" target="_blank" style="color: var(--pink);">@handle</a> and come say hi</span> <span data-lang="da">Få en <a href="https://prompt.ac/get-handle" target="_blank" style="color: var(--pink);">@handle</a> og kom og sig hej</span> <span data-lang="de">Hol dir einen <a href="https://prompt.ac/get-handle" target="_blank" style="color: var(--pink);">@handle</a> und sag Hallo</span> <span data-lang="es">Obtén un <a href="https://prompt.ac/get-handle" target="_blank" style="color: var(--pink);">@handle</a> y ven a saludar</span> <span data-lang="zh">获取<a href="https://prompt.ac/get-handle" target="_blank" style="color: var(--pink);">@handle</a>来打个招呼吧</span> </div> <div class="flip-face-progress flip-face-progress-back"> <div class="flip-face-progress-bar" id="chatFlipProgressBack"></div> </div> </div> </div> </div>
<!-- WIP: notepat module <div class="link-block notepat-section"> <strong>🎹 notepat</strong> <a href="https://aesthetic.computer/notepat" target="_blank">aesthetic.computer/notepat</a> <span class="link-desc" data-lang="en">Musical keyboard</span> <span class="link-desc" data-lang="da">Musikkeyboard</span> </div> -->
<div class="tv-section-wrap"> <div class="tv-backdrop-glow" id="tvBackdropGlow"></div> <div class="tv-section"> <div class="tv-header" id="tvHeader"> <strong>📼 tapes</strong> </div> <div class="tv-player" id="tvPlayer"> <div class="module-loading" id="tvLoading">loading</div> </div> <div class="module-caption"> <span data-lang="en">Prefix a prompt with <span class="cmd-highlight">tape</span> to record it</span> <span data-lang="da">Sæt <span class="cmd-highlight">tape</span> før en prompt for at optage</span> <span data-lang="de">Setze <span class="cmd-highlight">tape</span> vor einen Prompt zum Aufnehmen</span> <span data-lang="es">Prefija un prompt con <span class="cmd-highlight">tape</span> para grabarlo</span> <span data-lang="zh">在提示前加 <span class="cmd-highlight">tape</span> 即可录制</span> </div> </div> </div>
<!-- Apps Flip: Desktop / Mobile --> <div class="apps-flip-container" id="appsFlipContainer"> <div class="apps-flip-card" id="appsFlipCard"> <!-- Front: Desktop (active when not flipped) --> <div class="apps-flip-face apps-flip-front desktop-face"> <div class="desktop-face-header" onclick="toggleAppsFlip()"> <span class="face-label"> <span data-lang="en">desktop</span> <span data-lang="da">skrivebord</span> <span data-lang="de">Desktop</span> <span data-lang="es">escritorio</span> <span data-lang="zh">桌面</span> </span> <div class="desktop-header-ticker"> <div class="desktop-header-ticker-content" id="changelogTicker"> <span>Loading changelog...</span> </div> </div> </div> <div class="desktop-platforms-grid"> <a href="https://github.com/whistlegraph/aesthetic-computer/releases/download/v0.1.8/Aesthetic-Computer-0.1.8-universal.dmg" class="desktop-platform-btn plat-mac" title="Download for macOS" target="_blank"> <span class="plat-icon"><svg viewBox="0 0 384 512" xmlns="http://www.w3.org/2000/svg"><path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/></svg></span> <span class="plat-name">Mac</span> <span class="plat-arch" id="macVersion">v0.1.8</span> </a> <a href="https://github.com/whistlegraph/aesthetic-computer/releases/download/v0.1.8/Aesthetic-Computer-Setup-0.1.8.exe" class="desktop-platform-btn plat-win" title="Download for Windows" target="_blank"> <span class="plat-icon"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#0078D4" d="M0 3.5L10 2.2V11.5H0V3.5ZM11 2L24 0V11.5H11V2ZM0 12.5H10V21.8L0 20.5V12.5ZM11 12.5H24V24L11 22V12.5Z"/></svg></span> <span class="plat-name">Windows</span> <span class="plat-arch" id="winVersion">v0.1.8</span> </a> <a href="https://github.com/whistlegraph/aesthetic-computer/releases/download/v0.1.8/Aesthetic-Computer-0.1.8.AppImage" class="desktop-platform-btn plat-linux" title="Download for Linux" target="_blank"> <span class="plat-icon"><svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="70" rx="30" ry="25" fill="#F5C400"/><ellipse cx="50" cy="45" rx="25" ry="28" fill="#333"/><ellipse cx="50" cy="50" rx="18" ry="20" fill="#fff"/><circle cx="42" cy="38" r="4" fill="#333"/><circle cx="58" cy="38" r="4" fill="#333"/><circle cx="43" cy="37" r="1.5" fill="#fff"/><circle cx="59" cy="37" r="1.5" fill="#fff"/><ellipse cx="50" cy="48" rx="4" ry="3" fill="#F5A000"/><ellipse cx="35" cy="85" rx="10" ry="6" fill="#F5A000"/><ellipse cx="65" cy="85" rx="10" ry="6" fill="#F5A000"/></svg></span> <span class="plat-name">Linux</span> <span class="plat-arch" id="linuxVersion">v0.1.8</span> </a> </div> <div class="desktop-tools-row"> <a href="https://marketplace.visualstudio.com/items?itemName=aesthetic-computer.aesthetic-computer-code" class="desktop-vscode-link" target="_blank"> <img class="vscode-icon-img" id="vscodeIcon" src="https://aesthetic-computer.gallerycdn.vsassets.io/extensions/aesthetic-computer/aesthetic-computer-code/1.219.1/1767559081239/Microsoft.VisualStudio.Services.Icons.Default" alt="VS Code" onerror="this.style.display='none';this.nextElementSibling.style.display='inline'"> <span class="vscode-icon" style="display:none">💜</span> <span class="vscode-label">VS Code</span> <span class="vscode-ver" id="vscodeExtVersion">v1.217.0</span> </a> <a href="https://assets.aesthetic.computer/ableton/notepat-ableton-2025-alpha.amxd.zip" class="desktop-ableton-link" target="_blank" download> <svg class="ableton-icon" viewBox="0 0 51 24" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M0 0h3v24H0V0zm6 0h3v24H6V0zm6 0h3v24h-3V0zm6 0h3v24h-3V0zm9 0h24v3H27V0zm0 7h24v3H27V7zm0 7h24v3H27v-3zm0 7h24v3H27v-3z"/></svg> <span class="ableton-label">Ableton</span> <span class="ableton-tag">notepat</span> </a> </div> <div class="module-caption apps-flip-trigger" onclick="toggleAppsFlip()"> <span data-lang="en">Desktop apps and plugins are free</span> <span data-lang="da">Desktop-apps og plugins er gratis</span> <span data-lang="de">Desktop-Apps und Plugins sind kostenlos</span> <span data-lang="es">Las apps de escritorio y plugins son gratis</span> <span data-lang="zh">桌面应用和插件免费</span> </div> <div class="flip-face-progress"> <div class="flip-face-progress-bar" id="appsFlipProgress"></div> </div> </div> <!-- Back: Mobile (ghost overlay when not flipped, active when flipped) --> <div class="apps-flip-face apps-flip-back mobile-face"> <div class="mobile-face-header" onclick="toggleAppsFlip()"> <span class="face-label"> <span data-lang="en">mobile</span> <span data-lang="da">mobil</span> <span data-lang="de">Mobil</span> <span data-lang="es">móvil</span> <span data-lang="zh">移动</span> </span> </div> <div class="mobile-face-content"> <img class="mobile-app-icon" id="iosAppIcon" src="https://is1-ssl.mzstatic.com/image/thumb/Purple126/v4/91/c2/9c/91c29cd6-ff12-a9ad-0a67-1d5700352b62/AppIcon-0-0-1x_U007epad-0-85-220.png/100x100bb.jpg" alt="Aesthetic Computer" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'"> <div class="mobile-app-icon-fallback" style="display:none">🎨</div> <div class="mobile-app-title">Aesthetic<span class="app-bounce-dot">.</span>Computer</div> <a href="https://apps.apple.com/ag/app/aesthetic-computer/id6450940883" class="mobile-store-link" target="_blank"> <span data-lang="en">iOS App Store</span> <span data-lang="da">iOS App Store</span> <span data-lang="de">iOS App Store</span> <span data-lang="es">iOS App Store</span> <span data-lang="zh">iOS应用商店</span> </a> </div> <div class="module-caption apps-flip-trigger" onclick="toggleAppsFlip()"> <span data-lang="en">Mobile app is free on iOS</span> <span data-lang="da">Mobil-appen er gratis på iOS</span> <span data-lang="de">Die mobile App ist kostenlos auf iOS</span> <span data-lang="es">La app móvil es gratis en iOS</span> <span data-lang="zh">移动应用在iOS上免费</span> </div> <div class="flip-face-progress flip-face-progress-back"> <div class="flip-face-progress-bar" id="appsFlipProgressMobile"></div> </div> </div> </div> </div>
<div class="shop-section" id="shopSection" hidden> <div class="shop-header"> <a href="https://shop.aesthetic.computer" class="shop-title" target="_blank"> <span data-lang="en">shop</span> <span data-lang="da">butik</span> <span data-lang="de">Shop</span> <span data-lang="es">tienda</span> <span data-lang="zh">商店</span> </a> </div> <div class="shop-products" id="shopProducts"> <div class="module-loading" id="shopLoading">loading</div> </div> <!-- Auto-advance progress bar --> <div class="shop-auto-progress"> <div class="shop-auto-progress-bar" id="shopAutoProgress"></div> </div> <div class="module-caption"> <span data-lang="en">Get books, bikes, artworks and more</span> <span data-lang="da">Få bøger, cykler, kunstværker og mere</span> <span data-lang="de">Holen Sie sich Bücher, Fahrräder, Kunstwerke und mehr</span> <span data-lang="es">Obtén libros, bicicletas, obras de arte y más</span> <span data-lang="zh">获取书籍、自行车、艺术品等</span> </div> </div>
<div class="invest-section" id="invest"> <div class="invest-header"> <span>📈</span> <span data-lang="en">Invest</span> <span data-lang="da">Investér</span> <span data-lang="de">Investieren</span> <span data-lang="es">Invertir</span> <span data-lang="zh">投资</span> </div> <div class="invest-content" data-lang="en"> <p><a href="https://aesthetic.direct" target="_blank">Aesthetic Inc.</a> was <a href="https://prompt.ac/booted-by" target="_blank">booted by</a> a handful of early supporters in 2025.<br><br>To join this equity holding community or increase your existing stake, <a href="https://calendly.com/aesthetic-computer/demo" class="demo-link" target="_blank">book a remote demo</a> with <a href="https://aesthetic.computer/@jeffrey" class="handle-link">@jeffrey</a>.</p> </div> <div class="invest-content" data-lang="da"> <p><a href="https://aesthetic.direct" target="_blank">Aesthetic Inc.</a> bliver <a href="https://prompt.ac/booted-by" target="_blank">startet af</a> en håndfuld tidlige støtter, der deler i at reservere virksomhedskapital.<br><br>For at blive en del af dette fællesskab, <a href="https://calendly.com/aesthetic-computer/demo" class="demo-link" target="_blank">book en fjern-demo</a> med <a href="https://aesthetic.computer/@jeffrey" class="handle-link">@jeffrey</a>.</p> </div> <div class="invest-content" data-lang="de"> <p><a href="https://aesthetic.direct" target="_blank">Aesthetic Inc.</a> wurde von einer Handvoll früher Unterstützer <a href="https://prompt.ac/booted-by" target="_blank">gestartet</a>.<br><br>Um dieser Investoren-Community beizutreten, <a href="https://calendly.com/aesthetic-computer/demo" class="demo-link" target="_blank">buchen Sie eine Demo</a> mit <a href="https://aesthetic.computer/@jeffrey" class="handle-link">@jeffrey</a>.</p> </div> <div class="invest-content" data-lang="es"> <p><a href="https://aesthetic.direct" target="_blank">Aesthetic Inc.</a> fue <a href="https://prompt.ac/booted-by" target="_blank">iniciada por</a> un puñado de primeros partidarios.<br><br>Para unirte a esta comunidad de inversores, <a href="https://calendly.com/aesthetic-computer/demo" class="demo-link" target="_blank">reserva una demo remota</a> con <a href="https://aesthetic.computer/@jeffrey" class="handle-link">@jeffrey</a>.</p> </div> <div class="invest-content" data-lang="zh"> <p><a href="https://aesthetic.direct" target="_blank">Aesthetic Inc.</a> 由一小群早期支持者<a href="https://prompt.ac/booted-by" target="_blank">启动</a>。<br><br>要加入这个股权持有社区,请与 <a href="https://aesthetic.computer/@jeffrey" class="handle-link">@jeffrey</a> <a href="https://calendly.com/aesthetic-computer/demo" class="demo-link" target="_blank">预约远程演示</a>。</p> </div> <a href="https://aesthetic.computer/+" class="invest-mug" id="investMug" title="Buy a Mug" target="_blank"> <img class="invest-mug-image" id="investMugImage" alt="Mug preview" /> <span class="invest-mug-icon" id="investMugIcon"></span> <span class="invest-mug-caption" id="investMugCaption"><span class="mug-color">white</span><span class="mug-of"> mug of </span><span class="mug-code">CODE</span><span class="mug-via"> in $give</span></span> </a> </div>
<div class="page-footer"> <div class="footer-auth" id="footerAuth"> <div class="footer-auth-buttons" id="footerAuthButtons"> <button class="footer-login-btn" id="footerLoginBtn"> <span data-lang="en">Log in</span> <span data-lang="da">Log ind</span> <span data-lang="de">Anmelden</span> <span data-lang="es">Entrar</span> <span data-lang="zh">登录</span> </button> <button class="footer-signup-btn" id="footerSignupBtn"> <span data-lang="en">I'm new</span> <span data-lang="da">Jeg er ny</span> <span data-lang="de">Ich bin neu</span> <span data-lang="es">Soy nuevo</span> <span data-lang="zh">我是新用户</span> </button> </div> <div class="footer-user-stack" id="footerUserStack" style="display: none;"> <a class="footer-user-menu" id="footerUserMenu" href="#"> <span class="footer-user-handle" id="footerUserHandle"></span> </a> <a class="footer-logout-link" id="footerLogoutLink" href="#"> <span data-lang="en">logout?</span> <span data-lang="da">log ud?</span> <span data-lang="de">abmelden?</span> <span data-lang="es">¿salir?</span> <span data-lang="zh">登出?</span> </a> </div> </div> </div>
<!-- AT Protocol Federation Info --> <div class="hacker-section"> <div class="hacker-card"> <a href="https://at.aesthetic.computer" target="_blank" class="hacker-card-title"> <span class="at-line-1"><span class="at-handle-prefix" id="atHandlePrefix"></span><span class="ac-dot">.</span><span class="at-blue">at</span><span class="ac-dot">.</span><span class="ac-name">Aesthetic<span class="ac-dot">.</span>Computer</span> <span class="at-gray">is federated</span></span> <span class="at-line-2"><span class="at-gray">on the</span> <img src="https://assets.aesthetic.computer/atproto-icon.png" alt="" class="at-icon"><span class="at-blue">AT</span><span class="at-gray">Protocol</span></span> </a> </div> </div>
<a id="pals" onclick="window.scrollTo({top: 0, behavior: 'smooth'}); return false;" href="#"> <div class="pals-logo-container"> <img src="https://aesthetic.computer/purple-pals.svg" alt="" class="pals-logo"> <img src="https://aesthetic.computer/purple-pals.svg" alt="" class="pals-logo-pink"> </div> <span data-lang="en">↑ Back to Top</span> <span data-lang="da">↑ Tilbage til top</span> <span data-lang="de">↑ Nach oben</span> <span data-lang="es">↑ Volver arriba</span> <span data-lang="zh">↑ 返回顶部</span> </a> </div> </main>
<!-- Login prompt modal for monthly subscriptions (native dialog) --> <dialog class="login-modal" id="loginModal"> <div class="login-modal-title"> <span data-lang="en">Log in for Monthly</span> <span data-lang="da">Log ind for månedligt</span> <span data-lang="de">Anmelden für Monatlich</span> <span data-lang="es">Iniciar sesión para Mensual</span> <span data-lang="zh">登录以订阅</span> </div> <div class="login-modal-text"> <span data-lang="en">Log in first so we can link your subscription to your account.</span> <span data-lang="da">Log ind først, så vi kan tilknytte dit abonnement til din konto.</span> <span data-lang="de">Melden Sie sich an, damit wir Ihr Abonnement mit Ihrem Konto verknüpfen können.</span> <span data-lang="es">Inicia sesión primero para vincular tu suscripción a tu cuenta.</span> <span data-lang="zh">请先登录,以便将您的订阅关联到您的账户。</span> </div> <div class="login-modal-buttons"> <button class="login-modal-btn secondary" id="loginModalCancel"> <span data-lang="en">Not now</span> <span data-lang="da">Ikke nu</span> <span data-lang="de">Nicht jetzt</span> <span data-lang="es">Ahora no</span> <span data-lang="zh">暂不</span> </button> <button class="login-modal-btn primary" id="loginModalConfirm"> <span data-lang="en">Log in</span> <span data-lang="da">Log ind</span> <span data-lang="de">Anmelden</span> <span data-lang="es">Iniciar sesión</span> <span data-lang="zh">登录</span> </button> </div> </dialog>
<script> // ===== VS Code Detection & External Link Handling ===== // Check if running inside VS Code webview (sandboxed iframe) function isInVSCode() { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('vscode') === 'true' || (window.acVSCODE && window.parent !== window); }
// Open URL externally - handles VS Code sandboxed iframe case function openExternal(url) { if (isInVSCode()) { // In VS Code extension webview, send message to parent to open externally window.parent.postMessage({ type: 'openExternal', url }, '*'); return true; } // Normal browser - open in new tab window.open(url, '_blank', 'noopener'); return true; }
// Intercept all target="_blank" links for VS Code compatibility document.addEventListener('click', (e) => { const link = e.target.closest('a[target="_blank"]'); if (link && isInVSCode()) { e.preventDefault(); openExternal(link.href); } });
// Dev/prod URL detection for KidLisp.com links const isDev = window.location.hostname === 'localhost' || window.location.hostname.includes('local.'); const kidlispUrl = isDev ? 'https://localhost:8888/kidlisp.com' : 'https://kidlisp.com'; // Update static kidlisp links to use dev URL if on localhost document.addEventListener('DOMContentLoaded', () => { const headerLink = document.getElementById('kidlispHeaderLink'); if (headerLink && isDev) { headerLink.href = kidlispUrl; } }); // ====== Theme: System Default (Light/Dark Mode) ====== // Apply light-mode class based on system preference (CSS rules use body.light-mode) localStorage.removeItem('give-theme-preference'); // Clear any old stored preference function applySystemTheme() { if (window.matchMedia('(prefers-color-scheme: light)').matches) { document.body.classList.add('light-mode'); document.body.classList.remove('dark-mode'); } else { document.body.classList.remove('light-mode'); document.body.classList.add('dark-mode'); } } // Apply on load applySystemTheme(); // Listen for system preference changes window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', applySystemTheme); // ====== A/B/C/D/E Test: Starting Price ====== // Monthly options (60% total): D: $8/mo (30%), E: $2/mo (30%) // One-time options (40% total): A: $32 (13.3%), B: $64 (13.3%), C: $128 (13.3%) const abcTestRandom = Math.random(); const abcTestVariant = abcTestRandom < 0.3 ? 'E' : abcTestRandom < 0.6 ? 'D' : abcTestRandom < 0.733 ? 'A' : abcTestRandom < 0.866 ? 'B' : 'C'; let abcTestStartPrice = abcTestVariant === 'A' ? 32 : abcTestVariant === 'B' ? 64 : abcTestVariant === 'C' ? 128 : abcTestVariant === 'D' ? 8 : 2; let abcTestIsMonthly = abcTestVariant === 'D' || abcTestVariant === 'E'; // Check for URL amount override (e.g., from kidlisp.com guides) const urlAmountParams = new URLSearchParams(window.location.search); const urlAmountOverride = urlAmountParams.get('amount'); if (urlAmountOverride) { const parsedAmount = parseInt(urlAmountOverride, 10); if (!isNaN(parsedAmount) && parsedAmount > 0 && parsedAmount <= 10000) { abcTestStartPrice = parsedAmount; abcTestIsMonthly = false; // URL override = one-time by default console.log(`💝 Amount override from URL: $${parsedAmount}`); } } // Apply A/B/C test price to all USD widgets immediately document.querySelectorAll('.gift-widget[data-currency="usd"]').forEach(widget => { const amountEl = widget.querySelector('.gift-amount'); const slider = widget.querySelector('input[type="range"]'); const btn = widget.querySelector('.gift-btn'); const monthlyCheck = widget.querySelector('.monthly-checkbox'); const monthlyLabel = widget.querySelector('.gift-monthly-check'); if (amountEl) amountEl.textContent = `$${abcTestStartPrice}`; if (slider) { // Adjust slider range for monthly mode if (abcTestIsMonthly) { slider.min = 1; slider.max = 500; } slider.value = abcTestStartPrice; } if (btn) btn.innerHTML = abcTestIsMonthly ? `Give <span class="price">$${abcTestStartPrice}/mo</span>` : `Give <span class="price">$${abcTestStartPrice}</span>`; // For variant C, check the monthly box if (abcTestIsMonthly && monthlyCheck) { monthlyCheck.checked = true; if (monthlyLabel) monthlyLabel.classList.add('checked'); widget.classList.add('monthly-mode'); } });
// ====== Jeffreys Ken Burns Canvas Slideshow ====== // POI manifest source of truth: papers/jeffrey-platter/manifest.json, // synced to ./jeffreys-manifest.json by papers/jeffrey-platter/sync.mjs. // Schema: { buckets: { shoot, candids } }. POI types: 'f'=face, 'b'=body, 'h'=hand. let headshotsData = {}; let jeffreysData = {}; async function loadJeffreysManifest() { const res = await fetch('./jeffreys-manifest.json', { cache: 'no-cache' }); const manifest = await res.json(); headshotsData = manifest.buckets.shoot.items; jeffreysData = manifest.buckets.candids.items; }
// AC Screenshots - photographic development moments (environmental/atmospheric) // URL pattern: https://assets.aesthetic.computer/screenshots/images/{imageRef} // All 90 screenshots - wide format environmental shots const screenshotsData = { 'september-16-2022-at-12-37-pm.webp': { focal: [50, 63], pois: [{"t":"s","box":[-0.4,28.3,101.2,70]}], aspect: 0.75, src: 'screenshots' }, 'february-8-2023-at-8-56-pm.webp': { focal: [31, 64], pois: [{"t":"s","box":[-0.1,34.1,62.6,59.7]}], aspect: 0.75, src: 'screenshots' }, 'december-5-2021-at-11-44-pm.webp': { focal: [45, 45], pois: [{"t":"s","box":[0.2,23.2,90.1,42.9]},{"t":"s","box":[15.7,0,71,26.8]}], aspect: 0.75, src: 'screenshots' }, 'may-24-2023-at-6-06-pm.webp': { focal: [50, 50], pois: [], aspect: 0.75, src: 'screenshots' }, 'january-31-2023-at-1-57-pm.webp': { focal: [50, 64], pois: [{"t":"s","box":[0.3,29.2,98.3,69.9]}], aspect: 0.75, src: 'screenshots' }, 'may-15-2023-at-8-29-pm.webp': { focal: [50, 63], pois: [{"t":"s","box":[0.8,28.5,98.9,69.1]}], aspect: 0.75, src: 'screenshots' }, 'december-2nd-2023-at-3-42-pm.webp': { focal: [50, 57], pois: [{"t":"s","box":[-0.1,15.8,100.2,83]}], aspect: 0.75, src: 'screenshots' }, 'october-29-2023-at-6-28-pm.webp': { focal: [67, 76], pois: [{"t":"s","box":[36.1,52.6,60.8,47]},{"t":"s","box":[44.7,30.8,29,21.2]}], aspect: 0.75, src: 'screenshots' }, 'september-11-2023-at-12-35-pm.webp': { focal: [50, 62], pois: [{"t":"s","box":[-2.3,24.3,104.2,74.4]}], aspect: 0.75, src: 'screenshots' }, 'june-14-2023-at-6-33-pm.webp': { focal: [52, 70], pois: [{"t":"s","box":[6.2,41.6,92.5,57.2]}], aspect: 0.75, src: 'screenshots' }, 'june-22-2023-at-4-22-pm.webp': { focal: [65, 55], pois: [{"t":"s","box":[30.7,25.3,68.9,59]}], aspect: 0.75, src: 'screenshots' }, 'february-28-2023-at-11-47-pm.webp': { focal: [53, 73], pois: [{"t":"s","box":[17.9,47.8,70,50.4]}], aspect: 0.75, src: 'screenshots' }, 'november-25-2022-at-7-34-pm.webp': { focal: [29, 30], pois: [{"t":"s","box":[45,17.3,33.4,29.3]},{"t":"s","box":[10.7,14.3,36.7,32.3]}], aspect: 0.75, src: 'screenshots' }, 'may-6-2023-at-10-21-pm.webp': { focal: [46, 67], pois: [{"t":"s","box":[3.5,32.5,84.6,68]}], aspect: 0.75, src: 'screenshots' }, 'july-11-2023-at-10-41-am.webp': { focal: [50, 50], pois: [], aspect: 0.75, src: 'screenshots' }, 'january-30-2024-at-6-59-pm.webp': { focal: [50, 50], pois: [], aspect: 0.75, src: 'screenshots' }, 'january-29-2024-at-4-10-pm.webp': { focal: [50, 50], pois: [], aspect: 0.75, src: 'screenshots' }, 'august-22-2023-at-6-51-pm.webp': { focal: [37, 58], pois: [{"t":"s","box":[0.6,18.3,72.8,78.7]}], aspect: 0.75, src: 'screenshots' }, 'january-8-2023-at-4-12-pm.webp': { focal: [50, 63], pois: [{"t":"s","box":[0.3,25.4,99.4,74.4]}], aspect: 0.75, src: 'screenshots' }, 'november-2-2023-at-10-15-pm.webp': { focal: [53, 40], pois: [{"t":"s","box":[41.8,31.4,21.6,17]}], aspect: 0.75, src: 'screenshots' }, 'june-7-2023-at-10-50-pm.webp': { focal: [50, 63], pois: [{"t":"s","box":[-1.2,27.8,102,70.1]}], aspect: 0.75, src: 'screenshots' }, 'october-4-2022-at-11-12-am.webp': { focal: [55, 72], pois: [{"t":"s","box":[10.5,44.4,88.5,55.3]}], aspect: 0.75, src: 'screenshots' }, 'february-1-2023-at-5-37-pm.webp': { focal: [54, 62], pois: [{"t":"s","box":[16.5,43.3,74.9,37.8]}], aspect: 0.75, src: 'screenshots' }, 'march-17-2023-at-1-29-pm.webp': { focal: [49, 65], pois: [{"t":"s","box":[0.8,32,95.8,66.7]}], aspect: 0.75, src: 'screenshots' }, 'august-11-2023-at-5-36-pm.webp': { focal: [68, 70], pois: [{"t":"s","box":[36.8,41.3,62.9,57.4]}], aspect: 0.75, src: 'screenshots' }, 'october-27-2023-at-2-32-pm.webp': { focal: [45, 30], pois: [{"t":"s","box":[34.2,47.9,59.5,34.7]},{"t":"s","box":[11.9,7.8,66.2,44.7]}], aspect: 0.75, src: 'screenshots' }, 'january-20-2023-at-2-05-am.webp': { focal: [50, 70], pois: [{"t":"s","box":[0.2,41.7,100,57.4]}], aspect: 0.75, src: 'screenshots' }, 'november-2-2022-at-5-41-pm.webp': { focal: [33, 48], pois: [{"t":"s","box":[1,17,64.6,61.9]}], aspect: 0.75, src: 'screenshots' }, 'october-9-2023-at-3-17-pm.webp': { focal: [53, 70], pois: [{"t":"s","box":[6.7,40.2,91.8,59]}], aspect: 0.75, src: 'screenshots' }, 'june-17-2023-at-7-34-pm.webp': { focal: [50, 59], pois: [{"t":"s","box":[-0.8,19.2,101,79.7]}], aspect: 0.75, src: 'screenshots' }, 'june-8-2023-at-7-34-pm.webp': { focal: [58, 69], pois: [{"t":"s","box":[15.3,38.9,86.1,60.2]}], aspect: 0.75, src: 'screenshots' }, 'june-3-2023-at-10-04-pm.webp': { focal: [75, 59], pois: [{"t":"s","box":[-0.5,42.2,49.5,53.8]},{"t":"s","box":[49.8,25.3,50.2,66.9]}], aspect: 0.75, src: 'screenshots' }, 'june-26-2023-at-6-52-pm.webp': { focal: [65, 40], pois: [{"t":"s","box":[42.7,23.3,44.8,32.8]},{"t":"s","box":[42.4,51,26.3,18.4]}], aspect: 0.75, src: 'screenshots' }, 'september-17-2023-at-12-21-am.webp': { focal: [50, 39], pois: [{"t":"s","box":[15.1,70.1,58.4,29.3]},{"t":"s","box":[0.9,0.7,98.2,76.8]}], aspect: 0.75, src: 'screenshots' }, 'march-13-2024-at-11-13-pm.webp': { focal: [41, 56], pois: [{"t":"s","box":[10.5,30.3,61.7,50.9]}], aspect: 0.75, src: 'screenshots' }, 'july-4-2023-at-3-26-pm.webp': { focal: [45, 81], pois: [{"t":"s","box":[7.4,63,76.1,36]}], aspect: 0.75, src: 'screenshots' }, 'january-27-2023-at-5-16-pm.webp': { focal: [32, 77], pois: [{"t":"s","box":[48.5,69.8,26.1,18.3]},{"t":"s","box":[18.9,67,25.8,19.1]}], aspect: 0.75, src: 'screenshots' }, 'january-21-2024-at-12-25-pm.webp': { focal: [56, 77], pois: [{"t":"s","box":[13.3,54.7,86.1,44.4]}], aspect: 0.75, src: 'screenshots' }, 'september-30-2023-at-1-15-am.webp': { focal: [57, 47], pois: [{"t":"s","box":[28.7,13.8,55.8,67]}], aspect: 0.75, src: 'screenshots' }, 'june-1-2023-at-6-52-pm.webp': { focal: [50, 57], pois: [{"t":"s","box":[0.7,15.1,98.6,84]}], aspect: 0.75, src: 'screenshots' }, 'june-13-2023-at-8-24-pm.webp': { focal: [48, 75], pois: [{"t":"s","box":[0.3,49.5,94.8,50.5]}], aspect: 0.75, src: 'screenshots' }, 'june-9-2023-at-11-00-pm.webp': { focal: [44, 71], pois: [{"t":"s","box":[5.9,43.6,76,55.1]}], aspect: 0.75, src: 'screenshots' }, 'april-24-2023-at-2-26-pm.webp': { focal: [50, 31], pois: [{"t":"s","box":[13.2,13.5,72.8,35.1]},{"t":"s","box":[41.4,59.4,58.6,39.1]}], aspect: 0.75, src: 'screenshots' }, 'september-29-2023-at-5-58-pm.webp': { focal: [50, 63], pois: [{"t":"s","box":[-0.5,29.1,100.6,68.7]}], aspect: 0.75, src: 'screenshots' }, 'june-3-2023-at-7-25-pm.webp': { focal: [55, 61], pois: [{"t":"s","box":[11.9,27.7,87.2,65.8]}], aspect: 0.75, src: 'screenshots' }, 'may-4-2023-at-8-59-pm.webp': { focal: [49, 50], pois: [{"t":"s","box":[38.2,41.6,21,17.2]}], aspect: 0.75, src: 'screenshots' }, 'may-6-2023-at-11-59-am.webp': { focal: [54, 69], pois: [{"t":"s","box":[8.9,38.4,90.3,60.9]}], aspect: 0.75, src: 'screenshots' }, 'december-31-2021-at-4-54-pm.webp': { focal: [50, 61], pois: [{"t":"s","box":[-1.1,23.3,101.5,76.2]}], aspect: 0.75, src: 'screenshots' }, 'august-6-2022-at-11-16-am.webp': { focal: [56, 68], pois: [{"t":"s","box":[12.9,40,86.7,55.6]},{"t":"s","box":[0.2,-0.3,97.6,44]}], aspect: 0.75, src: 'screenshots' }, 'november-28-2023-at-12-13-pm.webp': { focal: [53, 76], pois: [{"t":"s","box":[29.7,53.3,46.7,45.5]}], aspect: 0.75, src: 'screenshots' }, 'september-30-2023-at-1-21-pm.webp': { focal: [61, 54], pois: [{"t":"s","box":[21.9,14.9,77.8,78.2]}], aspect: 0.75, src: 'screenshots' }, 'february-3-2023-at-9-45-pm.webp': { focal: [50, 56], pois: [{"t":"s","box":[-0.2,12.9,100.6,86.7]}], aspect: 0.75, src: 'screenshots' }, 'september-27-2023-at-1-42-am.webp': { focal: [72, 63], pois: [{"t":"s","box":[13.8,22.7,34.3,25.2]},{"t":"s","box":[44.4,30.3,55.9,66.2]}], aspect: 0.75, src: 'screenshots' }, 'november-28-2023-at-4-35-pm.webp': { focal: [66, 78], pois: [{"t":"s","box":[36.4,55.9,59.3,44.2]}], aspect: 0.75, src: 'screenshots' }, 'april-13-2023-at-11-18-am.webp': { focal: [51, 70], pois: [{"t":"s","box":[2.2,40,96.9,59.5]}], aspect: 0.75, src: 'screenshots' }, 'february-13-2023-at-9-22-pm.webp': { focal: [50, 58], pois: [{"t":"s","box":[-0.5,16.7,101.1,82.2]}], aspect: 0.75, src: 'screenshots' }, 'july-14-2022-at-3-56-pm.webp': { focal: [68, 43], pois: [{"t":"s","box":[1,57.2,49.5,37.5]},{"t":"s","box":[52.4,54.4,45.1,39.1]},{"t":"s","box":[38.7,22.5,57.9,41.4]}], aspect: 0.75, src: 'screenshots' }, 'april-24-2023-at-9-42-am.webp': { focal: [50, 62], pois: [{"t":"s","box":[-0.6,26.1,101.2,71.9]}], aspect: 0.75, src: 'screenshots' }, 'june-26-2022-at-4-49-pm.webp': { focal: [50, 65], pois: [{"t":"s","box":[1.9,31.3,97.2,67.4]}], aspect: 0.75, src: 'screenshots' }, 'september-16-2023-at-10-42-pm.webp': { focal: [49, 46], pois: [{"t":"s","box":[34,16.1,49.7,42.1]},{"t":"s","box":[16.1,17.4,65.4,58]}], aspect: 0.75, src: 'screenshots' }, 'january-19-2023-at-3-46-pm.webp': { focal: [68, 70], pois: [{"t":"s","box":[36.2,39.8,63.4,59.7]}], aspect: 0.75, src: 'screenshots' }, 'november-7-2022-at-4-40-pm.webp': { focal: [58, 73], pois: [{"t":"s","box":[20.4,51.5,75.4,43.7]}], aspect: 0.75, src: 'screenshots' }, 'june-8-2023-at-6-57-pm.webp': { focal: [50, 64], pois: [{"t":"s","box":[0.5,29.6,98.8,69.4]}], aspect: 0.75, src: 'screenshots' }, 'march-1-2023-at-12-26-pm.webp': { focal: [55, 69], pois: [{"t":"s","box":[10.2,39.1,89,60.4]}], aspect: 0.75, src: 'screenshots' }, 'february-27-2023-at-6-07-pm.webp': { focal: [46, 55], pois: [{"t":"s","box":[8.1,29.5,75,50.3]}], aspect: 0.75, src: 'screenshots' }, 'may-4-2023-at-2-33-pm.webp': { focal: [50, 71], pois: [{"t":"s","box":[0.9,41.6,98.6,58.3]}], aspect: 0.75, src: 'screenshots' }, 'september-1-2023-at-11-18-pm.webp': { focal: [52, 77], pois: [{"t":"s","box":[21.3,58.5,61.5,37.8]}], aspect: 0.75, src: 'screenshots' }, 'december-5-2023-at-1-28-pm.webp': { focal: [51, 80], pois: [{"t":"s","box":[25.8,60.8,49.9,38.9]}], aspect: 0.75, src: 'screenshots' }, 'april-21-2023-at-11-12-am.webp': { focal: [54, 53], pois: [{"t":"s","box":[9.4,6.8,88.9,91.6]}], aspect: 0.75, src: 'screenshots' }, 'january-7-2023-at-8-16-pm.webp': { focal: [48, 54], pois: [{"t":"s","box":[-3.3,11.8,101.8,85.4]}], aspect: 0.75, src: 'screenshots' }, 'june-1-2023-at-8-18-pm.webp': { focal: [62, 59], pois: [{"t":"s","box":[23.2,26.2,76.7,66.1]}], aspect: 0.75, src: 'screenshots' }, 'march-28-2023-at-1-05-pm.webp': { focal: [76, 69], pois: [{"t":"s","box":[63.9,59.4,24.3,19.3]},{"t":"s","box":[38.1,60.5,23,19.3]},{"t":"s","box":[9.1,60.5,20.1,15.4]}], aspect: 0.75, src: 'screenshots' }, 'september-4-2022-at-6-30-pm.webp': { focal: [51, 76], pois: [{"t":"s","box":[1.5,53.2,98.5,46.2]}], aspect: 0.75, src: 'screenshots' }, 'february-7-2023-at-4-20-pm.webp': { focal: [44, 46], pois: [{"t":"s","box":[2.4,5.9,83.8,79.4]}], aspect: 0.75, src: 'screenshots' }, 'august-21-2022-at-10-19-pm.webp': { focal: [51, 61], pois: [{"t":"s","box":[1.8,22.5,97.8,76.7]}], aspect: 0.75, src: 'screenshots' }, 'july-11-2022-at-10-55-pm.webp': { focal: [52, 73], pois: [{"t":"s","box":[31.5,57,41.8,31.3]},{"t":"s","box":[25.4,12.7,49.4,23.1]}], aspect: 0.75, src: 'screenshots' }, 'july-22-2022-at-12-36-am.webp': { focal: [43, 38], pois: [{"t":"s","box":[16.5,19.2,53.3,38.6]}], aspect: 0.75, src: 'screenshots' }, 'november-9-2023-at-4-28-pm.webp': { focal: [60, 72], pois: [{"t":"s","box":[20.7,44.5,78.5,55.1]}], aspect: 0.75, src: 'screenshots' }, 'july-21-2022-at-12-09-am.webp': { focal: [48, 59], pois: [{"t":"s","box":[0.3,19.2,95,79.6]}], aspect: 0.75, src: 'screenshots' }, 'july-4-2022-at-12-11-am.webp': { focal: [49, 65], pois: [{"t":"s","box":[0.8,30.1,96.3,68.7]}], aspect: 0.75, src: 'screenshots' }, 'may-27-2023-at-1-18-pm.webp': { focal: [54, 70], pois: [{"t":"s","box":[8,40.5,91.9,59.1]}], aspect: 0.75, src: 'screenshots' }, 'march-8-2024-at-6-40-pm.webp': { focal: [49, 50], pois: [{"t":"s","box":[8.6,23.4,79.8,53.2]}], aspect: 0.75, src: 'screenshots' }, 'february-8-2023-at-12-15-pm.webp': { focal: [37, 60], pois: [{"t":"s","box":[5.3,34.7,64.3,49.8]}], aspect: 0.75, src: 'screenshots' }, 'december-22-2022-at-11-51-pm.webp': { focal: [57, 49], pois: [{"t":"s","box":[13.5,20.2,86.9,58.3]}], aspect: 0.75, src: 'screenshots' }, 'february-5-2023-at-3-10-pm.webp': { focal: [51, 71], pois: [{"t":"s","box":[13.5,43.3,75.5,56.1]}], aspect: 0.75, src: 'screenshots' }, 'june-17-2023-at-7-55-pm.webp': { focal: [45, 81], pois: [{"t":"s","box":[3.5,62.2,83.1,37.8]},{"t":"s","box":[56.4,64.2,40,35.7]}], aspect: 0.75, src: 'screenshots' }, 'september-12-2023-at-6-31-pm.webp': { focal: [50, 63], pois: [{"t":"s","box":[-0.2,27.3,101.2,70.5]}], aspect: 0.75, src: 'screenshots' }, 'december-4-2023-at-1-08-pm.webp': { focal: [63, 70], pois: [{"t":"s","box":[24.9,41.6,75.3,57.7]}], aspect: 0.75, src: 'screenshots' }, 'october-17-2022-at-9-33-pm.webp': { focal: [35, 27], pois: [{"t":"s","box":[17.3,12.8,34.7,27.5]}], aspect: 0.75, src: 'screenshots' }, 'november-7-2023-at-10-49-pm.webp': { focal: [47, 52], pois: [{"t":"s","box":[34.3,44.5,24.6,15.8]}], aspect: 0.75, src: 'screenshots' } };
// Merge all image data. Filled in by buildImageIndex() after // loadJeffreysManifest() resolves. let allImagesData = {}; let jeffreysImages = []; const screenshotsImages = Object.keys(screenshotsData); let headshotsImages = []; let allImages = []; function buildImageIndex() { allImagesData = { ...jeffreysData, ...screenshotsData, ...headshotsData }; jeffreysImages = Object.keys(jeffreysData); headshotsImages = Object.keys(headshotsData); allImages = [...jeffreysImages, ...screenshotsImages, ...headshotsImages]; } const DEBUG_FACES = false; // Set true for labels and coordinates const SHOW_POI_BOXES = true; // Matrix-style detection boxes (aesthetic) const POI_GLOW = true; // Soft radial glow on detected faces/bodies // Aesthetic POI box colors - cycling rainbow const poiColors = [ 'rgba(255, 107, 157, 0.7)', // pink 'rgba(78, 205, 196, 0.7)', // cyan 'rgba(255, 217, 61, 0.7)', // gold 'rgba(107, 203, 119, 0.7)', // green 'rgba(180, 130, 255, 0.7)', // purple 'rgba(255, 160, 100, 0.7)', // orange ]; let poiColorIndex = 0; let poiAnimPhase = 0;
function initJeffreysSlideshow() { const container = document.getElementById('jeffreysSlideshow'); if (!container) return;
// Create canvas const canvas = document.createElement('canvas'); canvas.className = 'jeffreys-canvas'; container.appendChild(canvas); const ctx = canvas.getContext('2d');
// Separate narrow (portrait) and wide images from all sources const narrowImages = allImages.filter(name => allImagesData[name].aspect < 0.7); const wideImages = allImages.filter(name => allImagesData[name].aspect >= 0.7); // Shuffle both arrays const shuffledNarrow = [...narrowImages].sort(() => Math.random() - 0.5); const shuffledWide = [...wideImages].sort(() => Math.random() - 0.5); // Create display queue: singles for wide, pairs for narrow const displayQueue = []; let narrowIdx = 0; let wideIdx = 0; // Interleave: 2 wide singles, then 1 narrow pair, repeat while (wideIdx < shuffledWide.length || narrowIdx < shuffledNarrow.length) { // Add 2 wide singles for (let i = 0; i < 2 && wideIdx < shuffledWide.length; i++) { displayQueue.push([shuffledWide[wideIdx++]]); } // Add 1 narrow pair (collage) if (narrowIdx + 1 < shuffledNarrow.length) { displayQueue.push([shuffledNarrow[narrowIdx], shuffledNarrow[narrowIdx + 1]]); narrowIdx += 2; } else if (narrowIdx < shuffledNarrow.length) { displayQueue.push([shuffledNarrow[narrowIdx++]]); } }
// Preload images - non-blocking with decode() const loadedImages = {}; const loadingImages = {}; // Track in-progress loads function preloadImage(name) { // Already loaded if (loadedImages[name]) return Promise.resolve(loadedImages[name]); // Already loading - return existing promise if (loadingImages[name]) return loadingImages[name]; // Start new load loadingImages[name] = new Promise(resolve => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = async () => { // Decode image off main thread to prevent jank when drawing try { if (img.decode) await img.decode(); } catch (e) { /* ignore decode errors */ } loadedImages[name] = img; delete loadingImages[name]; resolve(img); }; img.onerror = () => { delete loadingImages[name]; resolve(null); }; // Check source type: screenshots, headshots, or jeffreys const data = allImagesData[name]; if (data && data.src === 'screenshots') { img.src = `https://assets.aesthetic.computer/screenshots/images/${name}`; } else if (data && data.src === 'headshots') { img.src = `https://assets.aesthetic.computer/jeffreys/shoot/${name}`; } else { img.src = `https://assets.aesthetic.computer/jeffreys/jpg/${name}.jpg`; } }); return loadingImages[name]; } // Preload multiple images in parallel without blocking function preloadBatch(names) { names.forEach(name => preloadImage(name)); } // Load logo image for canvas rendering const logoImg = new Image(); logoImg.crossOrigin = 'anonymous'; logoImg.src = 'https://aesthetic.computer/purple-pals.svg'; let logoLoaded = false; logoImg.onload = () => { logoLoaded = true; }; // Generate QR code for give.aesthetic.computer (bottom-right of canvas) // Using larger cell size (6) for lower density / fewer pixels look const giveQr = qrcode(0, 'L'); giveQr.addData('https://give.aesthetic.computer'); giveQr.make(); const giveQrImg = new Image(); giveQrImg.src = giveQr.createDataURL(6, 0); // 6px cells for chunkier pixels let giveQrLoaded = false; giveQrImg.onload = () => { giveQrLoaded = true; }; // A/B/C test: use the global variant (already set above) // A: $64, B: $128, C: $8 monthly // Track current price and intensity from the widget let currentPrice = `$${abcTestStartPrice}`; let currentIntensity = 1; // 0-4 intensity level function updatePriceFromWidget() { const widget = container.closest('.gift-widget'); if (widget) { const amountEl = widget.querySelector('.gift-amount'); if (amountEl) { currentPrice = amountEl.textContent; // Check intensity class if (amountEl.classList.contains('intensity-4')) currentIntensity = 4; else if (amountEl.classList.contains('intensity-3')) currentIntensity = 3; else if (amountEl.classList.contains('intensity-2')) currentIntensity = 2; else if (amountEl.classList.contains('intensity-1')) currentIntensity = 1; else currentIntensity = 0; } } } // Watch for price changes const priceObserver = new MutationObserver(updatePriceFromWidget); const widget = container.closest('.gift-widget'); if (widget) { const amountEl = widget.querySelector('.gift-amount'); if (amountEl) { priceObserver.observe(amountEl, { childList: true, characterData: true, subtree: true, attributes: true }); currentPrice = amountEl.textContent; updatePriceFromWidget(); } } // Resize canvas to container let canvasW = 0, canvasH = 0; let needsRender = false; // Flag to trigger immediate render after resize function resizeCanvas() { const rect = container.getBoundingClientRect(); canvasW = rect.width; canvasH = rect.height; canvas.width = canvasW * window.devicePixelRatio; canvas.height = canvasH * window.devicePixelRatio; canvas.style.width = canvasW + 'px'; canvas.style.height = canvasH + 'px'; ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0); needsRender = true; // Mark for immediate re-render }
let currentDisplayIndex = 0; let currentAlpha = 0; let targetImages = []; let floatOffset = 0; let poiIndex = 0; // Current POI being focused on let poiTimer = 0; // Timer for switching POIs let globalRotation = 0; // Slow global rotation let colorPhase = 0; // For color channel cycling let verticalScrollPhase = 0; // For vertical Ken Burns scrolling (0 to 1) let swingPhase = 0; // Swinging rope-like motion let zoomPhase = 0; // Subtle zoom breathing let scrollZoom = 0; // Scroll-based Ken Burns zoom (-1 to 1) let scrollZoomTarget = 0; // Target scroll zoom (smoothly interpolated) // Floating gives are now rendered as HTML overlay (see startFloatingGives) // Track scroll position for Ken Burns scroll-zoom effect function updateScrollZoom() { const slideshow = container; if (!slideshow) return; const rect = slideshow.getBoundingClientRect(); const viewportH = window.innerHeight; // Calculate how far the slideshow has scrolled through the viewport // 0 = slideshow just entering viewport from bottom // 0.5 = slideshow centered in viewport // 1 = slideshow leaving viewport at top const scrollProgress = 1 - ((rect.top + rect.height / 2) / (viewportH + rect.height)); // Map to -1 to 1 range, with 0 when centered // Creates zoom-out at top, zoom-in at bottom effect scrollZoomTarget = (scrollProgress - 0.5) * 2; // Clamp to reasonable range scrollZoomTarget = Math.max(-1, Math.min(1, scrollZoomTarget)); } // Listen for scroll events window.addEventListener('scroll', updateScrollZoom, { passive: true }); window.addEventListener('resize', updateScrollZoom, { passive: true }); updateScrollZoom(); // Initial calculation
// Easing function for smooth bouncy motion function easeInOutSine(t) { return -(Math.cos(Math.PI * t) - 1) / 2; } // Bouncy ease for more organic feel function bouncyEase(t) { const s = Math.sin(t * Math.PI * 2); const s2 = Math.sin(t * Math.PI * 4) * 0.3; return (s + s2 * 0.5 + 1) / 2; }
// Draw POI with simple Ken Burns - just face/body with nice zoom and pan // verticalProgress is 0-1 for top-to-bottom scroll animation // zoomBreath and swingOffset add organic swaying motion function drawPoiWithContext(img, data, poi, destX, destY, destW, destH, alpha, rotation = 0, side = 'left', verticalProgress = 0.5, zoomBreath = 0, swingOffset = 0) { const imgW = img.width; const imgH = img.height; // Get POI box in image pixels const poiX = imgW * poi.box[0] / 100; const poiY = imgH * poi.box[1] / 100; const poiW = imgW * poi.box[2] / 100; const poiH = imgH * poi.box[3] / 100; const poiCenterX = poiX + poiW / 2; const poiCenterY = poiY + poiH / 2; ctx.save(); // Calculate zoom to COVER the canvas (always fill entire viewport) // Add subtle zoom breathing (±3%) for organic feel const scaleX = destW / imgW; const scaleY = destH / imgH; const zoomPulse = 1.0 + zoomBreath * 0.03; // Subtle 3% zoom oscillation const coverScale = Math.max(scaleX, scaleY) * 1.30 * zoomPulse; // 30% extra to prevent edge gaps + breathing // Scaled image dimensions const scaledW = imgW * coverScale; const scaledH = imgH * coverScale; // Position POI at 30% or 70% horizontally (avoiding center where logo is) // Add swing offset for rope-like delayed motion const swingX = swingOffset * 8; // Subtle horizontal swing const swingY = Math.sin(swingOffset * 0.7) * 5; // Delayed vertical bounce const targetX = (side === 'left' ? destW * 0.30 : destW * 0.70) + swingX; // For vertical images (aspect < 0.8), use vertical scroll animation // BUT: If we have a FACE, keep it on screen - don't scroll it off const isVertical = data.aspect < 0.8; const isFace = poi.t === 'f'; let targetY; if (isVertical && !isFace) { // Scroll from top (20%) to bottom (80%) based on verticalProgress - slower, dreamier // Only do this for non-face POIs (bodies, hands, computer pics) const easedProgress = (Math.sin((verticalProgress - 0.5) * Math.PI) + 1) / 2; // Ease in-out targetY = destH * (0.15 + easedProgress * 0.7) + swingY; } else if (isVertical && isFace) { // For faces in vertical images, keep face centered with gentle breathing only // Slight vertical drift but constrained to keep face visible const gentleDrift = Math.sin(verticalProgress * Math.PI) * 0.08; // Very subtle ±8% drift targetY = destH * (0.40 + gentleDrift) + swingY; // Face stays near 40% (upper-mid) } else { targetY = destH * 0.45 + swingY; // Slightly above center with swing } // Calculate offset to position POI center at target position let offsetX = targetX - (poiCenterX * coverScale); let offsetY = targetY - (poiCenterY * coverScale); // Clamp offsets so image always covers the entire canvas with extra padding // Add 5px overdraw padding to prevent edge artifacts from filters const edgePad = 5; const minOffsetX = destW - scaledW + edgePad; const maxOffsetX = -edgePad; offsetX = Math.max(minOffsetX, Math.min(maxOffsetX, offsetX)); // Bottom edge >= destH, top edge <= 0 const minOffsetY = destH - scaledH + edgePad; const maxOffsetY = -edgePad; offsetY = Math.max(minOffsetY, Math.min(maxOffsetY, offsetY)); // Create soft edge mask for blending (fade edges for layering) const gradient = ctx.createLinearGradient( side === 'left' ? destW * 0.5 : 0, 0, side === 'left' ? destW : destW * 0.5, 0 ); if (side === 'left') { gradient.addColorStop(0, `rgba(255,255,255,${alpha})`); gradient.addColorStop(1, `rgba(255,255,255,0)`); } else { gradient.addColorStop(0, `rgba(255,255,255,0)`); gradient.addColorStop(1, `rgba(255,255,255,${alpha})`); } // Crisp filter with sharpening via contrast boost ctx.filter = 'saturate(1.08) contrast(1.05)'; ctx.globalAlpha = alpha; // Draw image covering entire canvas ctx.drawImage(img, 0, 0, imgW, imgH, offsetX, offsetY, scaledW, scaledH); // Reset filter ctx.filter = 'none'; // Draw aesthetic animated POI box (no labels) if (SHOW_POI_BOXES) { // Cycle through colors with animation const color = poiColors[(poiColorIndex++) % poiColors.length]; const pulse = 0.5 + 0.5 * Math.sin(poiAnimPhase); ctx.strokeStyle = color; ctx.lineWidth = 2 + pulse; ctx.setLineDash([8, 4]); // Dashed line for matrix look ctx.lineDashOffset = -poiAnimPhase * 5; // Animated dash // Map POI box to canvas coords const canvasPoiX = offsetX + poiX * coverScale; const canvasPoiY = offsetY + poiY * coverScale; const canvasPoiW = poiW * coverScale; const canvasPoiH = poiH * coverScale; ctx.strokeRect(canvasPoiX, canvasPoiY, canvasPoiW, canvasPoiH); // Corner accents ctx.setLineDash([]); ctx.lineWidth = 3; const cornerLen = Math.min(12, canvasPoiW * 0.2, canvasPoiH * 0.2); // Top-left ctx.beginPath(); ctx.moveTo(canvasPoiX, canvasPoiY + cornerLen); ctx.lineTo(canvasPoiX, canvasPoiY); ctx.lineTo(canvasPoiX + cornerLen, canvasPoiY); ctx.stroke(); // Top-right ctx.beginPath(); ctx.moveTo(canvasPoiX + canvasPoiW - cornerLen, canvasPoiY); ctx.lineTo(canvasPoiX + canvasPoiW, canvasPoiY); ctx.lineTo(canvasPoiX + canvasPoiW, canvasPoiY + cornerLen); ctx.stroke(); // Bottom-left ctx.beginPath(); ctx.moveTo(canvasPoiX, canvasPoiY + canvasPoiH - cornerLen); ctx.lineTo(canvasPoiX, canvasPoiY + canvasPoiH); ctx.lineTo(canvasPoiX + cornerLen, canvasPoiY + canvasPoiH); ctx.stroke(); // Bottom-right ctx.beginPath(); ctx.moveTo(canvasPoiX + canvasPoiW - cornerLen, canvasPoiY + canvasPoiH); ctx.lineTo(canvasPoiX + canvasPoiW, canvasPoiY + canvasPoiH); ctx.lineTo(canvasPoiX + canvasPoiW, canvasPoiY + canvasPoiH - cornerLen); ctx.stroke(); } ctx.filter = 'none'; ctx.restore(); return { offsetX, offsetY, scaledW, scaledH }; }
// Draw image with focal point offset to left or right (avoiding center logo) // Animation flow: WIDE BODY SHOT → ZOOM INTO FACE/ACTIONS // drift controls progression: 0 = wide establishing shot, 1 = tight on face/action function drawImageFocused(img, data, destX, destY, destW, destH, alpha, drift, rotation = 0, forceOffset = null) { const imgW = img.width; const imgH = img.height; // Separate POIs by type for staged reveal const allPois = data.pois || []; const bodies = allPois.filter(poi => poi.t === 'b'); const faces = allPois.filter(poi => poi.t === 'f'); const hands = allPois.filter(poi => poi.t === 'h'); // Priority order: FACES FIRST (most important), then bodies, rarely hands // When faces exist, focus primarily on them const stagedPois = []; // Stage 1: If we have faces, they are the star - add them first and multiple times if (faces.length > 0) { // Add each face twice to spend more time on faces for (const face of faces) { stagedPois.push({ ...face, stage: 'closeup' }); stagedPois.push({ ...face, stage: 'closeup' }); // Double time on faces } } // Stage 2: Add widest body only as brief establishing shot const biggestBody = bodies.sort((a, b) => (b.box[2] * b.box[3]) - (a.box[2] * a.box[3]))[0]; if (biggestBody && faces.length === 0) { // Only use body as main focus if no faces stagedPois.push({ ...biggestBody, stage: 'wide' }); } else if (biggestBody) { // Brief body shot between face focus stagedPois.push({ ...biggestBody, stage: 'wide' }); } // Stage 3: Hand/action details (only if no faces and significant) for (const hand of hands) { const area = hand.box[2] * hand.box[3]; if (area > 5 && faces.length === 0) { // Only if no faces stagedPois.push({ ...hand, stage: 'action' }); } } // If no POIs, fall back to default focal if (stagedPois.length === 0) { stagedPois.push({ t: 'default', box: [data.focal[0] - 10, data.focal[1] - 10, 20, 20], stage: 'wide' }); }
// Calculate focal point - progress through stages with drift // drift cycles 0→1, we map to staged progression const stagePhase = (drift * 0.4 * stagedPois.length) % stagedPois.length; // Even slower const stageIdx = Math.floor(stagePhase); const nextStageIdx = (stageIdx + 1) % stagedPois.length; const rawBlend = stagePhase - stageIdx; const blend = easeInOutSine(rawBlend); const poi1 = getPoiCenter(stagedPois[stageIdx]); const poi2 = getPoiCenter(stagedPois[nextStageIdx]); // Smooth blend between stages const focalX = poi1.x + (poi2.x - poi1.x) * blend; const focalY = poi1.y + (poi2.y - poi1.y) * blend; const currentPoi = stagedPois[stageIdx]; // ZOOM: Wide for bodies, tight for faces/hands // Body/wide stage = 1.0-1.1 zoom (show more) // Face closeup = 1.4-1.6 zoom (intimate) // Hand/action = 1.3-1.5 zoom (detail) let zoomTarget1, zoomTarget2; const getZoomForStage = (poi) => { if (poi.stage === 'wide' || poi.t === 'b') return 1.1; if (poi.t === 'f') return 1.7; // Tighter on faces if (poi.t === 'h') return 1.4; return 1.15; }; zoomTarget1 = getZoomForStage(stagedPois[stageIdx]); zoomTarget2 = getZoomForStage(stagedPois[nextStageIdx]); // Smooth zoom transition with gentle pulse const baseZoom = zoomTarget1 + (zoomTarget2 - zoomTarget1) * blend; const zoomPulse = Math.sin(drift * Math.PI * 4) * 0.03; // Subtle breathing const zoom = baseZoom + zoomPulse; // Calculate source rect to show focal point const canvasRatio = destW / destH; const imgRatio = imgW / imgH; let srcW, srcH, srcX, srcY; if (imgRatio > canvasRatio) { // Image wider than canvas - crop sides srcH = imgH / zoom; srcW = srcH * canvasRatio; } else { // Image taller than canvas - crop top/bottom srcW = imgW / zoom; srcH = srcW / canvasRatio; } // OFFSET: Position face LEFT or RIGHT of center (not in center where logo is) // Decide offset direction based on face position - if face is left of center, show it left // If face is right of center, show it right. This feels more natural. let offsetDirection = forceOffset; if (offsetDirection === null) { // Auto-detect: push face to whichever side it's already leaning toward offsetDirection = focalX < 50 ? 'left' : 'right'; } // Calculate offset: we want face at ~30% or ~70% of canvas, not 50% // This means shifting the source crop so focal point maps to side of canvas const targetCanvasPercent = offsetDirection === 'left' ? 0.30 : 0.70; const focalImgX = imgW * focalX / 100; const focalImgY = imgH * focalY / 100; // Position srcX so that focalImgX lands at targetCanvasPercent of destW srcX = focalImgX - srcW * targetCanvasPercent; srcY = focalImgY - srcH / 2; // Vertical still centered on face // Clamp to image bounds srcX = Math.max(0, Math.min(imgW - srcW, srcX)); srcY = Math.max(0, Math.min(imgH - srcH, srcY)); ctx.save(); ctx.globalAlpha = alpha; // Apply rotation around center of destination const centerX = destX + destW / 2; const centerY = destY + destH / 2; ctx.translate(centerX, centerY); ctx.rotate(rotation); ctx.translate(-centerX, -centerY); // Scale up slightly to cover corners when rotated const rotScale = 1 + Math.abs(rotation) * 0.5; ctx.translate(centerX, centerY); ctx.scale(rotScale, rotScale); ctx.translate(-centerX, -centerY); ctx.drawImage(img, srcX, srcY, srcW, srcH, destX, destY, destW, destH); // Soft radial glow highlights on faces and bodies if (POI_GLOW && data.pois && data.pois.length > 0) { for (const poi of data.pois) { // Only glow faces and large bodies const area = poi.box[2] * poi.box[3]; if (poi.t === 'h') continue; // Skip hands if (poi.t === 'b' && area < 20) continue; // Skip small bodies const poiImgX = imgW * poi.box[0] / 100; const poiImgY = imgH * poi.box[1] / 100; const poiImgW = imgW * poi.box[2] / 100; const poiImgH = imgH * poi.box[3] / 100; // Check if POI is in visible region if (poiImgX + poiImgW > srcX && poiImgX < srcX + srcW && poiImgY + poiImgH > srcY && poiImgY < srcY + srcH) { // Map POI center to canvas coords const poiCenterX = destX + (poiImgX + poiImgW / 2 - srcX) / srcW * destW; const poiCenterY = destY + (poiImgY + poiImgH / 2 - srcY) / srcH * destH; const poiRadius = Math.max(poiImgW / srcW * destW, poiImgH / srcH * destH) * 1.3; // Create soft radial gradient glow - warmer for faces, cooler for bodies const glow = ctx.createRadialGradient( poiCenterX, poiCenterY, 0, poiCenterX, poiCenterY, poiRadius ); if (poi.t === 'f') { // Warm golden glow for faces glow.addColorStop(0, 'rgba(255, 245, 220, 0.18)'); glow.addColorStop(0.4, 'rgba(255, 230, 200, 0.10)'); glow.addColorStop(1, 'rgba(255, 220, 180, 0)'); } else { // Subtle cool glow for bodies glow.addColorStop(0, 'rgba(220, 230, 255, 0.12)'); glow.addColorStop(0.4, 'rgba(200, 215, 255, 0.06)'); glow.addColorStop(1, 'rgba(180, 200, 255, 0)'); } ctx.fillStyle = glow; ctx.fillRect(destX, destY, destW, destH); } } } // Debug: verbose POI info (only when DEBUG_FACES is true) if (DEBUG_FACES && data.pois) { const typeColors = { f: 'rgba(255, 0, 255, 0.9)', b: 'rgba(0, 255, 255, 0.7)', h: 'rgba(255, 255, 0, 0.5)' }; ctx.lineWidth = 2; for (const poi of data.pois) { const poiImgX = imgW * poi.box[0] / 100; const poiImgY = imgH * poi.box[1] / 100; const poiImgW = imgW * poi.box[2] / 100; const poiImgH = imgH * poi.box[3] / 100; if (poiImgX + poiImgW > srcX && poiImgX < srcX + srcW && poiImgY + poiImgH > srcY && poiImgY < srcY + srcH) { ctx.strokeStyle = typeColors[poi.t] || 'white'; const canvasX = destX + (poiImgX - srcX) / srcW * destW; const canvasY = destY + (poiImgY - srcY) / srcH * destH; const canvasFW = poiImgW / srcW * destW; const canvasFH = poiImgH / srcH * destH; ctx.strokeRect(canvasX, canvasY, canvasFW, canvasFH); // Label ctx.font = '10px monospace'; ctx.fillStyle = typeColors[poi.t]; ctx.fillText(poi.t.toUpperCase(), canvasX + 2, canvasY + 10); } } // Crosshair on current focal point const focalCanvasX = destX + (focalImgX - srcX) / srcW * destW; const focalCanvasY = destY + (focalImgY - srcY) / srcH * destH; ctx.fillStyle = 'rgba(0, 255, 0, 0.9)'; ctx.strokeStyle = 'rgba(0, 255, 0, 0.9)'; ctx.fillRect(focalCanvasX - 15, focalCanvasY - 2, 30, 4); ctx.fillRect(focalCanvasX - 2, focalCanvasY - 15, 4, 30); } // Aesthetic POI boxes (multicolored, animated, no labels) if (SHOW_POI_BOXES && !DEBUG_FACES && data.pois) { for (let i = 0; i < data.pois.length; i++) { const poi = data.pois[i]; const poiImgX = imgW * poi.box[0] / 100; const poiImgY = imgH * poi.box[1] / 100; const poiImgW = imgW * poi.box[2] / 100; const poiImgH = imgH * poi.box[3] / 100; if (poiImgX + poiImgW > srcX && poiImgX < srcX + srcW && poiImgY + poiImgH > srcY && poiImgY < srcY + srcH) { const canvasX = destX + (poiImgX - srcX) / srcW * destW; const canvasY = destY + (poiImgY - srcY) / srcH * destH; const canvasFW = poiImgW / srcW * destW; const canvasFH = poiImgH / srcH * destH; // Cycle colors per box const color = poiColors[(poiColorIndex + i) % poiColors.length]; const pulse = 0.5 + 0.5 * Math.sin(poiAnimPhase + i * 0.5); ctx.strokeStyle = color; ctx.lineWidth = 1.5 + pulse; ctx.setLineDash([6, 3]); ctx.lineDashOffset = -poiAnimPhase * 3; ctx.strokeRect(canvasX, canvasY, canvasFW, canvasFH); // Corner accents ctx.setLineDash([]); ctx.lineWidth = 2.5; const cornerLen = Math.min(10, canvasFW * 0.15, canvasFH * 0.15); ctx.beginPath(); ctx.moveTo(canvasX, canvasY + cornerLen); ctx.lineTo(canvasX, canvasY); ctx.lineTo(canvasX + cornerLen, canvasY); ctx.moveTo(canvasX + canvasFW - cornerLen, canvasY); ctx.lineTo(canvasX + canvasFW, canvasY); ctx.lineTo(canvasX + canvasFW, canvasY + cornerLen); ctx.moveTo(canvasX, canvasFH + canvasY - cornerLen); ctx.lineTo(canvasX, canvasFH + canvasY); ctx.lineTo(canvasX + cornerLen, canvasFH + canvasY); ctx.moveTo(canvasX + canvasFW - cornerLen, canvasFH + canvasY); ctx.lineTo(canvasX + canvasFW, canvasFH + canvasY); ctx.lineTo(canvasX + canvasFW, canvasFH + canvasY - cornerLen); ctx.stroke(); } } } ctx.restore(); }
// State for transitions - NOW WITH TWO SIMULTANEOUS POIs (left + right) let transitionProgress = 0; // 0 = showing old, 1 = showing new let isTransitioning = false; let transitionFadeSpeed = 0.012; // Updated during transition start let currentPoiIndex = 0; let poiPhase = 0; // For slow progression through POIs let initialFadeIn = 1; // Start at 1 - no fade, show immediately let isInitialized = false; // True once first images are loaded and ready // Two POIs always visible: left and right let leftPoi = null; let rightPoi = null; let prevLeftPoi = null; let prevRightPoi = null; // Build separate lists for jeffreys, headshots, and screenshots POIs const jeffreysPois = []; const headshotsPois = []; const screenshotsPois = []; for (const name of jeffreysImages) { const data = allImagesData[name]; if (data.pois && data.pois.length > 0) { for (const poi of data.pois) { if (poi.t === 'h') continue; if (poi.t === 'b' && poi.box[2] * poi.box[3] < 15) continue; jeffreysPois.push({ name, poi, data }); } } else { // No POIs - create virtual POI from focal point const virtualPoi = { t: 'v', box: [data.focal[0] - 15, data.focal[1] - 15, 30, 30] }; jeffreysPois.push({ name, poi: virtualPoi, data }); } } // Headshots are face-focused professional shots (temporarily disabled) // for (const name of headshotsImages) { // const data = allImagesData[name]; // if (data.pois && data.pois.length > 0) { // for (const poi of data.pois) { // headshotsPois.push({ name, poi, data }); // } // } // } for (const name of screenshotsImages) { const data = allImagesData[name]; if (data.pois && data.pois.length > 0) { for (const poi of data.pois) { if (poi.t === 'h') continue; if (poi.t === 'b' && poi.box[2] * poi.box[3] < 15) continue; screenshotsPois.push({ name, poi, data }); } } else { // Screenshots have no faces - create virtual POI from focal point for environmental shots const virtualPoi = { t: 'v', box: [data.focal[0] - 20, data.focal[1] - 15, 40, 30] }; screenshotsPois.push({ name, poi: virtualPoi, data }); } } // Shuffle all arrays const shuffle = arr => { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; }; shuffle(jeffreysPois); shuffle(headshotsPois); shuffle(screenshotsPois); // Prioritize faces in each (headshots are all faces already) jeffreysPois.sort((a, b) => (b.poi.t === 'f' ? 1 : 0) - (a.poi.t === 'f' ? 1 : 0)); screenshotsPois.sort((a, b) => (b.poi.t === 'f' ? 1 : 0) - (a.poi.t === 'f' ? 1 : 0)); // Interleave: jeffreys, headshot (25% chance), screenshot, repeat // Headshots sprinkled in probabilistically for variety const allPois = []; const maxLen = Math.max(jeffreysPois.length, screenshotsPois.length, headshotsPois.length); let headshotIdx = 0; for (let i = 0; i < maxLen; i++) { if (i < jeffreysPois.length) allPois.push(jeffreysPois[i]); // 25% chance to insert a headshot after a jeffreys pic if (headshotIdx < headshotsPois.length && Math.random() < 0.25) { allPois.push(headshotsPois[headshotIdx++]); } if (i < screenshotsPois.length) allPois.push(screenshotsPois[i]); // 15% chance to insert a headshot after a screenshot if (headshotIdx < headshotsPois.length && Math.random() < 0.15) { allPois.push(headshotsPois[headshotIdx++]); } } // Add any remaining headshots at the end while (headshotIdx < headshotsPois.length) { allPois.push(headshotsPois[headshotIdx++]); } // Frame timing for 24fps throttle let lastFrameTime = 0; const TARGET_FPS = 24; const FRAME_INTERVAL = 1000 / TARGET_FPS; // ~41.67ms let isPaused = false; // Pause when not visible function render(timestamp) { // Skip rendering if paused (not visible) if (isPaused) { requestAnimationFrame(render); return; } // Initialize lastFrameTime on first call (timestamp may be undefined if called directly) if (!timestamp || lastFrameTime === 0) { lastFrameTime = timestamp || performance.now(); requestAnimationFrame(render); return; } // Throttle to 24fps (but force render after resize to prevent flicker) if (timestamp - lastFrameTime < FRAME_INTERVAL && !needsRender) { requestAnimationFrame(render); return; } needsRender = false; // Clear force-render flag const deltaTime = Math.min(timestamp - lastFrameTime, 100); // Cap at 100ms to prevent huge jumps lastFrameTime = timestamp; // Time multiplier (normalize to 60fps base, so animations stay same speed) const dt = deltaTime / 16.67; // 16.67ms = 60fps const w = canvasW; const h = canvasH; // Update phases - SLOW and dreamy (time-based) globalRotation += 0.000015 * dt; // Even slower rotation floatOffset += 0.00003 * dt; // Slower float poiPhase += 0.00005 * dt; // Slower POI progression verticalScrollPhase += 0.0003 * dt; // Much slower vertical scroll (~55 sec full cycle) if (verticalScrollPhase > 1) verticalScrollPhase = 0; swingPhase += 0.00007 * dt; // Slow pendulum swing zoomPhase += 0.00004 * dt; // Very slow zoom breathing poiAnimPhase += 0.05 * dt; // Animate POI box dash offset // Update transition progress in render loop (smoother than setInterval) if (isTransitioning) { transitionProgress += transitionFadeSpeed * dt; if (transitionProgress >= 1) { transitionProgress = 1; isTransitioning = false; prevLeftPoi = null; prevRightPoi = null; } } // Smooth scroll zoom interpolation (buttery smooth tracking, time-based) scrollZoom += (scrollZoomTarget - scrollZoom) * 0.08 * dt; // Swinging rope motion - delayed sinusoids for organic feel const swing1 = Math.sin(swingPhase * Math.PI * 2); const swing2 = Math.sin(swingPhase * Math.PI * 2 - 0.5); // Delayed follow const swing3 = Math.sin(swingPhase * Math.PI * 1.3); // Different frequency // Add scroll-based rotation for extra cinematic parallax feel const scrollRotation = scrollZoom * 0.008; // Subtle tilt when scrolling const rotation = swing1 * 0.006 + swing3 * 0.003 + scrollRotation; // Gentle compound rotation + scroll const wiggleX = swing1 * 1.5 + swing2 * 0.8 + scrollZoom * 3; // Swaying X + scroll parallax const wiggleY = swing2 * 1.2 + Math.sin(swingPhase * Math.PI * 1.7) * 0.6; // Bouncy Y // Clear completely each frame ctx.fillStyle = 'rgb(18, 14, 24)'; ctx.fillRect(0, 0, w, h); // Don't render until initialized with loaded images if (!isInitialized || allPois.length === 0 || (!leftPoi && !rightPoi)) { requestAnimationFrame(render); return; } // Animate initial fade-in (slow, smooth, time-based) if (initialFadeIn < 1) { initialFadeIn += 0.008 * dt; // ~2 seconds to fully fade in if (initialFadeIn > 1) initialFadeIn = 1; } const masterAlpha = easeInOutSine(initialFadeIn); // Smooth ease ctx.save(); ctx.translate(w/2 + wiggleX, h/2 + wiggleY); ctx.rotate(rotation); ctx.translate(-w/2, -h/2); // Draw fading out previous POIs during transition (behind current) // Use a smooth sine-based vertical progress for nice top-to-bottom scroll const vp = (Math.sin(verticalScrollPhase * Math.PI * 2 - Math.PI/2) + 1) / 2; // 0→1→0 smooth // Zoom breathing - slow and subtle, PLUS scroll-based Ken Burns zoom // scrollZoom adds extra zoom based on scroll position: zoom in when scrolling down, out when scrolling up const zb = Math.sin(zoomPhase * Math.PI * 2) + scrollZoom * 0.5; // Swing offset for rope-like motion (different phase for left/right) const swingL = Math.sin(swingPhase * Math.PI * 2); const swingR = Math.sin(swingPhase * Math.PI * 2 - 0.3); // Slightly delayed for right // Screenshot shake - faster, jittery digital motion const shakePhase = swingPhase * 8; // Faster shake for screenshots const shakeX = Math.sin(shakePhase * Math.PI * 2) * 3 + Math.sin(shakePhase * Math.PI * 5.3) * 1.5; const shakeY = Math.cos(shakePhase * Math.PI * 1.7) * 2 + Math.sin(shakePhase * Math.PI * 3.1) * 1; // Ease transition progress for smooth fade (no pop) const easedTransition = easeInOutSine(transitionProgress); // More dramatic fade curve - faster in middle const dramaticFade = Math.pow(easedTransition, 0.7); // Faster fade-in const dramaticFadeOut = Math.pow(1 - easedTransition, 0.7); // Faster fade-out if (isTransitioning) { if (prevLeftPoi) { const prevImg = loadedImages[prevLeftPoi.name]; if (prevImg && prevImg.complete && prevImg.naturalWidth > 0) { const isScreenshot = prevLeftPoi.data.src === 'screenshots'; const fadeAlpha = dramaticFadeOut * (isScreenshot ? 0.65 : 0.75) * masterAlpha; const swing = isScreenshot ? shakeX * 0.5 : swingL; drawPoiWithContext(prevImg, prevLeftPoi.data, prevLeftPoi.poi, 0, 0, w, h, fadeAlpha, 0, 'left', vp, zb, swing); } } if (prevRightPoi) { const prevImg = loadedImages[prevRightPoi.name]; if (prevImg && prevImg.complete && prevImg.naturalWidth > 0) { const isScreenshot = prevRightPoi.data.src === 'screenshots'; const fadeAlpha = dramaticFadeOut * (isScreenshot ? 0.6 : 0.7) * masterAlpha; const swing = isScreenshot ? shakeY * 0.5 : swingR; drawPoiWithContext(prevImg, prevRightPoi.data, prevRightPoi.poi, 0, 0, w, h, fadeAlpha, 0, 'right', vp, zb, swing); } } } // Draw LEFT POI (always on left side) if (leftPoi) { const leftImg = loadedImages[leftPoi.name]; if (leftImg && leftImg.complete && leftImg.naturalWidth > 0) { const isScreenshot = leftPoi.data.src === 'screenshots'; const baseAlpha = isScreenshot ? 0.7 : 0.8; // Faces brighter const alpha = (isTransitioning ? dramaticFade * baseAlpha : baseAlpha) * masterAlpha; const swing = isScreenshot ? shakeX : swingL; // Screenshots shake drawPoiWithContext(leftImg, leftPoi.data, leftPoi.poi, 0, 0, w, h, alpha, 0, 'left', vp, zb, swing); } } // Draw RIGHT POI (always on right side) - slightly lower alpha for layering if (rightPoi) { const rightImg = loadedImages[rightPoi.name]; if (rightImg && rightImg.complete && rightImg.naturalWidth > 0) { const isScreenshot = rightPoi.data.src === 'screenshots'; const baseAlpha = isScreenshot ? 0.65 : 0.75; // Faces brighter const alpha = (isTransitioning ? dramaticFade * baseAlpha : baseAlpha) * masterAlpha; const swing = isScreenshot ? shakeY : swingR; // Screenshots shake differently drawPoiWithContext(rightImg, rightPoi.data, rightPoi.poi, 0, 0, w, h, alpha, 0, 'right', vp, zb, swing); } } ctx.restore(); // Draw POI detection boxes AFTER all transforms (composited on top) // Psychedelic, subtle, flickering on/off if (SHOW_POI_BOXES) { // Flicker: boxes appear/disappear randomly const flickerPhase = Math.sin(Date.now() / 150) * Math.sin(Date.now() / 370); const showBoxes = flickerPhase > -0.3; // Show ~70% of the time with irregular rhythm if (showBoxes) { ctx.save(); ctx.filter = 'none'; // Psychedelic color cycling const hueShift = (Date.now() / 30) % 360; const pulse = 0.3 + 0.2 * Math.sin(Date.now() / 200); // Draw boxes for current left and right POIs const poisToDraw = []; if (leftPoi) poisToDraw.push({ poi: leftPoi, side: 'left', swing: swingL }); if (rightPoi) poisToDraw.push({ poi: rightPoi, side: 'right', swing: swingR }); for (const { poi: poiData, side, swing } of poisToDraw) { const img = loadedImages[poiData.name]; if (!img || !img.complete) continue; const data = poiData.data; const imgW = img.width; const imgH = img.height; // Recalculate with Ken Burns params (zoomBreath, swing) - MUST MATCH drawPoiWithContext exactly const scaleX = w / imgW; const scaleY = h / imgH; const zoomPulse = 1.0 + zb * 0.03; const coverScale = Math.max(scaleX, scaleY) * 1.30 * zoomPulse; // 30% extra - matches drawPoiWithContext const scaledW = imgW * coverScale; const scaledH = imgH * coverScale; const poiX = imgW * poiData.poi.box[0] / 100; const poiY = imgH * poiData.poi.box[1] / 100; const poiW = imgW * poiData.poi.box[2] / 100; const poiH = imgH * poiData.poi.box[3] / 100; const poiCenterX = poiX + poiW / 2; const poiCenterY = poiY + poiH / 2; // Include swing offset for Ken Burns tracking const swingX = swing * 8; const swingY = Math.sin(swing * 0.7) * 5; const targetX = (side === 'left' ? w * 0.30 : w * 0.70) + swingX; // Handle vertical images with vertical scroll (same logic as drawPoiWithContext) // BUT: Keep faces on screen - don't scroll them off const isVertical = data.aspect < 0.8; const isFace = poiData.poi.t === 'f'; let targetY; if (isVertical && !isFace) { const easedProgress = (Math.sin((vp - 0.5) * Math.PI) + 1) / 2; targetY = h * (0.15 + easedProgress * 0.7) + swingY; } else if (isVertical && isFace) { // Face in vertical image - gentle drift only, keep visible const gentleDrift = Math.sin(vp * Math.PI) * 0.08; targetY = h * (0.40 + gentleDrift) + swingY; } else { targetY = h * 0.45 + swingY; } // Apply same edge padding as drawPoiWithContext const edgePad = 5; let offsetX = targetX - (poiCenterX * coverScale); let offsetY = targetY - (poiCenterY * coverScale); offsetX = Math.max(w - scaledW + edgePad, Math.min(-edgePad, offsetX)); offsetY = Math.max(h - scaledH + edgePad, Math.min(-edgePad, offsetY)); // Draw boxes for ALL pois in this image for (let pi = 0; pi < (data.pois || []).length; pi++) { const p = data.pois[pi]; const canvasX = offsetX + (imgW * p.box[0] / 100) * coverScale; const canvasY = offsetY + (imgH * p.box[1] / 100) * coverScale; const canvasFW = (imgW * p.box[2] / 100) * coverScale; const canvasFH = (imgH * p.box[3] / 100) * coverScale; if (canvasX + canvasFW > 0 && canvasX < w && canvasY + canvasFH > 0 && canvasY < h) { // Psychedelic hue per POI type const typeHue = { f: 120, b: 60, h: 0, s: 180 }; const hue = ((typeHue[p.t] || 120) + hueShift) % 360; ctx.strokeStyle = `hsla(${hue}, 100%, 60%, ${pulse})`; ctx.globalAlpha = pulse; ctx.lineWidth = 1.5; ctx.strokeRect(canvasX, canvasY, canvasFW, canvasFH); // Subtle corner accents const cornerLen = Math.min(10, canvasFW * 0.15, canvasFH * 0.15); ctx.lineWidth = 2.5; ctx.strokeStyle = `hsla(${(hue + 60) % 360}, 100%, 70%, ${pulse * 1.5})`; ctx.beginPath(); ctx.moveTo(canvasX, canvasY + cornerLen); ctx.lineTo(canvasX, canvasY); ctx.lineTo(canvasX + cornerLen, canvasY); ctx.stroke(); ctx.beginPath(); ctx.moveTo(canvasX + canvasFW - cornerLen, canvasY); ctx.lineTo(canvasX + canvasFW, canvasY); ctx.lineTo(canvasX + canvasFW, canvasY + cornerLen); ctx.stroke(); ctx.beginPath(); ctx.moveTo(canvasX, canvasY + canvasFH - cornerLen); ctx.lineTo(canvasX, canvasY + canvasFH); ctx.lineTo(canvasX + cornerLen, canvasY + canvasFH); ctx.stroke(); ctx.beginPath(); ctx.moveTo(canvasX + canvasFW - cornerLen, canvasY + canvasFH); ctx.lineTo(canvasX + canvasFW, canvasY + canvasFH); ctx.lineTo(canvasX + canvasFW, canvasY + canvasFH - cornerLen); ctx.stroke(); } } } ctx.restore(); } } // Draw logo and price on canvas (after restore, no filters) ctx.filter = 'none'; // Ensure no filters affect logo/price
// Floating gives are rendered as HTML overlay for better performance // (canvas shadowBlur on many characters kills FPS)
// Draw logo and price on canvas if (logoLoaded && logoImg.complete) { // Parse current price to get dollar amount for subtle scaling const priceNum = parseFloat(currentPrice.replace(/[^0-9.]/g, '')) || 128; // Very subtle scale: $128 = 1.0 (default), range is 0.95 to 1.08 const scaleBase = Math.log2(Math.max(1, priceNum)) / Math.log2(128); // 0 at $1, 1 at $128 const priceScale = 0.95 + scaleBase * 0.13; // Range: 0.95 at $1, 1.08 at $128, ~1.15 at $1000 // Logo size with very subtle price-based scaling const baseSize = Math.min(w, h); const logoSize = baseSize * 0.85 * Math.min(priceScale, 1.12); // Cap at 1.12x const logoX = (w - logoSize) / 2; // Centered const logoY = (h - logoSize) / 2 + h * 0.08; // Move down slightly // Draw logo-shaped drop shadow for prominence (soft offset shadow) ctx.save(); const logoCenterX = logoX + logoSize / 2; const logoCenterY = logoY + logoSize / 2; ctx.globalAlpha = masterAlpha * 0.35; ctx.filter = 'brightness(0) blur(3px)'; // Black shadow with soft blur ctx.drawImage(logoImg, logoX + 2, logoY + 3, logoSize, logoSize); // Smaller offset ctx.restore(); // Logo intensity effects based on price - always visible, use color shifts not opacity const t = Date.now(); let logoHue = 0; // Draw logo with intensity-based effects ctx.save(); if (currentIntensity === 4) { // MAX INTENSITY: Multiple color-shifted logos stacked, never invisible logoHue = (t / 12) % 360; // Draw shadow/echo logos offset and color-shifted ctx.globalAlpha = masterAlpha * 0.4; ctx.filter = `hue-rotate(${logoHue + 120}deg) saturate(2)`; ctx.drawImage(logoImg, logoX - 4, logoY - 2, logoSize, logoSize); ctx.filter = `hue-rotate(${logoHue + 240}deg) saturate(2)`; ctx.drawImage(logoImg, logoX + 4, logoY + 2, logoSize, logoSize); // Main logo with rapid hue shift ctx.globalAlpha = masterAlpha; const flashPhase = Math.sin(t / 60); ctx.filter = `hue-rotate(${logoHue}deg) saturate(${1.5 + flashPhase * 0.5}) brightness(${1 + flashPhase * 0.3})`; ctx.shadowColor = `hsl(${logoHue}, 100%, 60%)`; ctx.shadowBlur = 30 + 15 * Math.abs(flashPhase); ctx.drawImage(logoImg, logoX, logoY, logoSize, logoSize); } else if (currentIntensity === 3) { // High: pulsing warm glow with color shift, dual logos logoHue = (t / 25) % 360; const pulse = Math.sin(t / 150); // Echo logo ctx.globalAlpha = masterAlpha * 0.3; ctx.filter = `hue-rotate(${logoHue + 60}deg)`; ctx.drawImage(logoImg, logoX + 2 * pulse, logoY + pulse, logoSize, logoSize); // Main logo ctx.globalAlpha = masterAlpha; ctx.filter = `hue-rotate(${logoHue * 0.3}deg) saturate(1.2)`; ctx.shadowColor = `hsl(${30 + pulse * 30}, 100%, 50%)`; ctx.shadowBlur = 25 + 10 * pulse; ctx.drawImage(logoImg, logoX, logoY, logoSize, logoSize); } else if (currentIntensity >= 2) { // Medium: gentle color pulse const pulse = Math.sin(t / 400); ctx.globalAlpha = masterAlpha; ctx.filter = `saturate(${1.1 + pulse * 0.1})`; ctx.shadowColor = 'rgba(100, 200, 100, 0.5)'; ctx.shadowBlur = 20; ctx.shadowOffsetY = 6; ctx.drawImage(logoImg, logoX, logoY, logoSize, logoSize); } else if (currentIntensity >= 1) { // Low: subtle shadow ctx.globalAlpha = masterAlpha; ctx.shadowColor = 'rgba(0, 0, 0, 0.7)'; ctx.shadowBlur = 20; ctx.shadowOffsetY = 6; ctx.drawImage(logoImg, logoX, logoY, logoSize, logoSize); } else { // Default: simple shadow ctx.globalAlpha = masterAlpha; ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'; ctx.shadowBlur = 15; ctx.shadowOffsetY = 4; ctx.drawImage(logoImg, logoX, logoY, logoSize, logoSize); } ctx.filter = 'none'; ctx.restore(); // Draw price at top-right of logo (very subtle scaling) ctx.save(); const fontScale = Math.min(priceScale, 1.08); // Even more subtle for price tag const fontSize = logoSize * 0.18 * fontScale; ctx.font = `bold ${fontSize}px 'YWFTProcessing-Regular', monospace`; // Price background const priceMetrics = ctx.measureText(currentPrice); const paddingX = fontSize * 0.4; const paddingY = fontSize * 0.25; const priceW = priceMetrics.width + paddingX * 2; const priceH = fontSize + paddingY * 2; // Position more to the right of logo const priceX = logoX + logoSize - priceW * 0.15; const priceY = logoY - priceH * 0.2; // Intensity-based colors with more dramatic high-end effects const t2 = Date.now(); let bgColor, borderColor, textColor, glowAmt; if (currentIntensity === 4) { // MAX: Flashing high contrast, rainbow cycling const flash = Math.sin(t2 / 60); const hue = (t2 / 8) % 360; bgColor = flash > 0 ? '#000' : `hsl(${hue}, 80%, 15%)`; borderColor = `hsl(${hue}, 100%, 60%)`; textColor = `hsl(${(hue + 180) % 360}, 100%, ${70 + flash * 20}%)`; glowAmt = 30 + 15 * Math.abs(flash); } else if (currentIntensity === 3) { // High: warm pulsing const pulse = Math.sin(t2 / 200); bgColor = '#2a2a1a'; borderColor = `hsl(${40 + pulse * 20}, 100%, 55%)`; textColor = `hsl(${50 + pulse * 10}, 100%, 70%)`; glowAmt = 18 + 6 * pulse; } else if (currentIntensity === 2) { bgColor = '#1a3a1a'; borderColor = '#00cc66'; textColor = '#00ffcc'; glowAmt = 10; } else if (currentIntensity === 1) { bgColor = '#0a2a0a'; borderColor = '#3a7a3a'; textColor = '#aaffaa'; glowAmt = 6; } else { bgColor = '#0a2a0a'; borderColor = '#2a5a2a'; textColor = '#88ff88'; glowAmt = 0; } // Price background gradient const bgGrad = ctx.createLinearGradient(priceX, priceY, priceX + priceW, priceY + priceH); bgGrad.addColorStop(0, bgColor); bgGrad.addColorStop(1, '#050505'); ctx.fillStyle = bgGrad; ctx.globalAlpha = masterAlpha * 0.95; // Rounded rect background ctx.beginPath(); const radius = 4; ctx.moveTo(priceX + radius, priceY); ctx.lineTo(priceX + priceW - radius, priceY); ctx.quadraticCurveTo(priceX + priceW, priceY, priceX + priceW, priceY + radius); ctx.lineTo(priceX + priceW, priceY + priceH - radius); ctx.quadraticCurveTo(priceX + priceW, priceY + priceH, priceX + priceW - radius, priceY + priceH); ctx.lineTo(priceX + radius, priceY + priceH); ctx.quadraticCurveTo(priceX, priceY + priceH, priceX, priceY + priceH - radius); ctx.lineTo(priceX, priceY + radius); ctx.quadraticCurveTo(priceX, priceY, priceX + radius, priceY); ctx.closePath(); ctx.fill(); // Border with intensity glow if (glowAmt > 0) { ctx.shadowColor = borderColor; ctx.shadowBlur = glowAmt; } ctx.strokeStyle = borderColor; ctx.lineWidth = currentIntensity >= 3 ? 4 : 3; // Thicker at high intensity ctx.globalAlpha = masterAlpha; ctx.stroke(); ctx.shadowBlur = 0; // Price text with intensity effects ctx.fillStyle = textColor; ctx.textBaseline = 'middle'; // At max intensity, add extra text effects if (currentIntensity === 4) { ctx.shadowColor = textColor; ctx.shadowBlur = 10 + 5 * Math.sin(t2 / 50); } ctx.fillText(currentPrice, priceX + paddingX, priceY + priceH / 2 + fontSize * 0.08); ctx.restore(); }
// Draw QR code in bottom-right corner // Note: giveQrImg is generated ONCE at init, not every frame if (giveQrLoaded && giveQrImg.complete) { ctx.save(); const qrSize = Math.min(w, h) * 0.28; // 28% of canvas size const qrPadding = 24; // More margin from edges const qrX = w - qrSize - qrPadding; // Right edge const qrY = h - qrSize - qrPadding; // Bottom edge // Pulsing white glow effect (use cached time for perf) const qrTime = Date.now(); const qrPulse = 0.88 + 0.12 * Math.sin(qrTime / 400); const qrGlow = 10 + 8 * Math.sin(qrTime / 300); // Bright white background with pulse - no border radius ctx.globalAlpha = masterAlpha * qrPulse; ctx.fillStyle = '#ffffff'; ctx.shadowColor = 'rgba(255, 255, 255, 0.95)'; ctx.shadowBlur = qrGlow; const bgPad = 5; // Thin white border ctx.fillRect(qrX - bgPad, qrY - bgPad, qrSize + bgPad * 2, qrSize + bgPad * 2); ctx.shadowBlur = 0; // Draw QR code (pre-generated image, not regenerated each frame) ctx.globalAlpha = masterAlpha; ctx.imageSmoothingEnabled = false; // Keep QR crisp ctx.drawImage(giveQrImg, qrX, qrY, qrSize, qrSize); ctx.imageSmoothingEnabled = true; ctx.restore(); } requestAnimationFrame(render); }
function transitionToNextPoi() { if (allPois.length < 2) return; // Get next two POIs (different images for left and right) currentPoiIndex = (currentPoiIndex + 2) % allPois.length; const nextLeftIdx = currentPoiIndex; const nextRightIdx = (currentPoiIndex + 1) % allPois.length; const nextLeftPoi = allPois[nextLeftIdx]; const nextRightPoi = allPois[nextRightIdx]; // Check if images are already loaded (they should be from background preload) const nextLeftImg = loadedImages[nextLeftPoi.name]; const nextRightImg = loadedImages[nextRightPoi.name]; if (!nextLeftImg?.complete || !nextLeftImg?.naturalWidth || !nextRightImg?.complete || !nextRightImg?.naturalWidth) { // Images not ready - trigger load and skip this transition preloadImage(nextLeftPoi.name); preloadImage(nextRightPoi.name); return; } // Preload NEXT batch in background (look ahead 4-8 images) const lookAhead = 8; for (let i = 2; i < lookAhead; i++) { const idx = (currentPoiIndex + i) % allPois.length; preloadImage(allPois[idx].name); } // Store previous for crossfade prevLeftPoi = leftPoi; prevRightPoi = rightPoi; leftPoi = nextLeftPoi; rightPoi = nextRightPoi; // Start transition isTransitioning = true; transitionProgress = 0; // Check if current POIs have faces - hold them longer const hasFaces = (leftPoi?.poi?.t === 'f') || (rightPoi?.poi?.t === 'f'); // Transition will be handled in render loop for smoother animation // Store fade speed as state transitionFadeSpeed = hasFaces ? 0.008 : 0.015; }
// Start slideshow async function start() { resizeCanvas(); window.addEventListener('resize', resizeCanvas); // Find first jeffrey (not screenshot) to show immediately const firstJeffrey = allPois.find(p => p.data.src !== 'screenshots'); const secondPoi = allPois.find(p => p !== firstJeffrey); // Get the gift-visual container to add loaded class const giftVisual = container.closest('.gift-visual'); if (firstJeffrey) { // Await first jeffrey image before starting render await preloadImage(firstJeffrey.name); leftPoi = firstJeffrey; isInitialized = true; // Start render loop with image already loaded render(); // Fade in the gift visual (remove blur) if (giftVisual) { requestAnimationFrame(() => giftVisual.classList.add('loaded')); } // Load second image in background, then set it if (secondPoi) { preloadImage(secondPoi.name).then(() => { rightPoi = secondPoi; currentPoiIndex = allPois.indexOf(secondPoi); }); } } else { // Fallback: start render and wait for any image render(); if (giftVisual) giftVisual.classList.add('loaded'); } // Aggressively preload remaining images in background const preloadCount = Math.min(16, allPois.length); preloadBatch(allPois.slice(0, preloadCount).map(p => p.name)); // Dynamic transition timing - faces held longer const scheduleNextTransition = () => { const hasFaces = (leftPoi?.poi?.t === 'f') || (rightPoi?.poi?.t === 'f'); const holdTime = hasFaces ? 7000 : 4000; setTimeout(() => { transitionToNextPoi(); scheduleNextTransition(); }, holdTime); }; // Start transitions after initial hold setTimeout(scheduleNextTransition, 4000); }
start(); // Pause slideshow when not visible (IntersectionObserver) const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { isPaused = !entry.isIntersecting; if (!isPaused) { // Reset lastFrameTime to avoid huge delta on resume lastFrameTime = 0; } }); }, { threshold: 0.1 }); observer.observe(container); }
// Initialize slideshow when DOM ready (after manifest fetch resolves) async function startJeffreysSlideshow() { try { await loadJeffreysManifest(); } catch (err) { console.warn('Jeffreys manifest fetch failed; slideshow will run with empty data', err); } buildImageIndex(); initJeffreysSlideshow(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startJeffreysSlideshow); } else { startJeffreysSlideshow(); }
// Language selector (dropdown) const langSelector = document.getElementById('langSelector'); const langFlag = document.getElementById('lang-flag'); const langText = document.getElementById('lang-text'); const langDropdown = document.getElementById('lang-dropdown'); const langOptions = langDropdown.querySelectorAll('.lang-option'); // Currency selector (pills) const currSelector = document.getElementById('currSelector'); const currOptions = currSelector.querySelectorAll('.curr-link'); const currencyPickers = document.querySelectorAll('.currency-picker');
// Language dropdown toggle langSelector.addEventListener('click', (e) => { if (e.target.closest('.lang-option')) return; e.stopPropagation(); langSelector.classList.toggle('open'); }); // Close dropdown on outside click document.addEventListener('click', () => { langSelector.classList.remove('open'); });
// Language selection langOptions.forEach(opt => { opt.addEventListener('click', (e) => { e.stopPropagation(); const lang = opt.dataset.lang; setLanguage(lang); updateLangUI(lang); langSelector.classList.remove('open'); savePrefs({ lang }); }); }); function updateLangUI(lang) { const langData = { en: { flagClass: 'fi fi-us', text: 'English' }, da: { flagClass: 'fi fi-dk', text: 'Dansk' }, de: { flagClass: 'fi fi-de', text: 'Deutsch' }, es: { flagClass: 'fi fi-es', text: 'Español' }, zh: { flagClass: 'fi fi-cn', text: '中文' } }; const info = langData[lang] || langData.en; // Update flag using class instead of textContent (for flag-icons) langFlag.className = 'lang-flag ' + info.flagClass; langText.textContent = info.text; }
// Consolidated localStorage for gift preferences const STORAGE_KEY = 'gift-prefs'; function getPrefs() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; } catch { return {}; } } function savePrefs(updates) { const prefs = { ...getPrefs(), ...updates }; localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs)); }
// Currency selection (pills) currOptions.forEach(opt => { opt.addEventListener('click', (e) => { e.stopPropagation(); const curr = opt.dataset.curr; setCurrency(curr); savePrefs({ currency: curr }); }); }); function setCurrency(curr) { currOptions.forEach(opt => opt.classList.toggle('active', opt.dataset.curr === curr)); currencyPickers.forEach(picker => { picker.style.display = picker.dataset.for === curr ? '' : 'none'; }); // Move the slideshow to the active currency panel (USD or DKK) const slideshow = document.getElementById('jeffreysSlideshow'); if (slideshow && (curr === 'usd' || curr === 'dkk')) { const targetPicker = document.querySelector(`.currency-picker[data-for="${curr}"]`); const targetVisual = targetPicker?.querySelector('.gift-visual'); const targetPlaceholder = targetPicker?.querySelector('.jeffreys-slideshow-dkk, .jeffreys-slideshow'); if (targetVisual && !targetVisual.contains(slideshow)) { // Insert slideshow as first child of gift-visual targetVisual.insertBefore(slideshow, targetVisual.firstChild); // Ensure target visual has loaded class (prevent blur) targetVisual.classList.add('loaded'); } // Trigger canvas resize and unpause after becoming visible requestAnimationFrame(() => { window.dispatchEvent(new Event('resize')); }); } // Toggle fiat vs crypto notes const isCrypto = curr === 'crypto'; document.querySelectorAll('.fiat-only').forEach(el => el.style.display = isCrypto ? 'none' : ''); document.querySelectorAll('.crypto-only').forEach(el => el.style.display = isCrypto ? '' : 'none'); }
// Check for thank-you mode const urlParams = new URLSearchParams(window.location.search); const isThanksMode = urlParams.has('thanks') || urlParams.get('thanks') === '1'; const thanksAmount = urlParams.get('amount'); const thanksCurrency = urlParams.get('currency') || 'usd'; // Check for prefilled email (from logged-in users via kidlisp.com or aesthetic.computer) const prefillEmail = urlParams.get('email'); if (prefillEmail) { // Clean up URL without reloading (remove email param for privacy) const cleanUrl = new URL(window.location.href); cleanUrl.searchParams.delete('email'); window.history.replaceState({}, '', cleanUrl.toString()); } if (isThanksMode) { document.body.classList.add('thanks-mode'); // Update amount display const amountEn = document.getElementById('thanksAmountEn'); const amountDa = document.getElementById('thanksAmountDa'); const formattedAmount = thanksAmount ? (thanksCurrency === 'dkk' ? `${thanksAmount} kr` : `$${thanksAmount}`) : '$25'; if (thanksAmount) { if (amountEn) amountEn.textContent = formattedAmount; if (amountDa) amountDa.textContent = formattedAmount; } // Format today's date for the canvas const today = new Date(); const formattedDate = today.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); // Generate QR code for give.aesthetic.computer const qr = qrcode(0, 'M'); qr.addData('https://give.aesthetic.computer'); qr.make(); const qrImg = new Image(); qrImg.src = qr.createDataURL(4, 0); // 🌈 PSYCHO THANKS CANVAS with pals.svg + heart behind const canvas = document.getElementById('thanksCanvas'); if (canvas) { const ctx = canvas.getContext('2d'); const W = canvas.width, H = canvas.height; // Cancel any previous animation if (window._thanksAnimFrame) { cancelAnimationFrame(window._thanksAnimFrame); } // Random start time so each gift looks different let t = Math.random() * 100; let palsImg = null; let palsLoaded = false; // Load pals.svg const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => { palsImg = img; palsLoaded = true; }; img.src = 'https://aesthetic.computer/purple-pals.svg'; function hslToRgb(h, s, l) { let r, g, b; if (s === 0) { r = g = b = l; } else { const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } function drawHeart(cx, cy, size, color, glow = 0) { ctx.save(); ctx.translate(cx, cy); ctx.beginPath(); const s = size; ctx.moveTo(0, s * 0.3); ctx.bezierCurveTo(-s * 0.5, -s * 0.3, -s, s * 0.1, 0, s); ctx.bezierCurveTo(s, s * 0.1, s * 0.5, -s * 0.3, 0, s * 0.3); ctx.closePath(); if (glow > 0) { ctx.shadowColor = color; ctx.shadowBlur = glow; } ctx.fillStyle = color; ctx.fill(); ctx.restore(); } // Draw static image (once pals loads) function drawStaticImage() { // Background gradient (static purple/dark) const grad = ctx.createRadialGradient(W/2, H/2, 0, W/2, H/2, W * 0.7); grad.addColorStop(0, 'rgb(35, 30, 50)'); grad.addColorStop(1, 'rgb(20, 15, 30)'); ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H); // Center position for the main composition const centerX = W / 2; const centerY = H / 2 - 40; // Random colors for effects const colors = [ 'rgb(255, 107, 157)', // pink 'rgb(78, 205, 196)', // cyan 'rgb(255, 217, 61)', // gold 'rgb(107, 203, 119)', // green 'rgb(167, 139, 250)', // purple 'rgb(255, 150, 100)', // orange ]; // Soft blurred glow behind pals - multiple layered radial gradients ctx.save(); for (let i = 0; i < 6; i++) { const angle = (i / 6) * Math.PI * 2; const color = colors[i % colors.length]; const glowX = centerX + Math.cos(angle) * 80; const glowY = centerY + Math.sin(angle) * 80; const glow = ctx.createRadialGradient(glowX, glowY, 0, glowX, glowY, 150); glow.addColorStop(0, color.replace('rgb', 'rgba').replace(')', ', 0.08)')); glow.addColorStop(0.5, color.replace('rgb', 'rgba').replace(')', ', 0.03)')); glow.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = glow; ctx.fillRect(0, 0, W, H); } ctx.restore(); // Central soft glow ctx.save(); const centerGlow = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, 200); centerGlow.addColorStop(0, 'rgba(167, 139, 250, 0.1)'); centerGlow.addColorStop(0.4, 'rgba(255, 107, 157, 0.05)'); centerGlow.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = centerGlow; ctx.fillRect(0, 0, W, H); ctx.restore(); // Heart glow effect const heartX = centerX + 170; const heartY = centerY - 160; ctx.save(); ctx.beginPath(); ctx.arc(heartX, heartY + 20, 40, 0, Math.PI * 2); const heartGlow = ctx.createRadialGradient(heartX, heartY + 20, 0, heartX, heartY + 20, 40); heartGlow.addColorStop(0, 'rgba(255, 100, 150, 0.3)'); heartGlow.addColorStop(1, 'rgba(255, 100, 150, 0)'); ctx.fillStyle = heartGlow; ctx.fill(); ctx.restore(); // Heart in TOP RIGHT - smaller solid pink heart const heartColor = 'rgb(230, 100, 120)'; ctx.save(); ctx.translate(heartX, heartY); drawHeart(0, 0, 55, heartColor, 0); ctx.restore(); // Draw pals.svg centered (moved down) if (palsLoaded && palsImg) { const palsSize = 400; ctx.save(); ctx.translate(centerX, centerY + 30); ctx.drawImage(palsImg, -palsSize/2, -palsSize/2, palsSize, palsSize); ctx.restore(); } // QR code in TOP LEFT with white border if (qrImg.complete && qrImg.naturalWidth > 0) { ctx.save(); ctx.fillStyle = 'white'; ctx.fillRect(14, 14, 82, 82); ctx.drawImage(qrImg, 18, 18, 74, 74); ctx.restore(); } // Helper to draw text with random colored characters (dot gets distinct color) function drawRainbowText(text, startX, y, centered = false) { ctx.save(); ctx.font = 'bold 36px "YWFTProcessing-Regular", sans-serif'; ctx.textBaseline = 'middle'; ctx.shadowBlur = 3; // Measure total width for centering let totalWidth = 0; for (let i = 0; i < text.length; i++) { totalWidth += ctx.measureText(text[i]).width; } let currentX = centered ? startX - totalWidth / 2 : startX; for (let i = 0; i < text.length; i++) { const char = text[i]; let color; if (char === '.') { // Dot gets cyan - distinct from other chars color = 'rgb(78, 205, 196)'; } else { color = colors[Math.floor(Math.random() * colors.length)]; } ctx.fillStyle = color; ctx.shadowColor = color; ctx.fillText(char, currentX, y); currentX += ctx.measureText(char).width; } ctx.restore(); } // Text centered under logo (moved up) const line1Y = H - 90; const line2Y = H - 48; // First line: "I gave " + "Aesthetic.Computer" rainbow - centered ctx.save(); ctx.font = 'bold 36px "YWFTProcessing-Regular", sans-serif'; ctx.textBaseline = 'middle'; ctx.textAlign = 'left'; // Calculate total width for centering const iGaveText = 'I gave '; const acText = 'Aesthetic.Computer'; const iGaveWidth = ctx.measureText(iGaveText).width; const acWidth = ctx.measureText(acText).width; const totalLine1 = iGaveWidth + acWidth; const line1Start = (W - totalLine1) / 2; // "I gave " in pink ctx.fillStyle = 'rgb(255, 180, 200)'; ctx.shadowColor = 'rgba(255, 180, 200, 0.5)'; ctx.shadowBlur = 8; ctx.fillText(iGaveText, line1Start, line1Y); ctx.restore(); // "Aesthetic.Computer" in rainbow (dot is cyan) drawRainbowText(acText, line1Start + iGaveWidth, line1Y, false); // Second line: price in GREEN, rest in pink - centered ctx.save(); ctx.font = 'bold 36px "YWFTProcessing-Regular", sans-serif'; ctx.textBaseline = 'middle'; const onText = ' on ' + formattedDate; const priceWidth = ctx.measureText(formattedAmount).width; const onWidth = ctx.measureText(onText).width; const totalLine2 = priceWidth + onWidth; const line2Start = (W - totalLine2) / 2; // Price in green ctx.fillStyle = 'rgb(107, 203, 119)'; ctx.shadowColor = 'rgba(107, 203, 119, 0.5)'; ctx.shadowBlur = 8; ctx.fillText(formattedAmount, line2Start, line2Y); // " on [date]" in pink ctx.fillStyle = 'rgb(255, 180, 200)'; ctx.shadowColor = 'rgba(255, 180, 200, 0.5)'; ctx.fillText(onText, line2Start + priceWidth, line2Y); ctx.restore(); } // Wait for images to load then draw once function tryDraw() { if (palsLoaded && qrImg.complete) { drawStaticImage(); } else { setTimeout(tryDraw, 100); } } tryDraw(); // Share functionality using Web Share API const shareBtn = document.getElementById('shareBtn'); const shareBtnDa = document.getElementById('shareBtnDa'); const shareBtnDe = document.getElementById('shareBtnDe'); const shareBtnEs = document.getElementById('shareBtnEs'); const shareBtnZh = document.getElementById('shareBtnZh'); async function shareImage() { // Capture canvas as PNG const dataUrl = canvas.toDataURL('image/png'); const blob = await (await fetch(dataUrl)).blob(); const file = new File([blob], 'i-gave-aesthetic-computer.png', { type: 'image/png' }); // Translated share text const shareTexts = { en: { title: 'I gave to Aesthetic.Computer!', text: `I gave ${formattedAmount} to Aesthetic.Computer on ${formattedDate}! 💖` }, da: { title: 'Jeg gav til Aesthetic.Computer!', text: `Jeg gav ${formattedAmount} til Aesthetic.Computer d. ${formattedDate}! 💖` }, de: { title: 'Ich habe Aesthetic.Computer unterstützt!', text: `Ich habe ${formattedAmount} an Aesthetic.Computer am ${formattedDate} gegeben! 💖` }, es: { title: '¡Le di a Aesthetic.Computer!', text: `¡Le di ${formattedAmount} a Aesthetic.Computer el ${formattedDate}! 💖` }, zh: { title: '我给了 Aesthetic.Computer!', text: `我在 ${formattedDate} 给了 Aesthetic.Computer ${formattedAmount}!💖` } }; const st = shareTexts[currentLang] || shareTexts.en; // Try Web Share API (great on mobile!) if (navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) { try { await navigator.share({ title: st.title, text: st.text, files: [file], url: 'https://give.aesthetic.computer' }); return; } catch (e) { if (e.name !== 'AbortError') console.log('Share cancelled or failed:', e); return; // User cancelled, don't fall through } } // Fallback: just download the image const link = document.createElement('a'); link.download = 'i-gave-aesthetic-computer.png'; link.href = dataUrl; link.click(); } [shareBtn, shareBtnDa, shareBtnDe, shareBtnEs, shareBtnZh].forEach(btn => { if (btn) btn.addEventListener('click', shareImage); }); } }
// Load saved preferences or detect from browser const savedPrefs = getPrefs(); let currentLang = savedPrefs.lang; // URL params can override saved prefs (for shareable links like ?lang=da¤cy=dkk) // Also check pathname for /da, /de, /es, /zh routes (Netlify rewrites don't pass query params to browser) const langParam = urlParams.get('lang'); const currencyParam = urlParams.get('currency'); const pathLang = window.location.pathname.match(/^\/(da|de|es|zh)\/?$/)?.[1]; if (pathLang) { // Path-based language (e.g., /da) takes highest priority currentLang = pathLang; } else if (langParam && ['en', 'da', 'de', 'es', 'zh'].includes(langParam)) { currentLang = langParam; } else if (!currentLang) { currentLang = navigator.language.startsWith('da') ? 'da' : 'en'; } setLanguage(currentLang); updateLangUI(currentLang); // Restore saved currency or use URL param (pathLang implies currency too) if (pathLang === 'da') { setCurrency('dkk'); } else if (currencyParam && ['usd', 'dkk', 'crypto', 'paypal'].includes(currencyParam)) { setCurrency(currencyParam); } else if (savedPrefs.currency) { setCurrency(savedPrefs.currency); } else if (currentLang === 'da') { setCurrency('dkk'); } else { setCurrency('usd'); }
const SHOP_PANEL_ENABLED = false; let floatingGivesStarted = false;
// Server suspension state - set true only during real outages. const SERVER_SUSPENDED = false; const suspendedMessages = { en: { offline: 'Servers offline - DigitalOcean suspended', help: 'Your support helps bring them back', give: 'give.aesthetic.computer' }, da: { offline: 'Servere offline - DigitalOcean suspenderet', help: 'Din støtte hjælper med at bringe dem tilbage', give: 'give.aesthetic.computer' }, de: { offline: 'Server offline - DigitalOcean gesperrt', help: 'Ihre Unterstützung hilft sie zurückzubringen', give: 'give.aesthetic.computer' }, es: { offline: 'Servidores sin conexión - DigitalOcean suspendido', help: 'Tu apoyo ayuda a restaurarlos', give: 'give.aesthetic.computer' }, zh: { offline: '服务器离线 - DigitalOcean 已暂停', help: '您的支持有助于恢复服务', give: 'give.aesthetic.computer' }, }; function renderSuspended(container, style = 'card') { const msg = suspendedMessages[currentLang] || suspendedMessages.en; if (style === 'card') { container.innerHTML = ` <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;min-height:200px;gap:0.6em;text-align:center;padding:1.5em;"> <div style="font-size:32px;line-height:1;opacity:0.6;">⚡</div> <div style="color:var(--pink,#ff6b9d);font-size:13px;font-weight:bold;">${msg.offline}</div> <div style="color:#888;font-size:12px;">${msg.help}</div> <a href="https://give.aesthetic.computer" style="color:#ff6b9d;font-size:12px;margin-top:0.3em;">${msg.give} -></a> </div>`; } else if (style === 'inline') { container.innerHTML = ` <div style="color:#888;font-size:12px;padding:0.8em;"> <span style="color:var(--pink,#ff6b9d);">⚡ ${msg.offline}</span> - <a href="https://give.aesthetic.computer" style="color:#ff6b9d;">${msg.give}</a> </div>`; } }
function setLanguage(lang) { currentLang = lang; // Remove all language classes and add current one document.body.classList.remove('lang-da', 'lang-zh', 'lang-de', 'lang-es'); if (lang !== 'en') document.body.classList.add(`lang-${lang}`); // Reload ticker with translated messages loadRecentGives();
// Update fiat button text (skip crypto and paypal widgets) document.querySelectorAll('.gift-widget:not(.crypto):not(.paypal)').forEach(widget => { const currency = widget.dataset.currency; const slider = widget.querySelector('input[type="range"]'); if (!slider) return; const value = parseInt(slider.value); const btn = widget.querySelector('.gift-btn'); const isMonthly = widget._isMonthly ? widget._isMonthly() : false; const fmtValue = value.toLocaleString('en-US'); if (currency === 'dkk') { // Use thin space ( ) to avoid YWFT font rendering regular space as + btn.innerHTML = lang === 'da' ? `Giv <span class="price">${fmtValue} kr</span>` : `Give <span class="price">${fmtValue} kr</span>`; } else if (isMonthly) { btn.innerHTML = lang === 'da' ? `Giv <span class="price">$${fmtValue}/md</span>` : `Give <span class="price">$${fmtValue}/mo</span>`; } else { btn.innerHTML = lang === 'da' ? `Giv <span class="price">$${fmtValue}</span>` : `Give <span class="price">$${fmtValue}</span>`; } }); // Update crypto send button text document.querySelectorAll('.gift-widget.crypto').forEach(widget => { const slider = widget.querySelector('.crypto-slider'); const sendBtn = widget.querySelector('.crypto-send'); if (slider && sendBtn) { const currency = slider.dataset.currency; const value = currency === 'eth' ? parseFloat(slider.value).toFixed(2) : parseInt(slider.value); const symbol = currency === 'eth' ? 'Ξ' : 'ꜩ'; sendBtn.textContent = lang === 'da' ? `Send ${value} ${symbol}` : `Send ${value} ${symbol}`; } }); }
// 🔗 Slider sync registry - keeps USD sliders in sync with each other const sliderRegistry = { usd: [], // Array of { slider, update, widget, monthlyCheck, monthlyLabel, setIsMonthly } dkk: [] }; function syncSliders(sourceCurrency, sourceSlider) { const value = parseInt(sourceSlider.value); const registry = sliderRegistry[sourceCurrency]; if (!registry) return; // Update all other sliders of the same currency registry.forEach(({ slider, update }) => { if (slider !== sourceSlider) { slider.value = value; update(); // Trigger visual update } }); } // Sync monthly checkbox state across all widgets of same currency function syncMonthly(sourceCurrency, sourceWidget, isMonthlyState) { const registry = sliderRegistry[sourceCurrency]; if (!registry) return; registry.forEach(({ widget, monthlyCheck, monthlyLabel, setIsMonthly, slider, update }) => { if (widget !== sourceWidget && monthlyCheck) { monthlyCheck.checked = isMonthlyState; monthlyLabel?.classList.toggle('checked', isMonthlyState); widget.classList.toggle('monthly-mode', isMonthlyState); // Adjust slider range const currency = widget.dataset.currency; if (isMonthlyState) { slider.min = 1; slider.max = currency === 'dkk' ? 3500 : 500; if (parseInt(slider.value) > parseInt(slider.max)) { slider.value = currency === 'dkk' ? 175 : 25; } } else { slider.min = 1; slider.max = currency === 'usd' ? 2048 : currency === 'dkk' ? 17500 : 500; } setIsMonthly(isMonthlyState); update(); } }); }
// Slider functionality document.querySelectorAll('.gift-widget:not(.crypto):not(.paypal)').forEach(widget => { const currency = widget.dataset.currency; const slider = widget.querySelector('input[type="range"]'); const amountEl = widget.querySelector('.gift-amount'); const btn = widget.querySelector('.gift-btn'); const monthlyCheck = widget.querySelector('.monthly-checkbox'); const monthlyLabel = widget.querySelector('.gift-monthly-check'); if (!slider || !amountEl || !btn) return; // Initialize isMonthly from A/B/C test state (for USD) or checkbox state let isMonthly = currency === 'usd' && typeof abcTestIsMonthly !== 'undefined' ? abcTestIsMonthly : (monthlyCheck?.checked || false);
// Monthly checkbox if (monthlyCheck) { monthlyCheck.addEventListener('change', async () => { // If checking monthly and not logged in, show native dialog if (monthlyCheck.checked && !acUser && !prefillEmail) { const dialog = document.getElementById('loginModal'); if (dialog) { // Show dialog dialog.showModal(); // Handle confirm (log in) const confirmBtn = document.getElementById('loginModalConfirm'); const cancelBtn = document.getElementById('loginModalCancel'); const handleConfirm = async () => { dialog.close(); cleanup(); // Keep checkbox checked - they want monthly // Scroll to login button and blink it const loginBtn = document.getElementById('footerLoginBtn'); if (loginBtn) { loginBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => { loginBtn.classList.add('blinking'); setTimeout(() => loginBtn.classList.remove('blinking'), 2000); }, 400); } }; const handleCancel = () => { dialog.close(); cleanup(); // Just close - do nothing else }; const cleanup = () => { confirmBtn?.removeEventListener('click', handleConfirm); cancelBtn?.removeEventListener('click', handleCancel); }; confirmBtn?.addEventListener('click', handleConfirm); cancelBtn?.addEventListener('click', handleCancel); // Don't uncheck - let them proceed with monthly even without login // The checkbox stays checked } } isMonthly = monthlyCheck.checked; monthlyLabel.classList.toggle('checked', isMonthly); widget.classList.toggle('monthly-mode', isMonthly); // Adjust slider range for monthly vs one-time if (isMonthly) { slider.min = currency === 'dkk' ? 1 : 1; slider.max = currency === 'dkk' ? 3500 : 500; slider.step = currency === 'dkk' ? 1 : 1; if (parseInt(slider.value) > parseInt(slider.max)) slider.value = currency === 'dkk' ? 175 : 25; } else { slider.min = currency === 'dkk' ? 1 : 1; slider.max = currency === 'usd' ? 2048 : currency === 'dkk' ? 17500 : 500; slider.step = currency === 'dkk' ? 1 : 1; } update(); // Sync monthly state to other widgets syncMonthly(currency, widget, isMonthly); }); }
function format(v) { // Use simple comma separator to avoid locale-specific characters // that may render incorrectly in YWFT font (e.g., narrow no-break space → +) const formatted = v.toLocaleString('en-US'); // For DKK, don't include space - we'll add spacing via CSS on the 'kr' suffix return currency === 'dkk' ? formatted + '|kr' : `$${formatted}`; }
const investSuggestion = widget.querySelector('.invest-suggestion'); const conversionEl = currency === 'dkk' ? document.getElementById('dkkConversion') : null; const logoEl = widget.querySelector('.gift-logo'); // Conversion rates (approximate) const USD_TO_DKK = 7.0; // ~7 DKK per USD const USD_TO_XTZ = 0.85; // ~$1.18 per XTZ const USD_TO_ETH = 0.00029; // ~$3400 per ETH const USD_TO_BTC = 0.0000105; // ~$95000 per BTC
// Character wiggle animation - only shake at intensity 4, colors change earlier let wiggleAnimId = null; function startWiggle(intensity) { if (wiggleAnimId) return; const t0 = performance.now(); // Only shake at intensity 4 const speed = intensity >= 4 ? 30 : 0; const amplitude = intensity >= 4 ? 3 : 0; function animate() { const t = (performance.now() - t0) / 1000; const chars = amountEl.querySelectorAll('.wiggle-char'); chars.forEach((char, i) => { const hue = ((t * 60 * intensity) + i * 40) % 360; // Only translate at intensity 4 if (intensity >= 4) { const x = Math.sin(t * speed + i * 2) * amplitude; const y = Math.cos(t * speed * 1.3 + i * 3) * amplitude; char.style.transform = `translate(${x}px, ${y}px)`; } else { char.style.transform = ''; } // Colors change at intensity 2+ if (intensity >= 2) { char.style.color = `hsl(${hue}, 100%, ${intensity >= 3 ? 75 : 60}%)`; } }); wiggleAnimId = requestAnimationFrame(animate); } animate(); } function stopWiggle() { if (wiggleAnimId) { cancelAnimationFrame(wiggleAnimId); wiggleAnimId = null; } const chars = amountEl.querySelectorAll('.wiggle-char'); chars.forEach(char => { char.style.transform = ''; char.style.color = ''; }); }
function update() { const value = parseInt(slider.value); const suffix = isMonthly ? '/mo' : ''; const text = format(value) + suffix; // Create wiggling characters // For DKK, '|' is a marker for the space before 'kr' - render as margin amountEl.innerHTML = text.split('').map(c => { if (c === '|') return '<span class="wiggle-char dkk-space"></span>'; if (c === ' ') return '<span class="wiggle-char"> </span>'; return `<span class="wiggle-char">${c}</span>`; }).join(''); const lang = document.body.classList.contains('lang-da') ? 'da' : 'en'; if (currency === 'dkk') { // Use innerHTML with thin space for DKK (YWFT renders regular space as +) const fmtVal = parseInt(slider.value).toLocaleString('en-US'); btn.innerHTML = lang === 'da' ? `Giv <span class="price">${fmtVal} kr</span>` : `Give <span class="price">${fmtVal} kr</span>`; } else if (isMonthly) { btn.innerHTML = lang === 'da' ? `Giv <span class="price">${format(value)}/md</span>` : `Give <span class="price">${format(value)}/mo</span>`; } else { btn.innerHTML = lang === 'da' ? `Giv <span class="price">${format(value)}</span>` : `Give <span class="price">${format(value)}</span>`; } // Show invest suggestion for USD at $1000+ if (investSuggestion && currency === 'usd' && !isMonthly) { investSuggestion.style.display = value >= 1000 ? 'block' : 'none'; } // Vegas intensity for USD (and DKK equivalent) - applies to both one-time and monthly if (currency === 'usd') { // Remove all intensity and effect classes amountEl.classList.remove('intensity-1', 'intensity-2', 'intensity-3', 'intensity-4', 'flames', 'lightning'); logoEl?.classList.remove('intensity-1', 'intensity-2', 'intensity-3', 'intensity-4'); slider.classList.remove('intensity-1', 'intensity-2', 'intensity-3', 'intensity-4'); conversionEl?.classList.remove('intensity-1', 'intensity-2', 'intensity-3', 'intensity-4'); // Remove power-of-2 color classes const pow2Classes = ['pow2-1', 'pow2-2', 'pow2-4', 'pow2-8', 'pow2-16', 'pow2-32', 'pow2-64', 'pow2-128', 'pow2-256', 'pow2-512', 'pow2-1024', 'pow2-2048']; pow2Classes.forEach(c => amountEl.classList.remove(c)); // Add appropriate intensity based on value (starts from $1) let intensity = 0; if (value >= 1024) { intensity = 4; } else if (value >= 256) { intensity = 3; } else if (value >= 64) { intensity = 2; } else if (value >= 1) { intensity = 1; } if (intensity > 0) { amountEl.classList.add(`intensity-${intensity}`); logoEl?.classList.add(`intensity-${intensity}`); slider.classList.add(`intensity-${intensity}`); conversionEl?.classList.add(`intensity-${intensity}`); startWiggle(intensity); } else { stopWiggle(); } // Add power-of-2 color class for exact powers of 2 const powersOf2 = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048]; if (powersOf2.includes(value)) { amountEl.classList.add(`pow2-${value}`); } // Special effects for max amounts if (isMonthly && value >= 500) { // Max monthly ($500/mo) - flames amountEl.classList.add('flames'); } else if (!isMonthly && value >= 2048) { // Max one-time ($2048) - flames + lightning amountEl.classList.add('flames', 'lightning'); } } else if (currency === 'dkk') { // Vegas intensity for DKK (convert to USD equivalent for thresholds) const usdEquiv = value / USD_TO_DKK; amountEl.classList.remove('intensity-1', 'intensity-2', 'intensity-3', 'intensity-4'); logoEl?.classList.remove('intensity-1', 'intensity-2', 'intensity-3', 'intensity-4'); slider.classList.remove('intensity-1', 'intensity-2', 'intensity-3', 'intensity-4'); conversionEl?.classList.remove('intensity-1', 'intensity-2', 'intensity-3', 'intensity-4'); let intensity = 0; if (usdEquiv >= 1024) { intensity = 4; } else if (usdEquiv >= 256) { intensity = 3; } else if (usdEquiv >= 64) { intensity = 2; } else if (usdEquiv >= 1) { intensity = 1; } if (intensity > 0) { amountEl.classList.add(`intensity-${intensity}`); logoEl?.classList.add(`intensity-${intensity}`); slider.classList.add(`intensity-${intensity}`); conversionEl?.classList.add(`intensity-${intensity}`); startWiggle(intensity); } else { stopWiggle(); } // Show currency conversions for DKK -> USD only if (conversionEl) { const usd = Math.round(usdEquiv); conversionEl.innerHTML = `<span>≈ $${usd}</span>`; } } }
slider.addEventListener('input', () => { update(); // Sync other sliders of the same currency syncSliders(currency, slider); }); update(); // Register this slider for syncing (include all needed refs for monthly sync) if (sliderRegistry[currency]) { sliderRegistry[currency].push({ slider, update, widget, monthlyCheck, monthlyLabel, setIsMonthly: (val) => { isMonthly = val; } }); } // Store isMonthly state on widget for button handler widget._isMonthly = () => isMonthly; });
// Invest link click handler - scroll and blink document.querySelectorAll('.invest-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const investSection = document.getElementById('invest'); if (investSection) { // Scroll to invest section investSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Add blinking animation after scroll completes setTimeout(() => { investSection.classList.add('blinking'); // Remove class after animation completes setTimeout(() => { investSection.classList.remove('blinking'); }, 1800); // 3 blinks × 0.6s }, 400); // Wait for scroll to start } }); });
// Cancel subscription link handler document.querySelectorAll('.cancel-subscription-link').forEach(link => { link.addEventListener('click', async (e) => { e.preventDefault(); const prompts = { en: { loginRequired: 'Please log in to manage your subscription.', loading: 'Loading...', error: 'Could not find a subscription for your account. Please try again or contact support.', errorGeneric: 'Something went wrong. Please try again.' }, da: { loginRequired: 'Log ind for at administrere dit abonnement.', loading: 'Indlæser...', error: 'Kunne ikke finde et abonnement for din konto. Prøv igen eller kontakt support.', errorGeneric: 'Noget gik galt. Prøv igen.' }, de: { loginRequired: 'Bitte melden Sie sich an, um Ihr Abonnement zu verwalten:', loading: 'Laden...', error: 'Kein Abonnement für Ihr Konto gefunden. Bitte versuchen Sie es erneut oder kontaktieren Sie den Support.', errorGeneric: 'Etwas ist schief gelaufen. Bitte versuchen Sie es erneut.' }, zh: { loginRequired: '请先登录以管理订阅。', loading: '加载中...', error: '找不到您账户的订阅。请重试或联系支持。', errorGeneric: '出了点问题。请重试。' }, es: { loginRequired: 'Inicia sesión para gestionar tu suscripción.', loading: 'Cargando...', error: 'No se encontró una suscripción para tu cuenta. Intenta de nuevo o contacta soporte.', errorGeneric: 'Algo salió mal. Intenta de nuevo.' } }; const t = prompts[currentLang] || prompts.en; const token = await getAuthToken(); if (!token || !acUser?.email) { alert(t.loginRequired); await footerLogin(); return; } link.style.pointerEvents = 'none'; const origText = link.innerHTML; link.textContent = t.loading; try { const res = await fetch('/api/give-portal', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ email: acUser.email }) }); const data = await res.json(); if (data.url) { window.location.href = data.url; } else { alert(data.error || t.error); link.innerHTML = origText; link.style.pointerEvents = ''; } } catch (err) { console.error('Portal error:', err); alert(t.errorGeneric); link.innerHTML = origText; link.style.pointerEvents = ''; } }); });
// Direct checkout (no modal) async function goToCheckout(amount, currency, isMonthly, btn) { const origText = btn.textContent; btn.disabled = true; btn.textContent = currentLang === 'da' ? 'Giver...' : 'Giving...';
try { const payload = { amount: amount * 100, currency, recurring: isMonthly }; // Include email if available (from URL prefill or logged-in user) const email = prefillEmail || acUser?.email; if (email) payload.email = email; const res = await fetch('https://aesthetic.computer/api/give', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if (data.url) window.location.href = data.url; else throw new Error(data.error || 'No URL returned'); } catch (err) { console.error('Give error:', err); btn.disabled = false; btn.textContent = origText; alert(currentLang === 'da' ? 'Noget gik galt' : 'Something went wrong'); } }
document.querySelectorAll('.gift-widget:not(.crypto):not(.paypal) .gift-btn').forEach(btn => { btn.onclick = () => { const currency = btn.dataset.currency; const widget = btn.closest('.gift-widget'); const slider = widget.querySelector('input[type="range"]'); const value = parseInt(slider.value); const isMonthly = widget._isMonthly ? widget._isMonthly() : false; goToCheckout(value, currency, isMonthly, btn); }; });
function copyAddress(el) { const text = el.dataset.copy; navigator.clipboard.writeText(text); el.classList.add('copied'); // Update hint text to "Copied!" const hints = el.querySelectorAll('.crypto-copy-hint'); hints.forEach(hint => { const origText = hint.textContent; hint.textContent = '✓ ' + (hint.dataset.lang === 'da' ? 'Kopieret!' : 'Copied!'); setTimeout(() => hint.textContent = origText, 1200); }); setTimeout(() => el.classList.remove('copied'), 1200); }
// Make crypto addresses/labels copyable document.querySelectorAll('.copyable').forEach(el => { el.addEventListener('click', () => copyAddress(el)); });
// Crypto prices and balances let cryptoPrices = { xtz: null, eth: null, btc: null }; let walletBalances = { xtz: null, eth: null, btc: null }; const WALLET_ADDRESSES = { btc: 'bc1q699gnqr92gvgv62nla82typpj3wls5a8xf59as', eth: '0x1D64E3eFa983D945cBFe29Ad5b3C8ABB53Aef023', xtz: 'tz1gkf8EexComFBJvjtT1zdsisdah791KwBE' }; async function fetchCryptoPrices() { try { const res = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=tezos,ethereum,bitcoin&vs_currencies=usd'); const data = await res.json(); cryptoPrices.xtz = data.tezos?.usd || null; cryptoPrices.eth = data.ethereum?.usd || null; cryptoPrices.btc = data.bitcoin?.usd || null; updateAllCryptoDisplays(); updateBalanceDisplays(); } catch (e) { console.log('Could not fetch crypto prices'); } } // Fetch Bitcoin balance via Blockstream API async function fetchBtcBalance() { try { const res = await fetch(`https://blockstream.info/api/address/${WALLET_ADDRESSES.btc}`); const data = await res.json(); // Balance is in satoshis, convert to BTC const funded = data.chain_stats?.funded_txo_sum || 0; const spent = data.chain_stats?.spent_txo_sum || 0; walletBalances.btc = (funded - spent) / 100000000; updateBalanceDisplays(); } catch (e) { console.log('Could not fetch BTC balance'); document.getElementById('balance-btc').innerHTML = '<span class="loading">—</span>'; } } // Fetch Ethereum balance via public RPC async function fetchEthBalance() { try { const res = await fetch('https://eth.llamarpc.com', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'eth_getBalance', params: [WALLET_ADDRESSES.eth, 'latest'], id: 1 }) }); const data = await res.json(); // Convert from wei (hex) to ETH walletBalances.eth = parseInt(data.result, 16) / 1e18; updateBalanceDisplays(); } catch (e) { console.log('Could not fetch ETH balance'); document.getElementById('balance-eth').innerHTML = '<span class="loading">—</span>'; } } // Fetch Tezos balance via TzKT API async function fetchXtzBalance() { try { const res = await fetch(`https://api.tzkt.io/v1/accounts/${WALLET_ADDRESSES.xtz}/balance`); const balance = await res.json(); // Balance is in mutez, convert to XTZ walletBalances.xtz = balance / 1000000; updateBalanceDisplays(); } catch (e) { console.log('Could not fetch XTZ balance'); document.getElementById('balance-xtz').innerHTML = '<span class="loading">—</span>'; } } function formatBalance(amount, decimals = 4) { if (amount === null || amount === undefined) return null; if (amount < 0.0001) return '< 0.0001'; return amount.toFixed(decimals).replace(/\.?0+$/, ''); } function updateBalanceDisplays() { // Bitcoin const btcEl = document.getElementById('balance-btc'); if (btcEl && walletBalances.btc !== null) { const btcFormatted = formatBalance(walletBalances.btc, 6); let html = `${btcFormatted} BTC`; if (cryptoPrices.btc) { const usdValue = (walletBalances.btc * cryptoPrices.btc).toFixed(2); html += ` <span class="usd-equiv">≈ $${usdValue}</span>`; } btcEl.innerHTML = html; } // Ethereum const ethEl = document.getElementById('balance-eth'); if (ethEl && walletBalances.eth !== null) { const ethFormatted = formatBalance(walletBalances.eth, 4); let html = `${ethFormatted} ETH`; if (cryptoPrices.eth) { const usdValue = (walletBalances.eth * cryptoPrices.eth).toFixed(2); html += ` <span class="usd-equiv">≈ $${usdValue}</span>`; } ethEl.innerHTML = html; } // Tezos const xtzEl = document.getElementById('balance-xtz'); if (xtzEl && walletBalances.xtz !== null) { const xtzFormatted = formatBalance(walletBalances.xtz, 2); let html = `${xtzFormatted} XTZ`; if (cryptoPrices.xtz) { const usdValue = (walletBalances.xtz * cryptoPrices.xtz).toFixed(2); html += ` <span class="usd-equiv">≈ $${usdValue}</span>`; } xtzEl.innerHTML = html; } } function updateAllCryptoDisplays() { document.querySelectorAll('.crypto-slider').forEach(slider => { const currency = slider.dataset.currency; const widget = slider.closest('.gift-widget'); updateCryptoDisplay(widget, slider, currency); }); } function updateCryptoDisplay(widget, slider, currency) { const amountEl = widget.querySelector('.gift-amount'); const sendBtn = widget.querySelector('.crypto-send'); const usdEl = widget.querySelector('.crypto-usd'); const value = currency === 'eth' ? parseFloat(slider.value).toFixed(2) : parseInt(slider.value); const symbol = currency === 'eth' ? 'Ξ' : 'ꜩ'; const emoji = currency === 'eth' ? '💎' : '🔮'; amountEl.textContent = `${emoji} ${value} ${symbol}`; sendBtn.textContent = `Send ${value} ${symbol}`; const price = cryptoPrices[currency]; if (price && usdEl) { const usdValue = (parseFloat(value) * price).toFixed(2); usdEl.textContent = `≈ $${usdValue}`; } }
// Crypto slider functionality document.querySelectorAll('.crypto-slider').forEach(slider => { const widget = slider.closest('.gift-widget'); const currency = slider.dataset.currency; slider.addEventListener('input', () => updateCryptoDisplay(widget, slider, currency)); updateCryptoDisplay(widget, slider, currency); }); // Fetch prices and balances on load fetchCryptoPrices(); fetchBtcBalance(); fetchEthBalance(); fetchXtzBalance();
// Tezos wallet connect (Beacon) async function sendTezos(amount) { const btn = document.querySelector('[data-currency="xtz"].crypto-send'); const origText = btn.textContent; try { btn.textContent = currentLang === 'da' ? 'Forbinder...' : 'Connecting...'; btn.disabled = true; // Use Beacon SDK via script tag approach if (!window.beacon) { // Load Beacon SDK await new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://unpkg.com/@airgap/beacon-sdk@4.0.12/dist/walletbeacon.min.js'; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } const client = new window.beacon.DAppClient({ name: 'Aesthetic Computer Gift' }); btn.textContent = currentLang === 'da' ? 'Vælg tegnebog...' : 'Select wallet...'; await client.requestPermissions(); btn.textContent = currentLang === 'da' ? 'Bekræft i tegnebog...' : 'Confirm in wallet...'; const response = await client.requestOperation({ operationDetails: [{ kind: 'transaction', destination: 'tz1WqvhbfEwfqAzMtEGxc7x6tpSqRMCNKPCT', amount: String(Math.floor(amount * 1000000)), // mutez fee: '1500', // suggested fee in mutez (~0.0015 XTZ) gasLimit: '10600', // typical gas for simple transfer storageLimit: '300' // typical storage for simple transfer }] }); btn.textContent = currentLang === 'da' ? '✓ Sendt!' : '✓ Sent!'; console.log('TX:', response.transactionHash); setTimeout(() => { btn.textContent = origText; btn.disabled = false; }, 3000); } catch (err) { console.error('Tezos error:', err); btn.textContent = err.message?.includes('Aborted') ? (currentLang === 'da' ? 'Annulleret' : 'Cancelled') : (currentLang === 'da' ? 'Fejl - prøv igen' : 'Error - try again'); setTimeout(() => { btn.textContent = origText; btn.disabled = false; }, 2000); } }
// Ethereum wallet connect async function sendEthereum(amount) { const btn = document.querySelector('[data-currency="eth"].crypto-send'); const origText = btn.textContent; try { btn.textContent = currentLang === 'da' ? 'Forbinder...' : 'Connecting...'; btn.disabled = true; if (!window.ethereum) { alert(currentLang === 'da' ? 'Installér MetaMask eller en anden Ethereum-tegnebog' : 'Please install MetaMask or another Ethereum wallet'); throw new Error('No wallet'); } btn.textContent = currentLang === 'da' ? 'Godkend forbindelse...' : 'Approve connection...'; const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); if (!accounts.length) throw new Error('No account'); const amountWei = '0x' + (BigInt(Math.floor(amount * 1e18))).toString(16); btn.textContent = currentLang === 'da' ? 'Bekræft i tegnebog...' : 'Confirm in wallet...'; const txHash = await window.ethereum.request({ method: 'eth_sendTransaction', params: [{ from: accounts[0], to: '0x1D64E3eFa983D945cBFe29Ad5b3C8ABB53Aef023', value: amountWei }] }); btn.textContent = currentLang === 'da' ? '✓ Sendt!' : '✓ Sent!'; console.log('TX:', txHash); setTimeout(() => { btn.textContent = origText; btn.disabled = false; }, 3000); } catch (err) { console.error('Ethereum error:', err); if (err.message !== 'No wallet') { btn.textContent = currentLang === 'da' ? 'Annulleret' : 'Cancelled'; } setTimeout(() => { btn.textContent = origText; btn.disabled = false; }, 2000); } }
// Wire up crypto send buttons document.querySelectorAll('.crypto-send').forEach(btn => { btn.addEventListener('click', () => { const widget = btn.closest('.gift-widget'); const slider = widget.querySelector('.crypto-slider'); const currency = slider.dataset.currency; const value = currency === 'eth' ? parseFloat(slider.value) : parseInt(slider.value); if (currency === 'xtz') { sendTezos(value); } else if (currency === 'eth') { sendEthereum(value); } }); });
// Live reload in dev mode if (location.hostname === 'localhost' || location.hostname.includes('local')) { const wsUrl = location.protocol === 'https:' ? 'wss://localhost:8889' : 'ws://localhost:8889'; function connectWS() { const ws = new WebSocket(wsUrl); ws.onopen = () => console.log('🔄 Live reload connected'); ws.onmessage = (e) => { try { const msg = JSON.parse(e.data); if (msg.type === 'reload' && msg.content?.piece === '*refresh*') { console.log('🔄 Reloading...'); location.reload(); } } catch {} }; ws.onclose = () => setTimeout(connectWS, 2000); ws.onerror = () => ws.close(); } connectWS(); }
// 🎬 Synchronized Panel Controller // All rotating panels wait until everything is preloaded, then start together const SYNC_INTERVAL = 8000; // 8 seconds - all panels use this const panelSync = { ready: { shop: false, kidlisp: false, chat: false, apps: false }, started: false, startersRun: false, // Track if starters have been executed starters: [], markReady(panel) { this.ready[panel] = true; this.checkAllReady(); }, registerStarter(fn) { this.starters.push(fn); // Only run immediately if starters have ALREADY been executed if (this.startersRun) fn(); }, checkAllReady() { if (this.started) return; const allReady = Object.values(this.ready).every(v => v); if (allReady) { this.started = true; // Small delay to ensure UI is settled, then start all at once setTimeout(() => { this.startersRun = true; this.starters.forEach(fn => fn()); }, 500); } } };
if (!SHOP_PANEL_ENABLED) { document.getElementById('shopSection')?.setAttribute('hidden', ''); panelSync.markReady('shop'); }
// Fetch shop products - rotating display with image cycling let shopProducts = []; let shopIndex = 0; let shopImageIndex = 0; let shopProgressAnim = null; let shopImageInterval = null; let shopLoadAttempts = 0; let shopLoaded = false; const SHOP_IMAGE_INTERVAL = 2000; // 2 seconds between images within same product const SHOP_PRODUCT_INTERVAL = 8000; // 8 seconds (synced with other panels) const SHOP_MAX_RETRIES = 5; const SHOP_RETRY_DELAYS = [500, 1000, 2000, 4000, 8000]; // Exponential backoff function animateShopProgress(duration) { const progressBar = document.getElementById('shopAutoProgress'); if (!progressBar) return; let startTime = null; function step(timestamp) { if (!startTime) startTime = timestamp; const elapsed = timestamp - startTime; const pct = Math.min((elapsed / duration) * 100, 100); progressBar.style.width = pct + '%'; if (elapsed < duration) { shopProgressAnim = requestAnimationFrame(step); } } if (shopProgressAnim) cancelAnimationFrame(shopProgressAnim); progressBar.style.width = '0%'; shopProgressAnim = requestAnimationFrame(step); }
function renderShopMaintenance(container, detail = '') { if (!SHOP_PANEL_ENABLED) return; if (SERVER_SUSPENDED) { renderSuspended(container, 'card'); return; } container.innerHTML = ` <div style="display:flex;align-items:center;justify-content:center;height:100%;min-height:300px;"> <div style="text-align:center;color:#888;"> <div style="font-size:120px;line-height:1;color:#ff4444;">✕</div> <div style="font-size:14px;margin-top:8px;">closed for maintenance</div> </div> </div> `; } async function loadShopProducts(isRetry = false) { if (!SHOP_PANEL_ENABLED) return; if (shopLoaded) return; // Already loaded successfully const container = document.getElementById('shopProducts'); shopLoadAttempts++; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout const res = await fetch('https://aesthetic.computer/api/shop', { signal: controller.signal }); clearTimeout(timeoutId);
if (!res.ok) { const retryAfterHeader = res.headers?.get?.('Retry-After'); const retryAfterSeconds = retryAfterHeader ? parseInt(retryAfterHeader, 10) : null; const err = new Error('Shop API HTTP ' + res.status); err.status = res.status; err.retryAfterMs = Number.isFinite(retryAfterSeconds) ? retryAfterSeconds * 1000 : null; throw err; }
const data = await res.json(); if (!data.products?.length) { renderShopMaintenance(container); panelSync.markReady('shop'); return; } shopProducts = data.products; shopLoaded = true; showShopProduct(0, true); // Initial load with isInitial=true // Mark ready and register starter (don't start cycling yet) panelSync.markReady('shop'); panelSync.registerStarter(() => startShopCycle()); } catch (e) { // Shop unavailable - show closed message renderShopMaintenance(container); panelSync.markReady('shop'); } } // Also retry when shop section becomes visible (for mobile where it may be off-screen initially) function setupShopVisibilityRetry() { if (!SHOP_PANEL_ENABLED) return; const shopSection = document.querySelector('.shop-section'); if (!shopSection || shopLoaded) return; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting && !shopLoaded && shopLoadAttempts > 0) { // Section became visible and we haven't loaded yet - retry shopLoadAttempts = 0; // Reset attempts for visibility-triggered retry loadShopProducts(true); } }); }, { threshold: 0.1 }); observer.observe(shopSection); } function startShopCycle() { if (!SHOP_PANEL_ENABLED) return; // Clear any existing intervals if (shopImageInterval) clearInterval(shopImageInterval); const p = shopProducts[shopIndex]; if (!p) return; const images = p.images || [p.imageUrl].filter(Boolean); shopImageIndex = 0; if (images.length > 1) { // Multiple images: cycle through them, then advance product const totalDuration = images.length * SHOP_IMAGE_INTERVAL; animateShopProgress(totalDuration); shopImageInterval = setInterval(() => { shopImageIndex++; if (shopImageIndex >= images.length) { // All images shown, advance to next product clearInterval(shopImageInterval); shopIndex = (shopIndex + 1) % shopProducts.length; shopImageIndex = 0; showShopProduct(shopIndex); startShopCycle(); } else { // Show next image of same product updateShopImage(shopImageIndex); } }, SHOP_IMAGE_INTERVAL); } else { // Single image: just use standard interval animateShopProgress(SHOP_PRODUCT_INTERVAL); shopImageInterval = setInterval(() => { clearInterval(shopImageInterval); shopIndex = (shopIndex + 1) % shopProducts.length; shopImageIndex = 0; showShopProduct(shopIndex); startShopCycle(); }, SHOP_PRODUCT_INTERVAL); } } function updateShopImage(imgIndex) { const p = shopProducts[shopIndex]; if (!p) return; const images = p.images || [p.imageUrl].filter(Boolean); if (!images.length) return; // Find the currently visible layer's image wrap const visibleLayer = document.querySelector('.shop-product-layer.visible'); const wrap = visibleLayer ? visibleLayer.querySelector('.shop-product-img-wrap') : document.querySelector('.shop-product-img-wrap'); if (!wrap) return; const currentImg = wrap.querySelector('img'); if (!currentImg) return; // Preload the new image const newImg = new Image(); newImg.src = images[imgIndex]; newImg.alt = currentImg.alt; newImg.className = 'shop-img-front'; newImg.style.opacity = '0'; newImg.onload = () => { // Add new image on top wrap.insertBefore(newImg, wrap.firstChild); // Trigger reflow then fade in newImg.offsetHeight; newImg.style.opacity = '1'; // Remove old image after transition setTimeout(() => { if (currentImg && currentImg.parentNode) { currentImg.remove(); } }, 450); }; } function createProductHTML(p) { // Get all images const images = p.images || [p.imageUrl].filter(Boolean); const currentImage = images.length > 0 ? images[0] : ''; // Extract vendor from product data if available const vendor = p.vendor || ''; const vendorHtml = vendor ? `<span class="shop-product-vendor" style="color:#ff6b9d">${vendor.startsWith('@') ? vendor : '@' + vendor}</span>` : ''; // Process description - preserve blank lines as half-height spacers, wrap in inner div for scroll animation const desc = (p.description || '').trim(); const descContent = escapeHtml(desc).replace(/\n\n+/g, '<br><span style="display:block;height:0.4em"></span>').replace(/\n/g, '<br>'); const descHtml = desc ? `<div class="shop-product-desc"><div class="shop-product-desc-inner">${descContent}</div></div>` : ''; // Strip currency code from price (keep just symbol + number) const priceDisplay = (p.price || '').replace(/\s*USD$/i, '').replace(/\s*CAD$/i, '').replace(/\s*EUR$/i, '').trim(); return ` <a href="${p.shopUrl}" class="shop-product" target="_blank"> <div class="shop-product-img-wrap"> <img src="${currentImage}" alt="${escapeHtml(p.title)}" loading="lazy" class="shop-img-front"> </div> <div class="shop-product-overlay"> <span class="shop-product-title-row"> <span class="shop-product-title">${escapeHtml(p.title)}</span> ${vendorHtml} </span> ${descHtml} </div> ${p.available ? `<div class="shop-product-price">${priceDisplay}</div>` : `<div class="shop-product-sold">SOLD</div>` } </a> `; } let shopCurrentLayer = 'a'; // Track which layer is currently visible function showShopProduct(index, isInitial = false) { const container = document.getElementById('shopProducts'); const p = shopProducts[index]; if (!p) return; const productHTML = createProductHTML(p); // Initial load - set up dual layer structure if (isInitial || !container.querySelector('.shop-product-layer')) { container.innerHTML = ` <div class="shop-product-layer layer-back visible" data-layer="a">${productHTML}</div> <div class="shop-product-layer layer-front entering" data-layer="b"></div> `; shopCurrentLayer = 'a'; // Preload next product image preloadNextShopProduct(index); return; } // Crossfade transition const layerA = container.querySelector('[data-layer="a"]'); const layerB = container.querySelector('[data-layer="b"]'); if (shopCurrentLayer === 'a') { // A is visible, prepare B and fade it in layerB.innerHTML = productHTML; layerB.classList.remove('entering', 'exiting'); layerB.classList.add('layer-front'); layerA.classList.remove('layer-front'); layerA.classList.add('layer-back'); // Trigger reflow layerB.offsetHeight; // Start transition layerB.classList.add('visible'); layerA.classList.remove('visible'); layerA.classList.add('exiting'); shopCurrentLayer = 'b'; } else { // B is visible, prepare A and fade it in layerA.innerHTML = productHTML; layerA.classList.remove('entering', 'exiting'); layerA.classList.add('layer-front'); layerB.classList.remove('layer-front'); layerB.classList.add('layer-back'); // Trigger reflow layerA.offsetHeight; // Start transition layerA.classList.add('visible'); layerB.classList.remove('visible'); layerB.classList.add('exiting'); shopCurrentLayer = 'a'; } // Preload next product image preloadNextShopProduct(index); } function preloadNextShopProduct(currentIndex) { const nextIndex = (currentIndex + 1) % shopProducts.length; const nextProduct = shopProducts[nextIndex]; if (!nextProduct) return; const images = nextProduct.images || [nextProduct.imageUrl].filter(Boolean); if (images.length > 0) { const preloadImg = new Image(); preloadImg.src = images[0]; } } if (SHOP_PANEL_ENABLED) { setTimeout(loadShopProducts, 100); setTimeout(setupShopVisibilityRetry, 200); // Set up visibility observer }
// Load stats from metrics API async function loadStats() { try { const res = await fetch('https://aesthetic.computer/api/metrics'); const data = await res.json(); const fmt = n => n?.toLocaleString() || '—'; // Update stat values const handlesEl = document.querySelector('#stat-handles .stat-value'); const paintingsEl = document.querySelector('#stat-paintings .stat-value'); const moodsEl = document.querySelector('#stat-moods .stat-value'); const commandsEl = document.querySelector('#stat-commands .stat-value'); const messagesEl = document.querySelector('#stat-messages .stat-value'); if (handlesEl) handlesEl.textContent = fmt(data.handles); if (paintingsEl) paintingsEl.textContent = fmt(data.paintings); if (moodsEl) moodsEl.textContent = fmt(data.moods); // Commands = pieces + prompts (from docs) const commandCount = (data.pieces || 0) + 107; // 107 prompts from docs.js if (commandsEl) commandsEl.textContent = fmt(commandCount); // Messages = chatMessages (laer-klokken + chat combined) if (messagesEl) messagesEl.textContent = fmt(data.chatMessages); // Store raw values for sorting (kidlisp loaded separately) window._statValues = { 'stat-handles': data.handles || 0, 'stat-paintings': data.paintings || 0, 'stat-moods': data.moods || 0, 'stat-commands': commandCount, 'stat-messages': data.chatMessages || 0 }; sortStatBoxes(); } catch (e) { console.warn('Could not load stats:', e); if (SERVER_SUSPENDED) { // Show offline indicator on stat values document.querySelectorAll('.stat-value').forEach(el => { if (el.textContent === '—' || el.textContent === '...') { el.textContent = '—'; el.style.opacity = '0.4'; } }); } } } // Sort stat boxes by value (highest to lowest) function sortStatBoxes() { const section = document.querySelector('.stats-section'); if (!section || !window._statValues) return; const items = Array.from(section.querySelectorAll('.stat-item')); items.sort((a, b) => { const valA = window._statValues[a.id] || 0; const valB = window._statValues[b.id] || 0; return valB - valA; // Descending (highest first) }); // Reorder DOM elements items.forEach(item => section.appendChild(item)); } setTimeout(loadStats, 200);
// KidLisp count - loaded after apiBase is defined (see below)
// Format detailed timestamp for gives // Returns { time: string, useOn: boolean } - useOn=true for date-based times function formatGiveTime(timestamp) { if (!timestamp || typeof timestamp !== 'number') return { time: '', useOn: false }; const date = new Date(timestamp); const now = new Date(); const isToday = date.toDateString() === now.toDateString(); const isYesterday = new Date(now - 86400000).toDateString() === date.toDateString(); // Localized time format const locales = { en: 'en-US', da: 'da-DK', de: 'de-DE', es: 'es-ES', zh: 'zh-CN' }; const locale = locales[currentLang] || 'en-US'; const time = date.toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit', hour12: currentLang === 'en' || currentLang === 'es' }).toLowerCase(); // Translated "today at" and "yesterday at" const timeWords = { en: { today: 'today at', yesterday: 'yesterday at', at: 'at' }, da: { today: 'i dag kl.', yesterday: 'i går kl.', at: 'kl.' }, de: { today: 'heute um', yesterday: 'gestern um', at: 'um' }, es: { today: 'hoy a las', yesterday: 'ayer a las', at: 'a las' }, zh: { today: '今天', yesterday: '昨天', at: '' } }; const tw = timeWords[currentLang] || timeWords.en; if (isToday) return { time: `${tw.today} ${time}`, useOn: false }; if (isYesterday) return { time: `${tw.yesterday} ${time}`, useOn: false }; const diff = now - date; const days = Math.floor(diff / 86400000); if (days < 7) { const dayName = date.toLocaleDateString(locale, { weekday: 'short' }); return { time: tw.at ? `${dayName} ${tw.at} ${time}` : `${dayName} ${time}`, useOn: true }; } const dateStr = date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined }); return { time: dateStr, useOn: true }; }
// Gives - load from Stripe via API and display as horizontal ticker async function loadRecentGives() { const ticker = document.getElementById('givesTicker'); // Animated server cost number (static $800 with color animation) function getAnimatedCost() { return `<span class="server-cost-number" style="color: #ff6b9d; font-weight: bold;">$800</span>`; } // Translated ticker messages const tickerMessages = { en: { serverCost: `💸 Server bills to keep AC running cost upwards of <strong>${getAnimatedCost()}/month</strong>.`, cta: 'Paying what you want for AC keeps services online and free to use and helps <a href="https://aesthetic.computer/@jeffrey" class="pink-handle">@jeffrey</a> develop new features.', got: 'Got' }, da: { serverCost: `💸 Serverregninger for at holde AC kørende koster op mod <strong>${getAnimatedCost()} kr/måned</strong>.`, cta: 'Betal hvad du vil for AC og hold tjenester online og gratis og hjælp <a href="https://aesthetic.computer/@jeffrey" class="pink-handle">@jeffrey</a> med at udvikle nye funktioner.', got: 'Modtog' }, de: { serverCost: `💸 Serverkosten für AC laufen auf über <strong>${getAnimatedCost()}$/Monat</strong> hinaus.`, cta: 'Zahle was du willst für AC, halte die Dienste online und kostenlos und hilf <a href="https://aesthetic.computer/@jeffrey" class="pink-handle">@jeffrey</a> neue Funktionen zu entwickeln.', got: 'Erhalten' }, es: { serverCost: `💸 Las facturas del servidor para mantener AC funcionando cuestan más de <strong>${getAnimatedCost()}/mes</strong>.`, cta: 'Paga lo que quieras por AC para mantener los servicios en línea y gratuitos, y ayudar a <a href="https://aesthetic.computer/@jeffrey" class="pink-handle">@jeffrey</a> a desarrollar nuevas funciones.', got: 'Recibido' }, zh: { serverCost: `💸 维持 AC 运行的服务器费用高达 <strong>每月${getAnimatedCost()}美元</strong>。`, cta: '为 AC 支付你想要的金额,保持服务在线且免费,并帮助 <a href="https://aesthetic.computer/@jeffrey" class="pink-handle">@jeffrey</a> 开发新功能。', got: '收到' } }; const msgs = tickerMessages[currentLang] || tickerMessages.en; let serverCostItem = `<span class="gives-ticker-item gives-ticker-cta-item">${msgs.serverCost}</span>`; const ctaItem = `<span class="gives-ticker-item gives-ticker-cta-item">${msgs.cta}</span>`; try { // Use local function endpoint in dev, production URL otherwise const isDev = window.location.hostname === 'localhost' || window.location.hostname.includes('local.'); const givesUrl = isDev ? 'https://localhost:8888/.netlify/functions/gives' : 'https://aesthetic.computer/api/gives'; const res = await fetch(givesUrl); const data = await res.json(); // Update stats badges (subscribers shown in badge, not ticker) if (data.activeSubscribers > 0) { // No longer add to ticker - shown in monthly badge instead } // Update stats badges on canvas const givesStatsEl = document.getElementById('giveStatsGives'); const subsStatsEl = document.getElementById('giveStatsSubs'); if (givesStatsEl && data.monthlyCount > 0) { // Get current month name (short form) const monthNames = { en: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], da: ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'], de: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'], es: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'], zh: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], }; const month = new Date().getMonth(); const monthName = (monthNames[currentLang] || monthNames.en)[month]; // Format: "Got $X so far from N gives this Jan." const gotLabels = { en: 'Got', da: 'Fik', de: 'Erhielt', es: 'Recibido', zh: '收到' }; const soFarLabels = { en: 'so far from', da: 'indtil nu fra', de: 'bisher von', es: 'hasta ahora de', zh: '至今来自' }; const givesLabels = { en: 'gives this', da: 'gaver i', de: 'Gaben im', es: 'donaciones en', zh: '捐赠于' }; const valueEl = givesStatsEl.querySelector('.ticker-stat-value'); const labelEl = givesStatsEl.querySelector('.ticker-stat-label'); const amountEl = givesStatsEl.querySelector('.ticker-stat-amount'); // Build the full text: "Got $X so far from N gives this Jan." const got = gotLabels[currentLang] || gotLabels.en; const soFar = soFarLabels[currentLang] || soFarLabels.en; const givesText = givesLabels[currentLang] || givesLabels.en; // Hide the old value/label elements, use amount for full text if (valueEl) valueEl.style.display = 'none'; if (labelEl) labelEl.style.display = 'none'; if (amountEl && data.monthlyTotalUSD > 0) { amountEl.innerHTML = `<span class="got">${got}</span> <span class="amount-value">$${data.monthlyTotalUSD}</span> <span class="so-far">${soFar} <span class="count-value">${data.monthlyCount}</span> ${givesText} ${monthName}.</span>`; } givesStatsEl.classList.add('visible'); } if (subsStatsEl && data.activeSubscribers > 0) { const subLabels = { en: 'monthly givers', da: 'månedlige givere', de: 'monatliche Geber', es: 'donantes mensuales', zh: '月度捐赠者' }; const valueEl = subsStatsEl.querySelector('.ticker-stat-value'); const labelEl = subsStatsEl.querySelector('.ticker-stat-label'); if (valueEl) valueEl.textContent = data.activeSubscribers; if (labelEl) labelEl.textContent = subLabels[currentLang] || subLabels.en; subsStatsEl.classList.add('visible'); } if (!data.gives?.length) { // Show CTA messages scrolling in the ticker when no gives ticker.innerHTML = `<div class="gives-ticker-track">${serverCostItem}${ctaItem}${serverCostItem}${ctaItem}${serverCostItem}${ctaItem}</div>`; // Adjust speed for CTA-only ticker const track = ticker.querySelector('.gives-ticker-track'); if (track) { const contentWidth = track.scrollWidth / 3; const speed = Math.max(25, contentWidth / 30); track.style.animationDuration = `${speed}s`; } return; } givesData = data.gives; // Translated "on" preposition for dates const onPrep = { en: 'on', da: 'd.', de: 'am', es: 'el', zh: '' }; const onText = onPrep[currentLang] || onPrep.en; // Build horizontal ticker items - "Got $5 today" or "Got $5 on Dec 15" format const giveItems = givesData.map(give => { const { time: timeStr, useOn } = formatGiveTime(give.createdAt); const hasNote = give.note && give.note.trim(); const noteHtml = hasNote ? `<span class="sep">:</span><span class="note">${give.note}</span>` : ''; const timeHtml = useOn ? `<span class="time">${onText ? onText + ' ' : ''}${timeStr}</span>` : `<span class="time">${timeStr}</span>`; // Monthly donations show with /mo suffix and yellow styling if (give.isRecurring) { return `<span class="gives-ticker-item monthly"><span class="got">${msgs.got}</span><span class="amount">${give.amountDisplay}/mo</span>${timeHtml}${noteHtml}</span>`; } return `<span class="gives-ticker-item"><span class="got">${msgs.got}</span><span class="amount">${give.amountDisplay}</span>${timeHtml}${noteHtml}</span>`; }); // Intersperse give items with server messages for better readability // Pattern: serverCost, 2-3 gives, cta, 2-3 gives, repeat const interspersedItems = []; const chunkSize = Math.max(2, Math.ceil(giveItems.length / 4)); // Split gives into ~4 groups for (let i = 0; i < giveItems.length; i++) { if (i === 0) interspersedItems.push(serverCostItem); interspersedItems.push(giveItems[i]); // After every chunk, add a server message if ((i + 1) % chunkSize === 0 && i < giveItems.length - 1) { interspersedItems.push(i % (chunkSize * 2) === chunkSize - 1 ? ctaItem : serverCostItem); } } // End with CTA if we didn't just add one if (giveItems.length > 0) interspersedItems.push(ctaItem); const items = interspersedItems.join(''); // Duplicate content for seamless scroll (tripled) ticker.innerHTML = `<div class="gives-ticker-track">${items}${items}${items}</div>`; // Adjust animation speed based on content length (content is tripled) const track = ticker.querySelector('.gives-ticker-track'); if (track) { const contentWidth = track.scrollWidth / 3; // one-third since tripled const speed = Math.max(20, contentWidth / 35); // ~35px per second track.style.animationDuration = `${speed}s`; } // Float all gives on the jeffreys slideshow (with or without notes) if (givesData.length > 0) { startFloatingGives(givesData); } } catch (e) { console.error('Gives load error:', e); if (SERVER_SUSPENDED) { renderSuspended(ticker, 'inline'); } else { const errorTexts = { en: 'Unable to load gives', da: 'Kunne ikke indlæse gaver', de: 'Konnte Gaben nicht laden', es: 'No se pudieron cargar las donaciones', zh: '无法加载捐赠' }; ticker.innerHTML = `<div class="gives-ticker-empty">${errorTexts[currentLang] || errorTexts.en}</div>`; } } } // Floating gives on jeffreys canvas function startFloatingGives(allGives) { // Only allow one instance of floating gives if (floatingGivesStarted) return; floatingGivesStarted = true; const slideshow = document.getElementById('jeffreysSlideshow'); if (!slideshow) { floatingGivesStarted = false; return; } let giveIndex = 0; const visibleGives = new Set(); // Track gives currently on screen by unique key let isShowingGive = false; // Prevent concurrent calls // Format time ago (localized) function timeAgo(dateStr) { const date = new Date(dateStr); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); // Translated time ago strings const timeAgoTexts = { en: { now: 'now', mAgo: 'm ago', hAgo: 'h ago', dAgo: 'd ago' }, da: { now: 'nu', mAgo: 'm siden', hAgo: 't siden', dAgo: 'd siden' }, de: { now: 'jetzt', mAgo: 'm her', hAgo: 'h her', dAgo: 'T her' }, es: { now: 'ahora', mAgo: 'm', hAgo: 'h', dAgo: 'd' }, zh: { now: '现在', mAgo: '分钟前', hAgo: '小时前', dAgo: '天前' } }; const ta = timeAgoTexts[currentLang] || timeAgoTexts.en; const locales = { en: 'en-US', da: 'da-DK', de: 'de-DE', es: 'es-ES', zh: 'zh-CN' }; if (diffMins < 1) return ta.now; if (diffMins < 60) return `${diffMins}${ta.mAgo}`; if (diffHours < 24) return `${diffHours}${ta.hAgo}`; if (diffDays < 7) return `${diffDays}${ta.dAgo}`; return date.toLocaleDateString(locales[currentLang] || 'en-US', { month: 'short', day: 'numeric' }); } // Convert text to animated characters - alphabet soup on strings with depth layering function animateText(text) { return [...text].map((char, i) => { if (char === ' ') return ' '; // Random soup properties - each letter dangling on its own string const delay = i * 0.06 + Math.random() * 0.2; const duration = 2.0 + Math.random() * 2.0; // 2-4s const swingX = (Math.random() - 0.5) * 10; // -5 to 5px const swingY = -6 - Math.random() * 10; // -6 to -16px const rotBase = (Math.random() - 0.5) * 8; // -4 to 4deg base tilt const rotSwing = 3 + Math.random() * 6; // 3-9deg swing // Random z-index for depth layering relative to slideshow layers: // 1 = behind color tint overlay (behind logo/photos visible through) // 3 = between overlays (middle depth) // 10 = in front of everything (clearly above logo) const zIndex = Math.random() < 0.35 ? 1 : (Math.random() < 0.5 ? 3 : 10); return `<span class="char" style="--char-delay: ${delay.toFixed(2)}s; --char-duration: ${duration.toFixed(1)}s; --swing-x: ${swingX.toFixed(1)}px; --swing-y: ${swingY.toFixed(1)}px; --rot-base: ${rotBase.toFixed(1)}deg; --rot-swing: ${rotSwing.toFixed(1)}deg; --char-z: ${zIndex}">${char}</span>`; }).join(''); } function showNextGive() { // Prevent concurrent calls if (isShowingGive) return; isShowingGive = true; // Find a give that isn't currently visible (use unique key: amount + timestamp) let attempts = 0; const startIndex = giveIndex; let give = allGives[giveIndex]; let giveKey = `${give.amountDisplay}-${give.createdAt || give.timestamp}`; while (visibleGives.has(giveKey) && attempts < allGives.length) { giveIndex = (giveIndex + 1) % allGives.length; give = allGives[giveIndex]; giveKey = `${give.amountDisplay}-${give.createdAt || give.timestamp}`; attempts++; } // If all gives are visible, skip this cycle if (visibleGives.has(giveKey)) { giveIndex = (startIndex + 1) % allGives.length; isShowingGive = false; return; } // Mark as visible IMMEDIATELY before any async stuff visibleGives.add(giveKey); // Move to next for subsequent calls giveIndex = (giveIndex + 1) % allGives.length; // Build text like ticker: "Got $5 today" or "Got $5: note" const suffix = give.isRecurring ? '/mo' : ''; const { time: timeStr, useOn } = formatGiveTime(give.createdAt || give.timestamp); const amountText = give.amountDisplay + suffix; const hasNote = give.note && give.note.trim(); // Translated "on" preposition for dates const onPrep = { en: 'on', da: 'd.', de: 'am', es: 'el', zh: '' }; const onText = onPrep[currentLang] || onPrep.en; const timeText = useOn ? (onText ? `${onText} ${timeStr}` : timeStr) : timeStr; // Position: mostly left side (80%), occasionally center (20%) const posRand = Math.random(); const isLeftSide = posRand < 0.8; // Create HTML floating give element (GPU-accelerated CSS animations) const floatEl = document.createElement('div'); floatEl.className = `floating-give ${isLeftSide ? 'left-side' : 'center-side'}${give.isRecurring ? ' monthly' : ''}`; // Start from below bottom edge of container (100-110%) so they float up into view const topPercent = 100 + Math.random() * 10; floatEl.style.top = `${topPercent}%`; // Random drift values for ambient floating const driftStart = (Math.random() - 0.5) * 60; const driftEnd = (Math.random() - 0.5) * 80; floatEl.style.setProperty('--drift-start', `${driftStart}px`); floatEl.style.setProperty('--drift-end', `${driftEnd}px`); // Helper to wrap text in character spans for soup animation // Use spread operator [...text] to properly handle emojis (multi-codepoint) const wrapChars = (text, extraClass = '') => { return [...text].map((ch, i) => { const duration = 2 + Math.random() * 2; const delay = Math.random() * 2; const zIndex = Math.floor(Math.random() * 10); return `<span class="char ${extraClass}" style="--char-duration:${duration}s;--char-delay:${delay}s;--char-z:${zIndex}">${ch === ' ' ? ' ' : ch}</span>`; }).join(''); }; // Build HTML like ticker: "Got $5 today" or "Got $5: note" // Get localized "Got" text based on current language const gotTexts = { en: 'Got', da: 'Modtog', de: 'Erhalten', es: 'Recibido', zh: '收到' }; const gotText = gotTexts[currentLang] || gotTexts.en; const gotSpan = `<span class="got">${wrapChars(gotText)}</span>`; const amountSpan = `<span class="amount">${wrapChars(amountText)}</span>`; const timeSpan = `<span class="time">${wrapChars(timeText)}</span>`; const noteSpan = hasNote ? `<span class="note">: ${wrapChars(give.note.trim())}</span>` : ''; floatEl.innerHTML = gotSpan + amountSpan + timeSpan + noteSpan; // Add to slideshow container const container = document.querySelector('.jeffreys-slideshow'); if (container) { container.appendChild(floatEl); } isShowingGive = false; // Remove element after animation completes setTimeout(() => { visibleGives.delete(giveKey); floatEl.remove(); }, 30000); }
// Floating animations disabled // setTimeout(showNextGive, 3000); // setInterval(() => { // if (document.visibilityState === 'visible') { // showNextGive(); // } // }, 12000 + Math.random() * 6000); } setTimeout(loadRecentGives, 300); // Pause ticker when not visible const tickerObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { const ticker = entry.target; if (entry.isIntersecting) { ticker.classList.remove('paused'); } else { ticker.classList.add('paused'); } }); }, { threshold: 0.1 }); const tickerEl = document.getElementById('givesTicker'); if (tickerEl) tickerObserver.observe(tickerEl); // Drag to scroll ticker if (tickerEl) { let isDragging = false; let startX = 0; let currentOffset = 0; let trackOffset = 0; let resumeTimeout = null; function getTrack() { return tickerEl.querySelector('.gives-ticker-track'); } function getTrackOffset() { const track = getTrack(); if (!track) return 0; const transform = getComputedStyle(track).transform; if (transform === 'none') return 0; const matrix = new DOMMatrixReadOnly(transform); return matrix.m41; } function startDrag(clientX) { const track = getTrack(); if (!track) return; if (resumeTimeout) clearTimeout(resumeTimeout); isDragging = true; startX = clientX; trackOffset = getTrackOffset(); tickerEl.classList.add('dragging'); // Apply current position as inline style immediately track.style.transform = `translateX(${trackOffset}px)`; } tickerEl.addEventListener('mousedown', (e) => { startDrag(e.clientX); e.preventDefault(); }); tickerEl.addEventListener('touchstart', (e) => { startDrag(e.touches[0].clientX); }, { passive: true }); window.addEventListener('mousemove', (e) => { if (!isDragging) return; const track = getTrack(); if (!track) return; const deltaX = e.clientX - startX; currentOffset = trackOffset + deltaX; track.style.transform = `translateX(${currentOffset}px)`; }); window.addEventListener('touchmove', (e) => { if (!isDragging) return; const track = getTrack(); if (!track) return; const deltaX = e.touches[0].clientX - startX; currentOffset = trackOffset + deltaX; track.style.transform = `translateX(${currentOffset}px)`; }, { passive: true }); function stopDragging() { if (!isDragging) return; const track = getTrack(); isDragging = false; // Keep manual position for 2s before resuming animation resumeTimeout = setTimeout(() => { tickerEl.classList.remove('dragging'); if (track) track.style.transform = ''; }, 2000); } window.addEventListener('mouseup', stopDragging); window.addEventListener('touchend', stopDragging); } // Animate server cost colors (cycling hue animation) let serverCostPhase = Math.random() * Math.PI * 2; let serverCostPaused = false; function animateServerCost() { if (!serverCostPaused) { serverCostPhase += 0.02; // Smooth oscillation document.querySelectorAll('.server-cost-number').forEach(el => { // Cycle through colors (pink → cyan → gold → green → pink) const hue = (serverCostPhase * 30) % 360; el.style.color = `hsl(${hue}, 70%, 65%)`; }); } requestAnimationFrame(animateServerCost); } setTimeout(animateServerCost, 500); // Start after ticker loads // Pause server cost animation when ticker not visible const costObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { serverCostPaused = !entry.isIntersecting; }); }, { threshold: 0.1 }); if (tickerEl) costObserver.observe(tickerEl);
// Determine API base URL (relative for local dev, absolute for production) const apiBase = window.location.hostname === 'localhost' || window.location.hostname.includes('local.') ? '' // Use relative URLs in dev : 'https://aesthetic.computer';
// Oven URL for webp generation (always use production oven) const OVEN_URL = 'https://oven.aesthetic.computer';
// KidLisp count from dedicated endpoint async function loadKidlispCount() { try { const res = await fetch(`${apiBase}/api/kidlisp-count`); const data = await res.json(); const count = data.count || 0; const el = document.querySelector('#stat-kidlisp .stat-value'); if (el && count) el.textContent = count.toLocaleString(); // Update stat values for sorting and re-sort if (window._statValues) { window._statValues['stat-kidlisp'] = count; sortStatBoxes(); } } catch (e) { console.warn('Could not load kidlisp count:', e); } } setTimeout(loadKidlispCount, 400);
// TV video feed - load and cycle through tape videos let tvTapes = []; let tvIndex = 0; let tvPlayer = null; let tvMuted = true; // Preserve mute state across tape changes let tvErrorCount = 0; let tvPlayId = 0; // Track current play session to prevent stale callbacks const TV_MAX_ERRORS = 5; // Stop trying after too many consecutive errors // Scrubbing state - defined once outside playTVVideo to avoid duplicate listeners let tvIsScrubbing = false; let tvScrubFn = null; document.addEventListener('mousemove', (e) => { if (tvIsScrubbing && tvScrubFn) tvScrubFn(e); }); document.addEventListener('mouseup', () => { tvIsScrubbing = false; }); async function loadTVFeed() { const container = document.getElementById('tvPlayer'); try { const res = await fetch(`${apiBase}/api/tv?types=tape&limit=20&filter=recent`); const data = await res.json(); // Get tapes from mixed array or media.tapes const tapes = data.mixed || data.media?.tapes || []; // Filter for tapes with completed MP4s (most reliable), then any with videoUrl tvTapes = tapes.filter(item => item.media?.videoUrl && item.media?.mp4Status === "complete" ); // Fallback to any tape with videoUrl if no complete MP4s if (!tvTapes.length) { tvTapes = tapes.filter(item => item.media?.videoUrl); } if (!tvTapes.length) { if (SERVER_SUSPENDED) { renderSuspended(container, 'card'); } else { container.innerHTML = '<div class="tv-no-signal">No signal</div>'; } return; }
playTVVideo(0); } catch (e) { console.error('TV load error:', e); if (SERVER_SUSPENDED) { renderSuspended(container, 'card'); } else { container.innerHTML = '<div class="tv-no-signal">No signal</div>'; } } } function playTVVideo(index) { const container = document.getElementById('tvPlayer'); const header = document.getElementById('tvHeader'); const tape = tvTapes[index];
// Increment play ID to invalidate callbacks from previous video loads const currentPlayId = ++tvPlayId;
if (!tape) { container.innerHTML = '<div class="tv-no-signal">No signal</div>'; return; } // Update header with current tape code, timestamp, and upcoming codes const tapeCode = tape.code ? `!${tape.code}` : '📼 tapes'; // Format timestamp let timestampStr = ''; if (tape.createdAt || tape.timestamp) { const date = new Date(tape.createdAt || tape.timestamp); const options = { month: 'short', day: 'numeric', year: 'numeric' }; timestampStr = `taped on ${date.toLocaleDateString('en-US', options)}`; } // Get next 2-3 upcoming codes with their indices const upcomingCodes = []; for (let i = 1; i <= 3; i++) { const nextIndex = (index + i) % tvTapes.length; const nextTape = tvTapes[nextIndex]; if (nextTape && nextTape.code && upcomingCodes.length < 3) { upcomingCodes.push({ code: `!${nextTape.code}`, index: nextIndex }); } } const upcomingHtml = upcomingCodes.length ? `<span class="tv-upcoming">${upcomingCodes.map(c => `<span class="tv-upcoming-code" data-index="${c.index}">${c.code}</span>`).join('')}</span>` : ''; if (header) { header.innerHTML = ` <div class="tv-header-row"> <span class="tv-code">${tapeCode}</span> ${upcomingHtml} </div> ${timestampStr ? `<span class="tv-timestamp">${timestampStr}</span>` : ''} `; // Make upcoming codes clickable to skip to that tape header.querySelectorAll('.tv-upcoming-code').forEach(el => { el.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const targetIndex = parseInt(el.dataset.index, 10); if (!isNaN(targetIndex)) { tvIndex = targetIndex; playTVVideo(tvIndex); } }); }); // Make current tape code clickable to advance to next tape const currentCode = header.querySelector('.tv-code'); if (currentCode) { currentCode.style.cursor = 'pointer'; currentCode.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); tvIndex = (tvIndex + 1) % tvTapes.length; playTVVideo(tvIndex); }); } } // Remove ALL old slides and glows (not just one) to prevent overlap on rapid clicks // Also STOP old videos immediately to prevent continued loading const oldSlides = container.querySelectorAll('.tv-slide'); oldSlides.forEach(oldSlide => { const oldVideo = oldSlide.querySelector('video'); if (oldVideo) { oldVideo.pause(); oldVideo.removeAttribute('src'); oldVideo.load(); // Abort any ongoing fetch } oldSlide.classList.add('exiting'); setTimeout(() => oldSlide.remove(), 400); }); container.querySelectorAll('.tv-bg-glow').forEach(oldGlow => { oldGlow.pause(); oldGlow.removeAttribute('src'); oldGlow.style.opacity = '0'; setTimeout(() => oldGlow.remove(), 400); }); // Create background glow video (in container, not slide, to avoid clipping) const bgGlow = document.createElement('video'); bgGlow.className = 'tv-bg-glow'; bgGlow.src = tape.media.videoUrl; bgGlow.muted = true; bgGlow.playsInline = true; bgGlow.style.transition = 'opacity 0.4s ease'; container.insertBefore(bgGlow, container.firstChild); // Create backdrop glow video (behind the entire panel) const backdropEl = document.getElementById('tvBackdropGlow'); if (backdropEl) { backdropEl.innerHTML = ''; const backdropVideo = document.createElement('video'); backdropVideo.src = tape.media.videoUrl; backdropVideo.muted = true; backdropVideo.playsInline = true; backdropVideo.autoplay = true; backdropVideo.loop = true; backdropEl.appendChild(backdropVideo); } // Create new slide (main video only) const slide = document.createElement('div'); slide.className = 'tv-slide entering'; slide.innerHTML = ` <video src="${tape.media.videoUrl}" muted playsinline autoplay disablepictureinpicture disableremoteplayback controlslist="nodownload noremoteplayback noplaybackrate" ></video> `; container.appendChild(slide); // Ensure progress bar and timer exist (only once) if (!container.querySelector('.tv-progress')) { const progressEl = document.createElement('div'); progressEl.className = 'tv-progress'; progressEl.innerHTML = '<div class="tv-progress-bar"></div>'; container.appendChild(progressEl); } if (!container.querySelector('.tv-timer')) { const timerEl = document.createElement('span'); timerEl.className = 'tv-timer'; timerEl.textContent = '0:00 / 0:00'; container.appendChild(timerEl); } // Add mute button (only once) if (!container.querySelector('.tv-mute-btn')) { const muteBtn = document.createElement('button'); muteBtn.className = 'tv-mute-btn muted'; muteBtn.innerHTML = ` <svg viewBox="0 0 24 24"> <path d="M3 9v6h4l5 5V4L7 9H3z"/> <path class="sound-wave" d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/> <path class="sound-wave" d="M19 12c0 2.58-1.17 4.87-3 6.4v2.15c2.78-1.82 4.63-4.96 4.63-8.55 0-3.59-1.85-6.73-4.63-8.55v2.15c1.83 1.53 3 3.82 3 6.4z"/> </svg> `; muteBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (tvPlayer) { tvPlayer.muted = !tvPlayer.muted; tvMuted = tvPlayer.muted; // Store for next tape muteBtn.classList.toggle('muted', tvPlayer.muted); } }; container.appendChild(muteBtn); } tvPlayer = slide.querySelector('video'); tvPlayer.volume = 0.3; // Set default volume to 30% tvPlayer.muted = tvMuted; // Preserve mute state from previous tape // Update mute button visual state const muteBtn = container.querySelector('.tv-mute-btn'); if (muteBtn) muteBtn.classList.toggle('muted', tvMuted); // Sync background glow with main video (glow is in container, not slide) if (bgGlow) { tvPlayer.addEventListener('play', () => bgGlow.play()); tvPlayer.addEventListener('pause', () => bgGlow.pause()); tvPlayer.addEventListener('seeked', () => { bgGlow.currentTime = tvPlayer.currentTime; }); // Start bg glow when main video starts tvPlayer.addEventListener('canplay', () => { bgGlow.currentTime = tvPlayer.currentTime; if (!tvPlayer.paused) bgGlow.play(); }, { once: true }); } const progressBar = container.querySelector('.tv-progress-bar'); const progressContainer = container.querySelector('.tv-progress'); const timerEl = container.querySelector('.tv-timer'); // Format seconds as m:ss function formatTime(sec) { const m = Math.floor(sec / 60); const s = Math.floor(sec % 60); return `${m}:${s.toString().padStart(2, '0')}`; } // Scrubbing functionality - uses global document listeners defined once above function scrubTo(e) { if (!tvPlayer.duration) return; const rect = progressContainer.getBoundingClientRect(); const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); tvPlayer.currentTime = pct * tvPlayer.duration; } tvScrubFn = scrubTo; // Update global scrub function reference progressContainer.addEventListener('mousedown', (e) => { tvIsScrubbing = true; scrubTo(e); }); progressContainer.addEventListener('click', (e) => { e.stopPropagation(); // Prevent play/pause toggle scrubTo(e); }); // Toggle play/pause with big button feedback let bigPlayTimeout = null; function togglePlayback(e) { e.preventDefault(); e.stopPropagation(); const indicator = container.querySelector('.tv-play-indicator'); const bigPlay = container.querySelector('.tv-big-play'); if (tvPlayer.paused) { tvPlayer.play(); if (indicator) indicator.classList.remove('paused'); if (bigPlay) { bigPlay.classList.remove('paused'); bigPlay.classList.add('visible'); } } else { tvPlayer.pause(); if (indicator) indicator.classList.add('paused'); if (bigPlay) { bigPlay.classList.add('paused'); bigPlay.classList.add('visible'); } } // Hide big play button after delay if (bigPlayTimeout) clearTimeout(bigPlayTimeout); bigPlayTimeout = setTimeout(() => { if (bigPlay) bigPlay.classList.remove('visible'); }, 600); } // Add play/pause indicator (small) if (!container.querySelector('.tv-play-indicator')) { const indicator = document.createElement('div'); indicator.className = 'tv-play-indicator'; container.appendChild(indicator); } // Add big centered play/pause button if (!container.querySelector('.tv-big-play')) { const bigPlay = document.createElement('div'); bigPlay.className = 'tv-big-play'; container.appendChild(bigPlay); } // Make video and timer clickable to pause/play (not header - it has nav links) slide.addEventListener('click', togglePlayback); timerEl.addEventListener('click', togglePlayback); // Attach all event listeners once function attachListeners() { tvPlayer.addEventListener('timeupdate', () => { if (tvPlayer.duration) { const pct = (tvPlayer.currentTime / tvPlayer.duration) * 100; progressBar.style.width = pct + '%'; // Update timer: current / total const current = formatTime(tvPlayer.currentTime); const total = formatTime(tvPlayer.duration); if (timerEl) timerEl.textContent = `${current} / ${total}`; } }); tvPlayer.addEventListener('ended', () => { // Ignore if this video is from an old play session (user clicked rapidly) if (currentPlayId !== tvPlayId) { return; } tvErrorCount = 0; // Reset error count on successful play tvIndex = (tvIndex + 1) % tvTapes.length; playTVVideo(tvIndex); }); tvPlayer.addEventListener('canplay', () => { if (currentPlayId !== tvPlayId) { return; } tvErrorCount = 0; // Reset error count when video loads successfully }); tvPlayer.addEventListener('error', (e) => { // Ignore errors from old video loads (user clicked rapidly) if (currentPlayId !== tvPlayId) { return; } tvErrorCount++; if (tvErrorCount >= TV_MAX_ERRORS) { container.innerHTML = '<div class="tv-no-signal">No signal</div>'; return; } // Skip broken videos tvIndex = (tvIndex + 1) % tvTapes.length; setTimeout(() => playTVVideo(tvIndex), 500); }); }
attachListeners();
tvPlayer.play().then(() => { }).catch((err) => { // Autoplay blocked - add click overlay (keep video and listeners intact) const overlay = document.createElement('div'); overlay.className = 'module-loading'; overlay.style.cursor = 'pointer'; overlay.innerHTML = '▶️'; overlay.onclick = () => { tvPlayer.play(); overlay.remove(); }; container.insertBefore(overlay, container.firstChild); }); } setTimeout(loadTVFeed, 500); // AT Proto - cycle through actual records fetched from PDS let atRecords = []; let atRecordIndex = 0; let atCurrentSlot = 0; let atProgressInterval = null; const PDS_URL = 'https://at.aesthetic.computer'; const AT_CYCLE_DURATION = 5000; // 5 seconds per record function startATProgressBar() { const progressBar = document.getElementById('atProgressBar'); if (!progressBar) return; // Reset and animate progressBar.style.transition = 'none'; progressBar.style.width = '0%'; requestAnimationFrame(() => { requestAnimationFrame(() => { progressBar.style.transition = `width ${AT_CYCLE_DURATION}ms linear`; progressBar.style.width = '100%'; }); }); } async function loadATProtoRecords() { try { // Fetch user stats to get handles with ATProto accounts const statsRes = await fetch(`${apiBase}/.netlify/functions/atproto-user-stats?limit=20`); const statsData = await statsRes.json(); const users = (statsData.users || []).filter(u => u.handle && u.totalRecords > 0); // Fetch actual records from PDS for top users const recordPromises = users.slice(0, 8).map(async (user) => { const handle = user.handle; // e.g. "jeffrey.at.aesthetic.computer" const shortHandle = handle.replace('.at.aesthetic.computer', ''); try { // Fetch moods and paintings from this user's repo const [moodsRes, paintingsRes] = await Promise.all([ fetch(`${PDS_URL}/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=computer.aesthetic.mood&limit=3`), fetch(`${PDS_URL}/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=computer.aesthetic.painting&limit=2`) ]); const moodsData = await moodsRes.json(); const paintingsData = await paintingsRes.json(); const records = []; // Add moods (moodsData.records || []).forEach(r => { records.push({ type: 'mood', uri: r.uri, handle: shortHandle, collection: 'mood', content: r.value?.mood || '', when: r.value?.when }); }); // Add paintings (paintingsData.records || []).forEach(r => { const did = r.uri.split('/')[2]; const thumbCid = r.value?.thumbnail?.ref?.$link || r.value?.thumbnail?.ref; records.push({ type: 'painting', uri: r.uri, handle: shortHandle, collection: 'painting', thumbnail: thumbCid ? `${PDS_URL}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${thumbCid}` : null, when: r.value?.when }); }); return records; } catch (e) { return []; } }); const allRecordArrays = await Promise.all(recordPromises); atRecords = allRecordArrays.flat().sort(() => Math.random() - 0.5); // Shuffle if (atRecords.length > 0) { showATRecord(0, 0); startATProgressBar(); // Start cycling setInterval(() => { atRecordIndex = (atRecordIndex + 1) % atRecords.length; const nextSlot = atCurrentSlot === 0 ? 1 : 0; showATRecord(atRecordIndex, nextSlot); const currentEl = document.getElementById(`atRecord${atCurrentSlot}`); const nextEl = document.getElementById(`atRecord${nextSlot}`); if (currentEl) currentEl.classList.remove('active'); if (nextEl) nextEl.classList.add('active'); atCurrentSlot = nextSlot; startATProgressBar(); }, AT_CYCLE_DURATION); } else { // AT record elements no longer exist in the UI const atHandle0 = document.getElementById('atHandle0'); const atCenter0 = document.getElementById('atCenter0'); const atCollection0 = document.getElementById('atCollection0'); if (atHandle0) atHandle0.textContent = '@aesthetic'; if (atCenter0) atCenter0.innerHTML = '<div class="at-record-mood">🌐</div>'; if (atCollection0) atCollection0.textContent = 'status'; } } catch (e) { console.error('AT Proto records error:', e); // AT record elements no longer exist in the UI const atHandle0 = document.getElementById('atHandle0'); const atCenter0 = document.getElementById('atCenter0'); const atCollection0 = document.getElementById('atCollection0'); if (atHandle0) atHandle0.textContent = '@aesthetic'; if (atCenter0) atCenter0.innerHTML = '<div class="at-record-mood">🌐</div>'; if (atCollection0) atCollection0.textContent = 'status'; } } function showATRecord(index, slot) { const record = atRecords[index]; if (!record) return;
const uriEl = document.getElementById(`atUri${slot}`); const centerEl = document.getElementById(`atCenter${slot}`); const handleEl = document.getElementById(`atHandle${slot}`); const typeEl = document.getElementById(`atType${slot}`); const collectionEl = document.getElementById(`atCollection${slot}`);
// If elements don't exist, bail out if (!uriEl || !centerEl || !handleEl || !typeEl || !collectionEl) return;
// Extract interesting parts from URI for proof const uriParts = record.uri.replace('at://', '').split('/'); const did = uriParts[0]; const collection = uriParts[1]; const rkey = uriParts[2];
// Show more of the hash/rkey for proof const shortDid = did.substring(0, 16) + '...'; const displayUri = `${shortDid}/${collection}/${rkey}`; uriEl.textContent = displayUri; handleEl.textContent = `@${record.handle}`; typeEl.textContent = record.type; collectionEl.textContent = `computer.aesthetic.${record.collection}`;
if (record.type === 'mood') { centerEl.innerHTML = `<div class="at-record-mood">${escapeHtml(record.content)}</div>`; } else if (record.type === 'painting' && record.thumbnail) { centerEl.innerHTML = `<div class="at-record-thumb-wrap"><img class="at-record-thumb" src="${record.thumbnail}" alt="painting"></div>`; } else { centerEl.innerHTML = `<div class="at-record-mood">🎨</div>`; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } setTimeout(loadATProtoRecords, 600); // Linkify text - parse URLs, @handles, 'prompts', #paintings // Works on raw text, escapes only plain text segments function linkifyText(text) { // Parse elements with positions on RAW text (before escaping) const elements = []; // URLs (http://, https://, www.) const urlRegex = /(https?:\/\/[^\s<]+|www\.[^\s<]+)/gi; let match; while ((match = urlRegex.exec(text)) !== null) { elements.push({ type: 'url', text: match[0], start: match.index, end: match.index + match[0].length }); } // @handles const handleRegex = /@[a-z0-9]+([._][a-z0-9]+)*/gi; while ((match = handleRegex.exec(text)) !== null) { // Don't match inside URLs const inUrl = elements.some(e => e.type === 'url' && match.index >= e.start && match.index < e.end); if (!inUrl) { elements.push({ type: 'handle', text: match[0], start: match.index, end: match.index + match[0].length }); } } // 'prompts' (single-quoted words) const promptRegex = /'([^']+)'/g; while ((match = promptRegex.exec(text)) !== null) { const inUrl = elements.some(e => e.type === 'url' && match.index >= e.start && match.index < e.end); if (!inUrl) { elements.push({ type: 'prompt', text: match[0], inner: match[1], start: match.index, end: match.index + match[0].length }); } } // #paintings (hashtag codes) const paintingRegex = /#[a-z0-9]+/gi; while ((match = paintingRegex.exec(text)) !== null) { const inUrl = elements.some(e => e.type === 'url' && match.index >= e.start && match.index < e.end); if (!inUrl) { elements.push({ type: 'painting', text: match[0], start: match.index, end: match.index + match[0].length }); } } // Sort by position elements.sort((a, b) => a.start - b.start); // Build result string - escape plain text, but not the HTML links we create let result = ''; let lastEnd = 0; for (const el of elements) { // Add escaped text before this element result += escapeHtml(text.slice(lastEnd, el.start)); // Add linked element (escape the display text) if (el.type === 'url') { const href = el.text.startsWith('www.') ? 'https://' + el.text : el.text; result += `<a href="${escapeHtml(href)}" class="chat-link-url" target="_blank" rel="noopener">${escapeHtml(el.text)}</a>`; } else if (el.type === 'handle') { result += `<a href="https://aesthetic.computer/${escapeHtml(el.text)}" class="chat-link-handle" target="_blank">${escapeHtml(el.text)}</a>`; } else if (el.type === 'prompt') { result += `<a href="https://aesthetic.computer/${escapeHtml(el.inner)}" class="chat-link-prompt" target="_blank">${escapeHtml(el.text)}</a>`; } else if (el.type === 'painting') { const code = el.text.slice(1); // Remove # result += `<a href="https://aesthetic.computer/painting~${escapeHtml(code)}" class="chat-link-painting" target="_blank">${escapeHtml(el.text)}</a>`; } lastEnd = el.end; } // Add remaining escaped text result += escapeHtml(text.slice(lastEnd)); return result; } // Extract painting codes from text and generate thumbnail HTML function getPaintingThumbnails(text) { const codes = []; const paintingRegex = /#([a-z0-9]+)/gi; let match; while ((match = paintingRegex.exec(text)) !== null) { codes.push(match[1]); } if (!codes.length) return ''; const thumbs = codes.map(code => ` <div class="chat-painting-thumb"> <a href="https://aesthetic.computer/painting~${escapeHtml(code)}" target="_blank"> <img src="https://art.aesthetic.computer/${escapeHtml(code)}.png" alt="#${escapeHtml(code)}" loading="lazy" onload="this.classList.add('loaded')" onerror="this.parentElement.parentElement.style.display='none'" > <span class="thumb-code">#${escapeHtml(code)}</span> </a> </div> `).join(''); return `<div class="chat-painting-thumbs">${thumbs}</div>`; } // Chat Messages - Læer-Klokken messages (scrolling list) async function loadChatMessages() { const container = document.getElementById('chatMessages'); try { const res = await fetch(`${apiBase}/api/chat/messages?instance=clock&limit=25`); const data = await res.json(); const messages = data.messages || []; if (!messages.length) { container.innerHTML = '<div class="chat-msg"><div class="chat-text" style="color: var(--dim);">No messages yet</div></div>'; return; } // Messages come oldest-first from API, we want newest at top const html = messages.map(msg => { const when = new Date(msg.when); const ago = formatTimeAgo(when); // API returns 'from' field with '@' prefix if handle exists const handle = msg.from || 'anon'; const thumbs = getPaintingThumbnails(msg.text || ''); return ` <div class="chat-msg"> <div class="chat-msg-header"> <a href="https://aesthetic.computer/${handle}" class="chat-handle">${handle}</a> <span class="chat-time">${ago}</span> </div> <div class="chat-text">${linkifyText(msg.text || '')}</div> ${thumbs} </div> `; }).reverse().join(''); container.innerHTML = html; // Mark chat panel ready panelSync.markReady('chat'); } catch (e) { console.error('Chat load error:', e); if (SERVER_SUSPENDED) { renderSuspended(container, 'card'); } else { const loadErrorTexts = { en: 'Could not load', da: 'Kunne ikke indlæse', de: 'Konnte nicht laden', es: 'No se pudo cargar', zh: '无法加载' }; container.innerHTML = `<div class="chat-msg"><div class="chat-text" style="color: var(--dim);">${loadErrorTexts[currentLang] || loadErrorTexts.en}</div></div>`; } panelSync.markReady('chat'); // Still mark ready on error } } function formatTimeAgo(date) { const now = new Date(); const diff = now - date; const mins = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); // Translated time ago strings const timeAgoTexts = { en: { justNow: 'just now', mAgo: 'm ago', hAgo: 'h ago', dAgo: 'd ago' }, da: { justNow: 'lige nu', mAgo: 'm siden', hAgo: 't siden', dAgo: 'd siden' }, de: { justNow: 'gerade', mAgo: 'm her', hAgo: 'h her', dAgo: 'T her' }, es: { justNow: 'ahora', mAgo: 'm', hAgo: 'h', dAgo: 'd' }, zh: { justNow: '刚刚', mAgo: '分钟前', hAgo: '小时前', dAgo: '天前' } }; const ta = timeAgoTexts[currentLang] || timeAgoTexts.en; if (mins < 1) return ta.justNow; if (mins < 60) return `${mins}${ta.mAgo}`; if (hours < 24) return `${hours}${ta.hAgo}`; if (days < 7) return `${days}${ta.dAgo}`; // Use locale-appropriate date format const locales = { en: 'en-US', da: 'da-DK', de: 'de-DE', es: 'es-ES', zh: 'zh-CN' }; return date.toLocaleDateString(locales[currentLang] || 'en-US'); } function escapeHtml(str) { return str.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } // Unified module loading helpers function showModuleLoading(containerId, text = 'loading') { const container = document.getElementById(containerId); if (!container) return; container.innerHTML = `<div class="module-loading">${escapeHtml(text)}</div>`; } function hideModuleLoading(containerId) { const container = document.getElementById(containerId); if (!container) return; const loader = container.querySelector('.module-loading'); if (loader) loader.remove(); } setTimeout(loadChatMessages, 600); // Auto-scroll chat messages function autoScrollChat(containerId, speed = 0.5) { const container = document.getElementById(containerId); if (!container) return; let scrollPos = 0; let isPaused = false; // Pause on hover container.addEventListener('mouseenter', () => isPaused = true); container.addEventListener('mouseleave', () => isPaused = false); function scroll() { if (!isPaused && container.scrollHeight > container.clientHeight) { scrollPos += speed; // Reset to top when reaching bottom if (scrollPos >= container.scrollHeight - container.clientHeight) { scrollPos = 0; } container.scrollTop = scrollPos; } requestAnimationFrame(scroll); } requestAnimationFrame(scroll); } // Start auto-scroll for both chat containers at different speeds setTimeout(() => { autoScrollChat('chatMessages', 0.25); // laer-klokken: slower autoScrollChat('chatSystemMessages', 0.35); // chat: slightly faster }, 2000); // Wait for content to load // Chat System Messages (back face of flip card) async function loadChatSystemMessages() { const container = document.getElementById('chatSystemMessages'); if (!container) return; try { const res = await fetch(`${apiBase}/api/chat/messages?instance=system&limit=25`); const data = await res.json(); const messages = data.messages || []; if (!messages.length) { container.innerHTML = '<div class="chat-msg"><div class="chat-text" style="color: var(--dim);">No messages yet</div></div>'; return; } // Messages come oldest-first from API, we want newest at top const html = messages.map(msg => { const when = new Date(msg.when); const ago = formatTimeAgo(when); const handle = msg.from || 'anon'; const thumbs = getPaintingThumbnails(msg.text || ''); return ` <div class="chat-msg"> <div class="chat-msg-header"> <a href="https://aesthetic.computer/${handle}" class="chat-handle">${handle}</a> <span class="chat-time">${ago}</span> </div> <div class="chat-text">${linkifyText(msg.text || '')}</div> ${thumbs} </div> `; }).reverse().join(''); container.innerHTML = html; } catch (e) { console.error('Chat system load error:', e); const loadErrorTexts = { en: 'Could not load', da: 'Kunne ikke indlæse', de: 'Konnte nicht laden', es: 'No se pudo cargar', zh: '无法加载' }; container.innerHTML = `<div class="chat-msg"><div class="chat-text" style="color: var(--dim);">${loadErrorTexts[currentLang] || loadErrorTexts.en}</div></div>`; } } setTimeout(loadChatSystemMessages, 700); // Flip card toggle function with direction support function toggleChatFlip(direction) { const card = document.getElementById('chatFlipCard'); const container = document.querySelector('.chat-flip-container'); if (card && container) { const isFlipped = card.classList.contains('flipped'); // If direction specified, only flip if it makes sense if (direction === 'left' && isFlipped) { card.classList.remove('flipped'); container.classList.remove('flipped'); resetFlipProgress(); } else if (direction === 'right' && !isFlipped) { card.classList.add('flipped'); container.classList.add('flipped'); resetFlipProgress(); } else if (!direction) { card.classList.toggle('flipped'); container.classList.toggle('flipped'); resetFlipProgress(); } } } // Expose to global scope for onclick window.toggleChatFlip = toggleChatFlip; // Hover tilt effect for swivel preview function setupHoverTilt() { const container = document.querySelector('.chat-flip-container'); const card = document.getElementById('chatFlipCard'); if (!container || !card) return; container.addEventListener('mousemove', (e) => { if (card.classList.contains('flipped')) return; // Don't tilt when flipped const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; const width = rect.width; const third = width / 3; card.classList.remove('tilt-left', 'tilt-right'); if (x < third) { card.classList.add('tilt-left'); } else if (x > third * 2) { card.classList.add('tilt-right'); } }); container.addEventListener('mouseleave', () => { card.classList.remove('tilt-left', 'tilt-right'); }); // Click zones for directional swivel container.addEventListener('click', (e) => { // Don't trigger if clicking on a link or explicit trigger if (e.target.closest('a') || e.target.closest('.chat-flip-trigger')) return; const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; const width = rect.width; const isFlipped = card.classList.contains('flipped'); if (x < width / 2) { // Left side - swivel to show front (unflip) if (isFlipped) toggleChatFlip('left'); } else { // Right side - swivel to show back (flip) if (!isFlipped) toggleChatFlip('right'); } }); } setTimeout(setupHoverTilt, 100); // Auto-flip with progress bar const FLIP_INTERVAL = 8000; // 8 seconds between flips (synced with other panels) let flipProgress = 0; let flipPaused = false; function resetFlipProgress() { flipProgress = 0; const bar = document.getElementById('chatFlipProgress'); const barBack = document.getElementById('chatFlipProgressBack'); if (bar) bar.style.width = '0%'; if (barBack) barBack.style.width = '0%'; } function updateFlipProgress() { const bar = document.getElementById('chatFlipProgress'); const barBack = document.getElementById('chatFlipProgressBack'); const card = document.getElementById('chatFlipCard'); const container = document.querySelector('.chat-flip-container'); const isFlipped = card && card.classList.contains('flipped'); // Pause on hover if (container && !container.dataset.hoverBound) { container.dataset.hoverBound = 'true'; container.addEventListener('mouseenter', () => flipPaused = true); container.addEventListener('mouseleave', () => flipPaused = false); } if (!flipPaused) { flipProgress += 100 / (FLIP_INTERVAL / 100); // Update every 100ms // Only update the visible face's progress bar if (isFlipped) { if (barBack) barBack.style.width = flipProgress + '%'; if (bar) bar.style.width = '0%'; } else { if (bar) bar.style.width = flipProgress + '%'; if (barBack) barBack.style.width = '0%'; } if (flipProgress >= 100) { toggleChatFlip(); // resetFlipProgress is called inside toggleChatFlip } } setTimeout(updateFlipProgress, 100); } // Register chat flip starter with sync controller (don't start immediately) panelSync.registerStarter(() => updateFlipProgress()); // WebP/Image Dud Validator - Checks pixel data to detect blank/corrupted animations // For animated webps, samples multiple frames over time to avoid false positives // when first frame is blank (common with "(wipe white)" starts) // Returns a promise that resolves to { valid: bool, reason: string, samples: array } async function validateWebpImage(img, code = 'unknown') { return new Promise(async (resolve) => { // Create offscreen canvas to sample pixels const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d', { willReadFrequently: true }); // Sample at a reasonable size (don't need full resolution) const sampleSize = Math.min(64, img.naturalWidth, img.naturalHeight); canvas.width = sampleSize; canvas.height = sampleSize; // For animated webps, sample multiple frames over time // This avoids false positives when early frames are uniform const NUM_FRAMES = 5; const FRAME_DELAY = 150; // ms between frame samples let anyValidFrame = false; let lastResult = null; for (let frameNum = 0; frameNum < NUM_FRAMES; frameNum++) { if (frameNum > 0) { await new Promise(r => setTimeout(r, FRAME_DELAY)); } lastResult = await sampleImageOnce(ctx, canvas, img, sampleSize, code, frameNum, NUM_FRAMES); if (lastResult.valid) { anyValidFrame = true; break; // Found a good frame with varied content, accept it } } if (anyValidFrame) { resolve(lastResult); } else { // All frames were duds (uniform/transparent) - report the last result resolve(lastResult); } }); } // Helper to sample image pixels once - checks many points for uniformity async function sampleImageOnce(ctx, canvas, img, sampleSize, code, frameNum, totalFrames) { return new Promise((resolve) => { try { ctx.clearRect(0, 0, sampleSize, sampleSize); ctx.drawImage(img, 0, 0, sampleSize, sampleSize); const imageData = ctx.getImageData(0, 0, sampleSize, sampleSize); const pixels = imageData.data; // Sample many points across the image for better coverage // Grid pattern + corners + diagonals = 25 points const samplePoints = []; // 4x4 grid (16 points) for (let gy = 0; gy < 4; gy++) { for (let gx = 0; gx < 4; gx++) { const x = Math.floor(sampleSize * (0.1 + gx * 0.27)); const y = Math.floor(sampleSize * (0.1 + gy * 0.27)); samplePoints.push({ x, y, name: `grid-${gx}-${gy}` }); } } // Corners (4 points) samplePoints.push({ x: 2, y: 2, name: 'corner-tl' }); samplePoints.push({ x: sampleSize - 3, y: 2, name: 'corner-tr' }); samplePoints.push({ x: 2, y: sampleSize - 3, name: 'corner-bl' }); samplePoints.push({ x: sampleSize - 3, y: sampleSize - 3, name: 'corner-br' }); // Center cross (5 points) const mid = Math.floor(sampleSize / 2); samplePoints.push({ x: mid, y: mid, name: 'center' }); samplePoints.push({ x: mid, y: Math.floor(sampleSize * 0.2), name: 'center-top' }); samplePoints.push({ x: mid, y: Math.floor(sampleSize * 0.8), name: 'center-bot' }); samplePoints.push({ x: Math.floor(sampleSize * 0.2), y: mid, name: 'center-left' }); samplePoints.push({ x: Math.floor(sampleSize * 0.8), y: mid, name: 'center-right' }); const samples = []; let allTransparent = true; let firstColor = null; let maxColorDiff = 0; for (const pt of samplePoints) { const idx = (pt.y * sampleSize + pt.x) * 4; const r = pixels[idx]; const g = pixels[idx + 1]; const b = pixels[idx + 2]; const a = pixels[idx + 3]; samples.push({ ...pt, r, g, b, a }); // Check transparency if (a > 10) allTransparent = false; // Track color variance if (firstColor === null) { firstColor = { r, g, b, a }; } else { const colorDiff = Math.abs(r - firstColor.r) + Math.abs(g - firstColor.g) + Math.abs(b - firstColor.b); maxColorDiff = Math.max(maxColorDiff, colorDiff); } } // Determine if it's a dud - key metric is color variance // If all 25 sample points are within 30 total RGB difference, it's uniform const isUniform = maxColorDiff < 30; let valid = true; let reason = 'OK'; if (allTransparent) { valid = false; reason = 'ALL_TRANSPARENT'; } else if (isUniform && firstColor) { // Uniform/solid color = dud (whether black, white, or any color) valid = false; const hex = `#${firstColor.r.toString(16).padStart(2,'0')}${firstColor.g.toString(16).padStart(2,'0')}${firstColor.b.toString(16).padStart(2,'0')}`; reason = `UNIFORM_COLOR:${hex}`; }
resolve({ valid, reason, samples, code, maxColorDiff }); } catch (e) { console.error(`%c🚨 WEBP VALIDATE ERROR: $${code}`, 'color: red; font-size: 16px;', e); resolve({ valid: false, reason: 'CANVAS_ERROR', error: e.message, code }); } }); }
async function loadKidlispCarousel() { const carousel = document.getElementById('kidlispCarousel'); try { // Fetch top hits from the TV API const res = await fetch(`${apiBase}/api/tv?types=kidlisp&sort=hits&limit=100`); const data = await res.json(); const allHits = data.media?.kidlisp || data.mixed || []; // Filter to only show @jeffrey pieces or anonymous top hits const hits = allHits.filter(item => { const handle = item.owner?.handle?.replace('@', '') || item.handle || item.author || ''; // Show @jeffrey pieces or anonymous pieces (no handle/anon) return handle === 'jeffrey' || !handle || handle === 'anon'; }); if (!hits.length) { carousel.innerHTML = '<div style="text-align: center; color: #555; padding: 2em;">No pieces found</div>'; return; } // Shuffle for variety using Fisher-Yates function shuffle(arr) { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } const shuffledHits = shuffle(hits); // KidLisp syntax highlighter - matches kidlisp.com/index.html function highlightKidlisp(code) { if (!code) return ''; // Escape HTML first let html = code .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>'); // Full CSS color map (from num.mjs cssColors) const cssColors = { aliceblue: [240, 248, 255], antiquewhite: [250, 235, 215], aqua: [0, 255, 255], aquamarine: [127, 255, 212], azure: [240, 255, 255], beige: [245, 245, 220], bisque: [255, 228, 196], black: [0, 0, 0], blanchedalmond: [255, 235, 205], blue: [0, 0, 255], blueviolet: [138, 43, 226], brown: [165, 42, 42], burlywood: [222, 184, 135], cadetblue: [95, 158, 160], chartreuse: [127, 255, 0], chocolate: [210, 105, 30], coral: [255, 127, 80], cornflowerblue: [100, 149, 237], cornsilk: [255, 248, 220], crimson: [220, 20, 60], cyan: [0, 255, 255], darkblue: [0, 0, 139], darkcyan: [0, 139, 139], darkgoldenrod: [184, 134, 11], darkgray: [169, 169, 169], darkgrey: [169, 169, 169], darkgreen: [0, 100, 0], darkkhaki: [189, 183, 107], darkmagenta: [139, 0, 139], darkolivegreen: [85, 107, 47], darkorange: [255, 140, 0], darkorchid: [153, 50, 204], darkred: [139, 0, 0], darksalmon: [233, 150, 122], darkseagreen: [143, 188, 143], darkslateblue: [72, 61, 139], darkslategray: [47, 79, 79], darkslategrey: [47, 79, 79], darkturquoise: [0, 206, 209], darkviolet: [148, 0, 211], deeppink: [255, 20, 147], deepskyblue: [0, 191, 255], dimgray: [105, 105, 105], dimgrey: [105, 105, 105], dodgerblue: [30, 144, 255], firebrick: [178, 34, 34], floralwhite: [255, 250, 240], forestgreen: [34, 139, 34], fuchsia: [255, 0, 255], gainsboro: [220, 220, 220], ghostwhite: [248, 248, 255], gold: [255, 215, 0], goldenrod: [218, 165, 32], gray: [128, 128, 128], grey: [128, 128, 128], green: [0, 128, 0], greenyellow: [173, 255, 47], honeydew: [240, 255, 240], hotpink: [255, 105, 180], indianred: [205, 92, 92], indigo: [75, 0, 130], ivory: [255, 255, 240], khaki: [240, 230, 140], lavender: [230, 230, 250], lavenderblush: [255, 240, 245], lawngreen: [124, 252, 0], lemonchiffon: [255, 250, 205], lightblue: [173, 216, 230], lightcoral: [240, 128, 128], lightcyan: [224, 255, 255], lightgoldenrodyellow: [250, 250, 210], lightgray: [211, 211, 211], lightgrey: [211, 211, 211], lightgreen: [144, 238, 144], lightpink: [255, 182, 193], lightsalmon: [255, 160, 122], lightseagreen: [32, 178, 170], lightskyblue: [135, 206, 250], lightslategray: [119, 136, 153], lightslategrey: [119, 136, 153], lightsteelblue: [176, 196, 222], lightyellow: [255, 255, 224], lime: [0, 255, 0], limegreen: [50, 205, 50], linen: [250, 240, 230], magenta: [255, 0, 255], maroon: [128, 0, 0], mediumaquamarine: [102, 205, 170], mediumblue: [0, 0, 205], mediumorchid: [186, 85, 211], mediumpurple: [147, 112, 219], mediumseagreen: [60, 179, 113], mediumslateblue: [123, 104, 238], mediumspringgreen: [0, 250, 154], mediumturquoise: [72, 209, 204], mediumvioletred: [199, 21, 133], midnightblue: [25, 25, 112], mintcream: [245, 255, 250], mistyrose: [255, 228, 225], moccasin: [255, 228, 181], navajowhite: [255, 222, 173], navy: [0, 0, 128], oldlace: [253, 245, 230], olive: [128, 128, 0], olivedrab: [107, 142, 35], orange: [255, 165, 0], orangered: [255, 69, 0], orchid: [218, 112, 214], palegoldenrod: [238, 232, 170], palegreen: [152, 251, 152], paleturquoise: [175, 238, 238], palevioletred: [219, 112, 147], papayawhip: [255, 239, 213], peachpuff: [255, 218, 185], peru: [205, 133, 63], pink: [255, 192, 203], plum: [221, 160, 221], powderblue: [176, 224, 230], purple: [128, 0, 128], rebeccapurple: [102, 51, 153], red: [255, 0, 0], rosybrown: [188, 143, 143], royalblue: [65, 105, 225], saddlebrown: [139, 69, 19], salmon: [250, 128, 114], sandybrown: [244, 164, 96], seagreen: [46, 139, 87], seashell: [255, 245, 238], sienna: [160, 82, 45], silver: [192, 192, 192], skyblue: [135, 206, 235], slateblue: [106, 90, 205], slategray: [112, 128, 144], slategrey: [112, 128, 144], snow: [255, 250, 250], springgreen: [0, 255, 127], steelblue: [70, 130, 180], tan: [210, 180, 140], teal: [0, 128, 128], thistle: [216, 191, 216], tomato: [255, 99, 71], turquoise: [64, 224, 208], violet: [238, 130, 238], wheat: [245, 222, 179], white: [255, 255, 255], whitesmoke: [245, 245, 245], yellow: [255, 255, 0], yellowgreen: [154, 205, 50], darkbrown: [101, 67, 33], darkerbrown: [62, 39, 35], darksienna: [139, 90, 43] }; // Helper to create rainbow-highlighted text (per-character animation) function rainbowText(text) { return text.split('').map((char, i) => `<span class="hl-rainbow hl-rainbow-${i % 7}">${char}</span>` ).join(''); } // Helper to create zebra-highlighted text (alternating black/white) function zebraText(text) { return text.split('').map((char, i) => `<span class="hl-zebra hl-zebra-${i % 2}">${char}</span>` ).join(''); } // Helper to create fade-highlighted text (color gradient indicator) function fadeText(fadeStr) { // Parse fade string like "fade:red-blue" or "fade:red-white-blue:down" const parts = fadeStr.split(':'); if (parts.length < 2) return `<span class="hl-fade">${fadeStr}</span>`; const colors = parts[1].split('-'); const direction = parts[2] || ''; // Create colored segments for each color in the fade let result = '<span class="hl-fade" style="color: #ffb86c;">fade</span><span class="hl-fade-colon">:</span>'; colors.forEach((c, i) => { const rgb = cssColors[c.toLowerCase()]; if (rgb) { result += `<span class="hl-color" style="color: rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})">${c}</span>`; } else if (c === 'rainbow') { result += rainbowText(c); } else if (c === 'zebra') { result += zebraText(c); } else { result += `<span class="hl-fade">${c}</span>`; } if (i < colors.length - 1) result += '<span class="hl-fade-sep">-</span>'; }); if (direction) { result += `<span class="hl-fade-colon">:</span><span class="hl-fade-dir">${direction}</span>`; } return result; } // Helper to highlight $code references (lime green $ + lime identifier) function codeRefText(codeRef) { const dollar = codeRef[0]; const id = codeRef.slice(1); return `<span class="hl-code-ref">${dollar}</span><span class="hl-code-id">${id}</span>`; } // Helper to highlight #painting references (magenta # + orange identifier) function paintRefText(paintRef) { const hash = paintRef[0]; const id = paintRef.slice(1); return `<span class="hl-paint-ref">${hash}</span><span class="hl-paint-id">${id}</span>`; } // Use token placeholders to prevent double-replacement // Use \x01T{idx}T\x01 format to avoid number regex matching the idx const tokens = []; function token(match, cls, style) { const idx = tokens.length; const styleAttr = style ? ` style="${style}"` : ''; tokens.push(`<span class="${cls}"${styleAttr}>${match}</span>`); return `\x01T${idx}T\x01`; } function tokenRaw(html) { const idx = tokens.length; tokens.push(html); return `\x01T${idx}T\x01`; } // Order matters - most specific first // $code references (e.g., $mycode, $0, $test123) - limegreen $ + lime id html = html.replace(/(\$[0-9A-Za-z]+)/g, m => tokenRaw(codeRefText(m))); // #painting references (e.g., #abc123, #a1b2c3d4) - magenta # + orange id html = html.replace(/(#[0-9A-Fa-f]{1,8})\b/g, m => tokenRaw(paintRefText(m))); // Fade patterns (e.g., fade:red-blue, fade:red-white-blue:down) html = html.replace(/\b(fade:[a-z0-9-]+(?::[a-z]+)?)\b/gi, m => tokenRaw(fadeText(m))); // Rainbow keyword - animate the word itself html = html.replace(/\b(rainbow)\b/g, m => tokenRaw(rainbowText(m))); // Zebra keyword - animate the word itself html = html.replace(/\b(zebra)\b/g, m => tokenRaw(zebraText(m))); // Comments (lines starting with ;) html = html.replace(/(^|\n)(;[^\n]*)/g, (m, pre, comment) => pre + token(comment, 'hl-comment')); // Timing patterns - match kidlisp.mjs exactly: // 1. Cycle timers: 2s..., 3f..., 0.5s... (number + s/f + one or more dots) // 2. Delay timers: 1s, 0.5s, 3f, 1s! (number + s/f + optional !) // Match from start of token (after space or open paren) to avoid partial matches // Include data-duration for dynamic animation timing html = html.replace(/(^|[\s(])(\d*\.?\d+)([sf](\.{1,3}|!?))/g, (m, pre, num, suffix) => { const full = num + suffix; const unit = suffix[0]; // 's' or 'f' // Convert to seconds (frames assume 60fps) const duration = unit === 's' ? parseFloat(num) : parseFloat(num) / 60; const idx = tokens.length; tokens.push(`<span class="hl-timing hl-timing-active" data-duration="${duration}">${full}</span>`); return pre + `\x01T${idx}T\x01`; }); // Strings html = html.replace(/("[^"]*")/g, m => token(m, 'hl-string')); // Numbers (now safe since timing already tokenized) html = html.replace(/\b(\d+\.?\d*)\b/g, m => token(m, 'hl-number')); // Keywords (control flow) const keywords = ['def', 'fn', 'if', 'cond', 'let', 'loop', 'repeat', 'later', 'tap', 'drag', 'lift', 'key', 'draw', 'frame', 'now', 'once']; keywords.forEach(kw => { html = html.replace(new RegExp(`\\b(${kw})\\b`, 'g'), m => token(m, 'hl-keyword')); }); // API calls (drawing/commands) - but not rainbow/zebra (already handled) const apiCalls = ['wipe', 'ink', 'line', 'box', 'circle', 'text', 'pan', 'zoom', 'blur', 'noise', 'width', 'height', 'wiggle', 'grid', 'plot', 'stamp', 'scroll', 'contrast', 'fps']; apiCalls.forEach(api => { html = html.replace(new RegExp(`\\b(${api})\\b`, 'g'), m => token(m, 'hl-api')); }); // Colors - use actual color values from cssColors (skip rainbow/zebra as they're special) const colorNames = Object.keys(cssColors).filter(c => c !== 'rainbow' && c !== 'zebra'); colorNames.forEach(colorName => { const rgb = cssColors[colorName]; const colorStyle = `color: rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; html = html.replace(new RegExp(`\\b(${colorName})\\b`, 'gi'), m => token(m, 'hl-color', colorStyle)); }); // Parens html = html.replace(/([()])/g, m => token(m, 'hl-paren')); // Replace tokens back html = html.replace(/\x01T(\d+)T\x01/g, (_, idx) => tokens[parseInt(idx)]); return html; } // Preferred starter codes (nice spirals for page load) - one will always be first const STARTER_CODES = ['otoc', 'roz', 'cow', 'air']; const starterCode = STARTER_CODES[Math.floor(Math.random() * STARTER_CODES.length)]; // Find starter data in the full hits array let starterItem = hits.find(item => item.code === starterCode); // If not in hits, create minimal item (webp will still load from oven) if (!starterItem) { starterItem = { code: starterCode, hits: 0, source: '', owner: { handle: '@jeffrey' } }; } // Build slide list: starter first, then shuffled (excluding starter to avoid dupe) const filteredHits = shuffledHits.filter(item => item.code !== starterCode); const slides = [starterItem, ...filteredHits.slice(0, 19)]; let currentSlide = 0; let slideInterval = null; // Format date with time (e.g., "Jan. 5 @ 3:42pm") function formatDateTime(when) { if (!when) return ''; const date = new Date(when); const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const month = months[date.getMonth()]; const day = date.getDate(); const year = date.getFullYear(); const currentYear = new Date().getFullYear(); // Format time as 12-hour with am/pm let hours = date.getHours(); const mins = date.getMinutes().toString().padStart(2, '0'); const ampm = hours >= 12 ? 'pm' : 'am'; hours = hours % 12 || 12; const timeStr = `${hours}:${mins}${ampm}`; // Show year if not current year if (year !== currentYear) { return `${month}. ${day} '${String(year).slice(-2)} @ ${timeStr}`; } return `${month}. ${day} @ ${timeStr}`; } carousel.innerHTML = slides.map((item, i) => { const code = item.code || 'unknown'; const hitCount = item.hits || 0; const source = item.source || ''; const author = item.owner?.handle?.replace('@', '') || item.handle || item.author || ''; const when = item.when; const pieceUrl = `${kidlispUrl}/$${code}`; // Don't truncate - let scrolling handle long code const highlighted = highlightKidlisp(source); // Build author/time credit line with colored spans let creditHtml = ''; if (author && author !== 'anon') { const dateStr = when ? `<span class="credit-date"> · ${formatDateTime(when)}</span>` : ''; creditHtml = `<div class="kidlisp-slide-credit"><span class="credit-handle">@${escapeHtml(author)}</span>${dateStr}</div>`; } else if (when) { creditHtml = `<div class="kidlisp-slide-credit"><span class="credit-date">${formatDateTime(when)}</span></div>`; } // Wrap code chars in spans for animation const codeChars = ('$' + escapeHtml(code)).split('').map(c => `<span class="code-char">${c}</span>`).join(''); return ` <div class="kidlisp-slide" data-index="${i}" data-code="${escapeHtml(code)}" data-hits="${hitCount}" data-source="${escapeHtml(source).replace(/"/g, '"')}"> <a href="${pieceUrl}" target="_blank" title="Play $${escapeHtml(code)}"> <div class="module-loading"></div> </a> <div class="kidlisp-slide-code"><div class="kidlisp-slide-code-inner">${highlighted}</div></div> <div class="kidlisp-slide-qr-wrap"> <div class="kidlisp-slide-label">${codeChars}</div> <div class="kidlisp-slide-qr" data-url="${pieceUrl}"></div> </div> <div class="kidlisp-slide-plays">${hitCount.toLocaleString()} Hits</div> ${creditHtml} </div> `; }).join(''); // Apply timing durations to all timing elements carousel.querySelectorAll('.hl-timing-active[data-duration]').forEach(el => { const duration = parseFloat(el.dataset.duration) || 1; // Clamp duration between 0.1s and 10s for reasonable animation const clampedDuration = Math.max(0.1, Math.min(duration, 10)); el.style.setProperty('--timing-duration', clampedDuration + 's'); }); // Setup scrolling for code that overflows carousel.querySelectorAll('.kidlisp-slide-code').forEach(codeEl => { const inner = codeEl.querySelector('.kidlisp-slide-code-inner'); if (!inner) return; // Wait a tick for layout requestAnimationFrame(() => { const containerHeight = codeEl.clientHeight; const contentHeight = inner.scrollHeight; if (contentHeight > containerHeight + 20) { // Content overflows - enable scrolling const scrollDistance = contentHeight - containerHeight + 20; inner.classList.add('scrolling'); inner.style.setProperty('--scroll-distance', `-${scrollDistance}px`); // Longer scroll duration for more content (base 8s + 0.5s per 100px overflow) const scrollDuration = 8 + (scrollDistance / 200); inner.style.setProperty('--scroll-duration', `${scrollDuration}s`); } }); }); // Helper to update header code display with animation function updateHeaderCode(code, animate = false) { const headerCode = document.getElementById('kidlispHeaderCode'); if (!headerCode) return; // Build char spans with $ in different green than code function buildCodeChars(code) { const dollarSpan = '<span class="code-char code-dollar">$</span>'; const codeSpans = code.split('').map(c => `<span class="code-char code-name">${c}</span>`).join(''); return dollarSpan + codeSpans; } if (animate) { // Trigger blast-off on current code headerCode.classList.add('transitioning'); setTimeout(() => { // Update to new code with char spans headerCode.innerHTML = buildCodeChars(code); headerCode.classList.remove('transitioning'); }, 400); } else { // Just update without animation headerCode.innerHTML = buildCodeChars(code); } } // Set initial header link to first slide's code const firstSlide = carousel.querySelector('.kidlisp-slide'); if (firstSlide) { const firstCode = firstSlide.dataset.code; const headerLink = document.getElementById('kidlispHeaderLink'); if (headerLink && firstCode) { headerLink.href = `${kidlispUrl}/$${firstCode}`; updateHeaderCode(firstCode, false); } } // Advance to next slide (only to ready slides) function nextSlide() { const slideEls = carousel.querySelectorAll('.kidlisp-slide'); if (slideEls.length <= 1) return; const current = slideEls[currentSlide]; // Find next ready slide (skip non-ready ones) let attempts = 0; let nextIndex = currentSlide; do { nextIndex = (nextIndex + 1) % slideEls.length; attempts++; } while (!slideEls[nextIndex].classList.contains('ready') && attempts < slideEls.length); // If no ready slides found, stay on current if (attempts >= slideEls.length && !slideEls[nextIndex].classList.contains('ready')) { return; } currentSlide = nextIndex; const next = slideEls[currentSlide]; // Update header link to current piece const code = next.dataset.code; const headerLink = document.getElementById('kidlispHeaderLink'); if (headerLink && code) { headerLink.href = `${kidlispUrl}/$${code}`; updateHeaderCode(code, true); // Animate the header code change } // Transition current.classList.remove('active'); current.classList.add('exiting'); next.classList.add('active'); // Trigger code label blast-off animation on exiting slide const currentLabel = current.querySelector('.kidlisp-slide-label'); if (currentLabel) { currentLabel.classList.add('transitioning'); setTimeout(() => currentLabel.classList.remove('transitioning'), 500); } // Bounce press animation on the whole panel const previewPanel = carousel.closest('.kidlisp-preview'); if (previewPanel) { previewPanel.classList.remove('bounce-press'); // Force reflow to restart animation void previewPanel.offsetWidth; previewPanel.classList.add('bounce-press'); setTimeout(() => previewPanel.classList.remove('bounce-press'), 400); } // Clean up exiting class after transition setTimeout(() => current.classList.remove('exiting'), 500); // Restart progress bar animateKidlispProgress(); } // Progress bar animation const KIDLISP_INTERVAL = 8000; // 8 seconds (synced with other panels) let kidlispProgressAnim = null; function animateKidlispProgress() { const progressBar = document.getElementById('kidlispAutoProgress'); if (!progressBar) return; let startTime = null; function step(timestamp) { if (!startTime) startTime = timestamp; const elapsed = timestamp - startTime; const pct = Math.min((elapsed / KIDLISP_INTERVAL) * 100, 100); progressBar.style.width = pct + '%'; if (elapsed < KIDLISP_INTERVAL) { kidlispProgressAnim = requestAnimationFrame(step); } } if (kidlispProgressAnim) cancelAnimationFrame(kidlispProgressAnim); progressBar.style.width = '0%'; kidlispProgressAnim = requestAnimationFrame(step); } // Define starter function (called by panelSync when all ready) function startKidlispRotation() { // Guard against double-start if (slideInterval) return; // Find first ready slide and make it active const slideEls = carousel.querySelectorAll('.kidlisp-slide'); let foundReady = false; for (let i = 0; i < slideEls.length; i++) { if (slideEls[i].classList.contains('ready')) { // Clear previous active state slideEls.forEach(s => s.classList.remove('active')); slideEls[i].classList.add('active'); currentSlide = i; foundReady = true; // Update header link const code = slideEls[i].dataset.code; const headerLink = document.getElementById('kidlispHeaderLink'); if (headerLink && code) { headerLink.href = `${kidlispUrl}/$${code}`; } break; } } // If no ready slides yet, wait and try again if (!foundReady) { setTimeout(startKidlispRotation, 500); return; } animateKidlispProgress(); slideInterval = setInterval(nextSlide, KIDLISP_INTERVAL); } // Mark ready and register starter (don't start yet) panelSync.markReady('kidlisp'); panelSync.registerStarter(startKidlispRotation); // No pause on hover - slides continue advancing // Track shown sources to detect duplicates const shownSources = new Map(); // code -> source const MIN_SOURCE_LENGTH = 50; // Only check sources longer than this const SIMILARITY_THRESHOLD = 0.9; // 90% similar = duplicate // Simple similarity check using character trigrams (fast approximation) function getSourceSimilarity(source1, source2) { if (!source1 || !source2) return 0; // Normalize: lowercase, remove extra whitespace const norm1 = source1.toLowerCase().replace(/\s+/g, ' ').trim(); const norm2 = source2.toLowerCase().replace(/\s+/g, ' ').trim(); if (norm1 === norm2) return 1; if (norm1.length < 10 || norm2.length < 10) return 0; // Get trigrams (3-char sequences) function getTrigrams(str) { const trigrams = new Set(); for (let i = 0; i <= str.length - 3; i++) { trigrams.add(str.slice(i, i + 3)); } return trigrams; } const t1 = getTrigrams(norm1); const t2 = getTrigrams(norm2); // Jaccard similarity: intersection / union let intersection = 0; for (const t of t1) { if (t2.has(t)) intersection++; } const union = t1.size + t2.size - intersection; return union > 0 ? intersection / union : 0; } // Check if source is too similar to any already shown function isDuplicateSource(code, source) { if (!source || source.length < MIN_SOURCE_LENGTH) return false; for (const [shownCode, shownSource] of shownSources) { const similarity = getSourceSimilarity(source, shownSource); if (similarity >= SIMILARITY_THRESHOLD) { console.warn( `%c⚠️ DUPLICATE SOURCE: $${code} is ${(similarity * 100).toFixed(0)}% similar to $${shownCode}`, 'color: orange; font-size: 14px;' ); return true; } } return false; } // Load webp images for all slides (staggered to not overwhelm oven) const slideEls = carousel.querySelectorAll('.kidlisp-slide'); for (let i = 0; i < slideEls.length; i++) { const slide = slideEls[i]; const code = slide.dataset.code; // Stagger each request by 300ms if (i > 0) await new Promise(r => setTimeout(r, 300)); // Mark as validating (shows loading spinner only) slide.classList.add('validating'); const webpUrl = `${OVEN_URL}/grab/webp/256/256/$${code}?duration=6000&fps=10&quality=85&density=1`; const img = document.createElement('img'); img.crossOrigin = 'anonymous'; // Enable CORS for pixel validation // Don't add to DOM yet - validate offscreen first img.src = webpUrl; img.alt = code; img.onload = async () => { // Only show if image has actual content (not blank/error placeholder) // Check naturalWidth/Height to ensure it's a real image if (img.naturalWidth > 1 && img.naturalHeight > 1) { // Validate pixel content BEFORE adding to DOM (samples 5 frames over ~750ms) const validation = await validateWebpImage(img, code); slide.classList.remove('validating'); if (validation.valid) { // Check for duplicate source before showing const source = slide.dataset.source || ''; if (isDuplicateSource(code, source)) { // Duplicate detected - slide stays blank } else { // Track this source for future duplicate checks if (source.length >= MIN_SOURCE_LENGTH) { shownSources.set(code, source); } // Only now add to DOM since it's valid and unique const link = slide.querySelector('a'); if (link) { link.innerHTML = ''; link.appendChild(img); img.classList.add('loaded'); // Mark slide as ready - this reveals all content slide.classList.add('ready'); } } } else { // Dud detected - slide stays blank (no ready class) } } else { // Blank/tiny image - slide stays blank slide.classList.remove('validating'); } }; img.onerror = () => { // On error, slide stays blank slide.classList.remove('validating'); }; // Generate QR code for this slide (don't wait for image) const qrContainer = slide.querySelector('.kidlisp-slide-qr'); if (qrContainer && typeof qrcode !== 'undefined') { try { const qrUrl = `${kidlispUrl}/$${code}`; const qrGen = qrcode(0, 'L'); qrGen.addData(qrUrl); qrGen.make(); // Use SVG for better rendering const svg = qrGen.createSvgTag(2, 0); qrContainer.innerHTML = svg; const svgEl = qrContainer.querySelector('svg'); if (svgEl) { svgEl.style.display = 'block'; svgEl.style.width = '48px'; svgEl.style.height = '48px'; } } catch (e) { console.error('QR generation error:', e); } } } } catch (e) { console.error('KidLisp carousel load error:', e); if (SERVER_SUSPENDED) { renderSuspended(carousel, 'card'); } else { const loadErrorTexts = { en: 'Could not load', da: 'Kunne ikke indlæse', de: 'Konnte nicht laden', es: 'No se pudo cargar', zh: '无法加载' }; carousel.innerHTML = `<div style="text-align: center; color: #555; padding: 2em;">${loadErrorTexts[currentLang] || loadErrorTexts.en}</div>`; } panelSync.markReady('kidlisp'); // Still mark ready on error } } setTimeout(loadKidlispCarousel, 800); // Desktop Release Info - Fetch from GitHub async function loadDesktopReleaseInfo() { try { const res = await fetch('https://api.github.com/repos/whistlegraph/aesthetic-computer/releases/latest'); if (!res.ok) return; const release = await res.json(); const version = release.tag_name; // Update all platform version badges const macVer = document.getElementById('macVersion'); const winVer = document.getElementById('winVersion'); const linuxVer = document.getElementById('linuxVersion'); if (macVer) macVer.textContent = version; if (winVer) winVer.textContent = version; if (linuxVer) linuxVer.textContent = version; // Update download links with the latest version document.querySelectorAll('.desktop-platform-btn').forEach(btn => { const href = btn.getAttribute('href'); if (href && href.includes('v0.1.8')) { btn.setAttribute('href', href.replace(/v[\d.]+/g, version).replace(/[\d.]+(?=[-.](?:universal|Setup|AppImage))/g, version.replace('v', ''))); } }); // Build changelog ticker content const tickerEl = document.getElementById('changelogTicker'); if (tickerEl) { if (release.body) { const body = release.body; const items = []; // Extract list items from markdown const lines = body.split('\n'); for (const line of lines) { const boldMatch = line.match(/^[\-\*]\s*\*\*(.+?)\*\*\s*[-–]?\s*(.*)$/); const simpleMatch = line.match(/^[\-\*]\s+(.+)$/); if (boldMatch) { const [, bold, rest] = boldMatch; items.push(`${bold}${rest ? ': ' + rest : ''}`); } else if (simpleMatch && !simpleMatch[1].startsWith('**')) { items.push(simpleMatch[1]); } } if (items.length > 0) { // Duplicate for seamless loop const tickerText = items.map(i => `<span>${i}</span>`).join(''); tickerEl.innerHTML = tickerText + tickerText; } else { tickerEl.innerHTML = `<span>${release.name || version} released</span><span>${release.name || version} released</span>`; } } else { // No release notes - show version info with release date const dateStr = release.published_at ? new Date(release.published_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : ''; const msg = `${version} released${dateStr ? ' · ' + dateStr : ''}`; tickerEl.innerHTML = `<span>${msg}</span><span>${msg}</span>`; } } } catch (e) { console.warn('Could not load release info:', e); const tickerEl = document.getElementById('changelogTicker'); if (tickerEl) tickerEl.innerHTML = '<span>Visit GitHub for release notes</span><span>Visit GitHub for release notes</span>'; } } // Fetch VS Code extension version from marketplace async function loadVSCodeExtVersion() { try { // Use the VS Code Marketplace API const res = await fetch('https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json;api-version=6.0-preview.1' }, body: JSON.stringify({ filters: [{ criteria: [{ filterType: 7, value: 'aesthetic-computer.aesthetic-computer-code' }] }], flags: 0x200 // Include versions }) }); if (res.ok) { const data = await res.json(); const ext = data.results?.[0]?.extensions?.[0]; if (ext?.versions?.[0]?.version) { const versionEl = document.getElementById('vscodeExtVersion'); if (versionEl) versionEl.textContent = 'v' + ext.versions[0].version; } } } catch (e) { console.warn('Could not load VS Code extension version:', e); } } setTimeout(loadDesktopReleaseInfo, 900); setTimeout(loadVSCodeExtVersion, 1000);
// Apps Flip Card - Desktop / Mobile swivel function toggleAppsFlip(direction) { const card = document.getElementById('appsFlipCard'); const container = document.getElementById('appsFlipContainer'); if (card && container) { const isFlipped = card.classList.contains('flipped'); // If direction specified, only flip if it makes sense if (direction === 'left' && isFlipped) { card.classList.remove('flipped'); container.classList.remove('flipped'); resetAppsFlipProgress(); } else if (direction === 'right' && !isFlipped) { card.classList.add('flipped'); container.classList.add('flipped'); resetAppsFlipProgress(); } else if (!direction) { card.classList.toggle('flipped'); container.classList.toggle('flipped'); resetAppsFlipProgress(); } } } window.toggleAppsFlip = toggleAppsFlip; // Apps flip hover tilt effect (function setupAppsHoverTilt() { const container = document.getElementById('appsFlipContainer'); const card = document.getElementById('appsFlipCard'); if (!container || !card) return; container.addEventListener('mousemove', (e) => { if (card.classList.contains('flipped')) return; // Don't tilt when flipped const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; const width = rect.width; const third = width / 3; card.classList.remove('tilt-left', 'tilt-right'); if (x < third) { card.classList.add('tilt-left'); } else if (x > third * 2) { card.classList.add('tilt-right'); } }); container.addEventListener('mouseleave', () => { card.classList.remove('tilt-left', 'tilt-right'); }); // Click zones for directional swivel container.addEventListener('click', (e) => { // Don't trigger if clicking on a link if (e.target.closest('a')) return; const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; const width = rect.width; const isFlipped = card.classList.contains('flipped'); if (x < width / 2) { // Left side - swivel to show front (unflip) if (isFlipped) toggleAppsFlip('left'); } else { // Right side - swivel to show back (flip) if (!isFlipped) toggleAppsFlip('right'); } }); })(); // Apps flip auto-flip with progress bar const APPS_FLIP_INTERVAL = 8000; // 8 seconds between flips (synced with other panels) let appsFlipProgress = 0; function resetAppsFlipProgress() { appsFlipProgress = 0; const bar = document.getElementById('appsFlipProgress'); const barMobile = document.getElementById('appsFlipProgressMobile'); if (bar) bar.style.width = '0%'; if (barMobile) barMobile.style.width = '0%'; } function updateAppsFlipProgress() { const bar = document.getElementById('appsFlipProgress'); const barMobile = document.getElementById('appsFlipProgressMobile'); const card = document.getElementById('appsFlipCard'); const isFlipped = card && card.classList.contains('flipped'); appsFlipProgress += 100 / (APPS_FLIP_INTERVAL / 100); // Update every 100ms // Only update the visible face's progress bar if (isFlipped) { if (barMobile) barMobile.style.width = appsFlipProgress + '%'; if (bar) bar.style.width = '0%'; } else { if (bar) bar.style.width = appsFlipProgress + '%'; if (barMobile) barMobile.style.width = '0%'; } if (appsFlipProgress >= 100) { toggleAppsFlip(); } setTimeout(updateAppsFlipProgress, 100); } // Mark apps panel ready immediately (static content, no async load) panelSync.markReady('apps'); // Register apps flip starter with sync controller panelSync.registerStarter(() => updateAppsFlipProgress());
// 📊 Stats Section Visibility Manager // Pauses all floating stat spawners when stats section is not visible const statsVisibility = { isVisible: true, spawners: [], // { id, spawnFn, intervalMs, intervalId } register(spawnFn, intervalMs) { const spawner = { id: this.spawners.length, spawnFn, intervalMs, intervalId: null }; this.spawners.push(spawner); if (this.isVisible) { spawner.intervalId = setInterval(spawnFn, intervalMs); } return spawner.id; }, start() { if (this.isVisible) return; this.isVisible = true; for (const s of this.spawners) { if (!s.intervalId) { s.intervalId = setInterval(s.spawnFn, s.intervalMs); } } }, stop() { if (!this.isVisible) return; this.isVisible = false; for (const s of this.spawners) { if (s.intervalId) { clearInterval(s.intervalId); s.intervalId = null; } } } };
// Set up IntersectionObserver for stats section const statsSection = document.querySelector('.stats-section'); if (statsSection) { const statsObserver = new IntersectionObserver((entries) => { for (const entry of entries) { if (entry.isIntersecting) { statsVisibility.start(); } else { statsVisibility.stop(); } } }, { threshold: 0 }); statsObserver.observe(statsSection); }
// 🎨 Floating Paintings in Stats async function loadFloatingPaintings() { const statItem = document.getElementById('stat-paintings'); if (!statItem) return; const floatersContainer = statItem.querySelector('.stat-floaters'); if (!floatersContainer) return; try { // Fetch paintings from TV endpoint const res = await fetch(`${apiBase}/api/tv?types=painting&limit=50`); if (!res.ok) throw new Error('Failed to fetch paintings'); const data = await res.json(); const paintings = data.media?.paintings || []; if (!paintings.length) { return; }
// Group paintings by handle and take max 3-4 per user for variety const byHandle = {}; for (const p of paintings) { if (!p.media?.url) continue; if (!p.owner?.handle) continue; const h = p.owner.handle; if (!byHandle[h]) byHandle[h] = []; if (byHandle[h].length < 4) byHandle[h].push(p); } // Interleave paintings from different handles const mixed = []; const handles = Object.keys(byHandle); let moreToAdd = true; let idx = 0; while (moreToAdd) { moreToAdd = false; for (const h of handles) { if (byHandle[h][idx]) { mixed.push(byHandle[h][idx]); moreToAdd = true; } } idx++; } // Don't preload all - lazy load as needed let index = 0; function spawnFloater() { if (!mixed.length) return; const p = mixed[index % mixed.length]; index++; const floater = document.createElement('div'); floater.className = 'stat-floater'; // Random pixel size: 2x (32px), 3x (48px), or 4x (64px) const sizes = [32, 48, 64]; const size = sizes[Math.floor(Math.random() * sizes.length)]; // Random horizontal position (5% to 95%) const xPos = 5 + Math.random() * 90; floater.style.left = `calc(${xPos}% - ${size/2}px)`; floater.style.bottom = '-20px'; // Random duration between 10-20s (independent of size) const duration = 10 + Math.random() * 10; floater.style.setProperty('--float-duration', `${duration}s`); // Thumbnail - lazy load this image const thumb = document.createElement('img'); thumb.src = p.media.url; thumb.alt = p.code || 'painting'; thumb.style.width = `${size}px`; thumb.style.height = `${size}px`; thumb.loading = 'lazy'; floater.appendChild(thumb); // Code label if (p.code) { const code = document.createElement('span'); code.className = 'floater-code'; code.style.fontSize = '0.8em'; code.style.color = 'var(--gold)'; code.textContent = `#${p.code}`; floater.appendChild(code); } floatersContainer.appendChild(floater); // Remove after animation completes floater.addEventListener('animationend', () => floater.remove()); } // Spawn one immediately, then register with visibility manager spawnFloater(); statsVisibility.register(spawnFloater, 3000); } catch (e) { console.warn('Could not load floating paintings:', e); } } setTimeout(loadFloatingPaintings, 2000);
// 🏷️ Floating Handles in Stats async function loadFloatingHandles() { const statItem = document.getElementById('stat-handles'); if (!statItem) return; const floatersContainer = statItem.querySelector('.stat-floaters'); if (!floatersContainer) return; try { // Get handles from moods API (each mood has a handle) const res = await fetch(`${apiBase}/api/mood/all`); if (!res.ok) throw new Error('Failed to fetch moods for handles'); const data = await res.json(); const moods = data.moods || []; // Extract unique handles const handleSet = new Set(); for (const m of moods) { if (m.handle) handleSet.add(m.handle); } const handles = Array.from(handleSet);
if (!handles.length) return;
let index = 0; function spawnFloater() { const handle = handles[index % handles.length]; index++; const floater = document.createElement('div'); // Randomly choose left or right direction const goRight = Math.random() > 0.5; floater.className = goRight ? 'stat-floater float-horizontal-right' : 'stat-floater float-horizontal'; // Random vertical position (10% to 90%) const yPos = 10 + Math.random() * 80; floater.style.top = `${yPos}%`; if (goRight) { floater.style.left = '-80px'; } else { floater.style.right = '-50px'; } // Random duration between 8-14s const duration = 8 + Math.random() * 6; floater.style.setProperty('--float-duration', `${duration}s`); // Handle text const text = document.createElement('span'); text.className = 'floater-code'; text.style.fontSize = '0.9em'; text.style.color = 'var(--cyan)'; text.textContent = handle; floater.appendChild(text); floatersContainer.appendChild(floater); floater.addEventListener('animationend', () => floater.remove()); } spawnFloater(); statsVisibility.register(spawnFloater, 4000); } catch (e) { console.warn('Could not load floating handles:', e); } } setTimeout(loadFloatingHandles, 2500);
// 😊 Floating Moods in Stats async function loadFloatingMoods() { const statItem = document.getElementById('stat-moods'); if (!statItem) return; const floatersContainer = statItem.querySelector('.stat-floaters'); if (!floatersContainer) return; try { const res = await fetch(`${apiBase}/api/mood/all`); if (!res.ok) throw new Error('Failed to fetch moods'); const data = await res.json(); const moods = data.moods || []; // Group by handle, max 3 per user for variety const byHandle = {}; for (const m of moods) { if (!m.mood || !m.handle) continue; if (!byHandle[m.handle]) byHandle[m.handle] = []; if (byHandle[m.handle].length < 3) byHandle[m.handle].push(m); } // Interleave moods from different handles const mixed = []; const handles = Object.keys(byHandle); let moreToAdd = true; let idx = 0; while (moreToAdd) { moreToAdd = false; for (const h of handles) { if (byHandle[h][idx]) { mixed.push(byHandle[h][idx]); moreToAdd = true; } } idx++; }
if (!mixed.length) return;
let index = 0; function spawnFloater() { const m = mixed[index % mixed.length]; index++; const floater = document.createElement('div'); // Randomly choose left or right direction const goRight = Math.random() > 0.5; floater.className = goRight ? 'stat-floater float-horizontal-right' : 'stat-floater float-horizontal'; // Random vertical position (10% to 90%) const yPos = 10 + Math.random() * 80; floater.style.top = `${yPos}%`; if (goRight) { floater.style.left = '-80px'; } else { floater.style.right = '-50px'; } // Random duration between 10-18s (moods float slower) const duration = 10 + Math.random() * 8; floater.style.setProperty('--float-duration', `${duration}s`); // Mood text (truncate if too long) const text = document.createElement('span'); text.className = 'floater-code'; text.style.fontSize = '0.7em'; text.style.maxWidth = '80px'; text.style.overflow = 'hidden'; text.style.textOverflow = 'ellipsis'; text.style.color = 'var(--gold)'; const moodText = m.mood.length > 20 ? m.mood.slice(0, 18) + '…' : m.mood; text.textContent = moodText; floater.appendChild(text); floatersContainer.appendChild(floater); floater.addEventListener('animationend', () => floater.remove()); } spawnFloater(); statsVisibility.register(spawnFloater, 3500); } catch (e) { console.warn('Could not load floating moods:', e); } } setTimeout(loadFloatingMoods, 3000);
// 💻 Floating KidLisp Codes in Stats async function loadFloatingKidlisp() { const statItem = document.getElementById('stat-kidlisp'); if (!statItem) return; const floatersContainer = statItem.querySelector('.stat-floaters'); if (!floatersContainer) return; try { const res = await fetch(`${apiBase}/api/tv?types=kidlisp&limit=100`); if (!res.ok) throw new Error('Failed to fetch kidlisp'); const data = await res.json(); const kidlisps = data.media?.kidlisp || []; // Group by handle, max 4 per user for variety const byHandle = {}; for (const k of kidlisps) { if (!k.code) continue; const h = k.owner?.handle || 'anonymous'; if (!byHandle[h]) byHandle[h] = []; if (byHandle[h].length < 4) byHandle[h].push(k); } // Interleave from different handles const mixed = []; const handles = Object.keys(byHandle); let moreToAdd = true; let idx = 0; while (moreToAdd) { moreToAdd = false; for (const h of handles) { if (byHandle[h][idx]) { mixed.push(byHandle[h][idx]); moreToAdd = true; } } idx++; }
if (!mixed.length) return;
let index = 0; function spawnFloater() { const k = mixed[index % mixed.length]; index++; const floater = document.createElement('div'); floater.className = 'stat-floater'; // Random horizontal position (5% to 95%) const xPos = 5 + Math.random() * 90; floater.style.left = `${xPos}%`; floater.style.bottom = '-20px'; // Random duration between 10-18s const duration = 10 + Math.random() * 8; floater.style.setProperty('--float-duration', `${duration}s`); // Code text const text = document.createElement('span'); text.className = 'floater-code'; text.style.fontSize = '0.8em'; text.style.color = 'var(--magenta)'; text.textContent = `$${k.code}`; floater.appendChild(text); floatersContainer.appendChild(floater); floater.addEventListener('animationend', () => floater.remove()); } spawnFloater(); statsVisibility.register(spawnFloater, 3000); } catch (e) { console.warn('Could not load floating kidlisp:', e); } } setTimeout(loadFloatingKidlisp, 3500);
// 🧩 Floating Commands (pieces + prompts) in Stats async function loadFloatingCommands() { const statItem = document.getElementById('stat-commands'); if (!statItem) return; const floatersContainer = statItem.querySelector('.stat-floaters'); if (!floatersContainer) return; try { // Fetch pieces from API const res = await fetch(`${apiBase}/api/piece-hit?top=50`); if (!res.ok) throw new Error('Failed to fetch pieces'); const data = await res.json(); const pieces = (data.pieces || []).filter(p => { return p.piece && !p.piece.startsWith('api/') && !p.piece.includes('.'); }).map(p => ({ name: p.piece, type: 'piece' })); // Hard-coded prompts (commands you type in prompt) const prompts = [ 'tape', 'cut', 'keep', 'mint', 'print', 'scream', 'me', 'selfie', 'cam', 'flower', 'petal', 'sparkle', 'done', 'tezos', 'notifs', 'bro', 'sis', 'bf', 'gf', 'email', 'handle', 'logout', 'leave', 'join', 'mood', 'profile', 'code', 'upload', 'download', 'delete' ].map(name => ({ name, type: 'prompt' })); // Interleave pieces and prompts const commands = []; const maxLen = Math.max(pieces.length, prompts.length); for (let i = 0; i < maxLen; i++) { if (pieces[i]) commands.push(pieces[i]); if (prompts[i]) commands.push(prompts[i]); }
if (!commands.length) return;
let index = 0; function spawnFloater() { const cmd = commands[index % commands.length]; index++; const floater = document.createElement('div'); floater.className = 'stat-floater'; // Random horizontal position (5% to 95%) const xPos = 5 + Math.random() * 90; floater.style.left = `${xPos}%`; floater.style.bottom = '-20px'; // Random duration between 10-18s const duration = 10 + Math.random() * 8; floater.style.setProperty('--float-duration', `${duration}s`); // Command name - color coded const text = document.createElement('span'); text.className = 'floater-code'; text.style.fontSize = '0.7em'; // Green for pieces (apps), Yellow/gold for prompts (commands) text.style.color = cmd.type === 'piece' ? 'var(--green)' : 'var(--gold)'; text.textContent = cmd.name; floater.appendChild(text); floatersContainer.appendChild(floater); floater.addEventListener('animationend', () => floater.remove()); } spawnFloater(); statsVisibility.register(spawnFloater, 2500); } catch (e) { console.warn('Could not load floating commands:', e); } } setTimeout(loadFloatingCommands, 4000);
// 💬 Floating Messages in Stats async function loadFloatingMessages() { const statItem = document.getElementById('stat-messages'); if (!statItem) return; const floatersContainer = statItem.querySelector('.stat-floaters'); if (!floatersContainer) return; try { // Fetch recent messages from both laer-klokken and main chat const [clockRes, chatRes] = await Promise.all([ fetch(`${apiBase}/api/chat/messages?instance=clock&limit=30`), fetch(`${apiBase}/api/chat/messages?limit=30`) ]); const clockData = await clockRes.json(); const chatData = await chatRes.json(); const clockMsgs = (clockData.messages || []).map(m => ({ ...m, source: 'clock' })); const chatMsgs = (chatData.messages || []).map(m => ({ ...m, source: 'chat' })); // Interleave messages from both sources const allMsgs = []; const maxLen = Math.max(clockMsgs.length, chatMsgs.length); for (let i = 0; i < maxLen; i++) { if (clockMsgs[i]) allMsgs.push(clockMsgs[i]); if (chatMsgs[i]) allMsgs.push(chatMsgs[i]); } if (!allMsgs.length) return;
let index = 0; function spawnFloater() { const msg = allMsgs[index % allMsgs.length]; index++; const floater = document.createElement('div'); floater.className = 'stat-floater float-horizontal'; // Random vertical position (10% to 90%) const yPos = 10 + Math.random() * 80; floater.style.top = `${yPos}%`; floater.style.right = '-80px'; // Random duration between 8-14s const duration = 8 + Math.random() * 6; floater.style.setProperty('--float-duration', `${duration}s`); // Message text (truncated) const text = document.createElement('span'); text.className = 'floater-code'; text.style.fontSize = '0.65em'; text.style.maxWidth = '100px'; text.style.overflow = 'hidden'; text.style.textOverflow = 'ellipsis'; // Yellow for clock messages, cyan for chat text.style.color = msg.source === 'clock' ? 'var(--gold)' : 'var(--cyan)'; // Truncate message const msgText = (msg.text || '').slice(0, 25); const handle = msg.from || 'anon'; text.textContent = `${handle}: ${msgText}${msg.text?.length > 25 ? '…' : ''}`; floater.appendChild(text); floatersContainer.appendChild(floater); floater.addEventListener('animationend', () => floater.remove()); } spawnFloater(); statsVisibility.register(spawnFloater, 2000); } catch (e) { console.warn('Could not load floating messages:', e); } } setTimeout(loadFloatingMessages, 4500); // ====== Auth0 Authentication ====== let auth0Client = null; let acUser = null; let acHandle = null; async function initAuth0() { try { auth0Client = await auth0.createAuth0Client({ domain: 'aesthetic.us.auth0.com', clientId: 'LVdZaMbyXctkGfZDnpzDATB5nR0ZhmMt', authorizationParams: { redirect_uri: window.location.origin + window.location.pathname, audience: 'https://aesthetic.us.auth0.com/api/v2/' }, cacheLocation: 'localstorage', useRefreshTokens: true, useRefreshTokensFallback: true }); // Handle redirect callback if (window.location.search.includes('code=') || window.location.search.includes('error=')) { await auth0Client.handleRedirectCallback(); window.history.replaceState({}, document.title, window.location.pathname); } // Check if logged in locally first let isAuthenticated = await auth0Client.isAuthenticated(); // If not logged in locally, try silent auth to check for existing session from aesthetic.computer if (!isAuthenticated) { try { await auth0Client.getTokenSilently(); isAuthenticated = await auth0Client.isAuthenticated(); } catch (e) { // Silent auth failed - user not logged in on aesthetic.computer either } } if (isAuthenticated) { acUser = await auth0Client.getUser(); // Fetch handle from AC API using email try { const res = await fetch( `https://aesthetic.computer/user?from=${encodeURIComponent(acUser.email)}&withHandle=true` ); if (res.ok) { const data = await res.json(); if (data.handle) { acHandle = data.handle; } } } catch (e) { console.warn('Could not fetch handle:', e); } } updateFooterAuthUI(); } catch (e) { console.error('Auth0 init error:', e); updateFooterAuthUI(); } } function updateFooterAuthUI() { const authButtons = document.getElementById('footerAuthButtons'); const userStack = document.getElementById('footerUserStack'); const userMenu = document.getElementById('footerUserMenu'); const userHandle = document.getElementById('footerUserHandle'); if (acUser) { // Logged in - show handle + logout stack if (authButtons) authButtons.style.display = 'none'; if (userStack) userStack.style.display = 'flex'; if (userMenu) { const displayName = acHandle ? `@${acHandle}` : acUser.email?.split('@')[0] || 'User'; if (userHandle) userHandle.textContent = displayName; // Link to profile if we have a handle if (acHandle) { userMenu.href = `https://aesthetic.computer/@${acHandle}`; } else { userMenu.href = 'https://aesthetic.computer/profile'; } } } else { // Logged out - show auth buttons if (authButtons) authButtons.style.display = 'flex'; if (userStack) userStack.style.display = 'none'; } } async function footerLogin() { if (!auth0Client) await initAuth0(); if (!auth0Client) return; await auth0Client.loginWithRedirect(); } async function footerSignup() { if (!auth0Client) await initAuth0(); if (!auth0Client) return; await auth0Client.loginWithRedirect({ authorizationParams: { screen_hint: 'signup' } }); }
async function getAuthToken() { if (!auth0Client) await initAuth0(); if (!auth0Client) return null; try { return await auth0Client.getTokenSilently(); } catch { return null; } } // Footer auth button event listeners document.getElementById('footerLoginBtn')?.addEventListener('click', footerLogin); document.getElementById('footerSignupBtn')?.addEventListener('click', footerSignup); document.getElementById('footerLogoutLink')?.addEventListener('click', async (e) => { e.preventDefault(); if (!auth0Client) return; await auth0Client.logout({ logoutParams: { returnTo: window.location.origin + window.location.pathname } }); }); // Initialize auth initAuth0(); </script> <!-- 🔄 Development live reload via session-server --> <script> (function() { // Only run in development (localhost) if (window.location.hostname !== 'localhost' && !window.location.hostname.includes('local.')) return; let sessionWs = null; let reconnectInterval = null; function connect() { if (sessionWs && sessionWs.readyState === WebSocket.OPEN) return; if (reconnectInterval) { clearInterval(reconnectInterval); reconnectInterval = null; } const connectionUrl = window.location.hostname.includes('local.') ? 'wss://session.local.aesthetic.computer' : 'wss://localhost:8889';
try { sessionWs = new WebSocket(connectionUrl); } catch (error) { scheduleReconnect(); return; }
sessionWs.onopen = () => { };
sessionWs.onmessage = (e) => { try { const msg = JSON.parse(e.data); if (msg.type === 'reload') { const piece = msg.content?.piece; if (piece === '*refresh*' || piece === 'give.aesthetic.computer' || piece?.includes('give')) { setTimeout(() => window.location.reload(), 150); } } } catch (error) { // Ignore parse errors } };
sessionWs.onerror = () => { };
sessionWs.onclose = () => { sessionWs = null; scheduleReconnect(); }; } function scheduleReconnect() { if (!reconnectInterval) { reconnectInterval = setInterval(connect, 3000); } } // Connect on load connect(); })(); </script>
<script> // Fetch AT Protocol record counts (async function loadATStats() { const PDS = 'https://at.aesthetic.computer'; async function countRecords(collection) { try { // Use describeRepo to get rough stats, or listRecords with limit const res = await fetch(`${PDS}/xrpc/com.atproto.sync.listRepos?limit=1000`); if (!res.ok) return '?'; const data = await res.json(); // Count records across all repos for this collection let total = 0; for (const repo of data.repos || []) { try { const recRes = await fetch(`${PDS}/xrpc/com.atproto.repo.listRecords?repo=${repo.did}&collection=${collection}&limit=100`); if (recRes.ok) { const recData = await recRes.json(); total += (recData.records || []).length; } } catch (e) {} } return total; } catch (e) { console.error('[AT Stats]', e); return '?'; } }
// Simpler approach: just hit the TV API which already aggregates tapes try { const tvRes = await fetch('https://aesthetic.computer/api/tv'); if (tvRes.ok) { const tvData = await tvRes.json(); const tapesEl = document.getElementById('atTapesCount'); if (tapesEl && tvData.tapes) tapesEl.textContent = tvData.tapes.length; } } catch (e) {}
// For paintings/handles, show placeholder for now const paintingsEl = document.getElementById('atPaintingsCount'); const handlesEl = document.getElementById('atHandlesCount'); if (paintingsEl) paintingsEl.textContent = '~1k'; if (handlesEl) handlesEl.textContent = '~100'; })(); </script>
<script> // AT Protocol handle cycling animation (async function initATHandleCycler() { const prefixEl = document.getElementById('atHandlePrefix'); if (!prefixEl) return;
// Sample handles to show (fetched from PDS or fallback) let handles = []; // Try to fetch real handles from the PDS try { const res = await fetch('https://at.aesthetic.computer/xrpc/com.atproto.sync.listRepos?limit=100'); if (res.ok) { const data = await res.json(); // Extract handles from repo DIDs (we'll resolve them or use fallbacks) handles = (data.repos || []) .map(r => r.handle) .filter(h => h && !h.startsWith('did:') && h.endsWith('.aesthetic.computer')) .map(h => h.replace('.aesthetic.computer', '')) .filter(h => h && h !== 'at' && h !== 'art.at') .slice(0, 20); } } catch (e) { }
// Fallback handles if API fails or returns nothing if (!handles.length) { handles = ['jeffrey', 'kai', 'luna', 'zen', 'pixel', 'nova']; }
let currentIndex = 0; const defaultHandle = 'art'; let showingArt = true; // Toggle between user and art const scrambleChars = 'abcdefghijklmnopqrstuvwxyz0123456789';
function renderHandle(handle, isDefault = false) { // Create colored spans for each character with staggered animation delay const charClass = isDefault ? 'at-char-gray' : 'at-char'; return handle.split('').map((char, i) => { const delay = (i * 0.3).toFixed(1); return `<span class="${charClass}" style="animation-delay:-${delay}s">${char}</span>`; }).join(''); }
function scrambleTo(targetHandle, isDefault) { const charClass = isDefault ? 'at-char-gray' : 'at-char'; const maxLen = Math.max(prefixEl.textContent.length, targetHandle.length); let iterations = 0; const maxIterations = 6; const scrambleInterval = setInterval(() => { iterations++; let result = ''; for (let i = 0; i < maxLen; i++) { const targetChar = targetHandle[i] || ''; const progress = iterations / maxIterations; const charSettled = i < targetHandle.length && (iterations > maxIterations - 2 + i * 0.3); if (charSettled || iterations >= maxIterations) { // Show final character if (i < targetHandle.length) { const delay = (i * 0.3).toFixed(1); result += `<span class="${charClass}" style="animation-delay:-${delay}s">${targetChar}</span>`; } } else { // Show random scramble character const randChar = scrambleChars[Math.floor(Math.random() * scrambleChars.length)]; const delay = (i * 0.3).toFixed(1); result += `<span class="${charClass}" style="animation-delay:-${delay}s">${randChar}</span>`; } } prefixEl.innerHTML = result; if (iterations >= maxIterations) { clearInterval(scrambleInterval); } }, 30); }
function cycleHandle() { // Alternate: username -> art -> username -> art if (handles.length === 0) { // No handles, just scramble to art scrambleTo(defaultHandle, true); } else if (showingArt) { // Scramble to next username (colorful) scrambleTo(handles[currentIndex], false); currentIndex = (currentIndex + 1) % handles.length; showingArt = false; } else { // Scramble to art (gray) scrambleTo(defaultHandle, true); showingArt = true; } }
// Initial render - start with gray 'art' prefixEl.innerHTML = renderHandle(defaultHandle, true); prefixEl.classList.add('visible'); showingArt = true; // Show after a short delay setTimeout(() => prefixEl.classList.add('visible'), 500); // Cycle every 2 seconds setInterval(cycleHandle, 2000); })(); </script> <script> // Fetch latest mug for invest section (async function loadInvestMug() { const mugImage = document.getElementById('investMugImage'); const mugIcon = document.getElementById('investMugIcon'); const mugCaption = document.getElementById('investMugCaption'); const mugLink = document.getElementById('investMug'); if (!mugImage || !mugIcon) return; // Use localhost in dev, production otherwise const isDev = window.location.hostname === 'localhost' || window.location.hostname.includes('local'); const baseUrl = isDev ? 'https://localhost:8888' : 'https://aesthetic.computer'; // Update the link href to use correct domain if (mugLink) { mugLink.href = `${baseUrl}/+`; } let mug = null; try { const res = await fetch(`${baseUrl}/api/mugs?limit=20`); if (res.ok) { const data = await res.json(); // Find first mug with a preview image mug = (data.mugs || []).find(m => m.preview); if (mug) { mugImage.src = mug.preview; mugImage.style.display = 'block'; mugIcon.style.display = 'none';
// Update link to go directly to this mug's page if (mugLink && mug.code) { mugLink.href = `${baseUrl}/+${mug.code}`; } // Update caption with real data // Format: "white mug of #CODE in $viaCode" matching mug.mjs // sourceCode is the painting hash (e.g., "sb9"), via is the kidlisp code (e.g., "bop") if (mugCaption) { const colorLower = (mug.color || 'white').toLowerCase(); // Show sourceCode (painting hash) as main code, via is the kidlisp source const displayCode = mug.sourceCode || mug.code; const viaCode = mug.via; // Character-colored code: # in cyan, code chars in light blue const colorizeCode = (code) => { return code.split('').map(c => `<span class="char-code">${c}</span>`).join(''); }; const colorizeVia = (via) => { return via.split('').map(c => `<span class="char-via">${c}</span>`).join(''); }; // Add # prefix for painting codes (short codes are user paintings) const hasPrefix = displayCode && displayCode.length < 8; let codeHtml = hasPrefix ? `<span class="char-hash">#</span>${colorizeCode(displayCode)}` : colorizeCode(displayCode); let captionHtml = `<span class="mug-color">${colorLower}</span><span class="mug-of"> mug of </span><span class="mug-code">${codeHtml}</span>`; if (viaCode) { captionHtml += `<span class="mug-via"> in <span class="char-dollar">$</span>${colorizeVia(viaCode)}</span>`; } mugCaption.innerHTML = captionHtml; } } } } catch (e) { }
// Link always goes to /+ (singular mug route) })(); </script></body></html>