html {
  font-size: 14px;
}

@media (min-width: 768px) {
  html {
    font-size: 16px;
  }
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus,
.form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

/* The layout is a flex column with mt-auto on the footer, so no fixed offset is needed. */
body {
  margin-bottom: 0;
}

/* asp-validation-summary renders this class, instead of validation-summary-errors, when
   ModelState has nothing to report -- which is every GET. Bootstrap's alert-danger still paints
   it, so without this rule every page carrying a validation summary shows an empty red box. */
.validation-summary-valid {
  display: none;
}

/* --------------------------------------------------------------------------
   Rendered Markdown. The two class names are the two pipelines: .post-body is
   trusted output (posts, app descriptions, release notes), .comment-body is
   untrusted output (comments, and every member-submitted upload body). Rules
   naming both must be safe for user-authored content.
   -------------------------------------------------------------------------- */

.post-body,
.comment-body {
  overflow-wrap: break-word;
}

/* A long URL in a comment must not widen the page on mobile. */
.comment-body a {
  overflow-wrap: anywhere;
}

.post-body h2,
.post-body h3,
.post-body h4 {
  margin-top: 2rem;
  margin-bottom: 0.75rem;
}

/* Headings in untrusted content are visible but deliberately quiet. A comment is a few sentences
   inside somebody else's page, so its `#` is a way of labelling a paragraph -- rendered at the
   document's h1 size it would shout over the page's own title, which is why every level lands
   near body size and only the weight and the spacing say "heading". */
.comment-body h1,
.comment-body h2,
.comment-body h3,
.comment-body h4,
.comment-body h5,
.comment-body h6 {
  font-weight: 600;
  line-height: 1.3;
  margin-top: 1.25rem;
  margin-bottom: 0.35rem;
}

.comment-body h1 {
  font-size: 1.2rem;
}

.comment-body h2 {
  font-size: 1.1rem;
}

.comment-body h3 {
  font-size: 1.05rem;
}

.comment-body h4,
.comment-body h5,
.comment-body h6 {
  font-size: 1rem;
}

/* The h6 is the bottom of the scale and has no size left to give, so it separates itself the only
   other way a heading can. */
.comment-body h5,
.comment-body h6 {
  color: var(--bs-secondary-color);
}

/* A body opens flush with its container -- the margin above a leading heading would otherwise
   read as a gap in the comment thread. */
.post-body > :first-child,
.comment-body > :first-child {
  margin-top: 0;
}

.comment-body hr {
  margin: 1rem 0;
}

.post-body p,
.comment-body p {
  line-height: 1.7;
}

.post-body img {
  max-width: 100%;
  height: auto;
  border-radius: 0.375rem;
}

.post-body pre,
.comment-body pre {
  background: var(--bs-secondary-bg);
  padding: 1rem;
  border-radius: 0.375rem;
  /* Wide code scrolls inside its own box rather than the page. */
  overflow-x: auto;
}

.post-body :not(pre) > code,
.comment-body :not(pre) > code {
  background: var(--bs-secondary-bg);
  padding: 0.125rem 0.3rem;
  border-radius: 0.25rem;
  font-size: 0.9em;
}

.post-body blockquote,
.comment-body blockquote {
  border-left: 0.25rem solid var(--bs-border-color);
  padding-left: 1rem;
  color: var(--bs-secondary-color);
  margin-left: 0;
}

/* `display: block` is doing the work here: a table box ignores `overflow`, so without it a wide
   table widens the page itself on a phone instead of scrolling inside the article. The cost is
   that the table no longer stretches to the column, which is why the width is `max-content`
   capped at the column -- a two-column table sized to its own content reads better than one
   stretched across the page anyway. */
.post-body table,
.comment-body table {
  display: block;
  width: max-content;
  max-width: 100%;
  overflow-x: auto;
  margin-bottom: 1rem;
  border-collapse: collapse;
  font-size: 0.95em;
}

.post-body th,
.post-body td,
.comment-body th,
.comment-body td {
  border: 1px solid var(--bs-border-color);
  padding: 0.5rem 0.75rem;
  /* Markdig only emits an alignment for a column that asked for one; the rest read as prose. */
  text-align: left;
  vertical-align: top;
}

.post-body thead th,
.comment-body thead th {
  background: var(--bs-secondary-bg);
  white-space: nowrap;
}

/* Zebra striping, so a wide row is still readable after it has been scrolled sideways. Both
   Bootstrap themes define these, so it follows light/dark without a media query. */
.post-body tbody tr:nth-child(even),
.comment-body tbody tr:nth-child(even) {
  background: var(--bs-tertiary-bg);
}

.post-body caption,
.comment-body caption {
  caption-side: bottom;
  padding-top: 0.5rem;
  color: var(--bs-secondary-color);
  font-size: 0.9em;
}

/* --------------------------------------------------------------------------
   Embedded HTML5 apps
   -------------------------------------------------------------------------- */

.ratio > iframe {
  border: 0;
  border-radius: 0.375rem;
  background: #000;
}

/* --------------------------------------------------------------------------
   Character pack editor
   -------------------------------------------------------------------------- */

/* One column per trait, so any real roster scrolls in both directions. Freezing
   the header and the name column keeps a cell in the middle of it identifiable. */
.pack-grid-scroll {
  max-height: 70vh;
  overflow: auto;
}

.pack-grid thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--bs-body-bg);
}

