Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
209 kB · 7437 lines
CSS
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438/* One stylesheet. Dark by default, because the game is. */
/* --- the faces ----------------------------------------------------------- Four jobs, four families, self-hosted. Nothing is fetched from a font CDN: a third party in the critical path of the first paint is a third party who can take the page down, and the whole site is otherwise one bucket.
The files are in src/, not public/, so that Vite fingerprints them and they can be served immutable. A relative url() below is what puts them through the asset pipeline; moving them to public/ would serve them uncached on a path this stylesheet then has to hardcode.
Each is the woff2 latin and latin-ext subset from Google Fonts, with that API's own unicode-range kept: the site's copy is ASCII, so latin-ext is downloaded only when a handle or a post has an accent in it.
Licences and versions are in src/fonts/README.md. All four are OFL 1.1, whose text is committed beside them because the licence requires it, and whose reserved font names are why none of the files is subsetted or renamed.
Weights are only the ones the stylesheet asks for. scripts/fonts.test.mjs checks that in both directions, so an unused file fails and so does a weight a rule wants and no face supplies. */
@font-face { font-family: "Chakra Petch"; font-style: normal; font-weight: 600; font-display: swap; src: url("./fonts/chakra-petch-600-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face { font-family: "Chakra Petch"; font-style: normal; font-weight: 600; font-display: swap; src: url("./fonts/chakra-petch-600-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face { font-family: "Chakra Petch"; font-style: normal; font-weight: 700; font-display: swap; src: url("./fonts/chakra-petch-700-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face { font-family: "Chakra Petch"; font-style: normal; font-weight: 700; font-display: swap; src: url("./fonts/chakra-petch-700-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face { font-family: "Barlow Semi Condensed"; font-style: normal; font-weight: 400; font-display: swap; src: url("./fonts/barlow-semi-condensed-400-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face { font-family: "Barlow Semi Condensed"; font-style: normal; font-weight: 400; font-display: swap; src: url("./fonts/barlow-semi-condensed-400-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face { font-family: "Barlow Semi Condensed"; font-style: normal; font-weight: 500; font-display: swap; src: url("./fonts/barlow-semi-condensed-500-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face { font-family: "Barlow Semi Condensed"; font-style: normal; font-weight: 500; font-display: swap; src: url("./fonts/barlow-semi-condensed-500-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face { font-family: "Barlow Semi Condensed"; font-style: normal; font-weight: 600; font-display: swap; src: url("./fonts/barlow-semi-condensed-600-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face { font-family: "Barlow Semi Condensed"; font-style: normal; font-weight: 600; font-display: swap; src: url("./fonts/barlow-semi-condensed-600-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face { font-family: "Barlow Semi Condensed"; font-style: normal; font-weight: 700; font-display: swap; src: url("./fonts/barlow-semi-condensed-700-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face { font-family: "Barlow Semi Condensed"; font-style: normal; font-weight: 700; font-display: swap; src: url("./fonts/barlow-semi-condensed-700-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face { font-family: "Saira Semi Condensed"; font-style: normal; font-weight: 400; font-display: swap; src: url("./fonts/saira-semi-condensed-400-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face { font-family: "Saira Semi Condensed"; font-style: normal; font-weight: 400; font-display: swap; src: url("./fonts/saira-semi-condensed-400-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
@font-face { font-family: "Roboto Mono"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("./fonts/roboto-mono-400-700-latin-ext.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}
@font-face { font-family: "Roboto Mono"; font-style: normal; font-weight: 400 700; font-display: swap; src: url("./fonts/roboto-mono-400-700-latin.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}
:root { /* Dark unless the browser asks for light, and unless the masthead's theme control has been used — that writes data-theme on this element and the two rules below narrow color-scheme to one value.
Every colour that depends on the scheme is a light-dark() pair here rather than a second block of tokens under a media query. A media query cannot see an explicit choice that disagrees with the browser's, and a token that appears in one block and not the other keeps its dark value on a light page — which is the bug the camo editor already had once. */ color-scheme: dark light;
--bg: light-dark(#f4f6fa, #0b1017); --panel: light-dark(#ffffff, #131b26); --line: light-dark(#d8dfea, #22303f); --ink: light-dark(#15202b, #e6edf5); --muted: light-dark(#5a6b80, #8ea0b5); --accent: light-dark(#1f5fd0, #4c8dff); --danger: light-dark(#b3261e, #ff8a7a);
/* What is printed on a filled --accent, and the one token whose two halves are not the same idea. The accent is a dark blue in light mode and a bright one in dark mode, so the ink that reads on it flips with it: white was 5.8:1 on the light accent and 3.2:1 on the dark one, under AA's 4.5:1 and the reason every filled button on the site failed. The page's own dark background on the dark accent is 6.0:1.
Only a filled button uses this. --accent as text or as an outline is unchanged, and so is the blue itself. */ --on-accent: light-dark(#ffffff, #0b1017);
/* The way back into a match that is still going: the same filled link on the bar, in the one colour on the page that is not the site's blue. Green rather than a second blue, because the whole point of it is that a player can tell at a glance that the button is not the one they left. --danger's red is what a failed sign-in and a lost match are written in, and --stamp's amber belongs to the alpha stamp.
--on-resume is --on-accent's trick for --on-accent's reason: white reads on the dark green of light mode and not on the bright one of dark mode, where the page's own background does. contrast.test.mjs pins both pairings, so neither half of either can move on its own. */ --resume: light-dark(#0f7a4a, #35d08a); --on-resume: light-dark(#ffffff, #0b1017);
/* The insignia's teal. Reserved for the masthead, so the brand mark and the site chrome are visibly the same object. */ --brand: light-dark(#0e7a8c, #4fc1d6);
/* The alpha stamp's ink, and nothing else on the page is this colour — which is the point of it having a token rather than borrowing one. Amber and not --danger's red: the site works, it is only unfinished, and the red is what a failed sign-in and a lost match are already written in. */ --stamp: light-dark(#9a5411, #ffb066);
/* Where a firepower curve does its best, and nothing else on the page is this colour either. Orange rather than --accent's blue because the curve itself is drawn in the accent: the mark has to be legible *against* the line it sits on, and a second blue would read as part of it. Not --stamp, which is the alpha stamp's alone - the two are near neighbours and are never on screen together, and a curve outliving the stamp is the point of it having a token of its own. */ --peak: light-dark(#a1560f, #f0a355);
/* A figure that is the *absence* of a problem: a design whose sinks shed more than its guns make. Blue for cool, quieter than --accent so a stat painted in it does not read as something to click, and one step only — the scale it belongs to is warm and hot, and a second gradient at the cold end would make three colours out of a yes-or-no. */ --cool: light-dark(#3d6a99, #8fb4dd);
/* The four jobs the faces above are for. Named by job rather than by family so that swapping one is one line here, which is how this set was chosen: the alternatives were compared by overriding exactly these four.
--font-display is the loud one and is used sparingly — the wordmark, the hero's headline, card titles. --font-labels is the same family at a lighter weight on the small tracked capitals, so the chrome and the headline are visibly one object. --font-text is what sentences are set in, and it is semi-condensed because the hero's copy column is a fixed 28rem. --font-facts is the fact strip only, so those three cards read as plates rather than as more body copy; it is the closest relative to the display face on the page, which is the point.
--font-data is for figures and for anything that is really an address: battle values, the build stamp, the handle field. Tabular, so a column of numbers lines up. */ --font-display: "Chakra Petch", ui-sans-serif, system-ui, sans-serif; --font-labels: "Chakra Petch", ui-sans-serif, system-ui, sans-serif; --font-text: "Barlow Semi Condensed", ui-sans-serif, system-ui, sans-serif; --font-facts: "Saira Semi Condensed", ui-sans-serif, system-ui, sans-serif; --font-data: "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--radius: 6px; --measure: 34rem;
/* How wide the content column is on this page. Four screens set their own value, so this is a per-page number and nothing shared between pages may read it — the masthead used to, and the bar changed width on every navigation. #app is the only thing that gets it. */ --shell: var(--measure);}
/* What the theme control does, and all it does. Narrowing color-scheme is the whole mechanism: every light-dark() on the page resolves against it, form controls and scrollbars included. theme.ts sets the attribute, and the inline script in Base.astro sets it again before the first paint. */:root[data-theme="light"] { color-scheme: light;}
:root[data-theme="dark"] { color-scheme: dark;}
* { box-sizing: border-box;}
body { margin: 0; padding: 0; background: var(--bg); color: var(--ink); /* 1rem, not 16px. Every other size in this file is already relative, so a reader who raises their browser's default text size got a masthead and a footer at the new size around body copy still pinned to 16px — the chrome grew and the words they came for did not. The root element is where that setting lands, and this is the one declaration that was ignoring it.
1rem is 16px unless the reader has said otherwise, so nothing moves for anyone who has not.
1.05rem, not 1rem: --font-text is semi-condensed, so it sets a smaller x-height than the system sans it replaced and reads a step down at the same declared size. This is the step back. */ font: 1.05rem/1.55 var(--font-text); display: flex; flex-direction: column; align-items: center;}
/* --- the type ------------------------------------------------------------ Which of the four faces each part of the page is set in. Grouped here rather than spread across the rules that own everything else about those parts, because the grouping is the decision: four families over one page only holds together if the boundaries between them can be read in one place. Sizes, weights and tracking stay with their components.
The lists are closed on purpose. Anything not named here inherits --font-text from body, which is the right default for a sentence and the wrong one for a small tracked capital — a new label belongs in the second list. */
/* The loud face: the wordmark, the hero's headline, every card title. */.brand-name,.hero-copy h1,.page-head h1,.profile-name,.daily h2,.signin h2,.card h2,.mode-card h3,.post-list h2,.post :is(h2, h3, h4) { font-family: var(--font-display);}
/* The same family on the small tracked capitals, which is most of the chrome: the bar, the eyebrows, the stat captions, the alpha stamp. */.nav a,.nav .soon,.account-button,.theme-button,.theme-label,.stamp,.daily-eyebrow,.daily-soon,.daily-label,.pilot-label,.fact h2 { font-family: var(--font-labels);}
/* The fact strip's copy, and nothing else. Its headings are labels and stay in the label face above, so what changes is the three paragraphs: a second text face marks them as captions on the cards rather than a continuation of the hero's sentence. 0.92rem because Saira runs a touch smaller than Barlow. */.fact p { font-family: var(--font-facts); font-size: 0.92rem;}
/* Figures, and everything that is really an address rather than prose. Six of these carried a system monospace of their own until this list existed, which is how the site came to have two: one for the numbers on the front page and another for a DID on the pilot card.
The :not() is the era tile on the daily card. It is a .daily-value like the two battle values beside it — same class, same size, same weight — but what it holds is "FedCom Civil War", and a proper noun in the figures face is both wrong and wide enough to wrap the tile onto two lines. */.daily-value:not(.daily-era),.pilot-value,.pilot-did,.pilot-hover-did,.match-id,.camo-value,.camo-unit-kind,.dead-path,.signin input,.typeahead input,footer .debug { font-family: var(--font-data); font-variant-numeric: tabular-nums;}
/* The reading measure is right for a page of text and too narrow for three preview blocks side by side, so the editor screen widens the shell. Still set on body rather than on #app because the screens are told apart by what they render, and body is the only element above all of them. */body:has(.camo-editor) { --shell: 70rem;}
/* A player's page is a picture beside a column of type, and the reading measure is the width of the column alone: at that width the handle and the portrait fight over the same 30rem. */body:has(.profile) { --shell: 46rem;}
/* A match reads across one line — id, status, when, the way in — and the reading measure is not wide enough to hold four things without them colliding. */body:has(.matches) { --shell: 46rem;}
/* Play, keyed on the Modes rather than on the list above them. The list is the same one the matches played list shows and wants the same measure, but it takes itself away when nothing is live — so a width that depended on it would snap back to the reading measure at exactly the moment the daily card moves to the top of the screen. The Modes are on Play, only on Play, and never removed. */body:has(.mode-rows) { --shell: 46rem;}
/* And wider when the daily card is up. The card is a board, a column of text and the ground tile side by side, and 46rem folds the tile onto the blurb; it wants the room the front page gives it. Nothing scheduled means no card to make room for, and Play drops back to the line above. */body:has(.mode-rows):has(.daily) { --shell: 56rem;}
/* And the archive of past challenges, which is a stack of that same card and wants the same room. Without this it fell back to the reading measure: the text side of the card holds its 24rem floor, so the whole of the missing width came off the board, and every fight was drawn in a column narrower than one machine. */body:has(.daily-archive) { --shell: 56rem;}
/* Hangar is two panes with a strip across both: a library of designs, each a sprite and a row of figures, beside the force being built out of them. The reading measure fits neither, and at 46rem the strip's segments are too narrow to carry a name. */body:has(.hangar) { --shell: 70rem;}
/* The challenge screen holds a scenario list with a drawn preview beside it and a slot list with a callout beside that; the reading measure would stack all four. */body:has(.challenge) { --shell: 60rem;}
/* Home is a composition, not a page of text: it needs room for the copy at its reading measure and the insignia beside it. Signed in or out — the pilot card stands where the sign-in form stands, so the panel is the same shape. */body:has(.hero) { --shell: 56rem;}
/* A stack of blocks with one gap between them, so no screen has to remember to space its own cards. */#app { display: flex; flex-direction: column; gap: 1rem; width: 100%; max-width: var(--shell); padding: 2rem 1.25rem 4rem;}
#app[aria-busy="true"]::after { content: "Loading…"; color: var(--muted);}
/* render() parks focus here when a screen change destroyed whatever held it, so a screen reader names the new page and the next Tab carries on from it. Neither is a control the player aimed at, so neither gets a ring. Only tabindex="-1" is quietened: anything reachable by Tab keeps its own. */#app[tabindex="-1"]:focus,#app h1[tabindex="-1"]:focus { outline: none;}
/* --- site banner --------------------------------------------------------- One notice above the masthead, when site-banner.ts has one to give. It is not chrome: it does not stick, it scrolls away with the top of the page, and closing it takes it off this browser for good.
The alpha stamp's amber, which is the site's colour for "this is unfinished" and already the only thing on the page wearing it. A banner in --accent would read as something to do; a banner in --danger would read as something wrong. This is neither. */
.site-banner { display: flex; /* The body is a centred column, so an ordinary block in it is as wide as the content and not as wide as the window. The masthead states the same width for the same reason. */ width: 100%; align-items: baseline; justify-content: center; gap: 0.75rem; padding: 0.5rem 1.25rem; background: color-mix(in srgb, var(--stamp) 12%, var(--bg)); border-bottom: 1px solid color-mix(in srgb, var(--stamp) 35%, transparent); font-family: var(--font-text); font-size: 0.95rem;}
.site-banner-text { margin: 0; color: var(--ink);}
.site-banner-link { margin-left: 0.5ch; color: var(--stamp); font-weight: 600;}
/* A glyph, not a word: the word is there for a screen reader and the button is 2rem square for a thumb. */.site-banner-close { flex: none; padding: 0; width: 2rem; height: 2rem; background: none; border: 0; color: var(--muted); font-size: 1.2rem; line-height: 1; cursor: pointer;}
.site-banner-close:hover { color: var(--ink);}
/* --- masthead ------------------------------------------------------------ Insignia, wordmark and nav on one line, sticky, about 3.5rem tall. It is the only chrome on the page, so it carries the brand on its own: the mark, the name with the teal tld, and a teal seam along the bottom edge that starts under the mark and fades out across the bar.
Edge to edge, and the same on every screen. The bar used to take the page's --shell so the wordmark lined up with the content below it, which worked while there was one content width; there are four now, so it gave four mastheads and the chrome jumped sideways on every navigation. Nothing under 3.5rem tall is worth that, and a fixed inner width of its own would only pick one screen's measure to agree with. Below 34rem this is what the bar already looked like, so the narrow rules are unchanged. */
.masthead { position: sticky; top: 0; z-index: 10; width: 100%; /* A touch lighter than the page at the top, fading back into it, so the bar reads as a surface the content scrolls under. */ background: linear-gradient( 180deg, color-mix(in srgb, var(--panel) 60%, var(--bg)), var(--bg) ); border-bottom: 1px solid var(--line);}
.masthead-inner { display: flex; align-items: stretch; justify-content: space-between; gap: 1rem; width: 100%; min-height: 3.5rem; padding: 0 1.25rem;}
/* The nav, the theme control and the account travel together at the right-hand end, so they sit after the last destination rather than the nav being pushed to the middle of the bar between two things. */.masthead-end { display: flex; align-items: stretch; gap: 0.6rem;}
/* The seam: teal under the wordmark, gone a little past it. It used to hang off the inner so that it would start under the insignia rather than out in the bar's margin; the inner has no margin now, so it sits on the bar itself.
Fixed lengths, not percentages. 10rem and 24rem are what 30% and 70% came to on the 34rem bar the fade was drawn against; as percentages of a bar that is now as wide as the window they stretched into a teal wash across the whole top of the page. */.masthead::after { content: ""; position: absolute; inset: auto 0 -1px; height: 1px; background: linear-gradient( 90deg, var(--brand) 0, color-mix(in srgb, var(--brand) 40%, transparent) 10rem, transparent 24rem );}
.brand { display: flex; align-self: center; align-items: center; gap: 0.55rem; color: inherit; text-decoration: none;}
.brand-mark { display: block; width: auto; height: 2rem;}
/* Capitals, to match the hero's headline: the bar and the hero are the first two things read on the page, and one in caps and one not reads as two decisions. 650 is gone with the variable face it was interpolated from — --font-display ships 600 and 700, and 700 is the weight the headline uses. */.brand-name { font-size: 0.95rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;}
.brand-tld { color: var(--brand);}
.nav { display: flex; align-items: stretch;}
/* The bar is three runs of links, not one row: Play, the things a player manages between games, and the site's own pages. What sets a run apart is in destinations.ts as `separator`, and drawn here — a plain gap for the first boundary, a hairline for the second. The hairline is the stronger of the two on purpose: the reader is crossing from the game to the site. */.nav-group { display: flex; align-items: stretch; gap: 0.1rem;}
/* One split control: the filled way into a game with the day's challenge on its end, drawn as two links against each other rather than with the group's own hairline gap between them. Both halves go somewhere - there is no menu to open - so the second is a link like the first and not a button.
The segment is outlined rather than filled: account.ts recolours the prominent half to green when a match is still going, and a second filled half would either follow it there or sit beside it in the wrong blue. An outline is neither. */.nav-group[data-split] { gap: 0;}
/* The notches make the joint: the filled half keeps the top-left one and gives up the corner the segment starts at, and the segment takes the bottom-right. Together they are the shape one plate would have had. */.nav-group[data-split] a.prominent { margin-right: 0; clip-path: polygon(var(--notch) 0, 100% 0, 100% 100%, 0 100%, 0 var(--notch));}
.nav a.attached { align-self: center; padding: 0.3rem 0.7rem; border: 1px solid var(--line); border-left: 0; color: var(--ink); font-size: 0.7rem; clip-path: polygon( 0 0, 100% 0, 100% calc(100% - var(--notch)), calc(100% - var(--notch)) 100%, 0 100% );}
.nav a.attached:hover { color: var(--accent);}
.nav a.attached[aria-current="page"] { border-bottom-color: var(--line); color: var(--accent);}
.nav-group[data-separator="space"] { margin-left: 0.9rem;}
.nav-group[data-separator="rule"] { position: relative; margin-left: 0.9rem; padding-left: 0.9rem;}
/* Short and centred rather than full-height: a rule the height of the bar would read as a border between two panels. */.nav-group[data-separator="rule"]::before,.account::before { content: ""; position: absolute; left: 0; top: 50%; width: 1px; height: 1.1rem; transform: translateY(-50%); background: var(--line);}
/* Full-height hit targets, so the current-page marker can sit on the bar's own bottom edge rather than floating above it. */.nav a { display: flex; align-items: center; padding: 0 0.7rem; margin-bottom: -1px; border-bottom: 2px solid transparent; color: var(--muted); font-size: 0.76rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; text-decoration: none;}
/* `display: flex` above beats the browser's own rule for [hidden], so a destination the layout shipped hidden would be on the bar for everyone. */.nav a[hidden] { display: none;}
/* A destination with nothing behind it yet. Sized and spaced exactly like the links beside it — it is a section of the site, named early on purpose — and then stamped, which says in words what the dashed underline here used to say in a line nobody has to read as anything.
Not a link and not focusable, so there is nothing to click and nothing for a keyboard to land on; the label carries "(not ready yet)" for a screen reader, because a stamp says nothing to one.
A column, so the stamp goes under the name rather than across it. The label is not dimmed: nothing lies over it now, and `--muted` under an opacity is how this site's readable text has stopped being readable before. What says the destination is not a destination yet is the stamp, in words. */.nav .soon { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 0 0.7rem; color: var(--muted); font-size: 0.76rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; cursor: default;}
.nav a:hover { color: var(--ink);}
.nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--brand);}
.brand:focus-visible,.nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 3px;}
/* The one link that starts a game rather than reading about one. Filled like the global `button` rule (same --accent/--on-accent pairing, same radius) instead of the plain uppercase links beside it, but sized to the bar: it does not stretch full height or claim the page-scale button's own padding. */.nav a.prominent { align-self: center; margin: 0 0.2rem; padding: 0.3rem 0.85rem 0.3rem 1rem; border: 1px solid transparent; color: var(--on-accent); background: var(--accent); --plate-ring: var(--on-accent);}
.nav a.prominent:hover { color: var(--on-accent); opacity: 0.85;}
.nav a.prominent[aria-current="page"] { border-bottom-color: transparent;}
.nav a.prominent:focus-visible { outline: 0; box-shadow: inset 0 0 0 2px var(--plate-ring);}
/* The same link when the player has a match still going. account.ts adds the class and swaps the label with it, so the colour and the word change together: the fill is what says at a glance that the bar is not what it was, and "Resume" is what says it to a screen reader. */.nav a.prominent.resume { color: var(--on-resume); background: var(--resume);}
.nav a.prominent.resume:focus-visible { --plate-ring: var(--on-resume);}
/* Narrow: everything shrinks a step, but every label stays written out. Seven destinations still do not need a menu behind a button — Operations was the eighth, and is in the account menu now.
This used to start at 30rem, which was where five labels stopped fitting. Seven of them, three as long as Leaderboards, run out of room a good deal sooner. */@media (max-width: 52rem) { .masthead-inner { padding: 0 0.9rem; }
.brand-mark { height: 1.75rem; }
.brand-name { font-size: 1rem; }
.nav a, .nav .soon { padding: 0 0.45rem; font-size: 0.7rem; letter-spacing: 0.05em; }
/* The groups keep their separators here; what they give up is the room around them, which is the part that costs a label its place. */ .nav-group[data-separator="space"], .nav-group[data-separator="rule"] { margin-left: 0.5rem; }
.nav-group[data-separator="rule"] { padding-left: 0.5rem; }}
/* A second line. A signed-in player's eight labels and the account control together are wider than the bar well before a phone, tightening the type bought a label or two of that back and never all of it, and the labels that fell off the right-hand edge fell off silently — there was no scrollbar and no menu, they were simply gone.
So the bar wraps: who is here stays on the first line with the mark, because that is the one thing that has to be reachable from every screen, and the destinations take the line under it. It costs about 2rem of height and it is the whole nav rather than as much of the nav as fitted. A menu behind a button is still the better answer eventually; this is the one that is not a menu nobody can find. */@media (max-width: 68rem) { .masthead-inner { flex-wrap: wrap; align-items: center; padding: 0.35rem 0.9rem; }
/* The bar's right-hand end stops being a box of its own here, so the nav can wrap under the mark rather than under the account control: `contents` hands the nav, the theme control and the account to .masthead-inner as its own items, and the order below is what puts them back in bar order. */ .masthead-end { display: contents; }
/* Both against the right-hand edge, with the mark left where it is. */ .theme { order: 1; margin-left: auto; }
.account { order: 2; }
.nav { order: 3; flex-basis: 100%; /* Wraps again inside itself on a phone, where one line of ten labels is still too many: the groups break across lines rather than off the edge. */ flex-wrap: wrap; row-gap: 0.2rem; justify-content: flex-end; padding-top: 0.15rem; }}
/* One step tighter again. A line of its own is not enough on a phone: ten labels on it are still wider than a 360px screen, and the ones past the edge are as gone as they were before the bar wrapped. */@media (max-width: 40rem) { .nav a, .nav .soon { padding: 0 0.25rem; font-size: 0.66rem; letter-spacing: 0; }
.account-button { padding: 0 0.3rem; font-size: 0.66rem; letter-spacing: 0; }}
/* Below this the wordmark and the nav genuinely collide, so the mark carries the brand on its own — it is the same insignia, at the same place. Five labels run out of room sooner than three did, hence 26rem rather than 22. */@media (max-width: 26rem) { .brand-name { display: none; }
.masthead-inner { padding: 0.35rem 0.5rem; }
/* The last of the spacing the groups are set apart by. The rule stays: it is one pixel wide and it is the only thing saying where the game ends and the site begins. */ .nav-group[data-separator="space"], .nav-group[data-separator="rule"] { margin-left: 0.3rem; }
.nav-group[data-separator="rule"] { padding-left: 0.3rem; }}
/* --- the account control ------------------------------------------------- The right-hand end of the masthead: Login, or the avatar of whoever is signed in with a menu under it. Built by account.ts on every page.
The box is reserved at its signed-out width so the nav does not jump sideways when the session comes back.
Positioned, and with a left margin, for the hairline it carries: who is here is the last thing on the bar and is set off from the rest of it the same way the site's pages are set off from the game's. The rule itself is drawn with the nav's, up in the masthead block. */.account { position: relative; display: flex; align-items: stretch; justify-content: flex-end; min-width: 4.5rem; margin-left: 0.3rem; padding-left: 0.9rem;}
.account-control { position: relative; display: flex; align-items: stretch;}
/* Sized to the nav rather than to the page: the same uppercase label, the same full-height hit target, and none of the global button's fill — this is one more thing on the bar, not a call to action sitting on it. */.account-button,.theme-button { display: flex; align-self: stretch; align-items: center; gap: 0.45rem; padding: 0 0.7rem; border: 0; border-radius: 0; background: none; color: var(--muted); font-size: 0.76rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;}
.account-button:hover,.theme-button:hover { color: var(--ink);}
.account-button:focus-visible,.theme-button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 3px;}
/* A handle is a domain name and can be long. It is the avatar that identifies the account here; the text is a courtesy and gives way first. */.account-handle { /* Enough for a handle of about twenty characters, which is what a domain-shaped one comes to. Anything longer is cut rather than allowed to push the nav across the bar; the menu under it writes the name out. */ max-width: 10rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-transform: none; letter-spacing: 0; font-size: 0.8rem;}
.avatar { display: grid; place-items: center; flex: 0 0 auto; width: 1.75rem; height: 1.75rem; overflow: hidden; border: 1px solid var(--line); border-radius: 50%; background: var(--panel);}
.avatar-image { width: 100%; height: 100%; object-fit: cover;}
/* What stands in until the picture has decoded, and what stays for an account that has no picture at all. */.avatar-monogram { color: var(--ink); font-size: 0.8rem; font-weight: 700; letter-spacing: 0;}
/* Both panels that hang off the bar. Same surface, same shadow, same edge — they are two of one thing, and the account's is the one that sets the width. */.account-menu,.theme-menu { position: absolute; top: 100%; right: 0; z-index: 20; display: flex; flex-direction: column; align-items: stretch; gap: 0.15rem; min-width: 12rem; max-width: min(16rem, calc(100vw - 1.8rem)); padding: 0.4rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); box-shadow: 0 12px 28px rgb(4 8 14 / 0.45);}
.account-menu[hidden],.theme-menu[hidden] { display: none;}
/* Whose account this is. Not a link, and it wraps rather than being cut off: an unverified account is named by its DID here, and half a DID is no use. */.account-who { margin: 0; padding: 0.35rem 0.5rem 0.45rem; border-bottom: 1px solid var(--line); color: var(--muted); font-size: 0.78rem; overflow-wrap: anywhere;}
/* The rows are a link and a button, so both are written down to the same plain thing rather than either being left to its own defaults. */.account-item { align-self: stretch; width: 100%; padding: 0.4rem 0.5rem; border: 0; border-radius: 4px; background: none; color: var(--ink); font: inherit; font-size: 0.85rem; font-weight: 500; text-align: left; text-decoration: none;}
.account-item:hover { background: color-mix(in srgb, var(--ink) 8%, transparent);}
.account-signout { margin-top: 0.15rem; border-top: 1px solid var(--line); border-radius: 0 0 4px 4px; color: var(--muted);}
.account-menu .error { margin: 0.25rem 0 0; padding: 0 0.5rem; font-size: 0.75rem;}
/* Narrow: the avatar is the control. A handle beside it costs more room than the nav has, and the menu still names the account. */@media (max-width: 34rem) { .account-handle { display: none; }}
@media (max-width: 30rem) { .masthead-end { gap: 0.25rem; }
.account { min-width: 0; }
.account-button, .theme-button { padding: 0 0.45rem; font-size: 0.7rem; letter-spacing: 0.05em; }}
/* --- the theme control --------------------------------------------------- Its neighbour on the bar, and deliberately quieter: an icon with no label, because the account is who you are and this is only how the page is drawn. It is on every page, the blog's included, and it never waits on a session — which is the whole reason it is no longer a row in the account menu. */.theme { display: flex; align-items: stretch;}
.theme-control { position: relative; display: flex; align-items: stretch;}
.theme-button { /* No text to space out, so the label's tracking would only widen the box. */ padding: 0 0.5rem; letter-spacing: 0;}
.theme-icon { display: block; width: 1.05rem; height: 1.05rem;}
/* Narrower than the account's: one label and three short words is all that is in it, and matching a menu holding a handle would leave it half empty. */.theme-menu { min-width: 10.5rem;}
/* A label above the choices rather than beside them: "System" on its own says nothing about what it sets, and the group needs the name either way. */.theme-rows { display: flex; flex-direction: column; gap: 0.3rem; padding: 0.35rem 0.5rem 0.4rem;}
.theme-label { color: var(--muted); font-size: 0.72rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;}
/* One segmented control, so the three buttons read as one choice. The gap is drawn by the container's own background showing through. */.theme-choices { display: flex; gap: 1px; overflow: hidden; border: 1px solid var(--line); border-radius: 4px; background: var(--line);}
.theme-choice { flex: 1; padding: 0.3rem 0.25rem; border: 0; border-radius: 0; background: var(--panel); color: var(--muted); font: inherit; font-size: 0.78rem; text-align: center;}
.theme-choice:hover { color: var(--ink); background: color-mix(in srgb, var(--ink) 8%, var(--panel));}
.theme-choice[aria-pressed="true"] { background: color-mix(in srgb, var(--accent) 18%, var(--panel)); color: var(--ink); font-weight: 600;}
.theme-choice:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;}
/* --- the alpha stamp ----------------------------------------------------- One word, in three sizes, in the three places alpha-stamp.ts describes. A rubber stamp rather than a chip: ruled edge, tracked-out capitals, set at an angle to whatever it is sitting on, and translucent enough that the surface under it shows through the way paper shows through ink.
Every length is in em, so the whole mark scales from font-size alone and a variant sets one number instead of five. */
.stamp { display: inline-block; padding: 0.15em 0.45em 0.1em; border: 0.14em solid currentColor; border-radius: 0.2em; color: var(--stamp); font-size: 0.62rem; /* 700, not 800: --font-labels tops out at 700, and a weight the family does not have is synthesised rather than refused — the regular smeared sideways. fonts.test.mjs is what caught this one. */ font-weight: 700; line-height: 1.3; letter-spacing: 0.24em; /* The tracking is added after the last letter as well, so the word sits left of centre in its own box. The indent pays that back. */ text-indent: 0.24em; text-transform: uppercase; white-space: nowrap; transform: rotate(-6deg); opacity: 0.85;}
/* Beside the wordmark, and the one copy on the page a screen reader is given. It stays at the narrowest width, where the wordmark itself is dropped: the mark and the stamp are a smaller thing to fit than the name was, and the status is worth more room than the name it is stamped on. */.brand-block { display: flex; align-items: center; gap: 0.5rem;}
/* The bar is a flex row and the stamp is the smallest thing in it, so it is the first thing a browser would take the space out of. It is a fixed mark: what gives instead is the wordmark, which the narrow rules drop outright. */.stamp-brand { flex: none;}
/* Over the landing panel's top corner, across the cropped insignia. Big enough to read as stamped on the composition rather than pinned to it, and given the second rule that a stamp this size can carry without the two lines closing up. */.stamp-hero { position: absolute; top: clamp(1.1rem, 3vw, 2rem); right: clamp(1.1rem, 3vw, 2.25rem); /* Above the mark, which is at -1 inside the hero's own stacking context. */ z-index: 1; padding: 0.2em 0.6em 0.15em; font-size: clamp(0.78rem, 1.7vw, 1.05rem); outline: 0.08em solid currentColor; outline-offset: 0.18em; transform: rotate(-9deg); pointer-events: none;}
/* Above the version line, at the bottom of every page. Barely off square: the footer is a stack of short lines and a hard angle down there reads as a mistake rather than as the mark at the top of the page. */.stamp-foot { margin-bottom: 0.45rem; transform: rotate(-3deg);}
/* Phone: the hero's copy is a column the full width of the panel and the insignia has moved into the corner the stamp wants, so the big one goes. The masthead's is on the same screen, two lines above. */@media (max-width: 40rem) { .stamp-hero { display: none; }}
/* --- coming soon --------------------------------------------------------- The same rubber stamp, saying which pieces of the site are not there rather than that the site as a whole is unfinished. Four places, each of them a promise somebody could otherwise act on: the front-page facts that describe something nothing has been built for, the Modes on Play that cannot be started, the pilot card's figures that nothing reports, and the bar's two destinations with no address behind them. soon-stamp.ts and layouts/Base.astro write them.
--muted and not --stamp's amber. The amber is the alpha stamp's alone, and every "there is nothing here yet" mark the site already had — the future mode cards on Play, the pilot's unrecorded figures — is in the muted ink. Two stamps in two colours on one page also keep saying two things; two stamps in one colour would read as one mark repeated. */
.stamp-soon { color: var(--muted);}
/* Over a fact that is not true yet — across the middle of the card, over the words rather than tucked into a corner beside them. Ink on paper: the mark is on the plate, and the plate keeps saying what it said.
Sized off the card and not off the viewport: the strip is three columns wide at the top of this file and one column on a phone, so the card gets *wider* as the window narrows and a vw would shrink the mark exactly where it has the most room. `cqw` is a percentage of the card, which is the thing the stamp has to span, so one number holds at every column count. The stamp is about 11.6 times as wide as its type, so 5.6cqw lands it at two thirds of the card.
Given its own ground rather than laid straight over the sentence. Two marks in --muted crossing each other is two unreadable marks: the words under the stamp go, which is what a stamp does to a page, and every word still visible keeps the contrast it had. Nothing is dimmed — `--muted` under an opacity is how this site's readable text has stopped being readable before, and contrast.test.mjs exists to catch exactly that.
The ground is short of opaque on purpose. A ghost of the sentence through it is what makes this ink on paper rather than a sticker sat on top. */.fact-soon { position: relative; container-type: inline-size;}
.stamp-fact,.stamp-mode { position: absolute; top: 50%; left: 50%; /* Above the card's own text, which is in the normal flow at auto. */ z-index: 1; padding: 0.25em 0.5em 0.2em; background: color-mix(in srgb, var(--panel) 88%, transparent); transform: translate(-50%, -50%) rotate(-9deg); opacity: 1; pointer-events: none;}
.stamp-fact { font-size: clamp(0.72rem, 5.6cqw, 1.1rem);}
/* Over a Mode on Play. A smaller share of a smaller card: a Mode is a heading and one short line, so a mark at the facts' two thirds would be the card. The floor is lower for the same reason — two of these sit in a row that wraps to one card per line, and the card is at its narrowest well before the strip's is. */.stamp-mode { font-size: clamp(0.62rem, 4.2cqw, 0.92rem);}
/* Under the caption on a pilot-card figure nothing records. The one place the mark does not lie over anything: four of these sit across the card, so a tile is a number and a six-letter caption wide and has no middle to cross. It goes in the flow under the caption instead, at the smallest size the stamp is drawn at anywhere. */.stamp-tile { margin-top: 0.15rem; padding: 0.12em 0.3em 0.08em; border-width: 0.14em; font-size: 0.4rem; letter-spacing: 0.06em; text-indent: 0.06em; transform: rotate(-4deg);}
/* Under the label, in the flow, the way the pilot card's tiles carry it. A destination is one short word and the stamp is wider than the word, so there is no way to lay this one across the name and still have the name: turned flat it covered the word outright, and turned up on its side at 70 degrees it struck through the middle of it and through whatever the bar had wrapped underneath — LEADERBOARDS and OBSERVE were unreadable at every width, and on a wrapped bar the two marks came down on ABOUT and UPDATES on the row below. A stamp that hides a label says less than no stamp at all.
So it goes under, and the rule is that it never covers text: this and `.stamp-tile` are the two the surface has no middle to spare, and both sit in the flow.
One line, and small enough that "Coming soon" is about as wide as the word it sits under, so the bar keeps the width its labels ask for. What it costs instead is height — one short line under two of the ten — which the bar has and the row does not. Barely off square, like the footer's: a hard angle on a mark this small reads as a mistake. */.stamp-nav { margin-top: 0.15rem; padding: 0.1em 0.3em 0.05em; border-width: 0.12em; color: var(--muted); font-size: 0.42rem; line-height: 1.2; letter-spacing: 0.06em; text-indent: 0.06em; white-space: nowrap; transform: rotate(-3deg); opacity: 1; pointer-events: none;}
/* The bar's own narrow step, kept here rather than beside the masthead: `.stamp` is declared further down this stylesheet than the masthead is, so a rule up there that names nothing but .stamp-nav is overwritten by the base mark before a browser ever paints it. The alpha stamp's phone rule sits in this section for the same reason. */@media (max-width: 40rem) { .stamp-nav { font-size: 0.4rem; letter-spacing: 0.04em; text-indent: 0.04em; }}
/* Phone: the stamp steps aside, the way the hero's does, and the dashed underline it replaced comes back to mark the label on its own.
In the flow the mark costs width as well as height, and down here there is none of either to give: ten labels are already wider than the screen, and "Coming soon" is wider than "Observe" once the labels have given up their letter-spacing. It is also, by this size, type too small to be words. The dashes say the same thing in the room there is. A screen reader is told either way, by the "(not ready yet)" after the label, which is the copy that does not depend on how wide anything is.
The underline is on the text rather than on the element's bottom edge: the bar's bottom edge is where the current-page marker lives, and a dashed line down there reads as a half-marked current page. */@media (max-width: 30rem) { .stamp-nav { display: none; }
.nav .soon-label { text-decoration: underline dashed; text-underline-offset: 0.35em; text-decoration-thickness: 1px; }}
/* --- page heads ---------------------------------------------------------- For the screens that have something to say above their content: the sign-in pitch, and the ones that have to explain what went wrong. A screen whose title would only repeat the nav item the player clicked has no page head at all, and puts its h1 in .visually-hidden instead. */
.page-head { margin: 0 0 0.5rem;}
.page-head h1 { margin: 0; font-size: 1.65rem; line-height: 1.25; letter-spacing: -0.02em; text-wrap: balance;}
.page-head .lead { margin: 0.5rem 0 0; color: var(--muted); max-width: var(--measure); text-wrap: pretty;}
/* --- the landing hero ---------------------------------------------------- One framed panel. The copy and the sign-in form sit in a column on the left; the insignia is oversized, hung off the top and right, and cropped by the frame — which is the whole composition. It is deliberately not a centred mark over a centred headline.
The mark is masked so it fades out towards the copy: that keeps it from sitting behind anything anyone has to read, without needing a scrim over the artwork. */
.hero { position: relative; /* A stacking context of its own, so the mark's negative z-index stays inside this panel. The panel deliberately does not clip: the handle field's suggestions hang below it and an overflow rule here would cut them off. What crops the insignia is .hero-mark. */ isolation: isolate; min-height: 25rem; padding: clamp(1.5rem, 4vw, 2.75rem); border: 1px solid var(--line); border-radius: calc(var(--radius) * 2); background: radial-gradient( 82% 110% at 84% 22%, color-mix(in srgb, var(--brand) 15%, transparent), transparent 62% ), linear-gradient( 158deg, color-mix(in srgb, var(--panel) 88%, var(--brand)), var(--bg) 74% );}
/* The frame that does the cropping: the panel's own shape, clipping whatever the insignia hangs over the edge. Behind the copy but in front of the panel's background — a negative z-index child paints after the parent's background and before its text. */.hero-mark { position: absolute; inset: 0; z-index: -1; overflow: hidden; border-radius: inherit; pointer-events: none; /* Fades out towards the copy, which keeps the artwork from sitting behind anything anyone has to read without needing a scrim over it. */ mask-image: linear-gradient( 100deg, transparent 0 24%, rgb(0 0 0 / 0.5) 48%, #000 76% );}
/* A quarter of the patch is off the right edge on purpose: the frame cuts through the hex rather than framing it, which is the difference between a cropped mark and a big logo sitting in the corner. */.hero-insignia { position: absolute; display: block; top: 2%; right: -25%; height: 104%; width: auto; /* Behind the copy in every sense: it is the loudest thing in the panel at full strength, and the words have to win. */ opacity: 0.92;}
/* The light the system sits in. */.hero-mark::before { content: ""; position: absolute; inset: 12% -8% 12% 46%; border-radius: 50%; background: radial-gradient( closest-side, color-mix(in srgb, var(--brand) 26%, transparent), transparent ); filter: blur(1.75rem);}
.hero-copy { position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: 1.15rem; max-width: 28rem;}
/* The floor of the clamp is in rem, so it grows with the reader's text size while the viewport does not: at 200% text on a 320px phone "Sensors," alone is wider than the screen, and one word that cannot break took the whole document sideways with it.
`anywhere` and not `break-word`, which was tried first and did nothing: both break a word only when it will not fit, but only `anywhere` lets that break count towards the intrinsic width. `break-word` leaves the h1's min-content width at the longest word, so the element — and the page with it — is still sized by "Sensors,". Capping the size against vw instead would have kept the word whole by refusing the reader the text size they asked for, which is the thing this stylesheet is not allowed to do.
It is not pretty where it fires: at 200% text on a phone the headline breaks mid-word. Nothing fires at an ordinary text size, where the words fit and the headline is unchanged. `hyphens: auto` was tried to soften it and did nothing measurable in the browser this was checked in, so it is not here pretending to. */.hero-copy h1 { margin: 0; max-width: 18ch; /* Capitals, which is where --font-display's squared drawing shows: the same word in lowercase reads as a typeface with an accent, and in caps it reads as something stencilled onto a panel. The tracking goes positive for it — caps need air where the lowercase needed pulling together. */ text-transform: uppercase; font-size: clamp(1.9rem, 4.6vw, 2.6rem); line-height: 1.1; letter-spacing: 0.02em; text-wrap: balance; overflow-wrap: anywhere; /* Ink at the top falling towards the brand teal at the bottom, filled through the glyphs. Shallow on purpose: at 30% teal it reads as light across metal rather than as coloured type, and it is the one thing on the page that ties the headline to the insignia behind it.
-webkit-text-fill-color is what actually paints the transparent glyphs, and it is inherited — which is why .hero-em below has to set it back rather than only setting color. Both prefixed and unprefixed background-clip: the unprefixed property is what browsers support now, the prefixed one is what older Safari answers to. */ background: linear-gradient( 172deg, var(--ink) 8%, color-mix(in srgb, var(--ink) 30%, var(--brand)) 98% ); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent;}
/* The same teal dash as the masthead's seam and the fact strip's headings, at the top of the page. It is a block child of the h1 rather than a rule above it so that it moves with the headline at every width, and it keeps its own background: background-clip on the parent clips the parent's paint to the parent's glyphs, and a pseudo-element has none. */.hero-copy h1::before { content: ""; display: block; width: 3.4rem; height: 2px; margin-bottom: 0.95rem; background: var(--brand); opacity: 0.85;}
/* The word the headline lands on. -webkit-text-fill-color, not just color: the gradient above sets that property to transparent and it inherits, so a child that only sets color paints nothing. */.hero-em { color: var(--brand); -webkit-text-fill-color: var(--brand);}
.hero-lead { margin: 0; max-width: 26rem; font-size: 1.02rem; color: var(--muted); text-wrap: pretty;}
/* The host name the pitch ends on, which host-cycle.ts rotates.
Every name is in here, all in the one grid cell, so the box is as wide as the longest of them and stays that width for good. That is the whole point of it: the names are different lengths, this one sits at the end of a paragraph that wraps, and a box that changed width would re-wrap the sentence under the reader every few seconds. The slack after a short name is at the end of the last line, where there is nothing to see.
Start-aligned, so each name sits against the "on " before it rather than floating in the middle of the box. Hidden with visibility rather than display, because display: none would stop the unshown names sizing the box and the width would be back to changing.
A shade heavier than the line around it, and each name in its host's own colours over a halo of their second one — host-cycle.ts sets the two, because which colour belongs to Blacksky is a fact about Blacksky. The fallbacks are what the sentence would look like with the colours taken away. */.hero-cycle { display: inline-grid; vertical-align: top; perspective: 340px; font-weight: 500;}
.hero-cycle .cycle-word { grid-area: 1 / 1; justify-self: start; white-space: nowrap; visibility: hidden; color: var(--host-ink, var(--ink)); /* The second colour is an outline that fades rather than stops. Four shadows of the one colour at widening radii: the tight ones stack up almost solid against the letters, which is the only place a blurred colour still has the chroma to read as itself, and each one further out is weaker, so the edge feathers instead of ending. A single wide shadow washes out to white; a hard -webkit-text-stroke holds the hue but draws a line around the word.
host-cycle.ts supplies the colour flat, with no alpha of its own, so how far it carries is decided once here rather than seven times there. */ text-shadow: 0 0 0.035em color-mix(in srgb, var(--host-glow, transparent) 90%, transparent), 0 0 0.08em color-mix(in srgb, var(--host-glow, transparent) 75%, transparent), 0 0 0.17em color-mix(in srgb, var(--host-glow, transparent) 50%, transparent), 0 0 0.65em color-mix(in srgb, var(--host-glow, transparent) 32%, transparent);}
/* The one being shown, and — for the length of a swap — the one being replaced. cycle-here is the state with no motion attached, which is where the first name starts and where a player who asked for less motion stays. */.hero-cycle .cycle-here,.hero-cycle .cycle-in,.hero-cycle .cycle-out { visibility: visible;}
/* The form is a panel on a panel, so it is slightly translucent and lifted rather than another flat card the same colour as its surroundings.
.hero-outage is what stands in the same place when the control plane cannot be reached, so it is the same panel — named here rather than given a look of its own, because a second set of numbers is a second thing to keep in step. */.hero .signin,.hero .hero-outage { width: 100%; max-width: 27rem; margin-top: 0.15rem; background: color-mix(in srgb, var(--panel) 84%, transparent); backdrop-filter: blur(6px); box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 0.18);}
/* The one thing on the page to press. */.hero .signin button[type="submit"] { padding: 0.7rem 1.4rem; font-size: 1rem;}
/* --- the daily challenge ------------------------------------------------- One fight a day, on the front page, whoever is looking at it. The board on the left and the figures on the right down to a phone, where they stack.
The same blocks dress the archive at #daily, one card per past day, so a challenge looks the same wherever it is read. */
/* One line: the label, the way to the archive just after it, and the day this challenge ran pushed to the far end. */.daily-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.6rem;}
.daily-soon { margin-left: auto;}
.daily-eyebrow { margin: 0; color: var(--brand); font-size: 0.62rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase;}
/* The same words the masthead uses for a destination with nothing behind it, and the same dashed "there is nothing to press here". */.daily-soon { margin: 0; padding: 0.1rem 0.5rem; border: 1px dashed var(--line); border-radius: var(--radius); color: var(--muted); font-size: 0.6rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;}
.daily h2 { margin: 0.35rem 0 0.2rem; font-size: 1.25rem;}
.daily-blurb { margin: 0 0 0.9rem; max-width: var(--measure);}
/* The board keeps its own aspect ratio and gives up width first: the figures beside it are readable at any size and the map is not. It holds the tags and the watermark, so it stays tight to the canvas - the screen it sits in is `.daily-board` below, and anything positioned against a frame that was taller than its picture would drift off the machines it names. */.daily-map-frame { min-width: 0; /* A hairline where the hex field stops. The screen around it is taller than any board, and without this the texture just ends mid-bezel. */ outline: 1px solid rgb(255 255 255 / 8%);}
/* Pixel art: scaled up by whole hexes on a wide screen, and never smoothed. */.daily-map { display: block; width: 100%; height: auto; image-rendering: pixelated;}
/* The row under the card: the way into the fight, the scenario file, and the way to the archive. Play is the filled button every other primary action on the site is; the download is outlined beside it, since it leaves the site. */.daily-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; margin: 1rem 0 0; font-size: 0.9rem;}
/* The site's button rule sets align-self: flex-start, which is right in a card column and wrong in this row of links. */.daily-play { align-self: center;}
/* Why Play is disabled, or what it is worth. Empty on the day's own challenge until the control plane says otherwise, and an empty one must not leave the row's gap on both sides of nothing. */.daily-play-note:empty { display: none;}
/* One control, two halves, cut like a plate rather than rounded like a chip. The outer corners are notched - the lit half at its top left, the grey half at its bottom right - so the pair reads as one stamped object with a diagonal running through it, and a 2px seam of card between them keeps the two halves legible as two targets.
Both halves carry the same --notch rather than inheriting one from the group: each is a plate, and a plate sets its own. */.daily-launch { display: inline-flex; align-items: stretch; gap: 2px;}
.daily-launch > .daily-play,.daily-launch > .daily-download { --notch: 0.6rem;}
/* Both halves are plates (see .plate): the lit one cut at its top left, the grey one at its bottom right, so the pair carries one shape between them. */.daily-launch > .daily-play { margin: 0;}
.daily-download { display: inline-flex; align-items: center; padding: 0.4rem 0.8rem; text-decoration: none; /* A step smaller than a plate's default label: this half is quiet, and it carries a file name rather than an order. */ font-size: 0.75rem; /* Tracked capitals are wide, and this half is a file name: broken over two lines it stops reading as a label on a plate. */ white-space: nowrap; /* The grey half: present, clearly secondary, and clearly attached to the lit half rather than floating beside it. Opaque rather than a wash - over a dark card a 12% white was invisible, and an invisible half of a split button is just a button with a wide gap after it. */ color: color-mix(in srgb, var(--ink) 80%, var(--panel)); background: color-mix(in srgb, var(--ink) 22%, var(--panel)); border: 0; /* Grey rather than accent-filled, so the ring that shows on it is the accent one a quiet plate takes by default. */ --plate-ring: var(--accent);}
.daily-download:hover,.daily-download:focus-visible { color: var(--ink); background: color-mix(in srgb, var(--ink) 34%, var(--panel));}
/* A machine's name beside it on the board. daily.ts places every tag from the same `spotsFor` that stands the sprite, so a label cannot drift off the machine it names, and clamps each one to the slot that unit owns. */.daily-map-frame { position: relative;}
.daily-unit-tag { position: absolute; /* Centred under the machine, which is itself centred in its slot. The width is an inline style, because only daily.ts knows how many machines share a row - and it is what guarantees a tag cannot reach its neighbour. */ display: grid; justify-items: center; text-align: center; transform: translateX(-50%); line-height: 1.1; letter-spacing: 0.04em; text-transform: uppercase; pointer-events: none; /* At spaces only: a designation broken mid-word reads as a different machine (STILETT/O). A name with no space in it that still will not fit is the one case allowed to overrun its box, and it is rarer than a broken one. */ overflow-wrap: normal; word-break: keep-all; /* Floating over unit art and hex lines, so it carries its own contrast rather than relying on whatever happens to be behind it. */ text-shadow: 0 1px 2px rgb(0 0 0 / 90%), 0 0 6px rgb(0 0 0 / 80%);}
.daily-unit-chassis { font-size: 0.58rem; font-weight: 700; color: rgb(233 238 245);}
/* The variant, under the chassis it belongs to and in that force's colour, so a tag says which side its machine is on without a second legend. */.daily-unit-model { font-size: 0.54rem; font-variant-numeric: tabular-nums; color: rgb(151 186 240);}
/* Every side but the player's is opposition, however many of them there are. Keyed on "not the player" rather than on side 1, which left the second and third enemy lances reading in the player's own blue.
`--side-colour` is that force's exact shade, set inline beside data-side, so three lances of one team read as three shades of their team's red rather than as one flat opposition colour. The literal is the fallback for a tag drawn before the variable lands. */.daily-unit-tag:not([data-side="0"]) .daily-unit-model { color: var(--side-colour, rgb(240 150 158));}
/* Tonnage and price under each force's battle value: the two figures a player reads the shape of a lance from, and neither is worth the room the battle value gets. */.daily-bv-sub { margin: 0.1rem 0 0; font-size: 0.62rem; font-variant-numeric: tabular-nums; color: color-mix(in srgb, var(--ink) 48%, transparent);}
/* Which battle value this is. MegaMek computes BV 2.0, and a player comparing a fight against a printed record sheet needs to know which of the two systems the numbers came from. */.daily-bv-system { display: block; font-size: 0.55rem; font-weight: 700; letter-spacing: 0.14em; color: color-mix(in srgb, var(--brand) 72%, transparent);}
/* The era, over the empty ground between the two forces. Large and faint: it is the one thing on the card that is atmosphere rather than information, and it sits in the only band of the board no machine ever stands in. *//* Centred by default. daily.ts moves it to 41% - the gap between the north band (0-38%) and the centre band (42-72%) - but only when a force actually deploys in the centre, which is the only case that would collide. */.daily-era-watermark { position: absolute; inset-inline: 0; top: 50%; transform: translateY(-50%); display: flex; align-items: center; justify-content: center; gap: 0.5rem; pointer-events: none; font-size: 0.82rem; font-weight: 700; letter-spacing: 0.34em; /* The letter-spacing is on the right of the last glyph too, which throws a centred line visibly left. */ text-indent: 0.34em; text-transform: uppercase; white-space: nowrap; color: rgb(233 238 245 / 30%);}
/* Bigger than the inline era symbol elsewhere, and no extra opacity: the mask paints in currentcolor, which is already the faint wash above. */.daily-era-watermark > span:not(.daily-era-icon) { min-width: 0; overflow: hidden; text-overflow: ellipsis;}
.daily-era-watermark .daily-era-icon { width: 1.7rem; height: 1.7rem; flex: none;}
/* The years the era covers, quieter than its name and without the tracking - a date read as letterspaced caps is hard to take in as a number. */.daily-era-span { font-size: 0.72rem; font-weight: 400; letter-spacing: 0.06em; text-indent: 0; opacity: 0.72; font-variant-numeric: tabular-nums;}
/* --- the three facts ----------------------------------------------------- What is true today, including the part that is a limitation. */
.facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.75rem;}
.fact { padding: 0.9rem 1rem; border: 1px solid var(--line); border-radius: var(--radius); background: color-mix(in srgb, var(--panel) 55%, transparent);}
.fact h2 { margin: 0 0 0.35rem; color: var(--brand); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.11em; text-transform: uppercase;}
/* The same teal dash as the seam under the masthead, so the strip belongs to the same object as the bar above it. */.fact h2::before { content: ""; display: block; width: 1.4rem; height: 2px; margin-bottom: 0.55rem; background: var(--brand); opacity: 0.75;}
.fact p { margin: 0; color: var(--muted); font-size: 0.88rem; text-wrap: pretty;}
.fact a { color: var(--accent);}
@media (max-width: 46rem) { .facts { grid-template-columns: 1fr; }}
/* Phone: the panel is a column, so the mark moves to the top corner and is cropped harder. It is the same object in the same place — behind the words, coming in from the right — at the size that leaves the headline room. */@media (max-width: 40rem) { .hero { min-height: 0; border-radius: var(--radius); }
.hero-insignia { top: -4rem; right: -22%; /* Sized off the viewport, not the panel: the headline gets taller as the column narrows, and a fixed mark would end up underneath all of it. */ height: clamp(12rem, 70vw, 17rem); opacity: 0.45; }
/* Fades towards the bottom left rather than just the left: the headline and then the form are below, and the mark has to be gone before it gets there. */ .hero-mark { mask-image: linear-gradient( 35deg, transparent 0 26%, rgb(0 0 0 / 0.2) 52%, #000 86% ); }
.hero-copy, .hero .signin { max-width: none; }
/* Kept off the mark's corner: the accented word is the same teal as the artwork, and the two must not land on each other. */ .hero-copy h1 { max-width: 16ch; }}
.signin,.card { display: flex; flex-direction: column; gap: 0.6rem; padding: 1.25rem; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);}
.card h2 { margin: 0; font-size: 1.1rem;}
/* The sign-in form says what it is for before it asks for anything, so it has a heading and a line of explanation of its own. */.signin h2 { margin: 0; font-size: 1.02rem; letter-spacing: -0.01em; text-wrap: balance;}
.signin-blurb { margin: 0 0 0.15rem; color: var(--muted); font-size: 0.9rem; text-wrap: pretty;}
/* Sign in, beside whatever way out the frame around the form has added — the dialog's Not now. A row, so the way out never reads as the next step. */.signin-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center;}
label { font-size: 0.9rem; color: var(--muted);}
input { padding: 0.6rem 0.7rem; font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);}
input:focus-visible,button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px;}
/* Handle autocomplete. The dropdown lives in a closed shadow root, so the only way to dress it is the custom properties and ::part() names the component publishes. Its --radius default is our own token already, and it reads the theme through --panel and --line, so light mode follows for free. */.typeahead { display: block; /* The handle field sits in a flex column where the bare input used to, and a growable basis would stretch it down the whole form. */ flex: 0 0 auto; min-width: 0; /* Beats the component's own system-ui default, which is not our body font. */ font: inherit;
--color-background: var(--panel); --color-border: var(--line); --color-shadow: #000; --color-hover: var(--line); --color-avatar-fallback: var(--line);}
.typeahead input { width: 100%;}
.typeahead::part(user) { cursor: pointer;}
button { align-self: flex-start; padding: 0.6rem 1.1rem; font: inherit; font-weight: 600; color: var(--on-accent); background: var(--accent); border: 1px solid transparent; cursor: pointer;}
/* The quiet plate. Outlined, it lost its line along both cut edges and read as a frame someone had taken a bite out of; filled, it is the treatment the daily card's grey half and the waiting screen's way out already wear. Opaque rather than a wash - over a dark card a 12% white is invisible. */button.secondary { color: color-mix(in srgb, var(--ink) 85%, var(--panel)); background: color-mix(in srgb, var(--ink) 20%, var(--panel)); border-color: transparent; font-weight: 500;}
button.secondary:hover:not(:disabled) { color: var(--ink); background: color-mix(in srgb, var(--ink) 32%, var(--panel));}
button[disabled] { opacity: 0.6; cursor: progress;}
/* A button scaled to the row it sits in rather than to the page. Used by the camo editor's action rows and by the match list. The notch comes down with it: a 0.5rem cut on a 0.8rem button eats the label. */.small-btn { font-size: 0.8rem; padding: 0.35rem 0.6rem 0.35rem 0.7rem; --notch: 0.35rem;}
/* --- notched plates ------------------------------------------------------- The site's stamped shape, and the default for anything that reads as a button: the top-left and bottom-right corners taken off at 45 degrees, so an action reads as a plate bolted to the page rather than a rounded chip. .plate-tl and .plate-br cut one corner each, for the halves of a split control that make the shape up between them, and .plate is for the things that are not <button> elements - a link that starts a game, a track that belongs to the same family as the controls around it.
clip-path clips an element's outline and box-shadow along with its background, so a plate cannot wear the site's focus ring and a glow drawn as a box-shadow is cut away. The ring below is inset instead, and a glow has to be a drop-shadow filter, which is applied after the clip and so follows the cut edge.
--notch is a property rather than a constant: a full-page button and a 0.7rem chip want the same shape at different sizes. --plate-ring is the colour of the inset ring, and a control that sets its own background sets this with it.
The label is stencilled on: small tracked capitals in the label face, which is what the bar's links, the daily card's two halves and the waiting screen's way out already wore. The extra right padding pays back the letter-spacing added after the last letter, which is otherwise trapped inside the plate and pushes a centred label left of centre. */button,.plate { --notch: 0.5rem; --plate-ring: var(--accent); border-radius: 0; clip-path: polygon( var(--notch) 0, 100% 0, 100% calc(100% - var(--notch)), calc(100% - var(--notch)) 100%, 0 100%, 0 var(--notch) ); font-family: var(--font-labels); font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;}
/* A filled plate is accent on accent, and the only ring that shows against that is the one the label is already drawn in. */button:not(.secondary) { --plate-ring: var(--on-accent);}
/* Not everything shaped like a button is a plate. A tile you pick a picture out of, a round icon, a pill toggle and a row in a menu are their own shapes, and cutting a corner off a thumbnail says nothing about what the control does, any more than cutting one off a card would - and a Mode that is not built yet is a <div> beside one that is, so a shape only the button could wear would split the row in two. Each of these already sets its own radius further down this file, so the clip and the stencilled label are the whole of it. */.camo-die,.camo-cat,.camo-pattern,.camo-preset,.camo-choice,.camo-unit-btn,.camo-bg-btn,.camo-mm-tile,.camo-color,.account-button,.theme-button,.account-item,.theme-choice,.mode-card { clip-path: none;}
/* Most of them drop the stencilled label with the shape: a palette's name, a Mode's heading and a row in a menu are words to read, not a part number. The bar's own buttons are not in this list - they are chrome labels already, and set the same capitals a plate does for themselves. */.camo-die,.camo-cat,.camo-pattern,.camo-preset,.camo-choice,.camo-unit-btn,.camo-bg-btn,.camo-mm-tile,.account-item,.theme-choice,.mode-card { font-family: inherit; letter-spacing: normal; text-transform: none;}
/* The way out for anything else: it gives back the site's radius as well, because a control that never had a shape of its own would otherwise be left with the square corners the plate rule sets. */.no-plate { border-radius: var(--radius); clip-path: none;}
.plate-tl { clip-path: polygon(var(--notch) 0, 100% 0, 100% 100%, 0 100%, 0 var(--notch));}
.plate-br { clip-path: polygon( 0 0, 100% 0, 100% calc(100% - var(--notch)), calc(100% - var(--notch)) 100%, 0 100% );}
button:focus-visible,.plate:focus-visible { outline: 0; box-shadow: inset 0 0 0 2px var(--plate-ring);}
/* A control that opted out of the shape is not clipped, so it keeps wearing the outlined ring the rest of the site's focusable things wear. */.camo-die:focus-visible,.camo-cat:focus-visible,.camo-pattern:focus-visible,.camo-preset:focus-visible,.camo-choice:focus-visible,.camo-unit-btn:focus-visible,.camo-bg-btn:focus-visible,.camo-mm-tile:focus-visible,.camo-color:focus-visible,.account-button:focus-visible,.theme-button:focus-visible,.account-item:focus-visible,.theme-choice:focus-visible,.mode-card:focus-visible,.no-plate:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; box-shadow: none;}
/* About: cards of running text. The card is a flex column with a small gap, which is right for form controls and too tight for paragraphs, so the prose sets its own rhythm and a rule under each heading breaks the wall of text. */.prose { gap: 0.75rem;}
.prose h2 { padding-bottom: 0.6rem; border-bottom: 1px solid var(--line);}
.prose p { margin: 0;}
.prose a { color: var(--accent);}
/* The blog. Posts are markdown, so this is the one place in the site where the markup is not written by hand and every tag has to already look right. */
.post-list { display: flex; flex-direction: column; gap: 1rem; margin: 0; padding: 0; list-style: none;}
.post-list h2 { padding-bottom: 0; border-bottom: 0;}
.post-list h2 a { color: inherit; text-decoration: none;}
.post-list h2 a:hover { color: var(--accent);}
/* The date, and whatever else is true about the post rather than in it. */.post-meta { display: flex; flex-wrap: wrap; gap: 0.5rem; color: var(--muted); font-size: 0.85rem;}
.post-tag::before { content: "·"; margin-right: 0.5rem;}
.post-draft { color: var(--danger);}
/* The one link on the site that points at something for software to read, so it is set at the meta size and sits under the list rather than over it: it is what you look for once you have decided you want more of this. It keeps the accent a link has everywhere else, because it is still a link. */.feed-link { margin: 0; font-size: 0.85rem;}
/* Markdown's own headings start at h2: the title is the page's h1 and a post that repeated it would have two. */.post :is(h2, h3, h4) { margin: 0.75rem 0 0; font-size: 1.1rem; line-height: 1.3;}
.post h3,.post h4 { padding-bottom: 0; border-bottom: 0; color: var(--muted);}
.post :is(p, ul, ol, blockquote, pre) { max-width: var(--measure);}
.post :is(ul, ol) { margin: 0; padding-left: 1.25rem;}
.post li + li { margin-top: 0.35rem;}
.post blockquote { margin: 0; padding-left: 0.9rem; border-left: 2px solid var(--line); color: var(--muted);}
.post :is(code, pre) { font-size: 0.85em;}
.post :not(pre) > code { padding: 0.1rem 0.3rem; background: color-mix(in srgb, var(--line) 45%, transparent); border-radius: 3px;}
.post pre { overflow-x: auto; margin: 0; padding: 0.8rem; background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);}
.post :is(img, video) { max-width: 100%; height: auto; border-radius: var(--radius);}
.post hr { width: 100%; height: 1px; margin: 0.5rem 0; background: var(--line); border: 0;}
.post-back { margin: 0; font-size: 0.9rem;}
.post-back a { color: var(--accent);}
/* The trademark notices on the About page. Somebody else's boilerplate: it has to be there and it is not what anyone came to read. */.credits { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.25rem; padding-top: 0.75rem; border-top: 1px solid var(--line); color: var(--muted); font-size: 0.8rem;}
.credits p { margin: 0;}
/* --- About --------------------------------------------------------------- A hub and the pages under it. The tabs are on all of them, including the hub, so the set is navigable from wherever the reader landed — a link to the FAQ is a link into About, not out of it. They read as the masthead's links a size up: uppercase, spaced, marked underneath. */
.subnav { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 1.25rem; border-bottom: 1px solid var(--line);}
.subnav a { padding: 0.5rem 0.75rem; margin-bottom: -1px; border-bottom: 2px solid transparent; color: var(--muted); font-size: 0.78rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; text-decoration: none;}
.subnav a:hover { color: var(--ink);}
.subnav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--brand);}
.subnav a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 3px;}
/* The short description over the cards. A page-head's lead without the page head: About's h1 is hidden, so this is the first thing on the page. */.about-lead { margin: 0 0 1rem; max-width: var(--measure); color: var(--muted); text-wrap: pretty;}
/* One card per page under About. Side by side where there is room and stacked where there is not, so a fourth page is a card added to the row. */.about-pages { display: grid; gap: 0.75rem; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));}
/* A card that is entirely a link, like the mode cards on Play: the whole box is the hit target, so the heading is not a link inside a box that is not. */.page-card { display: flex; flex-direction: column; gap: 0.3rem; padding: 0.9rem 1rem; color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); text-decoration: none;}
.page-card:hover { border-color: var(--accent);}
.page-card h2 { margin: 0; padding: 0; border: 0; font-size: 1rem;}
.page-card p { margin: 0;}
/* --- FAQ ----------------------------------------------------------------- One card of foldable questions. The browser's own marker is a different shape and size in every browser and sits where it likes, so the summary hides it and draws its own: one triangle, masked from the SVG below and painted with a variable, that turns to point down when the question opens. */
.faq { border-top: 1px solid var(--line); padding: 0.75rem 0; /* Rounded joins, so the points read as soft rather than as needles. */ --faq-marker: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3.4 2.6 9.4 6l-6 3.4z' fill='%23000' stroke='%23000' stroke-width='1.7' stroke-linejoin='round'/%3E%3C/svg%3E");}
.faq:first-of-type { border-top: 0; padding-top: 0;}
.faq:last-of-type { padding-bottom: 0;}
.faq summary { cursor: pointer; font-weight: 600; /* A block-level row, so the click target is the whole line rather than just the words, and the triangle keeps its own column when a question wraps. */ display: flex; align-items: flex-start; gap: 0.55rem; list-style: none;}
/* Three spellings of the browser's own marker, because no one of them silences it everywhere: list-style above, and these two. */.faq summary::marker { content: "";}
.faq summary::-webkit-details-marker { display: none;}
.faq summary::before { content: ""; /* As tall as the first line of the question, so a question that wraps to two lines keeps its triangle beside the first one rather than centred against both. The em fallback is for browsers without lh units. */ height: 1.5em; height: 1lh; width: 0.78em; flex: none; background-color: var(--brand); mask: var(--faq-marker) center / 0.78em 0.78em no-repeat; -webkit-mask: var(--faq-marker) center / 0.78em 0.78em no-repeat; transition: transform 150ms ease;}
.faq[open] > summary::before { transform: rotate(90deg);}
@media (prefers-reduced-motion: reduce) { .faq summary::before { transition: none; }}
.faq summary:hover { color: var(--brand);}
.faq summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 3px;}
.faq-body { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 0.6rem; max-width: var(--measure);}
.faq-body p { margin: 0; color: var(--muted);}
/* Named for a screen reader and for the document outline, drawn for nobody. Not display:none, which would take it out of both. */.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;}
.status-list { margin: 0; padding-left: 1.1rem; display: flex; flex-direction: column; gap: 0.4rem;}
.status-list li { padding-left: 0.2rem;}
.status-list::marker,.status-list li::marker { color: var(--brand);}
.muted { color: var(--muted);}
.hint { margin: 0; color: var(--muted); font-size: 0.85rem;}
/* An error line is the one piece of text on the site that routinely contains something with no spaces in it — an origin, a handle, a match id — because the messages name what could not be reached. A long token cannot wrap, so it sets the width of whatever holds it, and on a phone at a large text size that is the width of the page: "Cannot reach the control plane at http://127.0.0.1:3000." on the front page's outage panel pushed a 390px viewport to 419px and scrolled the whole document sideways.
`anywhere` rather than `break-word`, matching the DID under a pilot's handle and the other places here that hold something machine-shaped: it lets the break count towards the intrinsic width, which is what stops the container being sized by the unbroken token in the first place. */.error { margin: 0; color: var(--danger); font-size: 0.9rem; overflow-wrap: anywhere;}
.error:empty { display: none;}
footer { margin-top: 2.5rem; color: var(--muted); font-size: 0.8rem;}
footer p { margin: 0;}
/* The one link inside the footer's prose. The footer is --muted, so an inherited-colour link would be invisible as a link; --accent on --bg is 5.38:1 in light mode and 5.96:1 in dark, and contrast.test.mjs pins both.
The underline is not decoration. --accent against the --muted it sits in is 1.07:1 in light mode and 1.20:1 in dark - the two are all but the same lightness - so hue is the only thing separating them, and the underline is what a reader who is not separating them by hue has instead. */.footer-disclaimer a { color: var(--accent); text-decoration: underline; text-underline-offset: 0.15em;}
/* The account and the repositories, on one line above the version. A row rather than a sentence: they are two addresses, not prose, and at a phone's width they wrap onto two lines instead of pushing the footer wide. */.footer-links { display: flex; flex-wrap: wrap; gap: 0.25rem 0.9rem; margin-top: 0.5rem;}
/* The version line: quiet, but there when a bug report needs it. */footer .debug { margin-top: 0.5rem; font-size: 0.7rem;}
/* A player's page. ------------------------------------------------------------------------- The same vocabulary as the card a link to it unfurls with - a tracked label in the brand colour, a rule running out from it, the picture under corner ticks, a standing read as instruments and counts read as a ruled row - and not the same object. The card is one fixed 1200x630 drawing for a timeline; this is a page that reflows, links onwards, and has a match history coming. A reader arriving from a post should recognise it, not feel they clicked an image. */.profile { display: flex; flex-direction: column; gap: 1.6rem;}
/* The label, the rule that turns it into a header rather than a caption, and whatever marks sit on the far right - the card draws the same line from the same place, and this is the row that has somewhere to put them. */.profile-eyebrow { display: flex; align-items: center; gap: 0.9rem; margin: 0;}
.profile-label { font-family: var(--font-labels); font-size: 0.72rem; font-weight: 600; letter-spacing: 0.34em; text-transform: uppercase; color: var(--accent);}
/* The rule, between the label and the marks. */.profile-eyebrow::after { content: ""; order: 1; flex: 1; height: 1px; background: var(--line);}
/* What there is to do about this player, and where else they are. Last in the row, so the rule runs out from the label and stops at them. */.profile-actions { order: 2; display: flex; align-items: center; gap: 0.5rem; margin: 0; padding: 0; list-style: none;}
/* The one action, in the filled treatment the masthead's Play wears: this is the thing to do on somebody else's page. */.profile-challenge { padding: 0.3rem 0.9rem; border: 1px solid transparent; color: var(--on-accent); background: var(--accent); font-size: 0.72rem; cursor: pointer; --plate-ring: var(--on-accent);}
.profile-challenge:hover:not(:disabled) { opacity: 0.85;}
/* Disabled only while a press is in flight - the session is read before the lobby opens, and a second press during that read would open two. Signed out is not this state: that press puts up the sign-in dialog. */.profile-challenge:disabled { opacity: 0.6; cursor: default;}
/* Their record, beside the action: an outline where Challenge is filled, because this is the page next door and not the thing to do here. Text, so it cannot use .profile-badge - that one is a fixed square cut for a mark. */.profile-record { padding: 0.3rem 0.9rem; border: 1px solid var(--line); color: var(--muted); font-family: var(--font-labels); font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; text-decoration: none; white-space: nowrap;}
.profile-record:hover { color: var(--accent); border-color: var(--accent);}
.profile-badge { display: grid; place-items: center; width: 1.9rem; height: 1.9rem; border: 1px solid var(--line); color: var(--muted); /* The notch every other plate on the site is cut with, at this size. */ clip-path: polygon( 0.35rem 0, 100% 0, 100% calc(100% - 0.35rem), calc(100% - 0.35rem) 100%, 0 100%, 0 0.35rem );}
.profile-badge:hover { color: var(--accent); border-color: var(--accent);}
.badge-mark { width: 1rem; height: 1rem;}
/* You follow them: a tick in the mark's low-right corner, on the page's own ground so it reads as a stamp on the badge rather than as part of the glyph under it. The badge's title says what it means. */.profile-badge { position: relative;}
.badge-tick { position: absolute; right: -0.18rem; bottom: -0.18rem; width: 0.72rem; height: 0.72rem; color: var(--accent);}
.profile-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.5rem;}
/* The column the name is measured against, rather than the window: what runs out beside a picture is this, and it is a good deal narrower than the page it sits on. The site already sizes three other things this way. */.profile-who { display: flex; flex-direction: column; gap: 0.9rem; min-width: 0; /* Take the row's spare width before the container is measured: an inline-size container stops sizing itself from its contents, so a flex item that is one and does not claim its share collapses to nothing and takes the name down to one letter a line. */ flex: 1; container-type: inline-size;}
/* A handle is a domain and can be long, so it sizes off the column rather than off the window, and it breaks rather than pushing the page wide. *//* A handle is a domain and can be long, and it is the one thing on the page that must stay readable at any width.
Sized off its own column, which is what it has to fit in: a cqw with no container above it silently measures the window instead, and the window is not what runs out beside a 8.5rem picture. That was how "@permadeath.com" came apart into "@permadeath.c" and "om".
`break-word` and not `anywhere`: both break a word too long for its column, and only `anywhere` also breaks one that would have fitted after a wrap. */.profile-name { margin: 0; font-size: clamp(1.35rem, 11cqw, 2.6rem); line-height: 1.05; overflow-wrap: break-word;}
.profile-links { display: flex; flex-wrap: wrap; gap: 0.4rem;}
/* The picture, under the ticks the card marks it with. */.profile-plate { position: relative; flex: none; padding: 0.55rem;}
.profile-plate::before,.profile-plate::after { content: ""; position: absolute; width: 1.1rem; height: 1.1rem; border: 2px solid var(--accent); opacity: 0.75;}
.profile-plate::before { top: 0; left: 0; border-right: 0; border-bottom: 0;}
.profile-plate::after { right: 0; bottom: 0; border-left: 0; border-top: 0;}
.profile-face { display: grid; place-items: center; width: 8.5rem; height: 8.5rem; border: 1px solid var(--line); background: var(--panel); overflow: hidden;}
.profile-portrait { width: 100%; height: 100%; object-fit: cover;}
.profile-monogram { font-family: var(--font-labels); font-size: 3rem; font-weight: 600; color: var(--muted);}
/* The small card over a player's matches: whose record this is. A label on a list rather than a second profile, so it borrows the face's drawing and nothing else of the profile head's size. */.pilot-strip { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;}
.pilot-strip-face { display: block; line-height: 0;}
.pilot-strip .profile-face { width: 3.5rem; height: 3.5rem;}
.pilot-strip .profile-monogram { font-size: 1.4rem;}
.pilot-strip-who { display: flex; flex-direction: column; gap: 0.15rem; /* The handle is a domain somebody else chose and can be long; the DID underneath always is. Neither may push the row wider than the page. */ min-width: 0;}
.pilot-strip-name { font-family: var(--font-labels); font-weight: 600;}
.pilot-strip-did { margin: 0; font-size: 0.8rem; color: var(--muted); overflow-wrap: anywhere;}
/* A standing: the word, a leader, the figure. The leader is what makes an em dash read as a gauge at rest rather than as a number somebody forgot. */.profile-scores { display: flex; flex-direction: column; gap: 0.9rem;}
.profile-scores .figure { display: flex; align-items: baseline; gap: 0.9rem;}
.profile-scores .figure::after { content: ""; order: 1; flex: 1; height: 1px; background: var(--line);}
.profile-scores .figure-value { order: 2; font-family: var(--font-labels); font-size: 1.7rem; font-weight: 600; font-variant-numeric: tabular-nums;}
/* Counts: read one at a time, so they are ruled apart rather than stacked. */.profile-counts { display: flex; flex-wrap: wrap; gap: 1.25rem 2.5rem; padding-top: 1.1rem; border-top: 1px solid var(--line);}
.profile-counts .figure { display: flex; flex-direction: column-reverse; gap: 0.15rem; min-width: 5.5rem;}
.profile-counts .figure-value { font-family: var(--font-labels); font-size: 1.9rem; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.1;}
.figure-label { font-family: var(--font-labels); font-size: 0.7rem; font-weight: 600; letter-spacing: 0.2em; text-transform: uppercase; color: var(--muted);}
/* A figure with no source yet, or one that is not ours to show. Dimmed rather than stamped: the reason is on the row, and the page is not the place for the site's rubber stamp on four rows out of five. */.figure-empty .figure-value { color: var(--muted);}
/* Narrow: the picture goes above the name rather than beside it, because a handle in the column that is left breaks into a stack of syllables.
46rem rather than a phone's width: what runs out is not the window, it is the column beside a 8.5rem picture, and a long handle runs out of that well before anything else on the page is in trouble. */@media (max-width: 46rem) { .profile-head { flex-direction: column-reverse; align-items: stretch; }
/* The plate is what the ticks are pinned to, so it has to stay the size of the picture: stretched across the column, they end up marking two corners of the page instead of two corners of a portrait. */ .profile-plate { align-self: flex-start; }
.profile-face { width: 6.5rem; height: 6.5rem; }}
/* The challenge screen's opponent choices. */.choice { display: flex; gap: 0.75rem; align-items: flex-start; padding: 0.9rem; border: 1px solid var(--line); border-radius: var(--radius);}
.choice:has(input:checked) { border-color: var(--accent);}
.choice input[type="radio"] { margin-top: 0.2rem; accent-color: var(--accent);}
/* Which camo to fight in, on the challenge screen. */.choice.camo-pick { flex-direction: column; /* `.choice` sets `flex-start`, which in a column is the horizontal axis: the strip would be as wide as one tile, and a grid with no definite width auto-fills a single column. */ align-items: stretch; gap: 0.35rem;
/* One tile, one width, everywhere the strip is drawn. The tiles used to be as wide as their names, so a camo called "i just liked this one" was three times the width of one called "pink" and no two rows lined up. 42px of picture plus the tile's own padding, with the name cut to fit. */ --camo-tile: 3.6rem;}
/* A grid rather than a wrapping flex row: every tile is one column wide and every row is the height of its tallest tile, which is what keeps the strip square as the collection grows. */.camo-choices { display: grid; grid-template-columns: repeat(auto-fill, var(--camo-tile)); justify-content: start; align-items: stretch; gap: 0.4rem;}
/* The picture at the size a match draws it, so the choice is made on the thing being decided rather than on a name. */.camo-choice { display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 0.2rem; width: var(--camo-tile); height: 100%; padding: 0.3rem; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 4px; cursor: pointer;}
.camo-choice img,.camo-choice-none { display: block; width: 42px; height: 36px; image-rendering: pixelated; border-radius: 2px;}
/* The roll's tile holds its picture before there is one to hold, and a `display` here beats the `hidden` attribute — so the srcless <img> was drawn as the browser's broken-picture mark. */.camo-choice img[hidden] { display: none;}
.camo-choice-none { line-height: 36px; text-align: center; color: var(--muted); background: color-mix(in srgb, var(--line) 40%, transparent);}
/* Cut to the tile rather than sizing it: the name is what a player reads second, after the picture they are actually choosing on. */.camo-choice-name { max-width: 100%; font-size: 0.62rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
.camo-choice.active { border-color: var(--accent);}
/* The tile that goes to the editor, shown only when there is nothing else in the strip. The same size and shape as a camo so the row does not jump when one is made, and dashed so it does not read as a fourth thing to pick. */.camo-make { display: flex; flex-direction: column; align-items: center; gap: 0.2rem; width: var(--camo-tile); height: 100%; padding: 0.3rem; border: 1px dashed var(--line); border-radius: 4px; color: var(--muted); text-decoration: none;}
.camo-make:hover { color: var(--ink); border-color: var(--accent);}
.camo-make-mark { display: block; width: 42px; height: 36px; line-height: 36px; text-align: center; font-size: 1.1rem; border-radius: 2px; background: color-mix(in srgb, var(--line) 40%, transparent);}
/* The camo choice, opened from inside the force it applies to rather than living in a strip of its own at the bottom of the screen. A real <dialog>, so the browser handles modality, focus and Escape. */.camo-modal { max-width: min(38rem, 92vw); padding: 0; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); color: var(--fg);}
.camo-modal::backdrop { background: rgb(0 0 0 / 60%);}
.camo-modal-body { padding: 1rem 1.1rem 1.1rem;}
.camo-modal-actions { justify-content: flex-end; margin-top: 0.75rem;}
/* Inside the dialog the strip is the content, not a section of a form, so it drops the boxed .choice treatment it wears on the old flat layout. */.camo-modal .camo-pick { border: 0; padding: 0; background: none;}
/* Roll one and keep it. Dashed like .camo-make, because both of them make a camo rather than being one to choose between. */.camo-choice-roll { border-style: dashed;}
/* The tile and its die. The die sits in the tile's top corner rather than inside it: a button cannot contain another button, and the two do different things - the tile wears the roll, the die replaces it.
`.camo-roll-tile` and not `.camo-roll`, which is the camo editor's own roll button: this span was inheriting that button's padding and font size, and was the one tile in the strip that did not line up. */.camo-roll-tile { position: relative; display: block; width: var(--camo-tile); height: 100%;}
.camo-roll-tile .camo-choice { width: 100%;}
/* Where the strip goes when the collection is longer than a page. Quiet, and absent entirely below one page - which is most collections. */.camo-pager { display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-top: 0.15rem; color: var(--muted); font-size: 0.7rem;}
/* Filled rather than outlined, for the reason `button.secondary` is: these buttons wear the site's cut corners, and an outline on a cut corner reads as a frame with a bite out of it. */.camo-page { padding: 0.05rem 0.45rem; color: color-mix(in srgb, var(--ink) 85%, var(--panel)); background: color-mix(in srgb, var(--ink) 20%, var(--panel)); border: 0; font-size: 0.9rem; line-height: 1.4; cursor: pointer;}
.camo-page:hover:not(:disabled) { color: var(--ink); background: color-mix(in srgb, var(--ink) 32%, var(--panel));}
.camo-page:disabled { opacity: 0.4; cursor: default;}
.camo-page-count { font-variant-numeric: tabular-nums;}
/* Drawn rather than typed: see `dieMark` in camo-picker.ts. Sized in `em` so it follows whatever the button it sits in is set in. */.camo-die-mark,.camo-make-mark svg { width: 1em; height: 1em;}
.camo-make-mark svg { display: inline-block; vertical-align: middle; font-size: 1.1rem; color: var(--muted);}
.camo-die { position: absolute; top: -0.35rem; right: -0.35rem; width: 1.35rem; height: 1.35rem; padding: 0; display: grid; place-items: center; font-size: 0.8rem; line-height: 1; border: 1px solid var(--line); border-radius: 50%; background: var(--panel); color: var(--fg); cursor: pointer;}
.camo-die:hover { border-color: var(--accent);}
.camo-die:disabled { opacity: 0.5; cursor: default;}
/* The button that opens the dialog, and the bot's reroll beside it. */.camo-open { flex: none;}
/* The camo choice as a control rather than a picture: the swatch, the word "Camo", the name of what is chosen, and a chevron for the chooser it opens. `.camo-chosen` and not `.camo-swatch`, which is the camo editor's own preview and is sized in hundreds of pixels. */.camo-chosen { display: inline-flex; gap: 0.5rem; align-items: center; /* `button` sets `align-self: flex-start`, which in a header line would sit the control above the heading's baseline rather than beside it. */ align-self: center; padding: 0.25rem 0.5rem 0.25rem 0.25rem; line-height: 1; text-align: left; border: 1px solid var(--line); border-radius: var(--radius); cursor: pointer;}
/* Lit on the way in, the same accent the strip's own tiles take when they are the one selected - so the control reads as the door to that strip. */.camo-chosen:hover,.camo-chosen:focus-visible { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--panel));}
/* Holds the square whichever half of it is showing, so the control keeps its width when a camo is swapped for None. */.camo-chosen-art-box { display: grid; place-items: stretch; width: 42px; height: 36px; overflow: hidden; background: var(--bg); border-radius: 2px;}
.camo-chosen-art { display: block; width: 42px; height: 36px; image-rendering: pixelated;}
.camo-chosen-text { display: grid; gap: 0.15rem; min-width: 4.5rem; max-width: 8rem;}
/* What the control is, said once and quietly. Without it the square named no subject at all and could as easily have been the force's badge. */.camo-chosen-kicker { font-size: 0.6rem; letter-spacing: 0.08em; color: var(--muted); text-transform: uppercase;}
/* What is chosen, named. Cut to the control's width rather than stretching it: the whole name is in the title and the accessible name. */.camo-chosen-name { overflow: hidden; font-size: 0.8rem; /* The player named this camo; the button styling that shouts every other label would hand it back in capitals. */ letter-spacing: normal; text-transform: none; color: var(--ink); text-overflow: ellipsis; white-space: nowrap;}
.camo-chosen-caret { font-size: 0.7rem; color: var(--muted);}
/* A `display` in a stylesheet beats the `hidden` attribute, and both halves of the swatch are hidden in turn - so both have to say so. */.camo-chosen-art[hidden],.camo-choice-none[hidden] { display: none;}
/* The camo matches start in. Lit rather than quiet when it is the one, so which card holds it is readable down a list of otherwise identical rows. */.camo-default.active { color: var(--on-accent); background: var(--accent);}
.choice select { max-width: 16rem; padding: 0.4rem; background: var(--panel); color: var(--muted); border: 1px solid var(--line); border-radius: var(--radius);}
/* The challenge screen's lineup: recent opponents and the players added by hand, both drawn as chips. */.lineup { display: flex; flex-wrap: wrap; gap: 0.4rem;}
.lineup:empty { display: none;}
/* The add form on the left; who you fought before beside it on the right, wrapping under on narrow screens. */.opponents-row { display: flex; flex-wrap: wrap; gap: 0.75rem 2rem; align-items: flex-start;}
.opponents-add,.opponents-recent { display: flex; flex-direction: column; gap: 0.35rem;}
.opponents-add { flex: 1 1 16rem;}
/* Sub-labels once there is more than one opponents-recent column beside the form: recent and suggested, each needs to say which it is. */.opponents-subhead { font-size: 0.75rem; font-weight: 600; color: var(--muted);}
/* Recent opponents stacked one per row, latest at the top. */.recent-list { flex-direction: column; align-items: flex-start;}
/* A pilot tile: avatar, handle and a rank row. Not the pilot card further down this file — that is Home's stats card (pilot.ts); this is the small chip challenge.ts uses for the lineup and the opponent strips. Angular rather than the pill it replaced — a clipped corner and the left accent bar `.callout` already uses for the same "dossier" feel, not a new visual language. `rank` is stubbed "n/a" everywhere until the game has a real stat to show it (pilotTile() in challenge.ts). */.pilot-tile { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0.7rem 0.3rem 0.55rem; font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-left: 3px solid var(--accent); border-radius: var(--radius); clip-path: polygon( 0 0, 100% 0, 100% calc(100% - 0.55rem), calc(100% - 0.55rem) 100%, 0 100% );}
.pilot-tile-face { display: inline-flex; gap: 0.45rem; align-items: center;}
/* A face on a card, sized to the text beside it. */.pilot-tile-avatar { width: 1.4rem; height: 1.4rem; border-radius: 50%; object-fit: cover; flex: none;}
.pilot-tile-info { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.2;}
.pilot-tile-handle { font-size: 0.8rem; font-weight: 600;}
/* Stubbed "n/a" today — see the note on pilotCard() in challenge.ts. */.pilot-tile-rank { font-size: 0.65rem; font-weight: 600; letter-spacing: 0.03em; text-transform: uppercase; color: var(--muted);}
.pilot-tile-remove { color: var(--muted); font-size: 0.75rem;}
/* The seat's controller: their face beside the select that decides it. */.seat-pick { display: flex; gap: 0.5rem; align-items: center;}
.seat-avatar { width: 1.6rem; height: 1.6rem; border-radius: 50%; object-fit: cover;}
.pilot-tile:not([disabled]):hover { background: color-mix(in srgb, var(--accent) 8%, var(--bg));}
.pilot-tile[disabled] { cursor: default; opacity: 0.7;}
/* A pilot tile dragged from Recent or Suggested, hovering a slot it can be dropped on. */.slot-row-drop-target { outline: 2px dashed var(--accent); outline-offset: 2px; border-radius: var(--radius);}
/* The typeahead field and its Add button on one line. */.lineup-add { display: flex; gap: 0.4rem; align-items: stretch; max-width: 24rem;}
/* `display: flex` on .lineup-add beats the browser's own rule for [hidden], the same way .pilot-hover's does, so a closed field has to be put away twice — without this every open force is showing a handle input again, which is the thing this control was for. */.lineup-add[hidden] { display: none;}
/* A refusal, worn as a badge beside the control that refused. */.pill { display: inline-flex; align-items: center; align-self: flex-start; padding: 0.15rem 0.6rem; font-size: 0.8rem; font-weight: 600; border-radius: 999px;}
.pill-error { color: var(--danger); border: 1px solid var(--danger); background: color-mix(in srgb, var(--danger) 10%, transparent);}
/* The display above beats the hidden attribute, so an empty pill has to be put away by emptiness itself. */.pill:empty { display: none;}
/* The greyed way to a second opponent, shown once the first is lined up. */.lineup-more { display: flex; gap: 0.5rem; align-items: center;}
.lineup-more button[disabled] { cursor: not-allowed;}
.lineup-add .typeahead { flex: 1 1 auto;}
.lineup-add button { align-self: stretch;}
.scenario-list { flex: 1 1 16rem; display: flex; flex-direction: column; gap: 0.5rem;}
/* The list of scenarios and, beside it, the chosen one drawn out. */.scenario-body { display: flex; flex-wrap: wrap; gap: 0.75rem 1.5rem; align-items: flex-start;}
/* One force per row: its title, then its units on hexes. */.scenario-preview { flex: 1 1 20rem; display: flex; flex-direction: column; gap: 0.75rem;}
.force-title { margin: 0 0 0.25rem;}
.force-hexes { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: flex-start;}
.force-cell { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; margin: 0; width: 84px;}
/* The compositor works at MegaMek's own sprite size; scaling up would only blur what the lobby draws sharp. */.force-unit { display: block; width: 84px; height: 72px; image-rendering: pixelated;}
/* A hex whose machine has not arrived yet. * * The sprites are fetched rather than bundled, so on a first visit there is a * moment between a hex existing and something standing in it. megamek-art.ts * paints a mark there and carries which state the canvas is in; this is what * makes waiting look deliberate — a slow breath while it waits, a short fade * when the machine lands. `missing` keeps the mark and stops the pulse: a hex * that will never fill should not look like one that still might. */.force-unit[data-state="loading"] { animation: unit-waiting 1.6s ease-in-out infinite;}
.force-unit[data-state="ready"] { animation: unit-arrived 0.18s ease-out;}
.force-unit[data-state="missing"] { opacity: 0.55;}
@keyframes unit-waiting { 0%, 100% { opacity: 0.55; } 50% { opacity: 0.9; }}
@keyframes unit-arrived { from { opacity: 0.6; } to { opacity: 1; }}
@media (prefers-reduced-motion: reduce) { .force-unit[data-state="loading"], .force-unit[data-state="ready"] { animation: none; }}
.force-unit-name { max-width: 84px; font-size: 0.62rem; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
/* lobby.ts's Force cards - one per scenario slot, replacing the flat .slot-row it used to render seats as. `--force-colour` is set inline per card (lobby.ts's forceCard): the side's own colour off daily.ts's list, so the force you are taking is the same blue here as on the card that sent you, and the opposition the same red. */.force-card { --force-colour: rgb(86 132 199); display: flex; flex-direction: column; gap: 0.6rem; padding: 0.8rem 0.9rem; border: 1px solid var(--line); border-top: 3px solid var(--force-colour); border-radius: var(--radius); background: color-mix(in srgb, var(--force-colour) 6%, var(--panel)); min-width: 0;}
/* The one that is yours, said with weight rather than with a label. */.force-card-mine { background: color-mix(in srgb, var(--force-colour) 11%, var(--panel));}
.force-card-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; justify-content: space-between;}
.force-card-name { font-size: 1.05rem; color: var(--force-colour);}
/* Team/alliance is stubbed "Team: n/a" against the hand-written catalog, which carries no such data (see teamPill). Muted and bordered like an ordinary badge, not the red .pill-error a real refusal uses - this reads as "not tracked yet", not "broken". */.force-card-team { color: var(--muted); font-weight: 600; border: 1px solid var(--line); background: var(--bg); cursor: help;}
/* A team the scenario actually declared: no tooltip, and no apology. */.force-card-team-known { color: var(--force-colour); border-color: color-mix(in srgb, var(--force-colour) 40%, transparent); cursor: default;}
.force-card-seat { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; justify-content: space-between; padding-bottom: 0.6rem; border-bottom: 1px solid var(--line);}
.force-unit-list { display: flex; flex-direction: column; gap: 0.3rem; margin: 0; /* No bullets: every row already opens with the machine's own picture, and a disc in front of that is a second, worse marker. */ padding: 0; list-style: none;}
.force-unit-row { display: flex; align-items: center; gap: 0.5rem; min-width: 0;}
/* The compositor still paints at MegaMek's own 84x72 (unitList's canvas width/height in lobby.ts); this only shrinks how it is shown, so a card's bulleted list reads as icons rather than the full preview's hexes. */.unit-icon { width: 40px; height: 34px; flex: none; border-radius: 2px;}
.force-unit-info { display: flex; flex-direction: column; line-height: 1.2; min-width: 0;}
/* The whole designation, chassis and model. Wrapping rather than truncating: the model is the half that tells two of the same chassis apart, and an ellipsis eats exactly that half.
Not `.force-unit-name` - that one is the scenario preview's own caption, which is 84px wide and truncates deliberately. Two classes because they are two labels with opposite requirements, not one shared by accident. */.force-unit-designation { font-size: 0.85rem; line-height: 1.15; overflow-wrap: anywhere;}
/* Where this force starts, under its name. */.force-card-zone { margin: -0.35rem 0 0; font-size: 0.75rem;}
/* Who is in it, when the scenario says. */.force-unit-crew { font-size: 0.72rem;}
/* What this machine is worth, or a dash where nothing knows. */.force-unit-figures { font-size: 0.72rem; font-variant-numeric: tabular-nums;}
/* What the whole force adds up to, under the rule its list sits on. */.force-card-total { margin: 0; font-size: 0.78rem; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--force-colour);}
/* The slot list beside the callout about the AI, stacking when narrow. */.slot-body { display: flex; flex-wrap: wrap; gap: 0.75rem 1.25rem; align-items: flex-start;}
/* A side note: what something on this screen is, said once, off to the side of the control it explains. */.callout { flex: 0 1 15rem; padding: 0.6rem 0.8rem; border: 1px solid var(--line); border-left: 3px solid var(--accent); border-radius: var(--radius);}
.callout .hint { margin: 0.25rem 0 0;}
/* One column per team, and the forces on a team stacked down it. Two teams is the common case and each gets half the width; a scenario whose factions are a free-for-all has one team each and so gets one column each, which is the old behaviour and the honest reading of no alliance data.
`--force-teams` is set inline by renderSeats. `align-self` is not decoration: the block around this is a column flex with `align-items: flex-start` (see .choice), which sizes its children to their content across the axis this grid needs the whole width of. Without it the grid collapses to a single column. */.slot-list { --force-teams: 2; align-self: stretch; display: grid; grid-template-columns: repeat(min(var(--force-teams), 3), minmax(0, 1fr)); gap: 0.75rem; align-items: start;}
/* The forces of one team, top to bottom. */.force-team { display: flex; flex-direction: column; gap: 0.75rem; min-width: 0;}
@media (max-width: 44rem) { .slot-list { grid-template-columns: minmax(0, 1fr); }}
.slot-row { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: center; justify-content: space-between;}
.slot-row select { padding: 0.4rem; color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);}
.slot-units { max-width: 34rem;}
.row { display: flex; gap: 0.6rem;}
/* The lobby's chat scrollback. Same "roughly a handful of rows before it scrolls" call as .camo-collection, so a long conversation cannot push Deploy off the screen. */.chat-scroll { display: flex; flex-direction: column; gap: 0.3rem; max-height: 12rem; overflow-y: auto; padding: 0.5rem; border: 1px solid var(--line); border-radius: var(--radius);}
.chat-line { margin: 0; overflow-wrap: anywhere;}
.chat-line strong { margin-right: 0.4rem;}
/* A locally-generated notice ("You are connected to chat"), never a line anyone said - muted and unlabeled so it reads apart from a real .chat-line's speaker + text shape. */.chat-system { color: var(--muted); font-style: italic;}
.chat-form { display: flex; gap: 0.5rem;}
.chat-form input { flex: 1 1 auto; min-width: 0; padding: 0.4rem; color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);}
/* Ready, and who is still to press it, under the chat form. The pressed state is the accent fill .camo-default uses for its own toggle: a plate has no outline to tint, so on is lit rather than outlined. */.lobby-ready-row { align-items: center; flex-wrap: wrap;}
.lobby-ready.active { color: var(--on-accent); background: var(--accent);}
.lobby-ready-row .hint { margin: 0;}
/* The lobby's other half — chat, the invite link and Ready — as one panel that stays put while the scenario list and the forces scroll behind it. Fixed rather than a block at the bottom of the card: a four-force scenario is taller than the viewport, and the conversation is the part a player waiting on somebody else needs in front of them the whole time.
Below the masthead's own z-index (10) would put the bar over it while scrolling; the waiting screen's fixed layer (30) still covers everything, which is right — that screen is a handover, not a panel. */.lobby-chat { position: fixed; right: 1rem; bottom: 1rem; z-index: 20; display: flex; flex-direction: column; gap: 0.5rem; width: min(20rem, calc(100vw - 2rem)); max-height: min(28rem, calc(100vh - 2rem)); padding: 0.75rem; overflow-y: auto; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: 0 6px 20px light-dark(rgb(15 23 34 / 18%), rgb(0 0 0 / 45%));}
/* --- who pilots an open force -------------------------------------------- lobby.ts's seatActions(): one question, four answers, where three loose buttons and a permanently open handle field used to be.
The answers are one plate between them rather than four: the ends wear the two cuts (.plate-tl, .plate-br) and the middles are square, which is what .plate-tl/.plate-br are for. The 1px gap is the card showing through, so the divisions cost no border that would double up against the cut edges.
The rule that squares the middles has to name what it is not, rather than a class of its own: `.plate-tl` and `.plate-br` are declared further up this file, and a `.seat-answer { clip-path: none }` down here would win over both and take the plate's shape off the ends. */.seat-fill { display: flex; flex-direction: column; gap: 0.5rem; /* The only thing in the seat row when a force is open, and the row is a space-between flex — without this the control sits against the card's left edge with the word "Open" and nothing else beside it. */ flex: 1 1 100%; min-width: 0;}
.seat-fill-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem;}
.seat-answers { display: inline-flex; gap: 1px;}
.seat-answers > button:not(.plate-tl):not(.plate-br) { clip-path: none;}
/* `button` sets align-self: flex-start for a button standing on its own in a column; these are in a row and have to fill its height, or the four are different heights the moment one of them wraps. */.seat-answer { align-self: stretch;}
/* The answer that is open: Invite, while its field is showing. The same accent fill every other pressed toggle on the site wears. */.seat-answer.active { color: var(--on-accent); background: var(--accent);}
/* The answer that is not built.
The mark lies across the button rather than beside it: set beside the label it made this one answer half again the width of the other three, and the four stopped reading as one plate. One line at a steep angle, which is what fits across four letters - the two-line mark the front page uses filled the button and buried the word under it.
Translucent, and the label under it is not dimmed to a ghost: at this size the two have to share the space, so the mark reads as ink over the word rather than instead of it.
`button[disabled]` fades the box, and a --muted mark under that opacity is a mark that cannot be read, so the box is not faded. The cursor is the button rule's `progress` otherwise, which promises something is on its way. */.seat-answer-soon { position: relative; padding-inline: 1.05rem; opacity: 1; cursor: default;}
.seat-answer-soon .soon-label { opacity: 0.7;}
.stamp-seat { position: absolute; top: 50%; left: 50%; z-index: 1; padding: 0.1em 0.3em 0.05em; border-width: 0.12em; font-size: 0.38rem; line-height: 1.2; letter-spacing: 0.08em; text-indent: 0.08em; white-space: nowrap; transform: translate(-50%, -50%) rotate(-50deg); opacity: 0.8; pointer-events: none;}
/* The team control on a Force card, where the team pill used to sit. Sized to the pill it replaced so a card's head does not jump when a scenario with teams loads next to one without. */.force-card-team-pick { max-width: 10rem; padding: 0.15rem 0.3rem; font-size: 0.8rem; color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);}
/* The card that opens when you point at another player. Positioned against the viewport by pilot-hover.ts, above the chat panel (20) it is usually opened from, and never under the pointer: it is a thing to read, and a card that intercepted the cursor would close itself the moment it appeared. */.pilot-hover { position: fixed; z-index: 25; width: 15rem; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem; pointer-events: none; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: 0 6px 20px light-dark(rgb(15 23 34 / 18%), rgb(0 0 0 / 45%));}
/* `display: flex` above beats the browser's own rule for [hidden], so a closed card needs saying twice: without this the card is on the screen from the moment a lobby is built, empty, with nobody pointing at anything. */.pilot-hover[hidden] { display: none;}
.pilot-hover-identity { display: flex; gap: 0.5rem; align-items: center; min-width: 0;}
.pilot-hover-avatar { width: 2.4rem; height: 2.4rem; border-radius: 50%; object-fit: cover;}
.pilot-hover-name { margin: 0; font-weight: 600; overflow-wrap: anywhere;}
/* The DID is the part that is not rented, and it is long. Small, dimmed and allowed to wrap rather than truncated: a truncated DID identifies nobody. */.pilot-hover-did { margin: 0; font-size: 0.7rem; color: var(--muted); overflow-wrap: anywhere;}
.pilot-hover-note { margin: 0;}
.pilot-hover-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.4rem; text-align: center;}
.pilot-hover-stat { display: flex; flex-direction: column;}
.pilot-hover-figure { font-size: 1.1rem; font-weight: 600;}
/* A figure nothing records yet. Dimmed so the card reads as three tiles, one of which is real, rather than as three numbers one of which is odd. */.pilot-hover-soon { color: var(--muted);}
.pilot-hover-label { font-size: 0.7rem; color: var(--muted);}
/* A name you can point at. Underlined on hover rather than always, so the scrollback does not read as a page of links. */.chat-speaker:hover,.chat-speaker:focus-visible,.lobby-watcher:hover .lobby-watcher-name,.lobby-watcher:focus-visible .lobby-watcher-name { text-decoration: underline dotted; cursor: help;}
/* Who is in the room, above what is being said in it. Wraps rather than scrolls: eight seats plus spectators is a few rows, and a row of names that scrolls sideways is a row of names nobody reads. */.lobby-roster { display: flex; flex-wrap: wrap; gap: 0.3rem 0.5rem;}
.lobby-roster-count { margin: 0;}
.lobby-watcher { display: inline-flex; gap: 0.3rem; align-items: center; max-width: 100%;}
.lobby-watcher-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
.lobby-chat-head { display: flex; gap: 0.5rem; align-items: center; justify-content: space-between;}
/* Back into the flow where a floating panel would cover the forces it is about. The card it follows is the same width, so this reads as the last block of the screen rather than a panel that lost its moorings. */@media (max-width: 64rem) { .lobby-chat { position: static; width: auto; max-height: none; box-shadow: none; }}
/* The address that was asked for, on the not-found screen. Someone else's URL, so it can be long and unbroken: let it wrap rather than widen the card. */.dead-path { align-self: flex-start; max-width: 100%; padding: 0.5rem 0.6rem; background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius); font-size: 0.85rem; overflow-wrap: anywhere;}
/* --- the waiting screen -------------------------------------------------- The match is booting, and the next thing this player sees is not a page of ours: the poll ends in a navigation to Suramadu, whose own boot frame is a bare dark viewport with the insignia over one tracked teal line. So this is a viewport too, masthead included, rather than a card sitting in the middle of the site chrome. The chrome disappearing at the handover was the jump; there is nothing to disappear now.
Fixed rather than a tall block, because the surface is the point — a position: fixed layer covers the sticky masthead (z-index 10) and the panels that hang off it (20) without either of them having to know. The menus in the top layer, which is every <dialog>, are unaffected.
--panel and not --bg: the box the heading is now inside is this, and it keeps the panel's surface rather than becoming the page. The teal rule across the top is the same 2px seam the boot frame carries. */.waiting { position: fixed; inset: 0; z-index: 30; display: flex; flex-direction: column; align-items: center; /* Safe centring: the column is short here and tall on a phone in landscape, and a centred flex column that outgrows its box loses the top of itself to a scrollbar that cannot reach it. */ justify-content: safe center; gap: 1.25rem; padding: 2rem 1.25rem; overflow-y: auto; background: var(--panel); border-top: 2px solid var(--brand); text-align: center;}
.waiting-logo { width: 7rem; margin: 0;}
/* Buttons on this site align to the start of the column they are in. This column is centred and has no edge for the way out to sit against, so it follows the rest rather than stranding itself in a corner of the screen. */.waiting button { align-self: center; margin-top: 1rem;}
/* The way out, cut like the daily card's grey half: an opaque plate in small tracked capitals with two corners notched. It abandons a launch, so it is deliberately the quiet thing on a screen whose loud thing is the teal line. The left padding carries the extra to clear the notch, so the label still sits centred inside the shape. */.waiting-cancel { --notch: 0.55rem; padding: 0.5rem 1.2rem 0.5rem 1.45rem; font-size: 0.75rem; letter-spacing: 0.12em; white-space: nowrap; color: color-mix(in srgb, var(--ink) 80%, var(--panel)); background: color-mix(in srgb, var(--ink) 22%, var(--panel)); border: 0; --plate-ring: var(--accent);}
.waiting-cancel:hover { color: var(--ink); background: color-mix(in srgb, var(--ink) 34%, var(--panel));}
/* The status line and the fact under it, in a column exactly as wide as the line: one grid track, sized by the widest item allowed to size it, and the heading is the only one allowed. `width: 0` keeps the fact out of that measurement, `min-width: 100%` fills the track the heading set. The fact used to set its own measure, which left two long lines under a status line a third of their width.
max-width, because the track would otherwise hold the heading on one line past the width where the screen has room for it. */.waiting-copy { display: grid; justify-items: center; /* The gap the column above sets, so this reads as a run of that column and not a block inside it — including the smaller one the short-screen rule drops to. */ gap: inherit; /* The fact is a fifth wider than the track (below), so the track gets a fifth less than the screen allows: the pair together still stops at the edge the column's padding sets. */ max-width: calc(100% / 1.2);}
/* A measure exactly as wide as a short tracked-capitals line is too narrow for prose — it broke a two-clause fact over four ragged lines. The fact takes a fifth more than the track and hangs a tenth over each side of it, which is still a column, not a paragraph the width of the screen. */.waiting-copy .trivia { width: 0; min-width: 120%;}
/* The status line, and the one thing on the screen in the brand's teal. Small tracked capitals in the display face, because that is what the boot frame sets it in — the two screens are one load, so they say it the same way. The padding-left is optical: letter-spacing is added after the last letter too, so centred capitals sit half a space left of the true centre. */.waiting-status { margin: 0; padding-left: 0.22em; font-family: var(--font-labels); font-size: 0.8rem; font-weight: 700; line-height: 1.55; letter-spacing: 0.22em; text-transform: uppercase; text-wrap: balance; color: var(--brand);}
/* The wait itself, under the line that names it: a segmented track with a scanline crossing it. A dot that blinks says only that a script is running; a sweep travelling a scale reads as something working its way through a sequence, which is what the container is doing while this is on screen.
Cut like the daily card's Play plate - the same notch on the same two corners - so the moving part of this screen belongs to the same object family as the site's controls. */.waiting-scan { --notch: 0.3rem; position: relative; overflow: hidden; width: min(13rem, 100%); height: 0.4rem; /* The scale under the sweep: ticks in the brand teal at a tenth, so the track is visible as a distance to be crossed before anything crosses it. */ background: repeating-linear-gradient( 90deg, color-mix(in srgb, var(--brand) 30%, transparent) 0 2px, transparent 2px 0.5rem );}
/* The scanline. It bounces rather than repeating in one direction: a sweep that restarts at the left edge reads as a progress bar and would be lying about knowing how far along the start is. */.waiting-scan-sweep { position: absolute; inset-block: 0; left: 0; width: 35%; background: linear-gradient( 90deg, transparent, var(--brand) 45% 55%, transparent ); animation: waiting-scan 1.9s ease-in-out infinite alternate;}
/* A phone in landscape is 26rem tall and this is a 7rem mark above four lines of fact: without this the Back button is below the fold on the one screen whose only way out it is. */@media (max-height: 34rem) { .waiting { gap: 0.85rem; }
.waiting-logo { width: 4.5rem; }
/* Holding five lines empty costs a fifth of this screen. Better the button moves when a long fact arrives than that it starts below the fold. */ .waiting-line { min-height: 0; }}
/* The rotating fact: the line, then the era and source under it. The fade lives on this wrapper so the whole block leaves and arrives together. */.trivia { display: flex; flex-direction: column; gap: 0.6rem; transition: opacity 0.4s ease;}
.trivia-fading { opacity: 0;}
/* `display: flex` above beats the browser's own rule for [hidden], so a failed start left the fact sitting above the error that replaced it. */.trivia[hidden] { display: none;}
.waiting-line { margin: 0; /* Room for a five-line fact, which is all but six of the deck at this width, so a longer one arriving in the rotation does not shove the button around every ten seconds. The fact sits centred in that room rather than at the top of it: a three-line one would otherwise hang two blank lines above its own source line. */ display: flex; align-items: center; justify-content: center; min-height: 5lh; color: var(--muted);}
.trivia-meta { margin: 0; display: flex; align-items: center; justify-content: center; gap: 1rem; color: var(--muted);}
.trivia-era { display: inline-flex; align-items: center; gap: 0.45rem; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.09em;}
/* The era symbol, drawn in the text colour: the source images are white glyphs on alpha, so they are used as a mask rather than shown as-is. Shared with the daily card's era, which is the same symbol in the same role. */.trivia-era-icon,.daily-era-icon { width: 1.15rem; height: 1.15rem; background-color: currentcolor; mask: var(--era-icon) center / contain no-repeat;}
.trivia-source { font-size: 0.8rem; color: var(--muted);}
@media (prefers-reduced-motion: reduce) { .trivia { transition: none; }}
@keyframes waiting-scan { from { transform: translateX(-20%); } /* The sweep is 35% of the track, so the track is 100/35 of the sweep and the far edge is that less its own width. In its own widths, because that is what a percentage in translateX means. */ to { transform: translateX(186%); }}
/* Still a lit track, just not a moving one: the screen keeps saying that something is in progress without anything travelling across it. */@media (prefers-reduced-motion: reduce) { .waiting-scan-sweep { animation: none; left: 50%; transform: translateX(-50%); }}
/* --- the pilot card ------------------------------------------------------ What stands in the hero once someone is signed in, where the sign-in form stands for everyone else. Same panel treatment as .hero .signin — translucent and lifted rather than a flat card the colour of the thing behind it — so signing in changes what the panel says and not what the page is.
Everything here is deliberately loose: this is the first pass at the shape, and half of what it counts does not exist yet. */
.pilot { width: 100%; max-width: 27rem; display: flex; flex-direction: column; gap: 0.85rem; margin-top: 0.15rem; padding: 1.1rem 1.25rem; border: 1px solid var(--line); border-radius: var(--radius); background: color-mix(in srgb, var(--panel) 84%, transparent); backdrop-filter: blur(6px); box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 0.18);}
.pilot-identity { display: flex; align-items: center; gap: 0.75rem; min-width: 0;}
/* Four times the masthead's, and squared off rather than round: a card, not an avatar chip. The .avatar rules it inherits already handle the image and the monogram standing in for it. */.pilot-avatar { flex: 0 0 auto; width: 3.5rem; height: 3.5rem; border-radius: var(--radius);}
/* .avatar-monogram sets its own size for the masthead, so the letter has to be grown here or a 3.5rem box holds a 0.8rem initial. */.pilot-avatar .avatar-monogram { font-size: 1.5rem;}
.pilot-who { min-width: 0;}
.pilot-name { margin: 0; font-size: 1.05rem; font-weight: 700; letter-spacing: -0.01em; overflow-wrap: anywhere;}
/* The account itself, under the name it rents. Small, monospace and dimmed: it is the true thing on the card and the least often read. */.pilot-did { margin: 0.1rem 0 0; color: var(--muted); font-size: 0.7rem; overflow-wrap: anywhere;}
/* Four tiles that stay four across down to the phone. They are two characters wide; wrapping them would only make the card taller. */.pilot-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.4rem;}
.pilot-stat { display: flex; flex-direction: column; align-items: center; gap: 0.1rem; padding: 0.55rem 0.25rem; border: 1px solid var(--line); border-radius: var(--radius); background: color-mix(in srgb, var(--bg) 45%, transparent); text-align: center;}
.pilot-value { font-size: 1.35rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums;}
.pilot-label { color: var(--muted); font-size: 0.62rem; font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase;}
/* A figure nothing records yet: dashed and stamped, so a dash reads as "not yet" rather than as zero or as a bug.
The dimming that used to sit beside the dashes has gone, as it has on the unbuilt Modes. It was `--muted` under an opacity over this tile's caption — the composition contrast.test.mjs exists to catch — and the stamp under the caption now says in words what the fade was hinting at. */.pilot-stat-soon { border-style: dashed;}
.pilot-actions { display: flex; flex-wrap: wrap; gap: 0.4rem;}
/* Links, not buttons: they go somewhere, and the hero already has one thing styled as the thing to press. */.pilot-action { padding: 0.4rem 0.75rem; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); font-size: 0.8rem; font-weight: 600; text-decoration: none;}
.pilot-action:hover { border-color: var(--brand); color: var(--brand);}
.pilot-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px;}
/* --- the matches screen -------------------------------------------------- One row per match, reading across: the id, what it is doing, when it started, and the way in. Rules between rows rather than gaps, so a long list is a ledger rather than a stack of blocks. */
/* The Modes, in rows of their own rather than one row that wraps: Play puts the three that start a match on the first and the two that do not on the second, and each row divides evenly between the cards in it. The gap here is the row gap, so the two rows sit closer together than #app's own stack does and read as one block. */.mode-rows { display: flex; flex-direction: column; gap: 0.6rem;}
/* The ways to start a match, side by side rather than stacked, so adding a Mode later is a card added to a row rather than a new section. Equal basis and equal grow on every card, so a row of three is thirds and a row of two is halves without either being told which it is. */.mode-row { display: flex; flex-wrap: wrap; gap: 0.6rem;}
/* --panel and not --bg: these used to sit inside a Start a match card, which the heading they were under went with. Standing on the page, a card filled in the page's own colour is a border around nothing. */.mode-card { flex: 1 1 10rem; display: flex; flex-direction: column; align-items: flex-start; gap: 0.3rem; padding: 0.75rem 0.9rem; color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); text-align: left; cursor: pointer;}
.mode-card:hover { border-color: var(--accent);}
.mode-card h3 { margin: 0; font-size: 0.95rem;}
/* A Mode that does not exist yet: dashed, stamped, and not hoverable, since there is nothing behind it to press.
The dashed border is the part that says "not a button" and it stays. The dimming that used to sit beside it has gone: the stamp says the same thing in words, and the fade was `--muted` under an opacity over this card's hint line — the composition contrast.test.mjs exists to catch. Losing it makes the hint readable and the mark on top of it full strength, which is the right way round for a card naming something a player might want. */.mode-card-soon { position: relative; container-type: inline-size; cursor: default; border-style: dashed;}
.mode-card-soon:hover { border-color: var(--line);}
.match-list { display: flex; flex-direction: column;}
.match-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.75rem; padding: 0.6rem 0; border-top: 1px solid var(--line);}
.match-row:first-child { padding-top: 0; border-top: 0;}
.match-row:last-child { padding-bottom: 0;}
/* What was played. The one thing on the row the player picked, so it leads and it is the only thing on the line at full ink.
It is also the only thing on the row allowed to shrink, which is what the explicit min-width buys: a flex item's default min-width is its content, so without this "The Battle of Monte Diablo" holds its full length and the row wraps instead — which put Resume on a line of its own. Ellipsising the name is the better trade, because the row's other four parts are each shorter than the word that would replace them. */.match-scenario { overflow: hidden; min-width: 4rem; max-width: 14rem; font-size: 0.9rem; text-overflow: ellipsis; white-space: nowrap;}
/* What a row is called when the scenario cannot name it: a match from before the column existed, or one played on a scenario a later image dropped. */.match-id { font-size: 0.85rem;}
/* Who it was against. Ellipsised rather than wrapped: a five-seat match would otherwise take the whole row from the status and the way in. */.match-vs { overflow: hidden; max-width: 14rem; font-size: 0.85rem; text-overflow: ellipsis; white-space: nowrap;}
/* Pushed to the far side, with the way in after it, so every row's action lands in the same column however long the status word is. */.match-when { margin-left: auto; font-size: 0.8rem;}
.match-action { display: flex; align-items: center; justify-content: flex-end; min-width: 6rem;}
.match-row a { color: var(--accent); font-size: 0.85rem;}
/* Narrow: the date is the first thing worth losing its own place on the line, so the row wraps and the action keeps the right edge. */@media (max-width: 30rem) { .match-when { margin-left: 0; }
.match-action { margin-left: auto; }}
/* --- camo editor --------------------------------------------------------- Every canvas here has a backing store at true pixel size and is scaled up by CSS, so what is on screen is exactly what a match will draw. Nearest neighbour throughout: smoothing a preview would hide the thing the preview exists to show. */
/* The editor's own surfaces, in one place and in both schemes. They are not the site's --panel and --line: this screen stacks three depths (a card, a well holding pixel art, a chip that can be chosen) where the rest of the site stacks one, and the well has to stay darker than the card in dark mode and lighter than the art in light mode. Written as tokens because the alternative was thirty literal hexes, which is how the roll buttons came to be var(--ink) on a fixed near-black - invisible in light mode.
The MegaMek dialog is named here too. A <dialog> is opened into document.body so that the top layer is not trapped by an ancestor's stacking context, which puts it outside .camo-editor and outside these tokens - and an unresolved var() is not an error, it is a chip with no background and no border. */.camo-editor,.camo-mm-modal { --camo-well: light-dark(#e8edf4, #0e1116); --camo-chip: light-dark(#f2f5fa, #161b23); --camo-chip-on: light-dark(#dbe7fb, #1d2836); --camo-edge: light-dark(#ccd6e4, #262d38); --camo-edge-strong: light-dark(#b6c4d8, #2b3648); --camo-pick: light-dark(#1f5fd0, #4f7fc4); --camo-chip-ink: var(--ink);}
.camo-editor canvas { image-rendering: pixelated; display: block; background: var(--camo-well); border-radius: 2px;}
/* The three blocks stretch to a common height rather than each ending where its own content happens to stop, so their bottoms line up whatever the labels say. */.camo-stage-row { display: flex; gap: 1rem; flex-wrap: wrap; align-items: stretch; margin: 1rem 0;}
/* The large view and the terrain chips under it, centred on each other. The chips are wider than the view, and left to itself the view sat against the left edge of the column they share. */.camo-stage-col { display: flex; flex-direction: column; align-items: center; min-width: 0;}
/* The same frame the unit and terrain buttons use, so the large view reads as one of them rather than as a panel with something else showing behind it. It is sized to the canvas: as a stretched block it was wider than the terrain it holds, and the leftover strip looked like a rendering fault. */.camo-stage-wrap { background: var(--camo-well); border: 1px solid var(--camo-edge); padding: 0.75rem; border-radius: 4px;}
/* Pinned to the swatch's own width so the block sits beside the units instead of being widened by its buttons until it wraps below them. */.camo-aside { display: flex; flex-direction: column; gap: 0.5rem; flex: 0 0 200px; width: 200px;}
.camo-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.65;}
/* Three columns of actual-size units. A 3x3 block of 84x72 sprites is the same 252x216 as the large view beside it, so the two line up. */.camo-fleet { display: grid; grid-template-columns: repeat(3, auto); align-content: start; gap: 0.25rem; margin: 0;}
/* Two columns once three no longer fit, the way the terrain chips wrap and the pattern grid halves. The sprites stay at actual size: judging whether a camo survives being drawn small is the whole point of this block, so the column goes rather than the pixels.
380px because three columns need 293px — 84px of canvas plus padding and border, three times, plus the gaps and the widest unit name — and the page and card each spend 20px a side around it, so they stop fitting just under 373px. Below this the grid pushed the whole document sideways rather than reflowing; at 320px it overran by 14px, and every 360px phone was scrolling too. */@media (max-width: 380px) { .camo-fleet { grid-template-columns: repeat(2, auto); }}
.camo-unit-btn { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.15rem; padding: 0.25rem 0.2rem; color: var(--camo-chip-ink); background: var(--camo-well); border: 1px solid var(--camo-edge); border-radius: 4px; cursor: pointer; width: auto;}
.camo-unit-btn:hover,.camo-bg-btn:hover { border-color: var(--camo-edge-strong);}
.camo-unit-btn.active { border-color: var(--camo-pick);}
.camo-unit-name { font-size: 0.62rem; opacity: 0.7; white-space: nowrap;}
/* Pattern on the left, colours over shape on the right. */.camo-controls { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1.25rem;}
@media (max-width: 720px) { .camo-controls { grid-template-columns: 1fr; }}
.camo-column { display: flex; flex-direction: column; gap: 1.25rem; min-width: 0;}
/* Section label on the left, its roll button on the right. */.camo-group-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;}
/* All three roll buttons are the same control, so they are the same button. */.camo-roll { /* A chip-sized plate: the same cut as the buttons above it, taken down to something that still reads at 0.7rem. Filled rather than outlined, for the reason the quiet plate is. */ --notch: 0.3rem; width: auto; align-self: auto; font-size: 0.7rem; font-weight: 500; padding: 0.25rem 0.55rem 0.25rem 0.65rem; color: var(--ink); background: var(--camo-chip); border: 0; cursor: pointer; white-space: nowrap;}
.camo-roll:hover { background: var(--camo-chip-on);}
/* Each control section is a panel of its own. Unframed, Pattern / Colours / Shape were three headings floating on one flat card, and the only thing telling you where one ended was the gap. */.camo-group { display: flex; flex-direction: column; gap: 0.5rem; min-width: 0; padding: 0.75rem; background: color-mix(in srgb, var(--camo-well) 55%, transparent); border: 1px solid var(--camo-edge); border-radius: var(--radius);}
/* 21 generators divide as 3x7 exactly, so say so rather than letting auto-fill land on a ragged last row. */.camo-patterns { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.3rem;}
.camo-pattern { font-size: 0.75rem; padding: 0.3rem 0.45rem 0.3rem 0.3rem; color: var(--camo-chip-ink); background: var(--camo-chip); border: 1px solid var(--camo-edge); border-radius: 3px; cursor: pointer; width: auto; text-align: left;}
.camo-pattern:hover { border-color: var(--camo-edge-strong);}
.camo-pattern.active { border-color: var(--camo-pick); background: var(--camo-chip-on);}
/* Half of 84x72, shown at its own size. The border is what stops a pattern whose edge colour matches the chip from bleeding into it. */.camo-pattern-thumb { flex: 0 0 auto; width: 42px; height: 36px; border: 1px solid var(--camo-edge);}
.camo-pattern.active .camo-pattern-thumb { border-color: var(--camo-pick);}
.camo-swatches { display: flex; gap: 0.4rem; flex-wrap: wrap;}
/* --- the preset picker --------------------------------------------------- A chip per group over a grid of swatch tiles, replacing one flat <select>. Thirty-eight presets do not fit a menu, and a menu shows names, which is the least useful thing about a colour scheme.
Pressed state is read off aria-pressed rather than a parallel .active class. The pattern grid above carries both and has to keep them in step; there is nothing to gain from a second copy of the same fact. */.camo-cats { display: flex; flex-wrap: wrap; gap: 0.3rem;}
.camo-cat { width: auto; font-size: 0.72rem; padding: 0.25rem 0.6rem; color: var(--camo-chip-ink); background: var(--camo-chip); border: 1px solid var(--camo-edge); border-radius: 999px; cursor: pointer;}
.camo-cat:hover { border-color: var(--camo-edge-strong);}
.camo-cat[aria-pressed="true"] { border-color: var(--camo-pick); background: var(--camo-chip-on);}
/* Three columns, to line up with the pattern grid in the column beside it. The largest group is twelve, which is four clean rows. */.camo-presets { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.3rem;}
.camo-preset { display: flex; flex-direction: column; gap: 0.2rem; width: auto; padding: 0.25rem; text-align: left; color: var(--camo-chip-ink); background: var(--camo-chip); border: 1px solid var(--camo-edge); border-radius: 3px; cursor: pointer;}
.camo-preset:hover { border-color: var(--camo-edge-strong);}
.camo-preset[aria-pressed="true"] { border-color: var(--camo-pick); background: var(--camo-chip-on);}
/* Equal bands, in palette order. Order is the design in the Pride palettes and the Pride pattern draws them exactly this way, so the tile is a preview of that pattern rather than an arbitrary arrangement of the same colours. */.camo-preset-bands { display: flex; height: 1.15rem; overflow: hidden; border-radius: 2px;}
.camo-preset-band { flex: 1 1 0; min-width: 0;}
.camo-preset-name { font-size: 0.68rem; line-height: 1.2; /* "Capellan Confederation" is wider than a third of the column at any size that keeps the bands readable, so it wraps rather than being clipped. */ overflow-wrap: anywhere;}
/* Same reasoning as the pattern grid: a third of a phone is not enough for a band strip and a faction name together. */@media (max-width: 560px) { .camo-presets { grid-template-columns: repeat(2, minmax(0, 1fr)); }}
/* --- MegaMek's own camos ------------------------------------------------- Wider than the sign-in dialog because the whole point is seeing the art, and scrolling inside the dialog rather than growing past the viewport. */.camo-mm-modal { overflow: auto; max-width: min(48rem, calc(100vw - 2rem)); max-height: min(40rem, calc(100vh - 4rem)); padding: 1rem; display: flex; flex-direction: column; gap: 0.6rem;}
.camo-mm-title { margin: 0; font-size: 1.1rem;}
.camo-mm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr)); gap: 0.4rem;}
.camo-mm-tile { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; width: auto; padding: 0.3rem; font-size: 0.68rem; line-height: 1.2; text-align: center; color: var(--camo-chip-ink); background: var(--camo-chip); border: 1px solid var(--camo-edge); border-radius: 3px; cursor: pointer;}
.camo-mm-tile:hover { border-color: var(--camo-edge-strong);}
/* 84x72 at its own size, and nearest-neighbour: these are pixel art, and a browser's default smoothing turns a hard-edged splinter pattern to mush. */.camo-mm-tile img { width: 84px; height: 72px; image-rendering: pixelated; border: 1px solid var(--camo-edge);}
/* The native control draws its own swatch inset in its own chrome, which is a different shape in every browser. Squared off, sized up and given the same border as everything else here, it reads as one of this page's chips. */.camo-color { width: 52px; height: 36px; padding: 0; border: 1px solid var(--camo-edge); border-radius: 3px; background: none; cursor: pointer;}
.camo-color:hover { border-color: var(--camo-pick);}
.camo-color::-webkit-color-swatch-wrapper { padding: 2px;}
.camo-color::-webkit-color-swatch { border: none; border-radius: 2px;}
.camo-color::-moz-color-swatch { border: none; border-radius: 2px;}
.camo-field { display: grid; grid-template-columns: 4.5rem 1fr 3rem; align-items: center; gap: 0.5rem; font-size: 0.8rem;}
/* Tabular figures and a fixed column: without both, the row twitches sideways as the number goes from 9% to 100% and back. */.camo-value { font-size: 0.72rem; font-variant-numeric: tabular-nums; color: var(--muted); text-align: right;}
/* accent-color dresses the track and the thumb in one property and leaves the platform's own control shape alone, which is the shape a player already knows how to grab. The default is the site accent, which is the same blue the chosen chip is outlined in. */.camo-range { width: 100%; accent-color: var(--accent); cursor: pointer;}
.camo-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center;}
/* Import, Download and Save share the swatch's width, so they are sized to sit on one line rather than wrapping into an uneven stack. They wrap rather than run off the column when there is no room for the line: tracked capitals are wide, and a button cannot shrink below its own label. */.camo-aside .camo-actions { flex-wrap: wrap;}
.camo-aside .camo-actions button { flex: 1 1 auto; padding-inline: 0.4rem; text-align: center;}
.camo-actions button { width: auto;}
/* Deliberately the biggest control in the column. Rolling a new camo is how a player finds out what this page can do, so it gets a full-width accent button under the row of small ones rather than a fourth chip squeezed into it. */.camo-randomize { align-self: stretch; width: 100%; padding: 0.7rem 1rem; /* The display face at the size the daily card's Play half uses: this is the other control on the site that is an order rather than a link. */ font-family: var(--font-display); font-size: 1rem; font-weight: 700; letter-spacing: 0.12em;}
.camo-randomize:hover { filter: brightness(1.12);}
.camo-file { display: none;}
/* The unit type, set in mono against the name's proportional face so the two read as label and caption rather than as one wrapped line. */.camo-unit-kind { font-size: 0.56rem; /* No opacity on top of this. --muted is already the dim end of the palette, and dimming it again composited to 3.4:1 against the well - under AA, on the smallest type on the page. The mono face is what separates this from the name above it. */ color: var(--muted); letter-spacing: 0.01em; white-space: nowrap;}
/* Carries .muted already, so it had --muted at 0.55 and landed at 2.9:1. A licence attribution is the last line on the page that should be hard to read. */.camo-credit { margin-top: 1rem;}
/* Swatch, then name, then the flag if there is one. The name takes the slack so every thumbnail in a column starts at the same x. */.camo-pattern { display: flex; align-items: center; gap: 0.4rem;}
.camo-pattern > span:not(.camo-pattern-emoji) { flex: 1 1 auto; min-width: 0; /* "Reaction diffusion" wraps at a space; "automaton" has nowhere to break and was clipped mid-word in the narrow grid. */ overflow-wrap: anywhere;}
/* A 42px swatch and a name do not both fit in a third of a phone. Two columns is eleven rows instead of seven, which is a scroll rather than a truncation. */@media (max-width: 560px) { .camo-patterns { grid-template-columns: repeat(2, minmax(0, 1fr)); }}
.camo-pattern-emoji { font-size: 0.85rem; line-height: 1;}
/* Terrain chips under the large view: the same unit and camo on each of the grounds a match draws, so the choice is made on the thing being decided. */.camo-backgrounds { display: flex; gap: 0.3rem; margin-top: 0.5rem; /* Four chips are wider than a phone, and they ran off the card rather than wrapping. They are a set of equals, so a second row costs nothing, and a short second row is centred under the first rather than hanging left. */ flex-wrap: wrap; justify-content: center;}
.camo-bg-btn { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; padding: 0.2rem; width: auto; color: var(--camo-chip-ink); background: var(--camo-well); border: 1px solid var(--camo-edge); border-radius: 3px; cursor: pointer;}
.camo-bg-btn.active { border-color: var(--camo-pick);}
/* --- the sign-in dialog -------------------------------------------------- A modal so that asking for an account never reflows what is behind it: the camo editor's grid, or a page under the masthead's Login button. */.modal { padding: 0; /* A dialog scrolls its overflow by default, which crops the handle suggestions to the box instead of letting them hang below it. The dialog is six short lines tall and will not need to scroll on its own. */ overflow: visible; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); color: var(--ink); max-width: min(26rem, calc(100vw - 2rem));}
.modal::backdrop { background: rgb(4 8 14 / 0.68);}
/* The same .signin form as the front page's, in a dialog that is already a panel: it keeps the column and the padding and drops the frame, so the two borders do not sit one inside the other. */.signin-modal { background: none; border: 0; border-radius: 0;}
/* --- the report dialog --------------------------------------------------- The account menu's "Report an issue": the flare form without a match. It borrows the sign-in form's column and drops the frame the same way; the description box is the one control nothing else has dressed. */.report { width: min(26rem, calc(100vw - 2rem)); background: none; border: 0; border-radius: 0;}
.report textarea { padding: 0.6rem 0.7rem; font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius); resize: vertical;}
.report textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px;}
/* The board's tags, one checkbox each. A wrapping row, so adding a fifth is an edit to the list and nothing else. */.report-tags { display: flex; flex-wrap: wrap; gap: 0.25rem 0.9rem;}
.report-tag { display: flex; align-items: center; gap: 0.35rem; color: var(--ink); cursor: pointer;}
.report-tag input { /* Beats the .report input padding, which is sized for a text field. */ padding: 0; cursor: pointer;}
.camo-namefield { display: flex; flex-direction: column; gap: 0.25rem;}
.camo-name { width: 100%; padding: 0.4rem 0.5rem; font-size: 0.85rem;}
.camo-namefield .error { font-size: 0.75rem;}
/* The two large previews. Sized here rather than inline so the scale can be responsive, and only ever at whole multiples of 84x72 - at a fractional scale some source pixels land on three screen pixels and some on four, which bands visibly on hard-edged patterns. 3x fits a laptop; 4x needs a wide enough window that the three columns still sit on one row. */.camo-stage { width: 252px; height: 216px;}
/* One step below the large view. The camo column only has to be as wide as its own buttons, and giving the width back here is what lets all four terrain chips sit in one row beside it. */.camo-swatch { width: 168px; height: 144px;}
@media (min-width: 1180px) { .camo-stage { width: 336px; height: 288px; }
.camo-swatch { width: 252px; height: 216px; }
.camo-aside { flex: 0 0 252px; width: 252px; }}
/* The pattern hint. Space kept for two lines whether or not the current pattern needs them, so choosing one never nudges the grid underneath. */.camo-pattern-hint { margin: 0; min-height: 2.7em; font-size: 0.8rem; line-height: 1.35;}
/* What the editor has to say — saving, importing, signing in. One line is held open across the full width, so the controls under it do not jump every time it speaks. */.camo-message { margin: 0; min-height: 1.35em; font-size: 0.8rem; line-height: 1.35;}
.signin a { color: var(--accent);}
/* --- the hero's motion --------------------------------------------------- All of it lives in here, so a visitor who has asked for less motion gets the composed still page rather than a page with the movement subtracted from it: the copy is simply up, and the planets stand where the logo draws them. */
@media (prefers-reduced-motion: no-preference) { /* One implicit end keyframe each: the finished state is whatever the rules above already say it is. */ @keyframes hero-rise { from { opacity: 0; transform: translateY(0.6rem); } }
@keyframes hero-mark-in { from { opacity: 0; transform: translate(1.75rem, 0.75rem) scale(1.03); } }
@keyframes hero-glow { 0%, 100% { opacity: 0.5; transform: scale(0.95); } 50% { opacity: 1; transform: scale(1.05); } }
@keyframes hero-orbit { to { offset-distance: 100%; } }
@keyframes hero-twinkle { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
.hero-copy > * { animation: hero-rise 0.7s cubic-bezier(0.2, 0.7, 0.25, 1) both; }
.hero-copy > *:nth-child(2) { animation-delay: 0.09s; }
.hero-copy > *:nth-child(3) { animation-delay: 0.18s; }
.hero-insignia { animation: hero-mark-in 1.1s cubic-bezier(0.2, 0.7, 0.25, 1) both; }
/* The host name rolling over, one line of type turning on its own axis: the old name pitches up and away, the new one comes up from under it.
The delay on the arriving name is the whole of it. Run together, both names sit at half opacity in the same place at the same moment and the reader gets one word printed over another, which is what this looked like first time round. Leaving is 0.23s, arriving starts at 0.16s, so the old name is all but gone before there is anything to read behind it. The rotation is what makes that a roll rather than two fades.
Both fill `both`: backwards, so the arriving name spends its delay at the start of its own animation rather than sitting there fully lit waiting to begin, and forwards, so the departed name stays departed until host-cycle.ts puts it back among the hidden. The arriving name paints on top — the names take their turns in list order, so DOM order says nothing about which of the two is the newer. */ @keyframes cycle-roll-in { from { opacity: 0; transform: translateY(0.5em) rotateX(-82deg); } }
/* The visibility in here is not decoration. opacity: 0 hides a word from the eye and from nothing else: left like that, the name that just rolled away stays in the page for a screen reader, and the sentence ends in two hosts. Between a visible keyframe and a hidden one the value is visible the whole way and hidden only at the very end, so this costs the roll nothing and the forwards fill keeps it hidden afterwards. */ @keyframes cycle-roll-out { to { opacity: 0; transform: translateY(-0.5em) rotateX(82deg); visibility: hidden; } }
.hero-cycle .cycle-in { z-index: 1; animation: cycle-roll-in 0.34s cubic-bezier(0.16, 0.84, 0.3, 1) 0.16s both; }
.hero-cycle .cycle-out { z-index: 0; animation: cycle-roll-out 0.23s cubic-bezier(0.45, 0, 0.9, 0.45) both; }
.hero-mark::before { animation: hero-glow 9s ease-in-out infinite; }
.hero-insignia .ins-stars path { animation: hero-twinkle 6s ease-in-out infinite; }
.hero-insignia .ins-stars path:nth-child(2) { animation-delay: -1.5s; }
.hero-insignia .ins-stars path:nth-child(3) { animation-delay: -3s; }
.hero-insignia .ins-stars path:nth-child(4) { animation-delay: -4.5s; }
/* The planets go round. Each path is its own ellipse from the drawing, so the dot tracks the orbit it was sitting on; the parent group's cant comes along for free. The transform that placed it is dropped, because offset-path is placing it now.
Guarded, because without offset-path the dots would be moved to the corner of the viewBox and the logo would look broken. */ @supports (offset-path: path("M0 0")) { .hero-insignia .ins-planet { transform: none; transform-box: view-box; offset-rotate: 0deg; animation: hero-orbit linear infinite; }
.hero-insignia .ins-planet-1 { offset-path: path("M44 160A84 26 0 1 1 212 160A84 26 0 1 1 44 160"); animation-duration: 38s; animation-delay: -5s; }
.hero-insignia .ins-planet-2 { offset-path: path("M70 160A58 18 0 1 1 186 160A58 18 0 1 1 70 160"); animation-duration: 26s; animation-delay: -17s; }
.hero-insignia .ins-planet-3 { offset-path: path("M94 160A34 10.5 0 1 1 162 160A34 10.5 0 1 1 94 160"); animation-duration: 17s; animation-delay: -6s; } }}
/* --- the player's own camo ------------------------------------------------ A list rather than a grid of thumbnails: the name is what a camo is picked by at launch, so it gets the room, and the picture is there to recognise it by rather than to judge it at. Judging happens in the previews above. */.camo-account { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; flex-wrap: wrap;}
.camo-account p { margin: 0; min-width: 0;}
/* Full width under the two control columns, so the panel is the width of the card rather than half of it. */.camo-mine { margin-top: 1.25rem;}
.camo-collection { display: flex; flex-direction: column; gap: 0.4rem; /* Roughly four rows before it scrolls. A collection is allowed to get long without pushing the controls above it off the screen. */ max-height: 15rem; overflow-y: auto;}
.camo-collection > p { margin: 0;}
.camo-mine-item { display: flex; align-items: center; gap: 0.6rem; padding: 0.35rem; border: 1px solid var(--camo-edge); border-radius: 3px; min-width: 0;}
.camo-mine-item.active { border-color: var(--camo-pick);}
/* Actual size. Every other preview on this page argues that a camo has to read at the size a match draws it, and a thumbnail that quietly upscaled would be the one place contradicting that. */.camo-mine-swatch { width: 42px; height: 36px; flex: 0 0 auto;}
.camo-mine-noimage { display: inline-block; width: 42px; height: 36px; flex: 0 0 auto; text-align: center; line-height: 36px; color: var(--muted); background: var(--camo-chip); border: 1px solid var(--camo-edge);}
.camo-mine-body { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; flex: 1 1 auto; min-width: 0;}
.camo-mine-name { font-size: 0.8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;}
.camo-mine-actions { display: flex; gap: 0.3rem; flex: 0 0 auto;}
.camo-mine-actions button { font-size: 0.7rem; padding: 0.2rem 0.45rem 0.2rem 0.55rem; --notch: 0.3rem;}
.camo-mine-rename { display: flex; align-items: center; gap: 0.4rem; flex: 1 1 auto; min-width: 0;}
.camo-mine-rename .camo-name { font-size: 0.8rem; padding: 0.25rem 0.35rem;}
/* Armed only after the first click, so the colour appears at the moment it means something rather than sitting there daring you. */.camo-danger.armed { color: var(--danger); background: color-mix(in srgb, var(--danger) 14%, var(--panel));}
/* --- the daily challenge card ---------------------------------------------- Board on the left, everything readable on the right. Tight rather than full-bleed: the card is an invitation, not the page.
The board is a screen: see the scanline treatment below. */
/* The board takes the larger share. It is the thing a reader looks at first, it carries the unit tags now, and a fixed 320px column left it smaller than the art's own 672px - so every machine on it was being scaled down. */.daily { display: grid; /* The text side has a floor rather than a share: it holds the readable column *and* the ground beside it, and a share of a card that is itself a share of the page left it too narrow for both - which is how the text came to be read through the terrain tile. 24rem is 15rem of readable column, the 1rem between them and the tile's 8rem. The board gives up the difference; it is the thing on the card that scales. */ grid-template-columns: minmax(0, 1.25fr) minmax(24rem, 1fr); gap: 1.5rem; align-items: stretch;}
/* Everything readable on the left, the ground beside it on the right. That is where the ground belongs, and the card's floor above is what keeps it there.
Flex rather than a two-column grid: what decides whether both fit is this box's own width, not the window's, and a grid holds its two tracks whatever is left over - which is how the text came to run on under the tile while the window was still 1440px wide. The line here gives way on its own, and only where there is genuinely no room for two columns: a phone. */.daily-text { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center;}
.daily-text-main { display: grid; /* Capped rather than auto: the launch control does not wrap, so an auto track sizes to it and the column runs on under whatever is beside it instead of the layout giving way. */ grid-template-columns: minmax(0, 1fr); gap: 0.35rem; align-content: center; /* 15rem is what the launch control needs on one line, and so the width at which this column would start crowding the ground beside it. The card's floor is this plus the gap and the tile. */ flex: 1 1 15rem; min-width: 0; /* The type below sizes off this column rather than the window: the card is drawn at three widths - front page, lobby, archive - and a size chosen for one of them is wrong in the other two. */ container-type: inline-size;}
.daily-text h2 { margin: 0; line-height: 1.1; /* Fluid between the archive's wide column and the lobby's narrow one. */ font-size: clamp(1.15rem, 9cqi, 1.6rem);}
.daily-blurb { margin: 0; max-width: 46ch;}
.daily-board .daily-map { width: 100%; height: auto; image-rendering: pixelated;}
/* The two forces, head to head. */.daily-bv { display: flex; align-items: center; gap: 0.6rem; margin: 0.15rem 0;}
.daily-bv-side { display: grid; gap: 0.05rem;}
.daily-bv-right { text-align: right;}
.daily-bv-n { margin: 0; /* Fluid, and smaller than it was: 1.85rem was set when this had the column to itself, and it is the figure the row is widest at. */ font-size: clamp(1.15rem, 8cqi, 1.6rem); font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; color: var(--side);}
.daily-bv-l { margin: 0; font-size: 0.6rem; letter-spacing: 0.12em; text-transform: uppercase; color: color-mix(in srgb, var(--ink) 55%, transparent);}
.daily-bv-vs { flex: 1; text-align: center; font-size: 0.62rem; letter-spacing: 0.16em; text-transform: uppercase; color: color-mix(in srgb, var(--ink) 42%, transparent);}
@media (max-width: 46rem) { .daily { grid-template-columns: minmax(0, 1fr); }}
/* The board reads as a screen: a scanline wash over it, a lit edge, and an inner shadow that sinks it into the card. The wash is 3.5% white on a 3px period - enough to register as a surface, not enough to fight the sprites.
The screen is the whole left column, not just the picture. A board is three tiles tall and eight wide, so it can never be as tall as the column of text beside it; letterboxing it inside a lit bezel turns that leftover into part of the instrument instead of a hole in the card. */.daily-board { position: relative; display: flex; align-items: center; justify-content: center; padding: 0.5rem; overflow: hidden; border: 1px solid color-mix(in srgb, var(--brand) 35%, transparent); border-radius: 0.3rem; background: rgb(0 0 0 / 35%); box-shadow: inset 0 0 30px rgb(0 0 0 / 60%);}
.daily-board::after { content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit; background: repeating-linear-gradient( to bottom, rgb(255 255 255 / 3.5%) 0 1px, transparent 1px 3px );}
.daily-eyebrow { color: var(--brand);}
/* Each figure lit in its own force's colour. */.daily-bv-n { text-shadow: 0 0 18px color-mix(in srgb, var(--side) 55%, transparent);}
/* The most important control on the card, sized like it. Everything else in the row beside it is a link. */.daily-play { /* PLAY, in the display face and tracked wide. The word is four letters and it is the one thing on the card that has to look like an order rather than a link, which is what the capitals and the air between them buy. The left padding carries the extra to clear the notch cut out of that corner, so the word still sits centred inside the shape. */ padding: 0.5rem 1.15rem 0.5rem 1.35rem; font-family: var(--font-display); font-size: 1rem; font-weight: 700; letter-spacing: 0.12em; white-space: nowrap; filter: drop-shadow(0 0 0 transparent); transition: filter 150ms ease;}
/* drop-shadow rather than box-shadow: the half is clipped, so a box-shadow is cut away and a filter follows the notched edge. */.daily-play:hover:not(:disabled) { filter: drop-shadow( 0 0 10px color-mix(in srgb, var(--brand) 60%, transparent) );}
.daily-play:focus-visible { outline: 0; box-shadow: inset 0 0 0 2px var(--on-accent);}
@media (prefers-reduced-motion: reduce) { .daily-play { transition: none; }}
/* --- what the fight changes about the game ------------------------------- Between the battle values and the way in, because it is the last thing a player needs before committing. A vertical list rather than a row of chips: these are read, not scanned, and there is room. */.daily-overrides { display: grid; gap: 0.5rem; margin: 0.15rem 0 0.1rem;}
.daily-over-group { display: grid; gap: 0.1rem;}
.daily-over-head { margin: 0; font-size: 0.58rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: color-mix(in srgb, var(--text) 40%, transparent);}
.daily-over { display: flex; align-items: baseline; gap: 0.4rem; margin: 0; font-size: 0.76rem; color: color-mix(in srgb, var(--text) 78%, transparent);}
/* The kind, as a mark rather than a word: a filled square for a rule of the game, a hollow ring for a setting on the opposition's pilot. */.daily-over-mark { width: 0.45rem; height: 0.45rem; flex: none; border-radius: 1px;}
.daily-over-rules .daily-over-mark { background: var(--brand);}
.daily-over-ai .daily-over-mark { border: 1px solid color-mix(in srgb, var(--danger) 75%, transparent); border-radius: 50%;}
/* The name keeps its own width. `min-width: 0` alone let a flex row squeeze it to a few characters when the value beside it was long - a spelled-out behaviour runs to a line and a half - and the label then overflowed its box and printed on top of the value. */.daily-over-name { min-width: 0; flex: 0 0 auto; max-width: 45%; overflow-wrap: break-word;}
/* MegaMek marks some rules unofficial. A scored fight leaning on one should say so rather than let a player discover it against a record sheet. */.daily-over-tag { font-size: 0.56rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.05rem 0.3rem; border-radius: 999px; color: #e0a542; border: 1px solid color-mix(in srgb, #e0a542 45%, transparent);}
.daily-over-value:empty { display: none;}
.daily-over-value { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: 700; color: color-mix(in srgb, var(--text) 55%, transparent);}
/* --- the ground the fight is on -------------------------------------------- Beside the text, not over the board: the board already carries the machines and their names, and an inset in its corner stood on whichever force deployed there. Here it runs the full height of the column, top to bottom, which is the only height the card has spare. */.daily-terrain-tile { /* The box is the map, exactly: the aspect ratio is set from the canvas the screen actually drew, so the frame hugs the ground and there is no letterbox inside it to read as wasted space. */ align-self: center; flex: 0 0 auto; width: 8rem; /* Centred once it has wrapped onto a line of its own, and inert while it is beside the text: there the text has taken every spare pixel already. */ margin-inline: auto; border: 1px solid color-mix(in srgb, var(--brand) 28%, transparent); border-radius: 0.25rem; overflow: hidden; background: rgb(11 16 23);}
/* Fills the box it was given rather than keeping its own aspect: the box was sized from this canvas, so the two already agree and nothing is stretched. */.daily-terrain { display: block; width: 100%; height: 100%;}
/* On a phone the ground has already wrapped under the text and has the whole width to itself, so it is drawn larger than the 8rem it gets beside a column of type. */@media (max-width: 34rem) { .daily-terrain-tile { width: min(60%, 12rem); }}
/* The card as the lobby shows it: the same figure the front page draws, with no eyebrow, no date badge and no way in - Deploy is the way in there. It sits inside a section rather than being one, so it drops the outer card's own frame and keeps only the two columns. */.daily-brief { align-self: stretch; border: 0; padding: 0; background: none;}
.daily-brief-name { margin: 0; font-size: 1.15rem;}
/* --- Hangar --------------------------------------------------------------- Building a force to a target. Three bands down the page: what the force is built to, what it is worth, and the two panes it is built in.
The strip is the meter. One segment per design, width its share of the target, so the picture and the number say the same thing and the gap at the end is what is left to spend. Over the target the whole force is scaled to fill the bar instead and a line marks where the target fell — segments running off the end say the same thing less clearly. */.hangar { display: flex; flex-direction: column; gap: 1rem;}
.hangar-targets { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1.25rem; padding: 0.75rem 1rem; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);}
.hangar-targets-label { display: flex; flex-direction: column; line-height: 1.2;}
.hangar-target { display: flex; align-items: center; gap: 0.4rem; font-size: 0.9rem;}
.hangar-target-label { color: var(--muted);}
.hangar-target-input { width: 6.5rem; padding: 0.3rem 0.45rem; font-family: var(--font-data); font-size: 0.9rem; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);}
.hangar-target-suffix,.hangar-lock { color: var(--muted); font-size: 0.85rem;}
.hangar-lock { display: flex; align-items: center; gap: 0.4rem;}
.hangar-meter { display: flex; flex-direction: column; gap: 0.5rem;}
.hangar-totals { display: flex; align-items: baseline; gap: 0.5rem;}
.hangar-total { font-family: var(--font-data); font-size: 1.6rem; font-variant-numeric: tabular-nums;}
/* Over the target is a warning, not an error: the force still plays. Amber is the stamp's colour, which is what the site already uses for "unfinished but working" rather than for "wrong". */.hangar-total.over { color: var(--stamp);}
.hangar-standing { margin-left: auto;}
.hangar-strip { position: relative; display: flex; min-height: 74px; background: var(--bg); border: 1px solid var(--line); border-radius: 3px; overflow: hidden;}
.hangar-seg { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; padding: 0.35rem 0.45rem; overflow: hidden; color: var(--on-accent); background: var(--accent); border-left: 1px solid var(--bg);}
/* Each segment is a step dimmer than the one before it, so a force of six reads as six rather than as one long bar. Set inline, per segment, because the ramp is positional: as nth-child rules the sixth segment matched both 2n and 3n, and every one of them outranked the over-target class below — which is how the machine that broke the target came to be drawn as if it had not. */.hangar-seg-over { color: var(--bg); background: repeating-linear-gradient( 135deg, var(--stamp) 0 4px, color-mix(in oklab, var(--stamp) 70%, var(--bg)) 4px 8px );}
.hangar-seg-name { font-family: var(--font-labels); font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
.hangar-seg-facts { font-size: 0.68rem; opacity: 0.8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
.hangar-seg-bv { display: flex; align-items: baseline; gap: 0.3rem; margin-top: auto; font-family: var(--font-data); font-size: 0.78rem;}
.hangar-seg-share { font-size: 0.66rem; opacity: 0.75;}
.hangar-target-line { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--ink);}
.hangar-warning { margin: 0; padding: 0.5rem 0.7rem; color: var(--ink); font-size: 0.9rem; background: color-mix(in oklab, var(--stamp) 12%, transparent); border: 1px solid var(--stamp); border-radius: var(--radius);}
.hangar-panes { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 22rem); gap: 1rem; align-items: start;}
.hangar-search-row { display: flex; align-items: center; gap: 0.6rem;}
/* Beside the count, and sized to its longest option so the count does not move when the order changes. */.hangar-sort { padding: 0.3rem 0.4rem; font-family: var(--font-text); font-size: 0.8rem; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);}
.hangar-search { flex-grow: 1; padding: 0.45rem 0.6rem; font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);}
.hangar-results,.hangar-roster { display: flex; flex-direction: column; gap: 0.35rem;}
/* The results and the roster are lists in the markup. The bullets and the indent go; the column they were already drawn as stays. */.hangar-hit-list,.hangar-row-list { display: flex; flex-direction: column; gap: 0.35rem; margin: 0; padding: 0; list-style: none;}
.hangar-hit,.hangar-row { display: grid; align-items: center; gap: 0.6rem; padding: 0.3rem 0.5rem 0.3rem 0.3rem; background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);}
/* The sprite at 84x72, which is the size MegaMek drew it: a row four lines tall had room for it, and pixel art at its own resolution is the one scale that needs no interpolation at all. The column pays for itself out of the Add button, which is a plus rather than a word. */.hangar-hit { grid-template-columns: 84px minmax(0, 1fr) 4rem 6rem auto;}
.hangar-row { grid-template-columns: 44px minmax(0, 1fr) auto;}
/* Name and model on one line, everything else on the next: the pane is 22rem and a four-column row wrapped every field onto a line of its own. */.hangar-row-title { min-width: 0;}
/* Two lines, because a long designation is two things. "Mechanized Assault XCT" and "Word of Blake Militia NBC" run together as one wrapped sentence when they share a line, and neither reads. The chassis is what a player scans for, so it gets the line. */.hangar-chassis { white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
.hangar-model { font-size: 0.85em; overflow-wrap: anywhere;}
/* A Clan design's other name, beside the chassis rather than under it. Dimmed because the chassis is what the column is scanned by, and present because it is half of what gets typed into the search box. */.hangar-second-name { opacity: 0.7;}
/* The roster column is narrow and a chassis there was being cut off mid-word with a Remove button beside it. Wrapping costs a row a line and shows the whole name, which is the point of the column. */.hangar-row-title .hangar-chassis { white-space: normal; overflow: visible; overflow-wrap: anywhere;}
.hangar-hit-name,.hangar-row-name { min-width: 0; font-family: var(--font-labels); letter-spacing: 0.04em;}
.hangar-hit-facts,.hangar-row-facts { font-family: var(--font-text); font-size: 0.8rem; letter-spacing: normal;}
/* The last term on the facts line: what kind of machine it is. Only an Omni has anything to explain, and only that one gets the cursor that says so. */.hangar-kind[title] { cursor: help;}
/* What the design moves, wears and shoots. Wraps rather than scrolls or clips: the pane is elastic and a figure that is not on screen is a figure the player has to go somewhere else for, which is the thing this line exists to stop. */.hangar-stats { display: flex; flex-wrap: wrap; gap: 0.15rem 0.45rem; font-family: var(--font-data); font-size: 0.74rem; font-variant-numeric: tabular-nums; letter-spacing: normal;}
/* The two lines are one block under the facts, not two paragraphs: the space that separates them from the name above is the same space every other line in the column gets, and between themselves they get none. */.hangar-hit-facts + .hangar-stats { margin-top: 0.15rem;}
.hangar-stat { cursor: help; white-space: nowrap;}
/* The figure in the page's ink and the word it is measured in a step back, so a column of rows scans as numbers rather than as five short sentences. A coloured chip keeps one colour throughout: the colour is the point there, and splitting it would say the number and the word disagree. */.hangar-stat-n { color: var(--ink);}
.hangar-stat-warm .hangar-stat-n,.hangar-stat-hot .hangar-stat-n,.hangar-stat-cool .hangar-stat-n { color: inherit;}
/* Space alone did not separate them: every figure is a number and a word, so "5/8/3 MP 230 armour" read as one phrase. The dot is the same one the facts line above uses, and it belongs to the figure in front of it rather than being a box of its own, so a line that wraps never opens with one. */.hangar-stat:not(:last-child)::after { content: "·"; margin-left: 0.45rem; color: var(--muted);}
/* Colour appears on this line only where one alpha strike costs the machine something: a movement point at 5 on MegaMek's heat scale, a shutdown roll at 14. Two thirds of the library is neither and stays the colour of every other figure, which is what keeps the other third worth looking at. */.hangar-stat-warm { color: var(--stamp);}
.hangar-stat-hot { color: var(--danger);}
/* The other end of the same line, and deliberately not the other end of a scale: a design that can fire everything and still move is one state, not a range of degrees of cold. */.hangar-stat-cool { color: var(--cool);}
.hangar-hit-mass,.hangar-hit-bv,.hangar-row-bv { font-family: var(--font-data); font-variant-numeric: tabular-nums; text-align: right;}
.hangar-hit-bv { display: flex; flex-direction: column; align-items: flex-end;}
.hangar-over { font-size: 0.7rem; color: var(--stamp);}
/* MegaMek's sprites are pixel art at 84x72, and the only sizes they may be drawn at are that and half of it. A fractional scale drops some columns and smears the rest into their neighbours, and what is lost is the silhouette - at which point an Atlas, a BattleMaster and a Locust are one grey smudge and the picture has stopped doing the one job it is in the row for. */.hangar-sprite { width: 84px; height: 72px; image-rendering: pixelated;}
.hangar-sprite-half { width: 42px; height: 36px;}
/* The plus is the row's full height rather than a plate floating in the middle of it: the row is four lines tall now, and a control that has to be aimed at is easier to hit when its target is the whole thing it acts on. */.hangar-add { align-self: stretch; display: grid; place-items: center; min-width: 2.4rem; padding: 0 0.5rem; font-size: 1.35rem; line-height: 1; letter-spacing: normal;}
.hangar-sprite-none { display: inline-block; background: color-mix(in oklab, var(--line) 60%, transparent); border-radius: 3px;}
/* A phone has no room for a full-size picture beside four columns of figures. Half, which is the other size the art may be drawn at, and the column goes with it. */@media (max-width: 40rem) { .hangar-hit .hangar-sprite { width: 42px; height: 36px; }
.hangar-hit { grid-template-columns: 42px minmax(0, 1fr) 3.5rem 5rem auto; }}
.hangar-ways-in { display: flex; align-items: center; gap: 0.75rem; margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--line);}
.hangar-unsaved { margin: 0.5rem 0 0;}
/* One column when the two panes stop fitting side by side: the force goes under the library rather than beside it, and the strip keeps its width. */@media (max-width: 60rem) { .hangar-panes { grid-template-columns: minmax(0, 1fr); }}
/* The force and the paint it fights in, stacked in the right-hand column. */.hangar-side { display: flex; flex-direction: column; gap: 1rem;}
.hangar-suggested { margin: 0 0 0.25rem;}
.hangar-more { align-self: flex-start; margin-top: 0.35rem;}
/* What the target is imposing on the search. Small print under the box: a player who knows the syntax should not have to read it, and one who does not should not have to guess why a search answered the way it did. The syntax itself is a dialog behind the Filters button — twenty keys is a reference, not a caption. */.hangar-term { font-family: var(--font-data); font-size: 0.9em; padding: 0.05rem 0.25rem; background: var(--bg); border: 1px solid var(--line); border-radius: 3px;}
.hangar-imposed { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem;}
.hangar-imposed .hangar-term { border-color: var(--accent);}
/* The filters in the box, as something to read and take off, and the little builder that writes one into it. Both are views of the query text: the box is what is being searched, and neither of these holds a filter of its own. */.hangar-pills { display: flex; flex-wrap: wrap; gap: 0.35rem;}
.hangar-pills:empty { display: none;}
.hangar-pill { display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.15rem 0.3rem 0.15rem 0.4rem; background: var(--panel); border: 1px solid var(--accent); border-radius: 999px;}
.hangar-pill code { font-family: var(--font-data); font-size: 0.78rem; border: 0; background: none; padding: 0;}
.hangar-pill-drop { padding: 0 0.25rem; font: inherit; line-height: 1; color: var(--muted); background: none; border: 0; border-radius: 999px; clip-path: none; letter-spacing: normal; text-transform: none; cursor: pointer;}
.hangar-pill-drop:hover { color: var(--ink);}
.hangar-builder { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem;}
.hangar-pick { padding: 0.25rem 0.35rem; font: inherit; font-size: 0.85rem; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);}
/* A key with one comparison - a flag is yes or no, and nothing else - leaves the control in place and says it is not a choice, rather than removing it and moving the two beside it. */.hangar-pick:disabled { opacity: 0.55;}
.hangar-op { font-family: var(--font-data); width: 4rem;}
.hangar-value { width: 9rem;}
/* The filter reference. A dialog because it is a page of it, and because a workbench should be able to put its manual away. */.hangar-help::backdrop { background: rgb(0 0 0 / 55%);}
.hangar-help { max-width: min(42rem, calc(100vw - 2rem)); color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);}
.hangar-help-body { display: flex; flex-direction: column; gap: 0.6rem; padding: 1.25rem;}
.hangar-help-body h2 { margin: 0; font-size: 1.1rem;}
.hangar-help-body h3 { margin: 0.4rem 0 0; font-size: 0.85rem;}
.hangar-help-body p { margin: 0; font-size: 0.9rem;}
.hangar-help-examples,.hangar-help-filters,.hangar-help-soon { display: flex; flex-direction: column; gap: 0.3rem; margin: 0; padding: 0; list-style: none; font-size: 0.9rem;}
.hangar-help-examples code,.hangar-help-filters code,.hangar-help-soon code,.hangar-help-keys code { font-family: var(--font-data); font-size: 0.85em; padding: 0.05rem 0.3rem; background: var(--bg); border: 1px solid var(--line); border-radius: 3px;}
/* Three columns rather than three boxes on a line: the keys line up and the examples end together, while each chip stays the width of its own text. Stretching every key to the width of "mass, tons, tonnage" made a column of mostly empty boxes. */.hangar-help-filters { display: grid; grid-template-columns: max-content 1fr max-content; align-items: baseline; gap: 0.35rem 0.6rem;}
.hangar-help-filters li { display: contents;}
.hangar-help-filters .hangar-key { justify-self: start; color: var(--ink);}
.hangar-help-filters .hangar-key-eg { justify-self: end; color: var(--muted);}
/* Narrow enough that three columns would wrap inside themselves: stack the row instead. */@media (width <= 34rem) { .hangar-help-filters { grid-template-columns: 1fr; gap: 0.15rem; }
.hangar-help-filters li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem; margin-bottom: 0.35rem; }
.hangar-help-filters .hangar-key-eg { margin-left: auto; }}
.hangar-help-keys code { display: block; padding: 0.4rem 0.5rem; line-height: 1.5;}
/* Dimmed, and marked: a term that cannot work yet should not read like one that can. */.hangar-help-soon li { opacity: 0.75;}
.hangar-soon-mark { margin-left: 0.4rem; font-family: var(--font-labels); font-size: 0.58rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--stamp); border: 1px solid var(--stamp); border-radius: 3px; padding: 0.05rem 0.3rem;}
.hangar-help-actions { justify-content: flex-end; margin-top: 0.4rem;}
/* A firepower curve, wherever one is drawn. `curve.ts` builds the shape and names these classes; every colour decision is here, so the same twenty-four numbers look the same in the Hangar, on a match report and on a saved list.
Sized by whoever places it: the svg fills its box and the viewBox does the rest, so a sparkline on a search hit and a panel-wide chart of a whole force are one component at two sizes. */.curve { display: block; width: 100%; overflow: visible;}
.curve-line { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round;}
/* The hexes the design lands nothing at, at either end of the board. Faded rather than dropped: a line that stopped would leave a reader working out whether the chart ends there or the guns do, and the flat run along the axis is the answer to "how far does this actually reach". */.curve-line-dead { opacity: 0.3;}
.curve-fill { fill: color-mix(in oklab, var(--accent) 14%, transparent);}
/* The axis is a real line and the hex marks are not: a reader needs to see where nought is, and the ruler behind the curve is there to be counted against rather than looked at. */.curve-axis { stroke: var(--line); stroke-width: 1;}
.curve-grid { stroke: color-mix(in oklab, var(--line) 55%, transparent); stroke-width: 1;}
/* Where the curve runs off the top of a shared scale. Dashed, because the machine does not stop there - the frame does. */.curve-ceiling { stroke: var(--peak); stroke-width: 1; stroke-dasharray: 3 3;}
/* The line between a run's two dots. Same weight as the curve it is part of, drawn over it, so the run reads as one mark from the first dot to the last rather than as two dots with somebody else's line between them. */.curve-peak-run { stroke: var(--peak);}
.curve-peak-dot { fill: var(--peak); stroke: var(--panel); stroke-width: 1.5;}
.curve-peak-line { stroke: var(--peak); stroke-width: 1.5;}
/* Light enough to read the curve through, dark enough to be a mark rather than a smudge: the light theme's peak is a dark orange and washes out fast over a white panel. It has to keep its colour as well as its weight - the band and the two dots that cap it are one mark, and a fifth of an orange over a near-black panel came out grey. */.curve-peak-band { fill: var(--peak); opacity: 0.28;}
.curve-peak-value,.curve-peak-hex { fill: var(--peak); font-family: var(--font-data); font-variant-numeric: tabular-nums; font-weight: 500;}
.curve-peak-value { font-size: 0.74rem;}
.curve-peak-hex { font-size: 0.68rem;}
.curve-hex { fill: var(--muted); font-family: var(--font-data); font-size: 0.68rem; font-variant-numeric: tabular-nums;}
/* One member of a force, under the total. Faint enough to read as backing and solid enough to follow, with its own best marked in the same weight: a lance covers a board when its machines peak in different places, and that is the thing this drawing is for. */.curve-faint { fill: none; stroke: var(--accent); stroke-width: 1; opacity: 0.35;}
.curve-faint-dot { fill: var(--accent); opacity: 0.5;}
/* The force's own curve, under the battle-value meter it belongs with: both answer "what is this force", and both change on every add and remove. *//* The force's heading and the camo it fights in, on one line. The choice is about these machines, so it sits with them rather than in a card of its own under the roster. */.hangar-force-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;}
.hangar-force-head h2 { margin: 0;}
/* Holds the swatch, and holds its place before the session read answers, so the heading does not shift when it arrives. Sized to the control rather than to the square it used to be. */.hangar-camo { display: block; min-width: 10.5rem; min-height: 44px;}
.hangar-force-curve { /* Capped rather than as wide as the meter above it. The chart is an svg scaled to its box, so its own writing scales with it: at the meter's full width the hex under the peak was set larger than the battle value, which is the one number on this screen that should be the loudest. */ max-width: 40rem; margin-top: 0.75rem; padding-top: 0.65rem; border-top: 1px solid var(--line);}
.hangar-force-curve-head { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; align-items: baseline; justify-content: space-between;}
.hangar-force-peak { color: var(--peak); font-family: var(--font-data); font-size: 0.82rem; font-variant-numeric: tabular-nums;}
/* A search hit's curve sits under its stat lines and inside the name column, so it is as wide as the words above it and no wider. */.hangar-curve { max-width: 150px; margin-top: 0.2rem;}