Something went wrong. Try again.
Paper Mario mod hosting site - AT Protocol appview starhaven.dev
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705/* Use to apply color to an element and its children. Supported by many components. Each says what color text on it is: white where that reads, --ink where the color is too light for it. */.accent-red { --accent: var(--red); --on-accent: white;}.accent-yellow { --accent: var(--gold); --on-accent: var(--on-gold);}.accent-green { --accent: var(--green); --on-accent: var(--ink);}.accent-blue { --accent: var(--blue); --on-accent: white;}.accent-mauve { --accent: var(--mauve); --on-accent: white;}
/* A popover menu */.menu { --accent: var(--text);
margin: var(--space-2) 0 0; padding: var(--space-2) 0; border: 2px solid var(--outline); border-radius: var(--space-3); background: var(--paper); box-shadow: 0 3px 0 var(--outline); color: var(--text);
position: fixed; inset: auto; top: anchor(bottom); right: anchor(right);
font-size: var(--text-sm);
& > ul { list-style: none; display: flex; flex-direction: column; margin: 0; padding: 0; min-inline-size: 10rem; }
& a, & button { display: flex; align-items: center; justify-content: flex-start; gap: var(--space-2); inline-size: 100%; min-block-size: 2.75rem; padding: var(--space-1) var(--space-4); border: 0; border-radius: 0; background: none; box-shadow: none; color: oklch(from var(--accent) min(l, 0.5) c h); font-weight: 400; text-align: start; text-decoration: none; cursor: pointer;
&:hover { background: var(--crust); }
& svg { inline-size: 1.1em; block-size: 1.1em; flex-shrink: 0; } }}
/* A callout. */[role="alert"] { padding: var(--space-3) var(--space-4); border-radius: 0; border: 4px solid var(--accent); background: oklch(from var(--accent) l c h / 0.15); margin-block-end: var(--space-4);
& > h2 { margin: 0; margin-bottom: var(--space-2); font-size: var(--text-lg); font-weight: 600; }}
/* A field's error message. */.error { color: var(--red);}
/* Off-screen but still in the accessibility tree/tab order, unlike the `hidden` attribute - for content (or a control, like a file input triggered by a styled label) that only needs to be reachable, not seen. */.visually-hidden { position: absolute; inline-size: 1px; block-size: 1px; padding: 0; margin: -1px; overflow: hidden; /* stylelint-disable-next-line plugin/no-unsupported-browser-features -- Only clip-path's url() and box values are partly supported; inset() works everywhere. */ clip-path: inset(50%); white-space: nowrap; border: 0;}
.error-list { list-style-type: none; margin: 0; padding: 0;}
.form-item { display: flex; flex-direction: column; gap: var(--space-1); margin: 0; margin-block-start: var(--space-4); border: none; padding: 0;
/* Not :has(.error) - every field carries an empty error slot, so the mark has to depend on the slot being shown rather than existing. */ &:has(.error:not([hidden])) { border-inline-start: 3px solid var(--red); padding-inline-start: var(--space-3); }
/* :not(.button): a label can also be a button in disguise (a styled file input trigger), and that should look and behave like any other .button, not like a field's own label. */ & label:not(.button), & legend { display: block; inline-size: 100%; font-size: var(--text-sm); font-weight: 700; padding: 0; }
& input, & textarea, & select { padding: 1px var(--space-2); border: 2px solid var(--surface2); border-radius: var(--space-1); background: var(--mantle); box-shadow: inset 0 2px 0 var(--surface0); }
& textarea { min-block-size: 6em; /* stylelint-disable-next-line plugin/no-unsupported-browser-features -- iOS Safari only can't resize it, which costs nothing. */ resize: vertical; }
& .hint { color: var(--grey); text-wrap: pretty; }}
/* A text input with an icon fixed inside its left edge. */.input-icon { position: relative; display: flex; align-items: center;
& svg { position: absolute; inset-inline-start: var(--space-2); inline-size: var(--space-4); block-size: var(--space-4); color: var(--grey); pointer-events: none; }
& input { inline-size: 100%; padding-inline-start: var(--space-6); }}
/* One checkbox and its label, side by side. */.checkbox-item { display: flex; align-items: center; gap: var(--space-2);
& input[type="checkbox"] { inline-size: var(--space-4); block-size: var(--space-4); accent-color: var(--accent); }}
/* A fieldset of radio buttons, one .radio-item per option. */.radio-group { & .radio-item { display: flex; align-items: center; gap: var(--space-2); margin-block-start: var(--space-1); }
& input[type="radio"] { inline-size: var(--space-4); block-size: var(--space-4); accent-color: var(--accent); }
& label { font-size: inherit; font-weight: 400; }}
/* The page content around a form page, which fills the space between the nav and the footer so the form page's background does too. */#page-content:has(> .form-page) { flex: 1; display: flex; flex-direction: column;}
/* A page with a form in the center of it, on a card drawn like a mod's. */.form-page { flex: 1; display: grid; place-items: center;
/* A component that drives its form takes the form's place. */ & > share-form { display: block; width: 100%; max-inline-size: 32rem; }
& > :is(form, section), & > share-form > form { width: 100%; max-inline-size: 32rem; margin-block: var(--space-6); padding: var(--space-6); border: 3px solid var(--outline); border-radius: 0.875rem; background: var(--paper); box-shadow: 0 6px 0 var(--outline);
@media (max-width: 40rem) { padding: var(--space-4); border-radius: var(--space-3); box-shadow: 0 5px 0 var(--outline); } }
@media (max-width: 40rem) { padding-inline: var(--space-4); }}
/* The sign-in and sign-up cards, whose main buttons take the card's full width. */.auth-card { /* Blue like most links, rather than the dark gold the yellow card would give them. */ & a:not(.button) { color: var(--blue); }
/* Wider than most buttons, so taller too. */ & :is(.auth-continue, .bluesky-button) { inline-size: 100%; margin-block-start: var(--space-4); padding-block: var(--space-2); }
& .bluesky-button { --accent: var(--bluesky); --on-accent: white; margin-block-start: 0;
& svg { inline-size: 1.25em; block-size: 1.25em; } }}
/* The small print under a card's main button. */.auth-terms { margin: var(--space-3) 0 0; color: var(--grey); font-size: var(--text-sm); text-align: center; text-wrap: pretty;}
/* At the foot of a card, the way to the other of signing in and signing up. */.auth-switch { margin: var(--space-6) 0 0; color: var(--grey); font-size: var(--text-sm); text-align: center;}
/* A text input with fixed text after what's typed, such as a handle's domain, drawn as part of the field. */.input-suffix { display: flex; align-items: stretch;
& > input { flex: 1; min-inline-size: 0; border-inline-end: none; border-start-end-radius: 0; border-end-end-radius: 0; }
& > span { display: flex; align-items: center; padding: 0 var(--space-2); border: 2px solid var(--surface2); border-inline-start: none; border-start-end-radius: var(--space-1); border-end-end-radius: var(--space-1); background: var(--mantle); box-shadow: inset 0 2px 0 var(--surface0); color: var(--grey); }}
/* A field's label with a (?) at the other end, opening a popover that says more about the field. */.label-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);}
.help-button { anchor-name: --help-button; padding: 0; border: none; box-shadow: none; background: none; color: var(--grey);
& svg { inline-size: 1.25rem; block-size: 1.25rem; }
&:hover { color: var(--text); }
&:active { transform: none; }}
.help-popover { position: fixed; inset: auto; top: anchor(bottom); right: anchor(right); max-inline-size: min(20rem, calc(100vw - 2rem)); margin: var(--space-2) 0 0; padding: var(--space-3) var(--space-4); border: 2px solid var(--outline); border-radius: var(--space-3); background: var(--paper); box-shadow: 0 3px 0 var(--outline); color: var(--text); font-size: var(--text-sm); position-anchor: --help-button;
& > p { margin: var(--space-1) 0 0; text-wrap: pretty; }
& .handle-example { color: var(--example); }}
/* A rule across the card with "or" in a gap in the middle of it. */.auth-or { display: flex; align-items: center; gap: var(--space-3); margin-block: var(--space-4); color: var(--grey); font-size: var(--text-sm);
&::before, &::after { content: ""; flex: 1; border-block-start: 2px solid var(--surface1); }}
/* Where a form's buttons sit. */.form-actions { display: flex; justify-content: flex-start; margin-block-start: var(--space-4); gap: var(--space-2);}
.button,button { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); padding: calc(var(--space-1) + 1px) var(--space-4); border-radius: var(--space-1);
border: 2px solid oklch(from var(--accent) calc(l - 0.2) c h); box-shadow: 0 2px 0 oklch(from var(--accent) calc(l - 0.2) c h);
background: var(--accent);
color: var(--on-accent); font: inherit; font-weight: 700; text-align: center; cursor: pointer; transition: transform 0.05s ease, box-shadow 0.05s ease;
text-decoration: none;
/* Drawn in the input fields' grey, which it often sits beside. */ &.secondary { border-color: var(--surface2); box-shadow: 0 2px 0 var(--surface2); background: transparent; color: var(--text); }
&:active { box-shadow: none; transform: translateY(2px); }
&:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
/* An SVG with only a viewBox has no intrinsic size, so it needs one giving to it. The gap above already spaces it from the label. */ & svg { inline-size: 1.1em; block-size: 1.1em; flex-shrink: 0; }}
/* The <upload-dialog> component's inner dialog. Previously scoped by shadow DOM; now in light DOM so htmx/commandfor can reach its contents. */.upload-dialog { padding: var(--space-6); inline-size: min(40rem, 92vw); border: 0; background: var(--base) url("/assets/textures/subtle-paper-pulp-repeat.webp") repeat; background-blend-mode: multiply; box-shadow: var(--space-2) var(--space-2) 0 -2px var(--outline);
&::backdrop { background: oklch(from var(--outline) l c h / 0.5); backdrop-filter: blur(4px); }
& > h2 { margin: 0 0 var(--space-4); }
& progress { inline-size: 100%; margin-block-start: var(--space-3); }}
/* The bar at the top of almost every page: the logo and controls, then a row of binder tabs sitting on a rule. */.site-header { position: relative; inline-size: 100%; max-inline-size: 75rem; margin-inline: auto; padding: var(--space-4) var(--space-6) 0;
/* What the page sits on, behind its tabs, from one side of the window to the other. */ &::before { content: ""; position: absolute; z-index: -1; inset: 0 calc(50% - 50vw); background-color: var(--crust); background-image: url("/assets/textures/subtle-paper-pulp-repeat.webp"); background-blend-mode: multiply; }
@media (max-width: 40rem) { padding: 0.625rem var(--space-3); border-block-end: 3px solid var(--outline); }}
/* The header of a page about one thing, such as signing in: the logo on its own, above a rule the width of the window. */.site-header-bare { padding-block-end: var(--space-4); border-block-end: 3px solid var(--outline); border-image: linear-gradient(var(--outline) 0 0) 0 0 1 0 / 0 0 3px 0 / 0 100vw;
@media (max-width: 40rem) { padding-block-end: 0.625rem; }}
.site-header-bar { display: flex; align-items: center; gap: var(--space-3);
& > * { margin: 0; }
@media (max-width: 40rem) { gap: var(--space-2); }}
/* Only at one size or the other. */.wide-only { @media (max-width: 40rem) { display: none !important; }}
.narrow-only { @media (min-width: 40.0625rem) { display: none !important; }}
/* The star and the STAR HAVEN logotype, glowing like the star does. */.site-logo { display: flex; align-items: center; gap: 0.625rem; margin-inline-end: auto;
& img { /* Sized by the block-size below, at the width and height attributes' ratio, so the header takes its shape before they load. */ inline-size: auto; filter: drop-shadow(0 0 0.375rem oklch(93% 0.1 92)); }
& .site-logo-star { block-size: 2.5rem; }
& .site-logo-text { block-size: 2.25rem; }
@media (max-width: 40rem) { gap: var(--space-2);
& .site-logo-star { block-size: 1.875rem; }
& .site-logo-text { block-size: 1.625rem; } }}
/* A 44px circle holding an icon. */.round-button { display: grid; place-items: center; flex-shrink: 0; inline-size: 2.75rem; block-size: 2.75rem; min-block-size: 0; padding: 0; border: 2px solid var(--outline); border-radius: 50%; box-shadow: none; background: var(--paper); color: var(--text);
&:active { transform: none; }
& svg { inline-size: 1.25rem; block-size: 1.25rem; }}
/* A rounded button for the bar's own actions, with a hard drop shadow. */.pill-button { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); block-size: 2.75rem; padding: 0 1.25rem; border: 2px solid var(--outline); border-radius: 999px; box-shadow: 0 3px 0 var(--outline); background: var(--paper); color: var(--text); font-size: 1.0625rem; font-weight: 400;
&:any-link { text-decoration: none; }
&:hover { color: var(--text); }
&:active { box-shadow: none; transform: translateY(3px); }
& svg { inline-size: 1.1em; block-size: 1.1em; }}
.login-button { border-color: oklch(from var(--gold) calc(l - 0.2) c h); box-shadow: 0 3px 0 oklch(from var(--gold) calc(l - 0.2) c h); background: var(--gold);
&, &:hover { color: var(--on-gold); }}
.discord-button { border-color: oklch(from var(--discord) calc(l - 0.2) c h); background: var(--discord);
&.pill-button { box-shadow: 0 3px 0 oklch(from var(--discord) calc(l - 0.2) c h); }
&, &:hover { color: white; }}
.account-button { anchor-name: --account-trigger; overflow: hidden;
& img { inline-size: 100%; block-size: 100%; object-fit: cover; }}
#account-menu { position-anchor: --account-trigger;}
.menu-button { background: var(--gold); color: var(--on-gold);}
/* Folded up into its round button until it has focus or holds a search. */.nav-search { display: flex; align-items: center; block-size: 2.75rem; border: 2px solid var(--outline); border-radius: 999px; background: var(--paper);
& input[type="search"] { inline-size: 0; min-inline-size: 0; padding: 0; border: 0; background: none; color: var(--text); font-size: var(--text-base); opacity: 0; transition: inline-size 0.15s ease, padding 0.15s ease;
&:focus-visible { outline: none; } }
& button { display: grid; place-items: center; inline-size: 2.5rem; block-size: 2.5rem; min-block-size: 0; padding: 0; border: 0; border-radius: 50%; box-shadow: none; background: none; color: var(--text);
&:active { transform: none; }
& svg { inline-size: 1.25rem; block-size: 1.25rem; } }
&:focus-within, &:has(input:not(:placeholder-shown)) { & input[type="search"] { inline-size: min(18rem, 40vw); padding-inline: var(--space-4) 0; opacity: 1; } }
/* The box's own ring, since its input has none. */ &:has(input:focus-visible) { outline: var(--ring); outline-offset: var(--ring-offset); }
@media (max-width: 40rem) { &:focus-within, &:has(input:not(:placeholder-shown)) { & input[type="search"] { inline-size: calc(100vw - 10rem); } } }}
/* On a phone, an open search box takes the wordmark's room. */.site-header-bar:has(.nav-search:focus-within) .site-logo-text { @media (max-width: 40rem) { display: none; }}
/* Binder tabs on a rule that runs the width of the window. The current one is the page's own paper, and joins it by covering the rule. */.site-tabs { display: flex; align-items: flex-end; gap: 0.375rem; margin-block-start: 1.125rem; padding-inline-start: 1.25rem; border-block-end: 3px solid var(--outline); border-image: linear-gradient(var(--outline) 0 0) 0 0 1 0 / 0 0 3px 0 / 0 100vw;
/* As tall as a current tab, so the header is the same height on a page without one. */ &::before { content: "\200b"; margin-block-end: -3px; margin-inline-end: -0.375rem; padding-block: 0.75rem 0.8125rem; border-block-start: 3px solid transparent; font-size: 1.25rem; }}
.site-tab { padding: 0.5625rem 1.375rem; border: 3px solid var(--outline); border-block-end: none; border-radius: var(--space-3) var(--space-3) 0 0; background: var(--accent); color: var(--on-accent); font-size: 1.125rem;
&:any-link { text-decoration: none; }
&:hover { color: var(--on-accent); }
&[aria-current="page"] { margin-block-end: -3px; padding: 0.75rem 1.625rem 0.8125rem; background-color: var(--base); background-image: url("/assets/textures/subtle-paper-pulp-repeat.webp"); background-blend-mode: multiply; color: oklch(from var(--accent) min(l, 0.5) c h); font-size: 1.25rem; }}
/* The homepage's intro is purple paper, so the current tab over it is too. */body:has(.home-intro) .site-tab[aria-current="page"] { background-color: var(--accent); color: var(--on-accent);}
/* The phone menu: a sheet of paper sliding in from the right. */.site-menu { margin: 0 0 0 auto; padding: 0; inline-size: calc(100% - 3.375rem); max-inline-size: 24rem; block-size: 100dvh; max-block-size: none; border: 0; border-inline-start: 3px solid var(--outline); background-color: var(--base); background-image: url("/assets/textures/subtle-paper-pulp-repeat.webp"); background-blend-mode: multiply; box-shadow: -8px 0 0 oklch(from var(--outline) l c h / 0.3); color: var(--text);
&::backdrop { background: oklch(from var(--outline) l c h / 0.4); }
&[open] { animation: site-menu-in 0.2s ease-out; }
& form { display: flex; justify-content: flex-end; }}
@keyframes site-menu-in { from { transform: translateX(100%); }}
.site-menu-sheet { display: flex; flex-direction: column; gap: 0.875rem;
& > * { margin: 0; }
min-block-size: 100%; padding: var(--space-3) var(--space-4) 1.25rem;
& nav { display: flex; flex-direction: column; gap: 0.875rem; }}
.site-menu-tab { display: flex; align-items: center; block-size: 3.5rem; padding: 0 1.125rem; border: 3px solid var(--outline); border-radius: var(--space-3); background: var(--accent); box-shadow: 0 4px 0 var(--outline); color: var(--on-accent); font-size: 1.375rem;
&:any-link { text-decoration: none; }
&:hover { color: var(--on-accent); }
&[aria-current="page"] { background: var(--paper); color: oklch(from var(--accent) min(l, 0.5) c h); }}
.site-menu-actions { display: flex; flex-direction: column; gap: 0.625rem; margin-block-start: auto;
& > * { margin: 0; }
& > .pill-button { block-size: 3.25rem; font-size: 1.1875rem; }
& > .discord-button { block-size: 3rem; box-shadow: none; font-size: 1.0625rem; }}
.site-menu-account { list-style: none; padding: var(--space-2) 0; border: 2px solid var(--outline); border-radius: var(--space-3); background: var(--paper);
& a, & button { display: flex; align-items: center; justify-content: flex-start; gap: var(--space-2); inline-size: 100%; min-block-size: 2.75rem; padding: 0 var(--space-4); border: 0; border-radius: 0; box-shadow: none; background: none; color: var(--text); font-size: 1.0625rem; font-weight: 400;
&:any-link { text-decoration: none; } }
& button { color: oklch(from var(--accent) min(l, 0.5) c h); }
& svg { inline-size: 1.1em; block-size: 1.1em; }}
.site-menu-legal { display: flex; justify-content: center; gap: 1.125rem; margin-block-start: var(--space-1); font-size: var(--text-sm); color: var(--grey);}
.card { padding: var(--space-4); border: 3px solid var(--outline); border-radius: var(--space-3); background: var(--paper); box-shadow: 0 6px 0 var(--outline);}
/* Images on the left, the card on the right, details below both - like a Scratch project page. */.mod-listing { margin: var(--space-6) auto; padding-inline: var(--space-4); width: 100%; max-width: 64rem;
display: grid; grid-template-columns: 2fr 1fr; grid-template-areas: "media card" "details details"; gap: var(--space-4);
@media (max-width: 40rem) { grid-template-columns: 1fr; grid-template-areas: "media" "card" "details"; }
& > media-carousel, & > .mod-image-placeholder { grid-area: media; }
/* What the mod is and how to get it at the top, who made it at the bottom, whatever the card's height. */ & > .card { display: grid; grid-area: card; grid-template-rows: 1fr auto; }
& > .mod-details { grid-area: details; }}
/* Who made a mod: their avatar beside their handle, under the title. */.mod-author { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: var(--space-2); margin-block: var(--space-2) var(--space-3); color: var(--grey); text-decoration: none;
&:hover .mod-author-name { text-decoration: underline; }
& img { inline-size: 1.75rem; block-size: 1.75rem; border-radius: 50%; object-fit: cover; }
& .mod-author-names { display: flex; flex-direction: column; min-inline-size: 0; }
& :is(.mod-author-name, .mod-author-handle) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
& .mod-author-handle { color: var(--example); font-size: var(--text-sm); }}
.mod-game { color: var(--example); font-size: var(--text-sm);}
.mod-summary { align-self: start;}
.mod-credits-section { align-self: end;}
.mod-credits-heading { margin-block: var(--space-4) 0; color: var(--example); font-size: var(--text-sm);}
.mod-credits { display: grid; gap: var(--space-2); margin-block: var(--space-2) 0; padding: 0; list-style: none;
& .mod-credit { display: flex; align-items: center; gap: var(--space-2); color: var(--grey); }
& img { inline-size: 1.75rem; block-size: 1.75rem; border-radius: 50%; object-fit: cover; }
/* The name and handle on one line, the role on its own beneath. */ & .mod-credit-names { display: flex; flex-direction: column; min-inline-size: 0; overflow-wrap: anywhere; }
/* The name and the handle stay on one line, the handle giving way first. */ & .mod-credit-who { display: flex; align-items: baseline; gap: 0 var(--space-2); min-inline-size: 0; max-inline-size: 100%;
& > * { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
& .mod-credit-name { flex-shrink: 0; max-inline-size: 100%; } }
& a.mod-credit-name { color: inherit; text-decoration: none;
&:hover { text-decoration: underline; } }
& :is(.mod-credit-handle, .mod-credit-role) { color: var(--example); font-size: var(--text-sm); }}
.mod-image-placeholder { width: 100%; aspect-ratio: 4 / 3; border-radius: var(--space-3); overflow: hidden;}
.mod-details { margin-block: var(--space-3);}
/* What people write about their mods, rendered from Markdown. */.markdown { line-height: 1.6; overflow-wrap: break-word;
& > * + * { margin-block-start: var(--space-3); }
& :is(ul, ol) { padding-inline-start: var(--space-5); }
& blockquote { padding-inline-start: var(--space-3); border-inline-start: 4px solid var(--grey); }
& pre { overflow-x: auto; }
& img { max-inline-size: 100%; block-size: auto; }
& :is(th, td) { padding: var(--space-1) var(--space-2); border: 1px solid var(--grey); }
/* Scrolls sideways by itself, so a wide table doesn't widen the page on a phone. */ & table { display: block; max-inline-size: 100%; overflow-x: auto; border-collapse: collapse; }}
/* A Markdown field that shows formatting as it's typed. The editor's theme in markdown_input.js styles its text, because CodeMirror's styles aren't in a layer and so outrank any rule here. */markdown-input { display: block;}
/* Draws the toolbar and the editor as one box that looks like a textarea. A page can restyle it with the --markdown-input-* properties, as the bio does. */.markdown-input-frame { border: 2px solid var(--markdown-input-border, var(--surface2)); border-radius: var(--markdown-input-radius, var(--space-1)); background: var(--markdown-input-background, var(--mantle)); box-shadow: var(--markdown-input-shadow, inset 0 2px 0 var(--surface0));
&:focus-within { outline: var(--ring); outline-offset: var(--ring-offset); }}
.markdown-input-toolbar { display: flex; flex-wrap: wrap; gap: 2px; padding: 2px; border-block-end: 1px solid var(--markdown-input-border, var(--surface2));
&[hidden] { display: none; }}
/* An icon-only button. This rule is as specific as the form's label and button rules and comes after them, so it overrides them. */.markdown-input-toolbar :is(button, label).markdown-input-button { display: grid; place-items: center; inline-size: 2rem; block-size: 2rem; padding: 0; border: none; border-radius: var(--space-1); background: none; box-shadow: none; color: var(--text); cursor: pointer;
&:active { transform: none; }
&:hover { background: var(--surface0); }
/* Inside the button, since the field's focus ring surrounds it. */ &:focus-visible, &:has(input:focus-visible) { outline: var(--ring); outline-offset: -2px; }
& svg { inline-size: 1.125rem; block-size: 1.125rem; }}
/* A .button placed directly in a .form-item is otherwise still a child of its column flex, which stretches it to the full row's width by default. */.form-item > .button { align-self: flex-start;}
/* One box for a version's download: a link typed into it, or a file uploaded through or dropped on it, which then takes the link's place. */download-fields { display: flex;}
.download-box { display: flex; align-items: stretch; gap: var(--space-2);
&.dropping { outline: 2px dashed var(--outline); outline-offset: 4px; }
/* As tall as the button and the shadow under it, so the two line up. */ & > :is(input[type="url"], .download-file) { margin-block-end: -2px; }
& input[type="url"] { flex: 1; min-inline-size: 0; }
& > .download-upload { flex-shrink: 0; }}
/* A chosen file, in the link field's place and drawn like it. */.download-file { display: flex; flex: 1; align-items: center; gap: var(--space-2); min-inline-size: 0; padding: 1px var(--space-2); border: 2px solid var(--surface2); border-radius: var(--space-1); background: var(--mantle); box-shadow: inset 0 2px 0 var(--surface0);
& > .download-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
& > .download-clear { flex-shrink: 0; padding: 0; border: none; box-shadow: none; background: none; color: var(--text); }}
/* A mod's versions, newest first. */.version-list { list-style-type: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3);}
.version { padding-block-end: var(--space-3); border-block-end: 2px solid var(--surface1);
&:last-child { border-block-end: none; }
& p { color: var(--grey); text-wrap: pretty; }}
/* The version and its download button sit on one line until there is no room for the two of them. */.version-header { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-2);
& h3 { margin: 0; }}
.version-actions { display: flex; flex-wrap: wrap; gap: var(--space-2);}
/* A mod's sections. Underlines rather than the filled blocks of the top-level nav, so the two strips do not read as the same level of navigation. */.mod-tabs { display: flex; gap: var(--space-4); margin-block-end: var(--space-4); border-block-end: 2px solid var(--grey);
& > a { padding: var(--space-2) var(--space-1); color: var(--grey); text-decoration: none;
/* Reserves the active underline's height on every tab, so selecting one does not shift the row. */ border-block-end: 3px solid transparent; margin-block-end: -2px;
&:hover { color: inherit; }
&.mod-tab-active { color: inherit; border-block-end-color: var(--accent); } }}
/* Three equal sections under the download button: the star toggle, then how often the mod was downloaded and its page viewed. */.mod-engagement { display: grid; grid-template-columns: repeat(3, 1fr); margin-block-start: var(--space-3); overflow: hidden; border: 2px solid var(--surface2); border-radius: var(--space-1); box-shadow: 0 2px 0 var(--surface2);
& > * { display: flex; align-items: center; justify-content: center; gap: var(--space-2); min-inline-size: 0; font-variant-numeric: tabular-nums; font-weight: 700; }
& > * + * { border-inline-start: 2px solid var(--surface2); }
& svg { flex-shrink: 0; inline-size: 1.1em; block-size: 1.1em; }}
.mod-stat { padding-block: var(--space-2); color: var(--grey);}
/* The star toggle fills its section, so the whole section is the button. */.star { display: flex;
& form { display: flex; flex: 1; margin: 0; }
& .star-button { display: flex; flex: 1; align-items: center; justify-content: center; gap: var(--space-2); padding: var(--space-2); border: none; border-radius: 0; box-shadow: none; background: transparent; color: var(--text); font-weight: inherit;
&:hover, &:focus-visible { background: var(--mantle); }
&:active { transform: none; }
&[aria-pressed="true"] { background: oklch(from var(--gold) l c h / 0.3);
& svg { fill: var(--gold); stroke: var(--on-gold); } } }
& .star-count { font-variant-numeric: tabular-nums; font-weight: 700; }}
.download-button { inline-size: 100%; font-size: 1.125rem; padding-block: var(--space-3);}
.search-page { margin: var(--space-6) auto; inline-size: 100%; max-inline-size: 72rem; padding-inline: var(--space-4);}
/* Both grids fill as many columns as fit, landing on 4 at the page's own max width, and fewer as the viewport narrows. */.search-users { list-style-type: none; margin: 0 0 var(--space-5); padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: var(--space-4);
& a { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); color: inherit; text-decoration: none; text-align: center; }}
.search-user-names { display: flex; flex-direction: column; min-inline-size: 0; overflow-wrap: anywhere;}
.search-user-handle { color: var(--example); font-size: var(--text-sm);}
.search-avatar { inline-size: 4.5rem; block-size: 4.5rem; border-radius: 50%; background: var(--crust); object-fit: cover;}
/* A GitHub-style profile page: avatar and bio at the top, that actor's mods below. */.actor-page { margin: var(--space-6) auto; inline-size: 100%; max-inline-size: 72rem; padding-inline: var(--space-4);
& h2 { margin-block-end: var(--space-3); }}
.actor-header { display: flex; align-items: flex-start; gap: var(--space-4); margin-block-end: var(--space-5);}
.actor-avatar { inline-size: 8rem; block-size: 8rem; border-radius: 50%; flex-shrink: 0;}
.actor-avatar-img { object-fit: cover;}
/* Wraps the avatar for the owner only, so hovering it to change the photo doesn't look or act like anything for a visitor. */.actor-avatar-edit { position: relative; display: block; flex-shrink: 0; cursor: pointer; border-radius: 50%; overflow: hidden;
& .actor-avatar-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: oklch(0% 0 0 / 0.4); color: oklch(97% 0 0); opacity: 0; transition: opacity 0.15s ease;
& svg { inline-size: 1.5rem; block-size: 1.5rem; } }
&:hover .actor-avatar-overlay, &:focus-within .actor-avatar-overlay { opacity: 1; }}
.actor-header-info { flex: 1; min-inline-size: 0;
& h1 { margin: 0 0 var(--space-2); }}
.actor-handle { margin: calc(var(--space-2) * -1) 0 var(--space-2); color: var(--example);}
.actor-bio { color: var(--grey); text-wrap: pretty;}
.actor-bio-form { max-inline-size: 32rem;}
.actor-name-input { inline-size: 100%; margin-block-end: var(--space-2); padding: var(--space-2); border: 2px solid var(--outline); border-radius: var(--space-2); background: var(--paper); font: inherit;}
.actor-bio-input { inline-size: 100%; min-block-size: 5rem; padding: var(--space-2); border: 2px solid var(--outline); border-radius: var(--space-2); background: var(--paper); font: inherit; /* stylelint-disable-next-line plugin/no-unsupported-browser-features -- iOS Safari only can't resize it, which costs nothing. */ resize: vertical;}
/* The bio's editor, drawn like the textarea it replaces. */.actor-bio-form markdown-input { --markdown-input-min-block-size: 5rem; --markdown-input-padding: var(--space-2); --markdown-input-border: var(--outline); --markdown-input-radius: var(--space-2); --markdown-input-background: var(--paper); --markdown-input-shadow: none;}
.placeholder { font-size: 2em; margin: auto;}
/* Under every page, however short. */.site-footer { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-5); margin-block-start: auto; padding: 1.25rem; border-block-start: 3px solid var(--outline); color: var(--grey); font-size: 0.9375rem;}
.legal-document { max-inline-size: 42rem; margin-inline: auto; padding: var(--space-6); line-height: 1.6;
& > * + * { margin-block-start: var(--space-4); }
& :is(h1, h2) { text-wrap: balance; }
& > h2 { margin-block-start: var(--space-7); }
& > h2 + * { margin-block-start: var(--space-2); }
& :is(p, li) { text-wrap: pretty; }
& :is(ul, ol) { padding-inline-start: var(--space-5); }
& li + li { margin-block-start: var(--space-2); }
& table { border-collapse: collapse; }
& :is(th, td) { padding: var(--space-1) var(--space-2); border: 1px solid var(--grey); text-align: start; vertical-align: top; }}
/* Mods other accounts have offered to the signed-in one, above every page. */.transfer-offers { list-style: none; margin: 0 auto var(--space-4); padding: var(--space-3) var(--space-4); max-inline-size: 60rem; background: oklch(from var(--accent) 95% calc(c * 0.3) h); border-radius: var(--space-1);
& li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
& li + li { margin-block-start: var(--space-2); }
& span { flex: 1; }
& form { margin: 0; }}
/* The account a transfer would go to, shown before it's offered. */.transfer-recipient { display: flex; align-items: center; gap: var(--space-3); margin-block-start: var(--space-4);}
.transfer-recipient-names { display: flex; flex-direction: column; min-inline-size: 0; overflow-wrap: anywhere;}
.transfer-recipient-handle { color: var(--example); font-size: var(--text-sm);}
.transfer-avatar { inline-size: 3rem; block-size: 3rem; border-radius: 50%; object-fit: cover;}
/* Stands in for the settings forms while a transfer is being accepted. */.transfer-progress { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-3); min-block-size: 24rem; text-align: center;
& p { margin: 0; }
& .transfer-recipient { margin-block-start: 0; }}
.transfer-progress-heading { font-weight: bold;}
.spinner { inline-size: 2.5rem; block-size: 2.5rem; border: 4px solid color-mix(in srgb, currentcolor 20%, transparent); border-block-start-color: currentcolor; border-radius: 50%; animation: spin 0.9s linear infinite;
@media (prefers-reduced-motion: reduce) { animation-duration: 3s; }}
@keyframes spin { to { rotate: 1turn; }}
/* The homepage's welcome, for visitors who aren't signed in: a band of purple paper with a torn bottom edge. */.home-intro { --outline: var(--ink); --ring: 2px solid white;
background-color: var(--mauve); background-image: url("/assets/textures/subtle-paper-pulp-repeat.webp"); background-blend-mode: multiply; color: white; /* stylelint-disable-next-line plugin/no-unsupported-browser-features -- Only clip-path's url() and box values are partly supported; polygon() works everywhere. */ clip-path: polygon( 0 0, 100% 0, 100% calc(100% - 14px), 97% 100%, 93% calc(100% - 10px), 88% 100%, 82% calc(100% - 12px), 76% 100%, 70% calc(100% - 8px), 63% 100%, 57% calc(100% - 12px), 50% 100%, 44% calc(100% - 9px), 37% 100%, 31% calc(100% - 13px), 24% 100%, 18% calc(100% - 9px), 11% 100%, 5% calc(100% - 12px), 0 100% );
@media (max-width: 40rem) { /* stylelint-disable-next-line plugin/no-unsupported-browser-features -- Only clip-path's url() and box values are partly supported; polygon() works everywhere. */ clip-path: polygon( 0 0, 100% 0, 100% calc(100% - 10px), 92% 100%, 80% calc(100% - 8px), 66% 100%, 52% calc(100% - 9px), 38% 100%, 24% calc(100% - 8px), 10% 100%, 0 calc(100% - 6px) ); }}
.home-intro-inner { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); align-items: center; gap: var(--space-7); max-inline-size: 75rem; margin-inline: auto; padding: var(--space-7) var(--space-6) 4rem;
@media (max-width: 40rem) { grid-template-columns: minmax(0, 1fr); padding: 1.375rem var(--space-4) 1.875rem; }}
.home-intro-text { display: flex; flex-direction: column; gap: 1.125rem;
& > * { margin: 0; }
& h1 { font-size: 3.25rem; line-height: 1.02; text-wrap: balance; }
& p { max-inline-size: 30em; color: var(--on-mauve-soft); font-size: 1.25rem; line-height: 1.45; text-wrap: balance; }
@media (max-width: 40rem) { gap: 0.625rem;
& h1 { font-size: 1.875rem; line-height: 1.05; }
& p { font-size: var(--text-base); line-height: 1.4; } }}
/* Three mods' images, scattered like photos on a desk. */.home-shots { position: relative; block-size: 21.25rem;
@media (max-width: 40rem) { display: none; }}
.home-shot { position: absolute; inline-size: 56%; aspect-ratio: 4 / 3; overflow: hidden; border: 4px solid var(--ink); border-radius: var(--space-2); background: white; box-shadow: 0 8px 0 oklch(from var(--ink) l c h / 0.45);
& img { inline-size: 100%; block-size: 100%; object-fit: cover; }
&:nth-child(1) { inset-block-start: 30px; inset-inline-start: 0; inline-size: 58%; rotate: -6deg; }
&:nth-child(2) { inset-block-start: 0; inset-inline-end: 0; rotate: 5deg; }
&:nth-child(3) { inset-block-end: 0; inset-inline-start: 24%; rotate: -1.5deg; }}
.home-star { position: absolute;
& path { stroke: var(--ink); stroke-width: 1.5; stroke-linejoin: round; }}
.home-star-big { inset-block-end: 18%; inset-inline-end: 4%; inline-size: 3.375rem; rotate: 14deg;
& path { fill: var(--yellow); }}
.home-star-small { inset-block-start: 0; inset-inline-start: 2%; inline-size: 2.125rem; rotate: -12deg;
& path { fill: var(--paper); }}
/* The homepage, and every mod's list. */.home-page { display: flex; flex-direction: column; gap: 2.5rem; inline-size: 100%; max-inline-size: 75rem; margin-inline: auto; padding: 1.75rem var(--space-6) 4rem;
& > * { margin: 0; }
@media (max-width: 40rem) { gap: 1.125rem; padding: var(--space-4); }}
.mod-lists { display: flex; flex-direction: column; gap: 2.5rem;
& > * { margin: 0; }
@media (max-width: 40rem) { gap: 1.125rem; }}
/* The featured mods: one big card at a time, with the ones either side peeking in. */.featured { display: flex; flex-direction: column; align-items: center; gap: 0.875rem;
@media (max-width: 60rem) { gap: 0.625rem; }}
/* The stage spans the whole window, so the cards either side of the current one show whole, cut off only by its edges. */.featured-stage { --featured-card: min(66rem, calc(100vw - 9rem)); --featured-gap: var(--space-5);
display: grid; justify-items: center; inline-size: 100vw; margin-inline: calc(50% - 50vw); padding-block: 2px 10px; overflow: hidden;
@media (max-width: 60rem) { --featured-card: 100%;
inline-size: 100%; margin-inline: 0;
/* The slides beside the current one sit a card's width off to either side, which would widen the page and have a phone zoom out to fit them. Clipped across only, so the card's shadow still shows. */ overflow: clip visible; }}
/* Every slide in the one grid cell, each moved along by how many places it is from the current one; the ones either side smaller and faded, and the rest out of sight. */.featured-track { display: grid; inline-size: var(--featured-card); /* stylelint-disable-next-line plugin/no-unsupported-browser-features -- Safari lacks only pinch-zoom, which this doesn't use. */ touch-action: pan-y;}
.featured-slide { grid-area: 1 / 1; translate: calc(var(--offset) * (100% + var(--featured-gap))) 0; transform-origin: calc(50% - var(--offset) * 50%) center; scale: 0.82; opacity: 0; visibility: hidden; transition: translate 0.45s cubic-bezier(0.2, 0.8, 0.2, 1), scale 0.45s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.45s ease, visibility 0.45s;
@media (prefers-reduced-motion: reduce) { transition: none; }}
.featured-slide-near { opacity: 0.45; visibility: visible; cursor: pointer;}
.featured-slide-current { z-index: 1; scale: 1; opacity: 1; visibility: visible;}
.featured-card { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); overflow: hidden; border: 3px solid var(--outline); border-radius: 0.875rem; background: var(--paper); box-shadow: 0 6px 0 var(--outline); color: var(--text);
&:any-link { text-decoration: none; }
@media (max-width: 60rem) { grid-template-columns: minmax(0, 1fr); border-radius: var(--space-3); box-shadow: 0 5px 0 var(--outline); }}
.featured-image { position: relative; aspect-ratio: 4 / 3; overflow: hidden; border-inline-end: 3px solid var(--outline);
&[data-youtube] { cursor: pointer; }
& > img { inline-size: 100%; block-size: 100%; object-fit: cover; }
@media (max-width: 60rem) { border-inline-end: none; border-block-end: 3px solid var(--outline); }}
.featured-info { display: grid; grid-template-rows: auto auto 1fr; gap: 0.875rem; padding: 1.375rem 1.375rem 1.25rem;
& > * { margin: 0; }
@media (max-width: 60rem) { padding: 0.625rem 0.875rem var(--space-3); }}
.featured-heading { display: flex; flex-direction: column; gap: var(--space-1);
@media (max-width: 60rem) { gap: 2px; }}
.featured-title { font-size: 1.875rem;
@media (max-width: 60rem) { font-size: 1.375rem; }}
.featured-tagline { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; min-block-size: 2lh; color: var(--example); text-wrap: pretty; font-size: 0.9375rem;
@media (max-width: 60rem) { font-size: var(--text-sm); }}
.featured-thumbs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.625rem;
& > div { aspect-ratio: 4 / 3; overflow: hidden; border: 2px solid var(--outline); border-radius: var(--space-2); background: var(--crust); }
& img { inline-size: 100%; block-size: 100%; object-fit: cover; }
@media (max-width: 60rem) { display: none; }}
/* One pill per featured mod, to jump straight to it. Each is drawn small but takes a full-size tap. */.featured-dots { display: flex;
&[hidden] { display: none; }
& > button { all: unset; display: grid; place-items: center; block-size: 2.75rem; padding-inline: 5px; cursor: pointer;
&::before { content: ""; inline-size: 14px; block-size: 8px; border-radius: 4px; background: var(--surface1); transition: inline-size 0.12s ease, background 0.12s ease; }
&:hover::before { background: var(--surface2); }
&:focus-visible { outline: var(--ring); outline-offset: -8px; border-radius: var(--space-2); } }
& > .featured-dot-current::before, & > .featured-dot-current:hover::before { inline-size: 22px; background: var(--outline); }}
/* "Show mods for", then every game in one segmented row. */.game-filter { display: flex; flex-wrap: wrap; align-items: center; gap: 0.875rem;
& > span { color: var(--grey); }
@media (max-width: 40rem) { & > span { display: none; } }}
/* A row of links joined into one pill, the current one filled. */.segmented { display: inline-flex; max-inline-size: 100%; overflow-x: auto; border: 2px solid var(--outline); border-radius: 1.375rem; background: var(--paper);
& > a { display: inline-flex; align-items: center; flex-shrink: 0; block-size: 2.75rem; padding: 0 0.875rem; border-inline-start: 2px solid var(--outline); color: var(--text); font-size: 0.9375rem; white-space: nowrap;
&:any-link { text-decoration: none; }
&:first-child { border-inline-start: none; }
&:hover { background: var(--crust); }
&:focus-visible { outline-color: var(--outline); outline-offset: -5px; }
&[aria-current="true"] { background: var(--outline); color: var(--base);
&:focus-visible { outline-color: var(--base); } }
@media (max-width: 40rem) { padding: 0 var(--space-3); font-size: var(--text-sm); } }}
.mod-section { display: flex; flex-direction: column; gap: 0.875rem;
& > * { margin: 0; }
@media (max-width: 40rem) { gap: 0.625rem; }}
.mod-section-header { display: flex; align-items: baseline; gap: var(--space-4);
& h2 { margin: 0; font-size: 1.75rem; }
& a { margin: 0 0 0 auto; font-size: 1.0625rem; }
@media (max-width: 40rem) { & h2 { font-size: 1.375rem; }
& a { font-size: 0.9375rem; } }}
.mod-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1.25rem; padding: 0; list-style: none;
@media (max-width: 75rem) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 50rem) { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }}
/* Narrower than a desktop, the homepage's lists show six: two rows of three, or three rows of two. */.mod-section .mod-grid > li:nth-child(n + 7) { @media (max-width: 75rem) { display: none; }}
.mod-card { display: flex; flex-direction: column; block-size: 100%; overflow: hidden; border: 2px solid var(--surface0); border-radius: var(--space-3); background: var(--paper); color: var(--text);
&:any-link { text-decoration: none; }
@media (max-width: 40rem) { border-radius: 0.625rem; }}
.mod-card-image { aspect-ratio: 4 / 3; overflow: hidden; background: var(--crust);
& > img { inline-size: 100%; block-size: 100%; object-fit: cover; }}
.mod-card-body { display: grid; flex: 1; grid-template-rows: auto auto 1fr; gap: var(--space-1); padding: var(--space-3) 0.875rem 0.875rem;
@media (max-width: 40rem) { padding: var(--space-2) 0.625rem 0.625rem; }}
.mod-card-title { font-size: 1.0625rem;
@media (max-width: 40rem) { font-size: 0.9375rem; }}
.mod-card-description { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; min-block-size: 2lh; color: var(--example); text-wrap: pretty; font-size: var(--text-sm);}
/* What a mod without an image shows in its place: its game and title on a sheet of paper. */.mod-placeholder { container-type: inline-size; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-2); inline-size: 100%; block-size: 100%; padding: 8%; overflow: hidden; background-color: var(--mantle); background-image: url("/assets/textures/subtle-paper-pulp-repeat.webp"); background-blend-mode: multiply; color: var(--grey); text-align: center;}
.mod-placeholder-game { padding: 1cqi 4cqi; border-radius: 999px; background: var(--surface0); color: var(--text); font-size: max(0.75rem, 6cqi);}
.mod-placeholder-title { display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; font-size: max(0.875rem, 9cqi); line-height: 1.15; text-wrap: balance;}
/* A mod's author, then its stars pushed to the end. */.mod-card-byline,.featured-byline { display: flex; grid-row: 3; align-items: center; align-self: end; gap: var(--space-2); color: var(--example); font-size: var(--text-sm); min-inline-size: 0;
& .byline-avatar { flex: none; inline-size: 1.25rem; block-size: 1.25rem; border-radius: 50%; object-fit: cover; }
& .byline-name { overflow: hidden; color: var(--grey); text-overflow: ellipsis; white-space: nowrap; }
/* Overlapping, so no one's avatar stands for the rest. */ & .byline-avatars { display: flex; flex: none;
& .byline-avatar { box-shadow: 0 0 0 2px var(--paper); }
& .byline-avatar + .byline-avatar { margin-inline-start: -0.4rem; } }
/* Everyone credited, as many as fit in two lines. */ & .byline-names { display: -webkit-box; min-inline-size: 0; overflow: hidden; color: var(--grey); overflow-wrap: anywhere; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; }
& .byline-stars { display: inline-flex; align-items: center; gap: 0.25rem; margin-inline-start: auto;
& svg { flex: none; inline-size: 1em; block-size: 1em; }
&.byline-starred svg { fill: var(--gold); stroke: var(--on-gold); } }}
.mod-card-byline { margin-block-start: var(--space-1);}
/* A mod's gallery, one slide at a time. Until its script has run, the first slide stands in for it, so the page keeps its shape once it does. */media-carousel { position: relative; display: block; inline-size: 100%; aspect-ratio: 4 / 3; background: #222; overflow: hidden;
&:not(:defined) > * { inline-size: 100%; block-size: 100%; object-fit: contain; }
&:not(:defined) > :not(:first-child) { display: none; }}
/* A YouTube video playing over a gallery image, and the button that starts it. */media-carousel > [data-youtube] { position: relative;
& > img { inline-size: 100%; block-size: 100%; object-fit: contain; }}
.video-player { position: absolute; inset: 0; background: black;
& iframe { inline-size: 100%; block-size: 100%; border: 0; }}
.video-play { all: unset; position: absolute; inset: 0; margin: auto; display: grid; place-items: center; inline-size: 3.5rem; block-size: 3.5rem; border-radius: 50%; background: oklch(0% 0 0 / 0.65); color: white; cursor: pointer;
&:hover, &:focus-visible { background: oklch(0% 0 0 / 0.85); }
&:has(~ .video-player) { display: none; }
& svg { inline-size: 1.75rem; block-size: 1.75rem; fill: currentcolor; }}