
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  font-family: var(--board-font);
  font-size: var(--board-size-ui);
  line-height: var(--board-leading-ui);
  color: var(--board-text);
  background: var(--board-bg);
  word-break: keep-all;
  overflow-wrap: anywhere;
  -webkit-font-smoothing: antialiased;
  /* The window's height at least, with the shell taking what the header and footer leave: a short page (sign-in, an empty search) keeps its footer on the window's bottom edge instead of stopping mid-window over bare ground. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
/* A flex item with auto side margins sizes to its content, so the shell states its width; .board-shell below caps it and centres it. */
.board-shell { flex: 1 0 auto; width: 100%; }
a { color: inherit; text-decoration: none; }
a:hover { text-decoration: underline; }
.gb-skip-link {
  position: fixed;
  top: 0.5rem;
  left: 0.5rem;
  z-index: 100;
  transform: translateY(-200%);
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--board-line);
  border-radius: var(--board-control-radius);
  background: var(--board-surface);
  color: var(--board-text);
  box-shadow: var(--board-shadow);
  font-weight: var(--board-weight-bold);
}
.gb-skip-link:focus { transform: none; }
#main-content { scroll-margin-top: calc(var(--gb-header-height, 4.0625rem) + var(--board-space-3)); }
#main-content:focus { outline: none; }
/* A link that wraps a block (gallery card, card listing, cover link) underlines every letter inside it; production underlines none. Inline links still do. */
a:has(:is(div, p, h1, h2, h3, h4, h5, h6, img, picture, figure, ul, ol, table, widget, article)):hover { text-decoration: none; }
h1, h2, h3, h4 { line-height: var(--board-leading-tight); margin: 0 0 var(--board-space-2); letter-spacing: -0.02em; font-weight: var(--board-weight-title); }
h1 { font-size: var(--board-size-page); }
h2 { font-size: var(--board-size-head); }
h3 { font-size: var(--board-size-ui); }
p { margin: 0 0 var(--board-space-3); }
img { max-width: 100%; height: auto; }
input, select, textarea, button {
  font: inherit;
  color: inherit;
}
/* The native box in the skin's ink rather than the browser's blue: the one colour on a screen is the black button. */
input[type="checkbox"], input[type="radio"] { accent-color: var(--board-ink); }
input[type="text"], input[type="search"], input[type="email"], input[type="password"], input[type="url"], input[type="date"], input[type="number"], select {
  min-height: var(--board-control-h);
  padding: var(--board-control-pad-y) var(--board-input-pad-x);
  border: 1px solid var(--board-line);
  border-radius: var(--board-control-radius);
  background: var(--board-surface);
  max-width: 100%;
}
/* A single select draws its own chevron: the native arrow hugs the border, so appearance goes and two gradients make a token-coloured caret whose right edge sits 0.7rem in — the same inset as the text on the left (board-skin.md §9). Multi-line selects keep the native control. */
select:not([multiple]):not([size]) {
  appearance: none;
  padding-right: var(--board-space-6);
  background-image: linear-gradient(45deg, transparent 50%, var(--board-mid) 50%), linear-gradient(135deg, var(--board-mid) 50%, transparent 50%);
  background-position: calc(100% - var(--board-input-pad-x) - 0.3rem) 50%, calc(100% - var(--board-input-pad-x)) 50%;
  background-size: 0.3rem 0.3rem, 0.3rem 0.3rem;
  background-repeat: no-repeat;
}
input[type="search"] { background: var(--board-surface); }
input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
textarea { width: 100%; min-height: 8rem; resize: vertical; border: 1px solid var(--board-line); border-radius: var(--board-control-radius); background: var(--board-surface); padding: var(--board-input-pad-x); }
input[type="text"]:focus, input[type="search"]:focus, input[type="email"]:focus, input[type="password"]:focus, input[type="url"]:focus, input[type="date"]:focus, input[type="number"]:focus, select:focus, textarea:focus, input:focus-visible {
  outline: 0;
  border-color: var(--board-focus-line, var(--board-accent));
  background-color: var(--board-surface);
  box-shadow: 0 0 0 3px var(--board-focus-ring, color-mix(in srgb, var(--board-accent) 18%, transparent));
}
button:focus-visible, a:focus-visible, summary:focus-visible { outline: 2px solid var(--board-accent); outline-offset: 2px; }
input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible { outline: 2px solid var(--board-accent); outline-offset: 2px; }
/* One button: the accent (ink by default). The quiet one is outlined. */
button, .gb-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--board-control-h);
  padding: var(--board-control-pad-y) var(--board-control-pad-x);
  border: 1px solid var(--board-line);
  border-radius: var(--board-control-radius);
  background: var(--board-surface);
  color: var(--board-text);
  font-weight: var(--board-weight-bold);
  line-height: var(--board-control-leading);
  cursor: pointer;
  white-space: nowrap;
}
button:hover, .gb-btn:hover { text-decoration: none; background: var(--board-tint); }
button[disabled] { opacity: 0.5; cursor: default; }
.gb-btn-primary, .btn-write, button[type="submit"]:not(.gb-link):not(.gb-reaction):not(.gb-danger) {
  background: var(--board-accent);
  border-color: var(--board-accent);
  color: var(--board-on-accent);
}
.gb-btn-primary:hover, .btn-write:hover, button[type="submit"]:not(.gb-link):not(.gb-reaction):not(.gb-danger):hover { background: var(--board-accent-hover); border-color: var(--board-accent-hover); color: var(--board-on-accent); }
/* A submit that should read quiet rather than filled opts out of the primary rule by name. */
button.gb-btn-quiet[type="submit"]:not(.gb-link):not(.gb-reaction):not(.gb-danger) { background: var(--board-surface); border-color: var(--board-line); color: var(--board-text); }
button.gb-btn-quiet[type="submit"]:not(.gb-link):not(.gb-reaction):not(.gb-danger):hover { background: var(--board-tint); border-color: var(--board-line); color: var(--board-text); }
.btn-block { display: block; text-align: center; }
button.gb-link, .gb-link { border: 0; background: none; padding: 0; min-height: 0; color: inherit; font-weight: inherit; cursor: pointer; }
button.gb-link:hover { background: none; text-decoration: underline; }
button.gb-danger, .gb-danger { color: var(--board-danger); }
/* The filled danger face — white on the danger tone wherever the class sits, including the delete-account submit. */
.btn-danger, .gb-auth .gb-auth-form button.gb-danger[type="submit"] { background: var(--board-danger-fill); border-color: var(--board-danger-fill); color: #fff; }
.btn-danger:hover, .btn-danger:focus-visible, .gb-auth .gb-auth-form button.gb-danger[type="submit"]:hover, .gb-auth .gb-auth-form button.gb-danger[type="submit"]:focus-visible { background: color-mix(in srgb, var(--board-danger-fill) 88%, black); border-color: color-mix(in srgb, var(--board-danger-fill) 88%, black); color: #fff; }
.gb-inline { display: inline; }
/* In the tree, out of sight: a heading a screen reader reads and the layout does not (skin-api.md §3s, the listing-under-the-post heading), the board search's switch, which a phone toggles by its label (§4), and Core's sr-only — the share button's only visible-to-a-reader word (§4). */
.gb-visually-hidden, .board-search-switch, .gb-sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }

.board-shell { max-width: var(--board-max); margin: 0 auto; padding: 0 var(--board-gap); }

/* The header: one row — brand, menu, search, the account, the switch — and a hairline. It stays with the reader. Its ground runs the full window like production's band, and it is translucent so the page reads through it as it scrolls under. */
.board-header { position: sticky; top: 0; z-index: 40; background: color-mix(in srgb, var(--board-surface) 94%, transparent); border-bottom: 1px solid var(--board-line); }
/*
 * headerAutohide on: the layout marks <html> and the runtime flips data-gb-header as the
 * reader scrolls (runtime.ts headerOnScroll, skin-api.md §3t). The slide and the delta thresholds
 * are the legacy bar's own (AppHeader.vue: .header--hidden at 0.22s ease, translateY(-100% - 1px)).
 * :not(:focus-within) is the CSS half of the runtime's guard: a bar the keyboard is inside stays
 * on screen. With the option off — the default — neither attribute is ever written and the bar
 * simply stays, which is also what a scriptless browser gets.
 */
:root[data-gb-header-scroll='hide'] .board-header { transition: transform 0.22s ease; }
:root[data-gb-header='hidden'] .board-header:not(:focus-within) { transform: translateY(calc(-100% - 1px)); }
@media (prefers-reduced-motion: reduce) {
  :root[data-gb-header-scroll='hide'] .board-header { transition: none; }
}
/* The row repeats the shell's column — the same --board-max and --board-gap — so the brand sits on the body's left edge and the last control on its right, while the band runs past both. One centred line: production's brand and menu links share a baseline and do not drift (board-skin.md §9). Its 4rem gives the 65px bar production has, with the hairline above adding the last pixel. */
.board-header-row { display: flex; align-items: center; gap: var(--board-space-2); max-width: var(--board-max); margin: 0 auto; padding: 0 var(--board-gap); height: 4rem; }
.board-spacer { flex: 1 1 auto; min-width: 0; }
/* The brand: production's 24px at 900, in the accent. Its right margin adds to the row's own gap, setting the brand and the first menu link --board-space-7 apart so the two do not read as glued (board-skin.md §9). The size sits just above --board-size-display, so it is a literal. */
.gb-brand { line-height: 1; font-size: 1.375rem; font-weight: 800; letter-spacing: -0.04em; color: var(--board-accent-text); margin-right: var(--board-space-6); }
.gb-brand:hover { text-decoration: none; }
/* The icon buttons in the row — search and the theme switch — are one shape: a 40px circle edged with the line's colour (production's icon-button). The phone's hamburger joins them, at its own size. */
.board-search-pop, .gb-theme-toggle { flex: none; }
.board-search-pop { position: relative; }
.board-search-pop > summary, .board-menu-pop > summary, .gb-theme-toggle { display: inline-flex; align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; min-height: 0; padding: 0; border: 1px solid var(--board-line); border-radius: 50%; background: none; color: var(--board-ink); cursor: pointer; list-style: none; }
.board-search-pop > summary::-webkit-details-marker, .board-menu-pop > summary::-webkit-details-marker { display: none; }
.board-search-pop > summary:hover, .board-menu-pop > summary:hover, .gb-theme-toggle:hover { background: var(--board-tint); color: var(--board-text); }
.board-search-pop[open] > summary, .board-menu-pop[open] > summary { background: var(--board-tint); color: var(--board-text); }
.board-search-pop .gb-site-search { --board-control-h: 2.25rem; --board-control-pad-y: 0; position: absolute; right: 0; top: calc(100% + var(--board-space-2)); z-index: 30; display: flex; gap: var(--board-space-1); padding: var(--board-space-2); background: var(--board-surface); border: 1px solid var(--board-line); border-radius: var(--board-radius); box-shadow: 0 8px 24px rgb(0 0 0 / 12%); }
.gb-site-search input { width: 15rem; }
/* Matches the primary rule's specificity so the popover's 검색 stays quiet grey, not the page's one black button. */
.gb-site-search button[type="submit"]:not(.gb-link):not(.gb-reaction):not(.gb-danger) { padding: 0 var(--board-space-3); border-color: var(--board-rule); background: var(--board-tint); color: var(--board-text); font-size: var(--board-size-meta); }
.gb-site-search button[type="submit"]:not(.gb-link):not(.gb-reaction):not(.gb-danger):hover { background: var(--board-tint-2); }
/* The switch: window chrome Core draws and the runtime answers (skin-api.md §3u); CSS picks the icon from the two selectors the tokens use. */
.gb-theme-toggle .gb-theme-icon { width: 1.15rem; height: 1.15rem; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.gb-theme-toggle .gb-theme-icon-sun { display: none; }
.gb-theme-toggle:focus-visible { outline: 2px solid var(--board-accent); outline-offset: 2px; }
@media (prefers-color-scheme: dark) {
  :root:root:not([data-theme="light"]) .gb-theme-toggle .gb-theme-icon-sun { display: block; }
  :root:root:not([data-theme="light"]) .gb-theme-toggle .gb-theme-icon-moon { display: none; }
}
:root:root[data-theme="dark"] .gb-theme-toggle .gb-theme-icon-sun { display: block; }
:root:root[data-theme="dark"] .gb-theme-toggle .gb-theme-icon-moon { display: none; }
.gb-account { position: relative; display: flex; align-items: center; gap: var(--board-space-2); flex: none; }
/* Production's 로그인 is a plain 16px word, weight 400, in the ink; 회원가입 beside it is the row's one filled button — accent ground, white 700 text, taken from the .gb-btn .gb-btn-primary rules above. Its 43.2px height and 8.8/12.8 padding are production's own, so the link sets the control tokens rather than re-declaring the properties. */
.gb-account .gb-sign-in { font-weight: var(--board-weight-normal); color: var(--board-ink); }
.gb-account .sign-up { --board-control-h: 2.7rem; --board-control-pad-x: 0.8rem; }
.gb-menu { position: relative; }
.gb-menu summary { list-style: none; cursor: pointer; font-weight: var(--board-weight-bold); padding: var(--board-space-1) 0; }
.gb-menu summary::-webkit-details-marker { display: none; }
/* Production's account summary: the 19px person, the name cut at 10rem with an ellipsis, the 15px chevron in the dim ink. The name is a span inside — .gb-viewer-name:empty::before fills it on a shared document, and marks beside it would break :empty on the summary itself. */
.gb-account-menu > summary { display: inline-flex; align-items: center; gap: 0.35rem; }
.gb-account-menu .gb-viewer-name { max-width: 10rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gb-account-menu > summary .gb-icon-chevron { color: var(--board-dim); }
/* Production's dropdown: 14px items with their icon .5rem ahead, a .35rem pad and .4rem radius on the panel, and its own shadow. In dark the same shadow stands — the skin's dark sheet drops --board-shadow only where it names it. */
.gb-menu-list {
  position: absolute;
  right: 0;
  top: 100%;
  z-index: 10;
  min-width: 10rem;
  display: flex;
  flex-direction: column;
  padding: 0.35rem;
  background: var(--board-surface);
  border: 1px solid var(--board-line);
  border-radius: 0.4rem;
  box-shadow: 0 10px 30px rgb(15 37 32 / 10%);
}
.gb-menu-list a, .gb-menu-list button { display: flex; align-items: center; gap: 0.5rem; width: 100%; min-height: 2.25rem; text-align: left; padding: 0.58rem 0.65rem; border: 0; border-radius: 0.35rem; background: none; color: var(--board-text); font-weight: var(--board-weight-normal); font-size: 0.875rem; }
.gb-menu-list a:hover, .gb-menu-list button:hover { background: var(--board-tint); text-decoration: none; }
/* The admin entry leads the menu — production's bold brand word, with the hairline under it. */
.gb-menu-list .gb-menu-admin { font-weight: var(--board-weight-bold); color: var(--board-accent-text); }
.gb-menu-list hr { margin: 0.15rem 0; border: 0; border-top: 1px solid var(--board-line); }
.gb-menu-list form { display: contents; }
/* 로그아웃 is a form's submit, so the shell's primary fill would paint it the page's one filled button; scoped with the same specificity, it stays a row like the links — no ground, the row's ink, the row's tint on hover. */
.gb-menu-list button[type="submit"]:not(.gb-link):not(.gb-reaction):not(.gb-danger) { justify-content: flex-start; text-align: left; background: none; color: var(--board-text); }
.gb-menu-list button[type="submit"]:not(.gb-link):not(.gb-reaction):not(.gb-danger):hover { background: var(--board-tint); color: var(--board-text); }
@media (pointer: coarse) {
  /* On touch hardware every control gets the 44px a thumb needs — the same floor .gb-more-menu's summary already gets in styles-inherited. */
  :root { --board-control-h: max(2.75rem, calc(2.75rem * var(--ui-density, 1))); }
  .gb-menu-list a,
  .gb-menu-list button,
  .gb-menu-list .gb-report > summary {
    min-height: 2.75rem;
  }
  .board-menu-pop > summary,
  .board-search-pop > summary,
  .gb-theme-toggle,
  .gb-header-icon,
  .gb-account-menu > summary,
  .board-search-pop .gb-site-search button {
    min-width: 2.75rem;
    min-height: 2.75rem;
  }
}
/* The header's round icon buttons — the notifications bell Core draws and the links a site adds under chrome.header.actions (the messages link is one): the same 40px bordered circle the search summary and theme switch are, so the row reads as one set. */
.gb-header-icon { position: relative; display: inline-flex; align-items: center; justify-content: center; flex: none; width: 2.5rem; height: 2.5rem; padding: 0; border: 1px solid var(--board-line); border-radius: 50%; background: none; color: var(--board-ink); }
.gb-header-icon:hover { background: var(--board-tint); color: var(--board-text); text-decoration: none; }
/* The unread pill straddling the circle's top-right edge, ringed in the header's face so it never touches the ink under it. */
.gb-header-icon .gb-badge { position: absolute; top: -0.35rem; right: -0.35rem; display: inline-flex; align-items: center; justify-content: center; min-width: 1.15rem; height: 1.15rem; padding: 0 0.3rem; border-radius: 999px; border: 2px solid var(--board-surface); background: var(--board-danger); color: #fff; font-size: 0.6875rem; font-weight: 700; }

/* The menu on a pointer: a plain row of links, the hamburger details' summary hidden and the details itself gone from the row — the nav is a row item in its own right (see the 47.99rem block for the phone). */
.board-menu-pop { display: none; }
.board-menu { display: block; }
.board-menu ul { display: flex; flex-wrap: nowrap; gap: 0 var(--board-space-5); list-style: none; margin: 0; padding: 0; }
/* Production's menu link: 16px at weight 600, in the ink. The 600 falls between the two weights the scale carries, so it is a literal; the row centres the link box, so it needs no padding of its own. Centred flex so a nav-badge pill sits after the title without lifting the box off the 26px line. */
.board-menu a { display: flex; align-items: center; gap: 0.32rem; font-weight: 600; font-size: var(--board-size-ui); color: var(--board-ink); }
.board-menu a:hover { color: var(--board-accent-text); text-decoration: none; }
/* The default accent is the ink, so colour alone cannot identify the current page. The underline sits on the title, not the flex link: a link's decoration reaches every flex item, so it would run under the nav-badge too, broken by the gap. */
.board-menu a[aria-current] { color: var(--board-accent-text); }
.board-menu a[aria-current] .nav-title { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: var(--board-space-2); }
/* Production's badge (the "NEW" pill by a menu item): literals, so the pill is the same in dark mode. Production's pill is inline, so its box is the 10px face's content area (15px) plus the pad, 17.6px; here it is a flex item, so the line height stands in for that area. */
.nav-badge { flex-shrink: 0; font-size: 10px; font-weight: 800; line-height: 15px; letter-spacing: 0.25px; padding: 0.08rem 0.34rem; border-radius: 999px; background: rgb(198, 56, 56); color: #fff; }

.board-body { display: grid; grid-template-columns: var(--board-rail-w) minmax(0, 1fr); gap: var(--board-space-7); padding: var(--board-space-6) 0 var(--board-space-8); align-items: start; }
/* No rail (board-skin.md §9), or one with nothing in it — a site that invites nobody in has no member box to draw, and with no menu the rail is empty (overseas-11): the column takes the whole width. */
.board-body:not(:has(> .board-rail:not(:empty))) { grid-template-columns: minmax(0, 1fr); }
.board-rail:empty { display: none; }
.board-main { min-width: 0; }
.board-rail { min-width: 0; }

/* The one reusable surface: a card the page ground shows through around. */
.board-card {
  background: var(--board-surface);
  border: 1px solid var(--board-line);
  border-radius: var(--board-radius-l);
  box-shadow: var(--board-shadow);
  padding: 0 var(--board-card-pad);
}
/* The phone layout stops just under 48rem, so a 768px tablet (iPad portrait, the `md` of the usual grids) keeps the wide one. The boundary is not one line but production's own per part (_breakpoints.scss, below() = max-width): the list table and the pager drop at 43.75rem (its tablet-narrow, 700px), the list's search and head at 37.5rem (mobile, 600px), the gallery and card grids at 47.5rem (tablet, 760px), and everything else — this card pad, the header, the tiles — stays at 47.99rem. max-width rather than a range query so older iOS Safari still reads it. */
@media (max-width: 47.99rem) {
  :root:root { --board-card-pad: calc(var(--board-space-4) * var(--ui-density, 1)); }
}

/* A box is a titled group: a small label over a list. No border, no band. */
.box { position: relative; margin: 0 0 var(--board-space-6); }
.box-title { font-size: var(--board-size-caption); font-weight: var(--board-weight-title); color: var(--board-dim); margin: 0 0 var(--board-space-1); letter-spacing: 0.06em; text-transform: uppercase; }
.box-nav ul { list-style: none; margin: 0; padding: 0; }
.box-nav ul ul { padding: 0; }
.box-nav li a { display: flex; align-items: center; gap: var(--board-space-2); min-height: 2.75rem; border-radius: var(--board-radius); padding: var(--board-space-2) var(--board-space-3); font-size: var(--board-size-ui); color: var(--board-mid); }
.box-nav li a:hover { color: var(--board-accent-text); background: var(--board-tint); text-decoration: none; }
.box-nav li li a { padding-left: var(--board-space-4); font-size: var(--board-size-meta); }
.box-nav a[aria-current] { color: var(--board-accent-text); background: var(--board-tint); box-shadow: inset 3px 0 var(--board-accent); font-weight: var(--board-weight-bold); }
.box-member { padding: 0 0 var(--board-space-5); border-bottom: 1px solid var(--board-line); }
.member-name { font-size: var(--board-size-ui); font-weight: var(--board-weight-bold); margin: 0 0 var(--board-space-2); }
.member-links { list-style: none; margin: 0 0 var(--board-space-2); padding: 0; font-size: var(--board-size-ui); color: var(--board-mid); }
.member-links li { padding: 2px 0; }
.box-member form { font-size: var(--board-size-meta); color: var(--board-dim); }
.box-member .btn-block { display: flex; padding: var(--board-space-3); }

@media (pointer: coarse) {
  .box-nav li a,
  .member-links a,
  .box-member .btn-block {
    min-height: 2.75rem;
  }

  .member-links a {
    display: inline-flex;
    align-items: center;
  }
}

/* The footer runs the full window like the header band: a surface strip with the hairline on top only, its row repeating the shell's column so the links and the copyright line up with the body. It stands outside .board-shell for that width (index.tsx, board-skin.md §9). Its pad is production's 23.2px, a literal off the spacing scale, and its line height is the 1.6 that fills the band's 73px. */
.board-footer { background: var(--board-surface); border-top: 1px solid var(--board-line); padding: 1.45rem 0; color: var(--board-dim); font-size: var(--board-size-ui); line-height: 1.6; }
.footer-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--board-space-2) var(--board-space-4); max-width: var(--board-max); margin: 0 auto; padding: 0 var(--board-gap); }
.gb-footer-links { display: flex; flex-wrap: wrap; align-items: center; gap: var(--board-space-4); }
.board-footer p { margin: 0; }
.footer-copy { font-size: var(--board-size-caption); }
/* Any nav in the row — the chrome.footer slot's and the site's footer menu — reads as one run of inline links at the row's size, with no rule or pad of its own, since the band draws the only hairline. */
.gb-footer-links nav { display: flex; flex-wrap: wrap; align-items: center; gap: var(--board-space-4); border-top: 0; padding-top: 0; color: var(--board-dim); font-size: var(--board-size-ui); }
.gb-footer-links nav a { color: inherit; }

/*
 * Core's composed page: the reading area beside a supporting sidebar, then the bottom band under
 * both (pages-and-layouts.md §4). Independent groups share the same gap in both directions.
 */
.board-main:has(> aside.gb-sidebar:not(:empty)) { display: grid; grid-template-columns: minmax(0, 1fr) var(--board-compose-w); gap: var(--board-space-6); align-items: start; }
.board-main > .gb-page-title { grid-column: 1 / -1; }
/* The bottom band takes the shell's whole width, under both columns (pages-and-layouts.md §5). */
.board-main > .gb-area-page-bottom { grid-column: 1 / -1; }
/* Zero-min columns and items: one unbreakable line inside a widget must not widen the page on a phone (board-skin.md §9). */
.board-main > .gb-area-page-main, .board-main > aside.gb-sidebar { min-width: 0; }
/*
 * The main area is two columns and each slot takes the width its layout asked for: span 1 is one
 * column, and anything else - span 2 up, full, or no span at all - runs the whole width. The site's
 * home is a market card a column over a full-width feed (site-composition.md §2).
 */
.gb-area-page-main { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--board-space-6); }
.gb-area-page-main > * { min-width: 0; }
.gb-area-page-main > .gb-widget-slot:not([data-span="1"]) { grid-column: 1 / -1; }
/*
 * The bottom band is three columns on the same reading: span 1 and span 2 keep their columns, and
 * span 3 up, full or none run the whole width (site-composition.md §2).
 */
.gb-area-page-bottom { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--board-space-6); }
.gb-area-page-bottom > * { min-width: 0; }
.gb-area-page-bottom > .gb-widget-slot[data-span="2"] { grid-column: span 2; }
.gb-area-page-bottom > .gb-widget-slot:not([data-span="1"]):not([data-span="2"]) { grid-column: 1 / -1; }
/*
 * The space side area stacks its widgets with the main grid's vertical gap, so two boxes never run
 * together (site-composition.md §2). A composed page's sidebar is one card instead: its widgets share
 * a single surface, each heading in its own band and a hairline between one widget and the next, so
 * the whole side reads as one box (board-skin.md §2).
 */
.gb-area-space-sidebar { display: flex; flex-direction: column; gap: var(--board-gap); }
/* The widgets above the board's head stack with production's 10px, so two of them never run together (site-composition.md §2, board-skin.md §9). */
.gb-area-space-top { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.625rem; }
.gb-area-page-sidebar { display: flex; flex-direction: column; background: var(--board-surface); border: 1px solid var(--board-line); border-radius: var(--board-radius-l); box-shadow: var(--board-shadow); padding: var(--board-card-pad); }
/* Supporting sections share one surface; only the boundary between them needs a rule. */
.gb-area-page-sidebar > .gb-widget-slot + .gb-widget-slot { margin-top: var(--board-space-5); padding-top: var(--board-space-5); border-top: 1px solid var(--board-line); }
/* The last row of a widget draws no line of its own: the widget's end is the one line below, and two rows of hairlines 20px apart read as a double rule (board-skin.md §2). */
.gb-area-page-sidebar .box-rows > li:last-child, .gb-area-page-sidebar .recent-comments > li:last-child { border-bottom: 0; }
/*
 * A widget that rendered nothing keeps its wrapper but takes no cell, taken out by Core's Area
 * setting the hidden attribute. The guard is here because an author rule that sets a value other
 * than none on the slot would outrank the browser's own rule for the attribute, exactly as the
 * pager's does below (skin-api.md §3a).
 */
.gb-widget-slot[hidden] { display: none; }
/*
 * Independent widgets have a surface. Page introductions and their prose stay on the ground,
 * while the supporting sidebar combines its widgets into one surface below. A
 * plugin that draws a bare `widget` into a content slot, with no `.gb-widget` of its own, gets the
 * same frame from the slot's own children; a card the plugin drew inside itself is not doubled.
 */
.gb-widget:not(.gb-widget-hero), .gb-slot > section {
  background: var(--board-surface);
  border: 1px solid var(--board-line);
  border-radius: var(--board-radius-l);
  box-shadow: var(--board-shadow);
  padding: var(--board-card-pad);
}
/* Inside a composed page's one-card sidebar the widgets are bands, not boxes, so they drop the card above; the rule sits after it, the two at the same weight (board-skin.md §2). */
.gb-area-page-sidebar .gb-widget, .gb-area-page-sidebar .gb-slot > section { background: none; border: 0; border-radius: 0; box-shadow: none; padding: 0; }
.gb-widget-hero { padding: var(--board-space-7); border: 1px solid var(--board-line); border-top: 3px solid var(--board-accent); border-radius: var(--board-radius-l); background: var(--board-surface); box-shadow: var(--board-shadow); }
.gb-widget.gb-widget-hero h2 { font-size: var(--board-size-title); line-height: var(--board-leading-tight); margin: 0 0 var(--board-space-3); padding: 0; border: 0; letter-spacing: -0.03em; max-width: 24ch; }
.gb-widget-hero p { margin: 0 0 var(--board-space-4); color: var(--board-mid); font-size: var(--board-size-ui); line-height: var(--board-leading-read); max-width: 40rem; }
.gb-widget-hero a.gb-cta, .gb-widget-hero p > a:only-child { display: inline-block; padding: var(--board-space-2) var(--board-space-4); background: var(--board-accent); color: var(--board-on-accent); border-radius: var(--board-radius); font-weight: var(--board-weight-bold); }
.gb-widget-markdown { line-height: var(--board-leading-read); font-size: var(--board-size-ui); color: var(--board-mid); max-width: 44rem; }
/* Introductory prose continues the page introduction; it is not another independently framed item. */
.gb-area-page-main > .gb-widget-slot > .gb-widget-markdown { background: none; border: 0; border-radius: 0; box-shadow: none; padding: 0; }
.gb-widget-hero > :last-child, .gb-widget-markdown > :last-child { margin-bottom: 0; }
.gb-widget-markdown > :first-child { margin-top: 0; }
.gb-widget-tags ul, .gb-widget-menu ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--board-space-1); }
/* A tag and its count form one readable unit rather than a run of unrelated words and digits. */
.gb-widget-tags li { display: inline-flex; align-items: baseline; gap: var(--board-space-1); padding-inline: var(--board-space-2); background: var(--board-tint); border-radius: var(--board-radius-s); }
.gb-widget-tags li a { display: inline-flex; align-items: center; min-height: var(--board-space-7); font-size: var(--board-size-meta); }
.gb-widget-tags li small { color: var(--board-dim); font-size: var(--board-size-caption); font-variant-numeric: tabular-nums; }
.gb-widget-stats { display: flex; gap: var(--board-space-6); color: var(--board-dim); }
/* A widget on the home: a bold name, 더 보기 at the far end, rows under a hairline. */
.box-list, .box-cards, .box-compact, .box-digest { margin: 0; }
/* In a composed grid a widget fills its cell, so two cards side by side end on one line (a bottom band's 읽을거리 beside a short tile). */
.gb-area > .gb-widget-slot > .gb-widget { height: 100%; }
/* 20px/600 in ink, over a light line: production's own weight for a home heading, heavier than the 16px row it heads and lighter than the page title (board-skin.md §8). */
.box .box-title { font-size: 1.25rem; font-weight: var(--board-weight-title); color: var(--board-ink); letter-spacing: -0.02em; text-transform: none; padding: 0 0 var(--board-space-2); margin: 0; border-bottom: 1px solid var(--board-line); }
.gb-sidebar .box .box-title, .gb-sidebar .gb-widget h2 { font-size: var(--board-size-ui); }
/* Title and action share normal flow: either may wrap in a narrow Builder column without covering the other. */
.box-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--board-space-2) var(--board-space-3); padding-bottom: var(--board-space-2); }
.box .box-head .box-title { flex: 1 1 10ch; min-width: 0; padding: 0; margin: 0; border: 0; }

