/* ---------------------------------------------------------------------------
   Live Viewer - stylesheet
   Mobile first. Every size here is chosen for a thumb on a phone, not a
   mouse on a desktop; the desktop browser is only the development view.
   --------------------------------------------------------------------------- */

:root {
  --bg:            #12131a;
  --bg-raised:     #1b1d27;
  --bg-pressed:    #262936;
  --line:          #2e3140;
  --text:          #eceef5;
  --text-dim:      #9aa0b4;
  --accent:        #6f8dff;

  --appbar-h:      3.5rem;   /* 56px - Android app bar height */
  --tap:           2.75rem;  /* 44px - minimum comfortable touch target */
  --radius:        0.75rem;

  /* Safe areas: 0 on a desktop browser, non-zero around a phone's notch,
     camera cutout, rounded corners and gesture bar. Read once here so the
     rest of the sheet can just use the variables. */
  --safe-top:      env(safe-area-inset-top, 0px);
  --safe-bottom:   env(safe-area-inset-bottom, 0px);
  --safe-left:     env(safe-area-inset-left, 0px);
  --safe-right:    env(safe-area-inset-right, 0px);
}

*, *::before, *::after { box-sizing: border-box; }

html {
  /* Stops Android's pull-to-refresh from firing when a feed is scrolled to
     the top - in an installed PWA that gesture just looks like a bug. */
  overscroll-behavior-y: none;
  /* Keep text at the author's size when the phone is rotated. */
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 1rem;
  line-height: 1.45;
  /* Nothing in this app should ever scroll sideways. */
  overflow-x: hidden;
}

/* Kill the grey flash Android paints over anything tapped; every
   interactive element below defines its own :active state instead, so a
   tap still feels acknowledged. */
button {
  -webkit-tap-highlight-color: transparent;
  /* Removes the ~300ms delay browsers keep in case a tap is a double-tap. */
  touch-action: manipulation;
  font: inherit;
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --- Screens ------------------------------------------------------------- */

.screen {
  display: flex;
  flex-direction: column;
  /* dvh tracks the viewport as Chrome's address bar slides away; vh does
     not, and leaves a dead strip at the bottom. The vh line is the fallback
     for older browsers, which ignore the dvh line below it. */
  min-height: 100vh;
  min-height: 100dvh;
}

.screen[hidden] { display: none; }

.screen__body {
  flex: 1;
  padding-left: var(--safe-left);
  padding-right: var(--safe-right);
  /* Clear the gesture bar / home indicator at the bottom of the screen. */
  padding-bottom: calc(1rem + var(--safe-bottom));
}

/* --- App bar ------------------------------------------------------------- */

.appbar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 0.25rem;
  min-height: var(--appbar-h);
  /* Painted behind the status bar, then padded clear of it. */
  padding: var(--safe-top) calc(0.5rem + var(--safe-right)) 0 calc(0.5rem + var(--safe-left));
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}

.appbar__title {
  /* Takes the slack, so any trailing control sits against the right edge. */
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 0 0.5rem;
  font-size: 1.125rem;
  font-weight: 600;
  /* A long source name truncates rather than wrapping the bar to two lines. */
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.iconbtn {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: var(--tap);
  height: var(--tap);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
}

.iconbtn svg { width: 1.5rem; height: 1.5rem; }
.iconbtn:active { background: var(--bg-pressed); }

/* --- Picker screen ------------------------------------------------------- */

.picker { padding: 1rem 1rem 0; }

.source { margin-bottom: 1.5rem; }

.source__name {
  margin: 0 0 0.5rem;
  padding: 0 0.25rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  /* Deliberately NOT text-transform: uppercase, which is the usual look for
     a section label like this. Site names are brands and several of them are
     lowercase on purpose - it turned "e621" into "E621". */
}

.source__feeds {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-raised);
  /* Clips the child rows' corners to the card's radius. */
  overflow: hidden;
}

/* One tappable row per feed. Full width and 56px tall: an easy thumb
   target anywhere along the row, not just on the text. */
