/* 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 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; } & :is(th, td) { padding: var(--space-1) var(--space-2); border: 1px solid var(--grey); } & table { border-collapse: collapse; } } /* 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; } } /* 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; } .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 { 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; } }