/*
 * A feed the layout arranged as a compact box (site-composition.md §2): one line per post with the
 * date and the marks taken off it, for a box a reader scans rather than reads. The rank the layout
 * asked for stands in its own column before the title, and the count is the bare number at the
 * row's end (board-skin.md §8).
 */
.box-compact .box-rows li { display: flex; align-items: center; gap: var(--board-space-2); height: 2.4375rem; padding: 0; border-bottom: 1px solid var(--board-line); font-size: var(--board-size-ui); }
/* A hairline belongs between two rows: the box's own edge ends the list. */
.box-compact .box-rows > li:last-child { border-bottom: 0; }
.box-compact .box-row { align-items: center; }
.box-compact .box-row-title { flex: 1; }
/* The rank's column: 19px and tabular, so 1..9 line up whether the box holds nine rows or twenty. */
.box-rank { flex: none; width: 1.1875rem; color: var(--board-accent-text); font-weight: var(--board-weight-bold); font-variant-numeric: tabular-nums; text-align: left; }
.box-count { flex: none; color: var(--board-accent-text); font-weight: var(--board-weight-bold); font-variant-numeric: tabular-nums; }
/* A sidebar box is shorter and lighter: 37px rows with no hairlines, and the count normal weight sitting right after the title rather than at the row's end, so it stays when the title runs out of room (board-skin.md §2). */
.gb-area-page-sidebar .box-compact .box-rows li { height: 2.3125rem; border-bottom: 0; }
.gb-area-page-sidebar .box-compact .box-row-title { flex: 0 1 auto; }
.gb-area-page-sidebar .box-count { font-weight: var(--board-weight-normal); }