.pack-grid .pack-grid-name {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--bs-body-bg);
  /* Sticky cells lose their collapsed borders, so the frozen column draws its own edge. */
  box-shadow: 1px 0 0 var(--bs-border-color);
}

.pack-grid thead .pack-grid-name {
  z-index: 3;
}

/* Save stays reachable from the bottom of a long roster instead of being scrolled past. */
.pack-actions {
  position: sticky;
  bottom: 0;
  z-index: 4;
  background: var(--bs-body-bg);
  border-top: 1px solid var(--bs-border-color);
}

/* --------------------------------------------------------------------------
   Avatars and profiles
   -------------------------------------------------------------------------- */

.avatar {
  border-radius: 50%;
  object-fit: cover;
  /* Uploads are re-encoded to a square, but a stale file must not stretch the row. */
  flex: 0 0 auto;
  background: var(--bs-secondary-bg);
}

.avatar-initials {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-weight: 600;
  line-height: 1;
  user-select: none;
}

.avatar-link {
  display: inline-flex;
  text-decoration: none;
}

/* The crop stage shows the full square that gets uploaded. Shared by every square image picker
   (avatar, app icon); the circular spotlight below is an avatar-only guide, not a second crop
   boundary. */
.crop-editor-stage {
  position: relative;
  width: 320px;
  height: 320px;
  margin: 0 auto;
  overflow: hidden;
  background: var(--bs-secondary-bg);
  touch-action: none;
  cursor: grab;
}

.crop-editor-stage:active {
  cursor: grabbing;
}

.crop-editor-stage canvas {
  display: block;
}

/* Avatars render circular, matching how .avatar actually shows the result; an app icon renders
   as a plain square, so its stage has no such overlay. */
.avatar-editor-stage::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  box-shadow: 0 0 0 999px rgba(0, 0, 0, 0.55);
  pointer-events: none;
}

/* The comment header is avatar + name + timestamp on one baseline. */
.comment-header {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.comment-author {
  text-decoration: none;
}

.comment-author:hover {
  text-decoration: underline;
}

/* Target of a #comment-<id> permalink, so an arriving reader sees which one they came for. */
.comment-item:target {
  background: var(--bs-secondary-bg);
  border-radius: 0.375rem;
  padding: 0.75rem;
  margin-left: -0.75rem;
  margin-right: -0.75rem;
}

/* --------------------------------------------------------------------------
   App catalog
   -------------------------------------------------------------------------- */

/* An icon is stored as a 512px square, so letting it fill the card width rendered it as a
   full-bleed block taller than the text beside it. It sits at icon size next to the title
   instead; the explicit box also stops a stale non-square file stretching the row. */
.app-icon {
  width: 64px;
  height: 64px;
  border-radius: 0.75rem;
  object-fit: cover;
  flex: 0 0 auto;
  background: var(--bs-secondary-bg);
}

/* The home page runs the same card in a narrower column, so the icon steps down rather than
   eating a third of the card's width. */
.app-icon-sm {
  width: 40px;
  height: 40px;
  border-radius: 0.5rem;
}

.app-card-header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}

/* A flex item's default min-width is auto, so a title with no spaces (a dotted app id, not
   just a name) refused to shrink below its own unwrapped width and overflowed the card. */