.feedbtn {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  min-height: var(--appbar-h);
  padding: 0.75rem 1rem;
  border: 0;
  border-top: 1px solid var(--line);
  background: transparent;
  text-align: left;
  cursor: pointer;
}

.feedbtn:first-child { border-top: 0; }
.feedbtn:active { background: var(--bg-pressed); }

.feedbtn__name { flex: 1; }

.feedbtn__chev {
  flex: 0 0 auto;
  width: 1.25rem;
  height: 1.25rem;
  color: var(--text-dim);
}

/* --- Feed screen --------------------------------------------------------- */

/* Tighter side padding than the picker: on a phone every pixel of width is
   thumbnail, and the grid's own gap already separates the tiles. */
.feed { padding: 0.25rem; }

.grid {
  display: grid;
  /* Three columns on a typical phone (~412px wide gives ~131px tiles), and
     more as the viewport grows, without a single media query - auto-fill
     works out the count from the minimum tile size, so landscape and tablets
     are handled for free. Three is the density that makes scanning a feed
     feel quick; two feels like scrolling through a slideshow. */
  grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
  gap: 0.25rem;
}

.tile {
  position: relative;
  display: block;
  /* Square tiles keep the grid tidy regardless of the source images'
     proportions; the image is cropped to fill, never squashed. */
  aspect-ratio: 1;
  overflow: hidden;
  /* It is a <button>, so the browser's own button chrome has to go. */
  padding: 0;
  border: 0;
  border-radius: 0.375rem;
  background: var(--bg-raised);
  cursor: pointer;
  /* A long press on a grid thumbnail should not raise the browser's
     save-image menu - here the thumbnail is a button, and the menu
     interrupts browsing. Deliberately NOT applied in the viewer, where
     long-pressing to save the actual image is a reasonable thing to want. */
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
}

/* The tap highlight is suppressed globally, so give the press its own
   feedback - otherwise a tap on a thumbnail feels like nothing happened. */
.tile:active { opacity: 0.72; }

.tile img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Fade in as each thumbnail arrives, rather than popping in over the
     placeholder. Starts at 0 and the load handler adds .tile--loaded. */
  opacity: 0;
  transition: opacity 180ms ease;
}

.tile--loaded img { opacity: 1; }

/* A thumbnail the CDN refused. Leave the tile as a quiet placeholder rather
   than a browser broken-image icon. */
.tile--broken img { display: none; }

.badge {
  position: absolute;
  right: 0.25rem;
  bottom: 0.25rem;
  display: flex;
  align-items: center;
  gap: 0.125rem;
  padding: 0.125rem 0.375rem 0.125rem 0.25rem;
  border-radius: 0.25rem;
  background: rgb(0 0 0 / 0.65);
  color: #fff;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  line-height: 1.3;
  /* Readable over a light thumbnail as well as a dark one. */
  text-shadow: 0 1px 2px rgb(0 0 0 / 0.6);
}

.badge svg { width: 0.875rem; height: 0.875rem; }

/* --- Feed footer (paging sentinel) --------------------------------------- */

/* Sits below the grid and is what the IntersectionObserver watches, so it
   must keep occupying space even when it renders nothing - a zero-height
   element would sit permanently against the bottom and never trigger. */
.feed-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  min-height: 4rem;
  padding: 1.25rem 1rem;
}

.feed-footer__text {
  margin: 0;
  font-size: 0.875rem;
  color: var(--text-dim);
  text-align: center;
}

/* --- Loading / empty / error states -------------------------------------- */

.status {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  margin: 3rem auto;
  max-width: 22rem;
  padding: 0 1.5rem;
  text-align: center;
  color: var(--text-dim);
}

.status__text { margin: 0; }

.status__detail {
  margin: 0;
  font-size: 0.875rem;
  color: var(--text-dim);
}

.status--error .status__text {
  color: var(--text);
  font-weight: 600;
}

.spinner {
  width: 1.75rem;
  height: 1.75rem;
  border: 2px solid var(--line);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 700ms linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

.btn {
  min-height: var(--tap);
  margin-top: 0.25rem;
  padding: 0 1.25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-raised);
  font-weight: 600;
  cursor: pointer;
}