/*
 * A feed the layout arranged as a digest (site-composition.md §2): a 72x54 thumbnail, the board's
 * name, the title and the post's excerpt — the shape a feed of mixed boards wants, where naming the
 * board is what a card's caption could not do (board-skin.md §8).
 */
.box-digest .box-rows li { display: flex; align-items: center; gap: var(--board-space-4); height: 4.6875rem; padding: 0; border-bottom: 1px solid var(--board-line); }
.box-digest .box-rows > li:last-child { border-bottom: 0; }
/* The picture is always a box, cover or none, so a column of rows keeps one left edge; a post without one gets the page icon in the same frame. */
.digest-cover { flex: none; display: flex; align-items: center; justify-content: center; width: 4.5rem; height: 3.375rem; background: var(--board-tint); border: 1px solid var(--board-line); border-radius: var(--board-radius-s); overflow: hidden; color: var(--board-dim); }
.digest-cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
.digest-body { flex: 1; min-width: 0; }
.digest-line { display: flex; align-items: baseline; gap: var(--board-space-2); margin: 0; }
.digest-space { flex: none; color: var(--board-accent-text); font-size: var(--board-size-caption); font-weight: var(--board-weight-bold); }
.digest-title { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--board-text); font-size: var(--board-size-ui); font-weight: 500; }
.digest-summary { margin: 0; color: var(--board-dim); font-size: var(--board-size-meta); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/*
 * The board directory Core renders as its own boards widget (site-composition.md §2, board-skin.md
 * §8): a grid of four tiles, one per board, each the board's name over its newest post over the word
 * to go in, and the whole tile one link to the board. The hairlines are drawn by the tile that has a
 * neighbour — a column's own edge ends the row and draws nothing. 286x112 in production, and the
 * name underlines on hover rather than the whole tile.
 */
.box-boards .board-tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); list-style: none; margin: 0; padding: var(--board-space-3) 0 0; }
.box-boards .board-tiles > li { min-width: 0; }
/* The vertical hairline sits before every tile but a row's first; the horizontal one above every tile from the second row on. */
.box-boards .board-tiles > li:not(:nth-child(4n + 1)) { border-left: 1px solid var(--board-line); }
.box-boards .board-tiles > li:nth-child(n + 5) { border-top: 1px solid var(--board-line); }
.board-tile { display: flex; flex-direction: column; gap: var(--board-space-2); min-height: 7rem; padding: 0.9rem 0.75rem; }
/* The name's row carries the layout's own word for the board (the notes prop) at its far end. */
.board-tile-top { display: flex; justify-content: space-between; align-items: baseline; gap: var(--board-space-2); min-width: 0; }
.board-tile-name { color: var(--board-ink); font-size: var(--board-size-ui); font-weight: var(--board-weight-bold); }
.board-tile-note { flex: none; color: var(--board-dim); font-size: var(--board-size-caption); }
/* The newest post's title on one line: a long title ends in an ellipsis rather than pushing the tile wider. */
.board-tile-latest { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--board-dim); font-size: var(--board-size-meta); }
.board-tile-go { color: var(--board-dim); font-size: var(--board-size-caption); }
.board-tile:hover .board-tile-name { text-decoration: underline; }
/* Two columns on a phone: the same hairlines, one shorter row apiece, and the four-column rules undone. */
@media (max-width: 47.99rem) {
  .box-boards .board-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .box-boards .board-tiles > li:not(:nth-child(4n + 1)) { border-left: 0; }
  .box-boards .board-tiles > li:nth-child(n + 5) { border-top: 0; }
  .box-boards .board-tiles > li:not(:nth-child(odd)) { border-left: 1px solid var(--board-line); }
  .box-boards .board-tiles > li:nth-child(n + 3) { border-top: 1px solid var(--board-line); }
}