.app-card-header > :last-child {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
   Inline embeds
   -------------------------------------------------------------------------- */

/* A placeholder that survived into a rendered body without being resolved -- a directive typed
   somewhere the resolver does not run, or nested inside a quote. Empty, so hidden: an invisible
   empty div is better than a mysterious gap. */
.content-embed:empty {
  display: none;
}

.content-embed-media {
  margin-inline: 0;
}

/* Both elements are sized by their own aspect ratio, capped at the column. A video with no
   intrinsic size known to the server would otherwise render at the browser's 300x150 default and
   then jump when the metadata lands. */
.content-embed-media img,
.content-embed-media video {
  display: block;
  max-width: 100%;
  height: auto;
}

/* A media reference mixed into a heading, a list item or a paragraph rather than sitting alone on
   its own line — see ContentEmbedInline. Capped the same way a block embed is, since nothing here
   states an explicit |text or |WIDTHxHEIGHT size by default; vertical-align keeps it level with
   the surrounding text instead of dropping to the image's own baseline. */
.content-embed-inline-media {
  max-width: 100%;
  vertical-align: middle;
}

/* --------------------------------------------------------------------------
   Image skeleton loading

   Any <img data-skeleton> on any page: site.js removes the attribute once the
   image has actually painted (or failed), so the shimmer never outlives a
   cached image and never lingers on a broken one.
   -------------------------------------------------------------------------- */

img[data-skeleton] {
  background: linear-gradient(100deg,
      var(--bs-tertiary-bg) 30%,
      var(--bs-secondary-bg) 50%,
      var(--bs-tertiary-bg) 70%);
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
}

@keyframes skeleton-shimmer {
  from {
    background-position: 150% 0;
  }

  to {
    background-position: -50% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  img[data-skeleton] {
    animation: none;
    background: var(--bs-secondary-bg);
  }
}

/* --------------------------------------------------------------------------
   Dropdowns

   Any <select data-combo> on any page: combo-select.js keeps the native control as the value and
   draws a searchable list over it, so what is styled here is a button that has to pass for a
   .form-select and a panel that has to pass for a dropdown menu. The native control is taken out
   of the layout rather than removed -- it is still what posts.
   -------------------------------------------------------------------------- */

.combo {
  position: relative;
  display: block;
}

.combo-native {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* .form-select supplies the frame and the caret; this only makes room for an icon beside the
   label and stops a long name pushing the caret off the end. */
.combo-button {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  text-align: left;
  cursor: pointer;
}

.combo-button.is-empty .combo-button-label {
  color: var(--bs-secondary-color);
}

.combo-button-label {
  flex: 1 1 auto;
  min-width: 0;
  /* A flex item is blockified, so a label with no text at all (an empty placeholder option, as
     the roster grid's trait cells use) is a block box with nothing in it -- zero height, not one
     line of it -- and the button then has only its padding to show, which is what reads as thin.
     Bootstrap's own line-height, reserved whether or not there is a character to set it with. */
  min-height: 1.5em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.combo-button-icon {
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  object-fit: cover;
  background: var(--bs-secondary-bg);
  flex: 0 0 auto;
}

/* The multi shape of the same button, over a <select multiple>: a chip per chosen value, wrapping
   rather than scrolling, so a cell holding four tags reads as four tags and not as an ellipsis.
   The button grows with them, which is why combo-select.js repositions the panel on every toggle. */
.combo-button-chips {
  display: flex;
  flex: 1 1 auto;
  flex-wrap: wrap;
  gap: 0.25rem;
  min-width: 0;
}

.combo-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  max-width: 100%;
  padding: 0 0.4rem;
  border-radius: 1rem;
  background: var(--bs-secondary-bg);
  font-size: 0.8em;
  line-height: 1.6;
}

.combo-chip-icon {
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 auto;
}

.combo-chip-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Fixed, and placed by the script: a picker sits inside form columns and cards that scroll, and a
   panel positioned inside one is a panel clipped by it. */
.combo-panel {
  position: fixed;
  z-index: 1055;
  display: flex;
  flex-direction: column;
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: 0.375rem;
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
  overflow: hidden;
}

.combo-search {
  flex: 0 0 auto;
  margin: 0.4rem;
  width: auto;
}

.combo-list {
  overflow-y: auto;
  margin: 0;
  padding: 0 0.25rem 0.25rem;
  list-style: none;
}

.combo-group {
  padding: 0.45rem 0.5rem 0.2rem;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--bs-secondary-color);
}

.combo-option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.45rem;
  border-radius: 0.375rem;
  cursor: pointer;
}

.combo-option.active {
  background: var(--bs-tertiary-bg);
}

.combo-option.chosen {
  color: var(--bs-primary);
}

.combo-option-empty {
  color: var(--bs-secondary-color);
  font-style: italic;
}

.combo-option-icon {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  object-fit: cover;
  background: var(--bs-secondary-bg);
  flex: 0 0 auto;
}

.combo-option-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* What kind of thing this is -- whatever the picker put on the option. Quiet and to the right, so
   the column of names stays the thing being read. */
.combo-option-note {
  flex: 0 0 auto;
  max-width: 45%;
  color: var(--bs-secondary-color);
  font-size: 0.76rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.combo-empty {
  margin: 0;
  padding: 0.5rem;
  color: var(--bs-secondary-color);
  font-size: 0.875rem;
}

/* Tag cells: which control is on offer.

   A tag trait's cell is several values and one comma-separated string on the wire, so the box is
   what posts and stays the cell; the picker beside it is a nameless <select multiple> that writes
   into it (tag-cell.js). Before that file has run -- or on a page where it never does -- the box
   is the whole control, which is what the cell was before the picker existed.

   Both shapes are named for the reason the icon cells' below are: combo-select.js wraps the select
   in a .combo, so there is a bare select to hide before it runs and a wrapper after it. Keyed off
   the root, so a row cloned by an editor after load is already in the right state. */
[data-tag-cell] .combo,
[data-tag-cell] [data-tag-picker] {
  display: none;
}

.tag-cell-ready [data-tag-cell] .combo {
  display: block;
}

/* Still posted, and still the cell: only the control somebody reaches for has changed. */
.tag-cell-ready [data-tag-cell] [data-tag-values] {
  display: none;
}

/* Icon cells: which picker is on offer.

   A row's own dropdown is rendered holding only what that row points at, and filled from the one
   shared library _IconLibrary renders per form -- so until icon-picker.js has run there is nothing
   in it to choose from, and the shared control is the picker instead. icon-picker.js puts
   .icon-picker-ready on the root once it can do the job, and the two swap. With no script at all
   the class never lands and the fallback stays, which is the whole point of it.

   Both shapes are named because combo-select.js wraps the select in a .combo and hides the select
   inside it: before that runs there is a bare select to hide, after it there is a wrapper, and a
   page where one script loaded and the other did not must not show an empty picker beside a
   working one.

   Keyed off the root rather than toggled per cell so a row cloned by an editor after load is
   already in the right state. */
[data-icon-cell] .combo,
[data-icon-cell] [data-icon-select] {
  display: none;
}

.icon-picker-ready [data-icon-cell] .combo,
.icon-picker-ready [data-icon-cell] [data-icon-select] {
  display: block;
}

/* A width rather than a max-width: the wrapper is a flex item sized by its button, and a button
   whose label is a filename would otherwise make one row's cell twice as wide as its neighbour's.
   The label ellipsizes inside it; the panel that opens is wider than this on purpose, which is
   what data-combo-min-width on the select states. */
.icon-picker-ready [data-icon-cell] .combo {
  width: 12rem;
}

/* !important: the fallback carries Bootstrap's .d-flex, whose own display:flex is !important and
   would otherwise outrank this rule and keep the control on screen after icon-picker.js is ready. */
.icon-picker-ready [data-icon-library-fallback] {
  display: none !important;
}

/* The other half of the same swap. "Use" is the no-script picker's second step -- choose above the
   grid, then press it on a row -- and with the script running the dropdown sets the row the moment
   something is chosen, so the button would only repeat what just happened. */
.icon-picker-ready [data-icon-cell] [data-icon-action="pick"] {
  display: none;
}

/* The picker draws the picture beside its name, so the cell's own thumbnail next to it is the same
   picture twice. It stays in the markup because with no script it is the only one there is, and
   because it is what the no-script handlers re-render a saved row with. */
.icon-picker-ready [data-icon-cell] [data-icon-thumb] {
  display: none;
}

/* A library picture is a picture of something rectangular -- a card, a screenshot, a banner -- and
   a circular crop of one is a crop. The character pickers keep their circles; these are the same
   widget wearing the shape its rows actually are. */
[data-icon-cell] .combo-button-icon,
[data-icon-cell] .combo-option-icon {
  border-radius: 0.25rem;
}

/* -------------------------------------------------------------------------
   Gacha build/team composer stat pickers (stat-plan.js)

   The free-text Levels/Main stats/Substats box is hidden rather than removed -- it stays the
   value MVC binds, this only draws over it -- so the layout below just has to look at home next
   to the .form-control fields it replaced.
   -------------------------------------------------------------------------- */

.stat-plan-rows {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin-bottom: 0.5rem;
}

.stat-plan-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.stat-plan-row > select {
  flex: 1 1 auto;
  min-width: 0;
}

.stat-plan-number {
  flex: 0 0 auto;
  width: 6rem;
}

.stat-plan-row-wrap {
  flex-direction: column;
  align-items: stretch;
}

.stat-plan-picks {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.stat-plan-picks > .combo,
.stat-plan-picks > select {
  flex: 1 1 12rem;
  min-width: 10rem;
}

.stat-plan-row-check {
  gap: 0.5rem;
}

.stat-plan-substat-label {
  flex: 1 1 auto;
}

/* Franchise Map */

.franchise-map-stage {
  position: relative;
}

.franchise-map-canvas {
  width: 100%;
  height: min(75vh, 900px);
  min-height: 420px;
  border: 1px solid var(--bs-border-color);
  border-radius: 0.375rem;
  overflow: hidden;
  background: #10131a;
  touch-action: none;
  cursor: grab;
}

.franchise-map-canvas:active {
  cursor: grabbing;
}

.franchise-map-canvas canvas {
  display: block;
  /* Safari's own equivalent of the Firefox canvas-drag-out quirk franchise-map.js's dragstart
     handler stops -- belt and suspenders, since -webkit-user-drag is Safari-only. */
  -webkit-user-drag: none;
  user-select: none;
}

.franchise-map-inspector,
.franchise-map-add-panel {
  position: absolute;
  top: 0.75rem;
  width: min(320px, calc(100% - 1.5rem));
  max-height: calc(100% - 1.5rem);
  overflow-y: auto;
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: 0.375rem;
  padding: 0.75rem 1rem;
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.25);
  z-index: 1;
}

/* Opposite corners so both can be open together without overlapping. */
.franchise-map-inspector {
  right: 0.75rem;
}

.franchise-map-add-panel {
  left: 0.75rem;
}

.franchise-map-panel {
  border: 1px solid var(--bs-border-color);
  border-radius: 0.375rem;
  padding: 0.75rem 1rem;
}

/* Bottom-left, clear of the top-anchored inspector and add-node panels above. */
.franchise-map-presence {
  position: absolute;
  bottom: 0.75rem;
  left: 0.75rem;
  width: min(240px, calc(100% - 1.5rem));
  max-height: calc(60% - 0.75rem);
  overflow-y: auto;
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: 0.375rem;
  padding: 0.5rem 0.75rem;
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.25);
  z-index: 1;
}

.franchise-map-presence summary {
  cursor: pointer;
}

.franchise-map-presence-editor-badge {
  font-size: 0.65rem;
}

/* Bottom-right, the fourth corner: top-left is add-node, top-right is the inspector, bottom-left
   is presence. */
.franchise-map-legend {
  position: absolute;
  bottom: 0.75rem;
  right: 0.75rem;
  width: min(260px, calc(100% - 1.5rem));
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: 0.375rem;
  padding: 0.5rem 0.75rem;
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.25);
  z-index: 1;
}

.franchise-map-legend summary {
  cursor: pointer;
}

/* Colors mirror LINK_STYLES in franchise-map.js -- keep the two in step. */
.franchise-map-legend-swatch {
  display: inline-block;
  width: 1.75rem;
  height: 0;
  border-top-width: 3px;
  border-top-style: solid;
  margin-right: 0.35rem;
  vertical-align: middle;
}

.franchise-map-legend-hard {
  border-top-color: #e0b93a;
}

.franchise-map-legend-link {
  border-top-color: #c3c3c3;
}

.franchise-map-legend-soft {
  border-top-color: #8a8a8a;
  border-top-style: dashed;
}

.franchise-map-node-label {
  font-size: 0.7rem;
}

/* The path finder's result row -- node chips (.combo-chip, reused as-is) joined by these
   connectors. Color and dash come from the crossed link's own style inline; this just adds the
   arrow glyph and makes both halves feel clickable, matching selectNode/selectLink below them. */
.franchise-map-path-chip,
.franchise-map-path-arrow {
  cursor: pointer;
}

.franchise-map-path-arrow {
  font-weight: 700;
  padding-bottom: 1px;
}

.franchise-map-path-chip:hover,
.franchise-map-path-arrow:hover {
  opacity: 0.75;
}