.btn:active { background: var(--bg-pressed); }

/* --- Fullscreen viewer --------------------------------------------------- */

/* While the viewer is open the feed must not scroll behind it. */
html.viewer-open { overflow: hidden; }

.viewer {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  /* Black rather than the app background: this is the one screen that is
     entirely about the image, and anything else tints it. */
  background: #000;
  /* A drag here is a swipe. Without this, dragging with a mouse selects the
     counter text and leaves a highlight behind. */
  user-select: none;
  -webkit-user-select: none;
}

.viewer[hidden] { display: none; }

/* The dialog takes focus when it opens so keys reach it; it is a container,
   not a control, so it must not draw a focus ring. The controls inside it
   keep theirs. */
.viewer:focus,
.viewer:focus-visible { outline: none; }

/* Floats over the media instead of taking a strip of the screen, so the
   image gets the whole display. The gradient keeps the controls readable
   over a bright image without painting a solid bar across it. */
.viewer__bar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: var(--safe-top) calc(0.5rem + var(--safe-right)) 0.75rem calc(0.5rem + var(--safe-left));
  background: linear-gradient(rgb(0 0 0 / 0.65), transparent);
  color: #fff;
  pointer-events: none;
}

/* The bar itself ignores taps so it cannot eat a swipe that starts near the
   top of the screen; its actual controls opt back in. */
.viewer__bar > * { pointer-events: auto; }

.viewer__count {
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 1px 3px rgb(0 0 0 / 0.8);
}

.viewer__stage {
  flex: 1;
  /* Without this a flex child refuses to shrink below its content size, and
     a tall image pushes the stage off the bottom of the screen. */
  min-height: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  /* Vertical drags are ours (swiping between posts); pinch-zoom stays with
     the browser so images can still be zoomed. */
  touch-action: pinch-zoom;
  /* Nothing here scrolls, so a stray flick must not chain through to the
     feed sitting behind the overlay. */
  overscroll-behavior: contain;
}

.viewer__media {
  display: flex;
  max-width: 100%;
  max-height: 100%;
  /* Hints that this element is about to be dragged, so the browser can put
     it on its own layer and the drag does not repaint the whole screen. */
  will-change: transform;
}

/* Sits over the media while the next page is being fetched, so swiping past
   the last loaded post looks like waiting rather than like nothing happened. */
.viewer__loading {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  background: rgb(0 0 0 / 0.45);
  pointer-events: none;
}

.viewer__loading[hidden] { display: none; }

.viewer__media img,
.viewer__media video {
  display: block;
  /* Bounded by the SAFE area, not the raw viewport. In landscape on a phone
     with a cutout the insets are on the left/right, and without this the
     image simply runs underneath the notch and loses an edge. Costs nothing
     on a phone without one, where every inset is 0. */
  max-width: calc(100vw - var(--safe-left) - var(--safe-right));
  max-height: calc(100dvh - var(--safe-top) - var(--safe-bottom));
  /* Fit whole, never crop: the grid crops, the viewer must not. */
  object-fit: contain;
  -webkit-user-drag: none;
}

/* Prev/next arrows. Hidden by default and shown only where there is a real
   pointer - on a phone, swiping is the natural gesture and permanent arrows
   would just sit on top of the image. */
.viewer__nav { display: none; }

@media (hover: hover) and (pointer: fine) {
  /* Stacked on the right rather than either side, because navigation is
     vertical - up above, down below, matching the swipe. Kept off the
     bottom centre, which is where a video's own controls live. */
  .viewer__nav {
    position: absolute;
    right: calc(0.75rem + var(--safe-right));
    z-index: 1;
    display: grid;
    place-items: center;
    width: 3rem;
    height: 3rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgb(0 0 0 / 0.45);
    color: #fff;
    cursor: pointer;
  }

  .viewer__nav:hover { background: rgb(0 0 0 / 0.7); }
  .viewer__nav svg { width: 1.5rem; height: 1.5rem; }
  .viewer__nav--prev { top: calc(50% - 3.5rem); }
  .viewer__nav--next { top: calc(50% + 0.5rem); }
}

/* --- Accessibility ------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