/* Account settings: provider rows grow a password field only for a sensitive unlink. */
.gb-account-providers ul { display: grid; gap: var(--board-space-2); margin: 0; padding: 0; list-style: none; }
.gb-account-providers li { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--board-space-3); padding: var(--board-space-3) 0; border-bottom: 1px solid var(--board-line); }
.gb-account-providers form.gb-inline { display: flex; align-items: flex-end; justify-content: flex-end; gap: var(--board-space-2); flex-wrap: wrap; }
.gb-account-providers .gb-provider-password { display: grid; gap: var(--board-space-1); margin: 0; color: var(--board-dim); font-size: var(--board-size-caption); font-weight: var(--board-weight-normal); }
.gb-account-providers .gb-provider-password input { width: min(14rem, 100%); }
@media (max-width: 47.99rem) {
  .gb-account-providers li { align-items: stretch; flex-direction: column; }
  .gb-account-providers form.gb-inline { justify-content: flex-start; }
  .gb-account-providers .gb-provider-password { flex: 1 1 100%; }
  .gb-account-providers .gb-provider-password input { width: 100%; }
}

/*
 * Search narrowing/recovery: the reader should see what changed the answer before the results, and
 * the recovery link sits with the empty result rather than looking like another search mode.
 */
.gb-search-scope {
  margin: calc(-1 * var(--board-space-2)) 0 var(--board-space-4);
  color: var(--board-dim);
  font-size: var(--board-size-meta);
}
.gb-search-scope a { color: var(--board-accent-text); font-weight: var(--board-weight-bold); }
.gb-search-empty-action { margin: var(--board-space-4) 0 0; text-align: center; }

@media (max-width: 60rem) {
  .board-body { grid-template-columns: minmax(0, 1fr); gap: var(--board-gap); padding-top: var(--board-space-5); }
  .board-rail { order: 2; padding-top: var(--board-space-4); border-top: 1px solid var(--board-line); }
  .box-member { display: none; }
  .board-main:has(> aside.gb-sidebar:not(:empty)) { grid-template-columns: minmax(0, 1fr); }
}

/*
 * Collapse the header before its menu and account controls run out of room. The brand may shorten,
 * but actions stay together on one row. Only the expanded navigation takes a second row.
 */
@media (max-width: 70rem) {
  /* The row wraps so the menu can take a line of its own; auto height lets it grow with that line, the min keeps the closed row production's 64px. No wrap gap: production puts the menu straight against the row, its hairline the only separation. */
  .board-header-row { position: relative; flex-wrap: wrap; height: auto; min-height: 4rem; row-gap: 0; }
  .board-spacer { display: none; }
  /* The phone shrinks the row: a 20px brand and 38px icon circles (production's own values at 390), the brand's right margin gone since the hamburger now leads it. */
  .gb-brand { flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 1.25rem; margin-right: 0; }
  .board-menu-pop > summary, .board-search-pop > summary, .gb-theme-toggle { width: 2.375rem; height: 2.375rem; }
  /* The account is the person icon alone at this width — the name would push the row past 360px. */
  .gb-account-menu .gb-viewer-name { display: none; }
  .gb-header-icon { width: 2.375rem; height: 2.375rem; }
  /* The hamburger leads the row: the base sheet hides the details on a pointer, here it comes back. */
  .board-menu-pop { display: block; order: -1; position: static; flex: none; align-self: center; }
  /* Block-level, not inline-flex: an inline summary inside a block details leaves a line box whose descender space pushes the hamburger off centre. */
  .board-menu-pop > summary { display: flex; }
  /* Production leaves the open hamburger as it is — no tint — so the base sheet's open ground stops here. */
  .board-menu-pop[open] > summary { background: none; color: var(--board-ink); }
  /* The menu is a line of the wrapped row, so it is in flow: opening it grows the header and shifts main, rather than overlaying the page. It shows only while its trigger's details is open — the open flag is the whole state, no script. */
  .board-menu { order: 1; flex-basis: 100%; display: none; }
  .board-header-row:has(> .board-menu-pop[open]) > .board-menu { display: block; border-top: 1px solid var(--board-line); padding: 0.35rem 0 var(--board-space-3); }
  /* One full-width column, rows touching — production's single column with no gap between links. */
  .board-menu ul { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; }
  /* Production's drawer link: 44px tall, 8.8/9.6 padding, 16px/600 ink on a 6.4px radius. Off the skin's scale, so literals. */
  .board-menu a { min-height: 2.75rem; padding: 0.55rem 0.6rem; border-radius: 0.4rem; line-height: 1.6; font-size: var(--board-size-ui); }
  .board-menu a[aria-current] { background: var(--board-tint); color: var(--board-accent-text); font-weight: var(--board-weight-bold); }
  .board-menu a[aria-current] .nav-title { text-decoration: none; }
  /* The search panel takes the same full-width placement; the row is now its containing block too. */
  .board-search-pop { position: static; }
  .board-search-pop .gb-site-search { left: 0; right: 0; top: 100%; width: auto; border-radius: 0 0 var(--board-radius) var(--board-radius); }
  /* The field takes what the button leaves: without min-width 0 it keeps its intrinsic width and pushes the button past a 390px screen. */
  .board-search-pop .gb-site-search input { width: auto; flex: 1 1 0; min-width: 0; }
  .board-search-pop .gb-site-search button { flex: none; }
  .gb-account .sign-up { --board-control-pad-x: 0.6rem; }
}

@media (max-width: 37.5rem) {
  .board-header-row { column-gap: var(--board-space-1); padding-inline: var(--board-space-3); }
  /* Sign-up remains on the sign-in screen; the header keeps one readable account entry on phones. */
  .gb-account .sign-up { display: none; }
  .gb-account .gb-sign-in { display: inline-flex; align-items: center; justify-content: center; min-height: 2.75rem; padding-inline: var(--board-space-1); }
}

@media (max-width: 47.99rem) {
  /*
   * One column on a phone: the two market cards and both bottom rows follow main then sidebar then
   * bottom, with nothing beside them and nothing to scroll sideways (board-skin.md §9). The span
   * rules are repeated at their own weight so an implicit column cannot widen the page.
   */
  .gb-area-page-main, .gb-area-page-bottom { grid-template-columns: minmax(0, 1fr); }
  .gb-area-page-main > .gb-widget-slot, .gb-area-page-main > .gb-widget-slot:not([data-span="1"]),
  .gb-area-page-bottom > .gb-widget-slot, .gb-area-page-bottom > .gb-widget-slot[data-span="2"],
  .gb-area-page-bottom > .gb-widget-slot:not([data-span="1"]):not([data-span="2"]) { grid-column: 1 / -1; }
}
