/* ============================================================================
   25 · MODULES HUB  (/dashboard/:id/modules)

   The card grid a server owner lands on: what Azra can do, what is running here,
   and a switch that applies the recommended default. Two families:

     .mm-*   the compact Discord transcript on each card. SHARED, not local to
             this page — it is emitted by messagePreview.js `chatMock`, which
             owns the chat-window look for the whole dashboard. It is styled here
             rather than in a part of its own only because this is currently its
             one consumer; if a second page mounts a transcript, move this block
             out before it grows page-specific rules.

     .mod-*  the card, the grid and the section heads.

   Part of the dashboard stylesheet, split out of the former single style.css.
   LOAD ORDER IS LOAD-BEARING and it is the numeric filename prefix — adding a
   part is dropping in a numbered file. See parts/README.md.
   ========================================================================== */

/* ===== The transcript mock (shared — see messagePreview.js `chatMock`) ===== */

/* DISCORD'S OWN DARK PALETTE, not the dashboard's and not an approximation of it.
   These are the real values from Discord's default dark theme — the mock is meant
   to read as a screenshot of the product, and a near-miss on the chat ground is
   what made it read as "more dashboard, styled slightly differently":
     #313338  background-primary   the chat area
     #2b2d31  background-secondary the channel header, and an embed's body
     #1e1f22  background-tertiary  inline code
     #3f4147  the hairline divider
     #dbdee1  text-normal          #949ba4 text-muted   #f2f3f5 header-primary
     #5865f2  blurple              the BOT tag, and the tint behind a mention
   Keep them literal. Resolving them against the dashboard's tokens is exactly how
   the mock drifts back into looking like the page around it. */
.mm-chat {
  background: #313338;
  border: 1px solid rgba(255, 255, 255, .06);
  border-radius: 10px;
  padding: .7rem .75rem .8rem;
  display: flex;
  flex-direction: column;
  gap: .5rem;
  font-size: .78rem;
  line-height: 1.42;
  color: #dbdee1;
  overflow: hidden;
  /* A two-line transcript beside a three-line one leaves the titles under them
     ragged across a row. The floor is not enough to align every card (a card with
     an embed is taller whatever we do) but it stops the shortest sitting well
     above the rest, which is what read as a broken grid. */
  min-height: 142px;
}

/* Discord's channel header is a distinct bar above the chat, not a caption on it —
   so it gets background-secondary and is bled out to the mock's own edges rather
   than sitting inside the padding. */
.mm-chan {
  margin: -.7rem -.75rem 0;
  padding: .45rem .75rem;
  background: #2b2d31;
  border-bottom: 1px solid #3f4147;
  font-size: .7rem;
  font-weight: 600;
  color: #f2f3f5;
}

.mm-msg { display: flex; gap: .5rem; align-items: flex-start; }

/* A letter tile rather than a real avatar: the mock is illustrative, so fetching
   thirteen faces off a CDN would cost a request each to say nothing extra — and a
   failed one would leave a hole in the middle of the card. */
.mm-avatar {
  flex-shrink: 0;
  width: 22px; height: 22px;
  border-radius: 50%;
  display: grid; place-items: center;
  font-size: .68rem; font-weight: 700; color: #fff;
  margin-top: .1rem;
}

.mm-body { min-width: 0; flex: 1; }

.mm-meta { display: flex; align-items: center; gap: .35rem; line-height: 1.2; }
.mm-meta b { font-size: .78rem; font-weight: 600; }
/* The BOT tag is blurple with white text, and it is NOT `var(--blurple)`: that
   token is remapped to the brand crimson in 01-tokens.css, so resolving it here
   would paint Discord's own badge the wrong colour. */
.mm-meta em {
  font-style: normal;
  font-size: .55rem; font-weight: 700; letter-spacing: .04em;
  background: #5865F2; color: #fff;
  border-radius: 3px; padding: .05rem .22rem;
}

.mm-text { word-break: break-word; }
.mm-text code {
  background: #1e1f22;
  border-radius: 3px; padding: .05rem .25rem;
  font-family: var(--font-mono); font-size: .72rem;
  color: #dbdee1;
}
.mm-text strong { color: #f2f3f5; font-weight: 700; }

/* Discord paints a mention as a tinted pill, and the log/preview code already
   emits `.wz-mention` for one — this is the same chip at transcript scale. */
.mm-text .wz-mention {
  background: rgba(88, 101, 242, .3);
  color: #c9cdfb;
  border-radius: 3px;
  padding: 0 .18rem;
  font-weight: 500;
}

.mm-embed {
  margin-top: .35rem;
  border-left: 4px solid #5865F2;
  border-radius: 4px;
  background: #2b2d31;
  padding: .45rem .6rem;
}
.mm-embed-title { font-weight: 700; color: #f2f3f5; font-size: .76rem; margin-bottom: .12rem; }
.mm-embed-text  { font-size: .73rem; color: #dbdee1; white-space: pre-line; }

/* Not a message — an aside for what a transcript cannot show. Deliberately has no
   avatar and no name, so it can never be read as something somebody said. */
.mm-note {
  margin: 0;
  font-style: italic;
  font-size: .72rem;
  color: #949ba4;
  padding-left: .1rem;
}

/* ===== The grid ===== */

.mod-sec { margin-bottom: 2rem; }
.mod-sec[hidden] { display: none; }

.mod-sec-head { margin-bottom: .9rem; }
.mod-sec-head h2 {
  font-size: 1rem; font-weight: 700; margin: 0 0 .15rem;
  color: var(--text-primary);
  display: flex; align-items: center; gap: .5rem;
}
.mod-sec-head p { margin: 0; font-size: .8125rem; color: var(--text-muted); }

/* 300px, not 330: the content column is the viewport minus the ~260px sidebar and
   the 2rem gutters, so 330 fell one card short of four columns on an ordinary
   1600px desktop and left the four Moderation cards as a row of three plus an
   orphan. At 300 the section fills exactly one row, and a narrower window still
   steps down to three and then two on its own. */
.mod-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1rem;
}

/* ===== One card ===== */

.mod-card {
  display: flex;
  flex-direction: column;
  background: var(--glass, rgba(17, 24, 39, .72));
  border: 1px solid var(--border);
  border-radius: 14px;
  overflow: hidden;
  /* A card is repainted in place on every toggle, so the busy overlay needs a
     positioned ancestor — `beginBusy` sets one itself, but declaring it here
     keeps the overlay's corners inside the card's own radius. */
  position: relative;
  transition: border-color .18s var(--ease), transform .18s var(--ease), box-shadow .18s var(--ease);
}
.mod-card[hidden] { display: none; }
.mod-card:hover {
  border-color: var(--border-strong);
  transform: translateY(-2px);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .32);
}

/* A paused module is dimmed rather than removed: the owner still has to be able
   to read what it does and see that somebody else switched it off. */
.mod-card--locked { opacity: .62; }
.mod-card--locked:hover { transform: none; box-shadow: none; }

.mod-preview { padding: .85rem .85rem 0; }

/* A grid row stretches every card to the height of the tallest, so a card with no
   trailing note used to leave a void between its state line and its footer. The
   info block is a column and the STATE LINE is pinned to its bottom, so the slack
   falls between the description and the state instead: the title and description
   sit together at the top, and the state (plus any note) sits directly above the
   controls it describes. `align-items: stretch` stays — footers lining up across a
   row reads far better than cards of ragged height. */
.mod-info {
  padding: .85rem;
  flex: 1;
  display: flex;
  flex-direction: column;
}

.mod-titlerow { display: flex; align-items: center; gap: .5rem; margin-bottom: .35rem; flex-wrap: wrap; }

.mod-icon {
  width: 26px; height: 26px; flex-shrink: 0;
  border-radius: 7px;
  display: grid; place-items: center;
  background: rgba(var(--blue-rgb), .14);
  border: 1px solid rgba(var(--blue-rgb), .30);
  color: var(--crimson-glow, #EF4444);
  font-size: .92rem;
}

.mod-title { margin: 0; font-size: .95rem; font-weight: 700; color: var(--text-primary); }

.mod-desc  { margin: 0 0 .6rem; font-size: .8rem; line-height: 1.5; color: var(--text-muted); }

/* The three things this module actually does. The tick is drawn in CSS rather than
   with an <ion-icon> per row: 13 cards × 3 rows is 39 web-component instances to
   render a shape two borders can make. */
.mod-gains {
  list-style: none;
  margin: 0 0 .6rem;
  padding: 0;
  display: flex; flex-direction: column; gap: .3rem;
}
/* NOT a flex row, for the same reason `.mod-state` is not one: these lines carry
   `<strong>` around the numbers that make them worth reading, and flex makes every
   text run between two elements its own item — so "Route any of **85** events to
   whichever channel you want" laid out as five independent boxes and came out
   reordered. The tick is absolutely positioned into a text indent instead, which
   leaves the line as ordinary wrapping text. */
.mod-gains li {
  position: relative;
  padding-left: 1rem;
  font-size: .775rem; line-height: 1.45; color: var(--text);
}
.mod-gains li::before {
  content: '';
  position: absolute;
  left: .15rem; top: .28rem;
  width: .3rem; height: .55rem;
  border: solid var(--pistachio-dark, #6FAF5A);
  border-width: 0 1.6px 1.6px 0;
  transform: rotate(45deg);
}
.mod-gains strong { color: var(--text-primary); font-weight: 700; }
.mod-gains code {
  font-family: var(--font-mono); font-size: .72rem;
  background: rgba(255, 255, 255, .06); border-radius: 4px; padding: 0 .25rem;
}

/* This server's own state — the one line on the card that is not the same for
   everybody, so it is the line that carries the accent AND the status dot.
   There is deliberately no status chip beside the title: it would be a second copy
   of this line, and the switch in the footer is a third. See the note in
   pages/modules.js. */
.mod-state {
  /* `margin-top: auto` is what pushes the state (and the note under it) to the
     bottom of the info column — see the note on .mod-info. It has to live in THIS
     rule rather than beside that one: a later `margin: 0` would reset it. */
  margin: auto 0 0;
  /* NORMAL TEXT FLOW, not flex. This line is a sentence that can carry a channel
     chip inside it ("Greeting new members in #welcome · goodbye on"), and flex
     makes every text run between two elements its own item — so the sentence broke
     into three fragments that wrapped independently instead of reading as one line.
     The dot is an inline-block for the same reason. */
  font-size: .78rem; font-weight: 600; color: var(--text); line-height: 1.45;
}
.mod-state:empty { display: none; }
.mod-state--on    { color: var(--pistachio-light); }
.mod-state--setup { color: #FBBF24; }
.mod-state--off   { color: var(--text-muted); }

.mod-note  { margin: .35rem 0 0; font-size: .73rem; line-height: 1.45; color: var(--text-muted); font-style: italic; }

/* A channel named inside a state line. `white-space: nowrap` so the chip is never
   split across two lines mid-name; the sentence around it still wraps normally. */
.mod-chan {
  font-family: var(--font-mono); font-size: .72rem;
  background: rgba(88, 101, 242, .18); color: #a5b4fc;
  border-radius: 4px; padding: 0 .25rem;
  white-space: nowrap;
}

/* ===== The "Paused" chip ===== */
/* The ONLY chip on a card. Every other state is said by the state line and the
   toggle; `locked` is the one fact neither can express — see the note in
   pages/modules.js. Do not add on/off/setup variants back. */

.mod-chip {
  display: inline-flex; align-items: center; gap: .28rem;
  font-size: .68rem; font-weight: 700; letter-spacing: .02em;
  padding: .16rem .45rem;
  border-radius: 999px;
  border: 1px solid transparent;
  white-space: nowrap;
}
.mod-chip ion-icon { font-size: .8rem; }

.mod-chip--locked { background: rgba(var(--red-rgb), .12); color: #FCA5A5; border-color: rgba(var(--red-rgb), .30); }

/* The state dot. It pulses ONLY when the module is running — that is the one state
   meaning "something is happening in your server right now", and a dot that pulses
   for every state says nothing at all. Motion is a real signal here rather than
   decoration, so under reduced-motion it is dropped rather than slowed. */
.mod-dot {
  flex-shrink: 0;
  display: inline-block; vertical-align: middle;
  margin-right: .4rem;
  width: 6px; height: 6px; border-radius: 50%;
  background: currentColor;
}
.mod-state--on .mod-dot {
  background: var(--pistachio-glow, #B7E778);
  box-shadow: 0 0 0 0 rgba(var(--green-rgb), .55);
  animation: mod-pulse 2.4s var(--ease) infinite;
}
@keyframes mod-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(var(--green-rgb), .5); }
  70%  { box-shadow: 0 0 0 5px rgba(var(--green-rgb), 0); }
  100% { box-shadow: 0 0 0 0 rgba(var(--green-rgb), 0); }
}
@media (prefers-reduced-motion: reduce) {
  .mod-dot { animation: none; }
  .mod-card:hover { transform: none; }
}

/* ===== Footer ===== */

.mod-foot {
  display: flex; align-items: center; justify-content: space-between; gap: .6rem;
  padding: .7rem .85rem;
  border-top: 1px solid var(--border);
  background: rgba(0, 0, 0, .16);
  /* Cards in a row are different heights (a three-line transcript beside a two-),
     so the footer is pushed to the bottom and every card's controls line up. */
  margin-top: auto;
}

/* A card with no switch has only its button. Stating the single-control layout
   rather than letting `space-between` imply it — the same call `.er-card-footer
   --solo` makes on the Logging page — so the button sits where every other card's
   button sits instead of jumping to the left edge. */
.mod-foot--solo { justify-content: flex-end; }

.mod-switch { display: inline-flex; align-items: center; gap: .5rem; min-width: 0; }
.mod-switch-label { font-size: .8rem; font-weight: 600; color: var(--text); white-space: nowrap; }

.mod-foot .btn { flex-shrink: 0; }

/* ===== Header search ===== */

.mod-search {
  position: relative;
  flex-shrink: 0;
  display: flex; align-items: center;
}
.mod-search ion-icon {
  position: absolute; left: .65rem;
  font-size: 1rem; color: var(--text-muted); pointer-events: none;
}
.mod-search input {
  width: 240px;
  padding: .5rem .7rem .5rem 2rem;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-sidebar);
  color: var(--text-primary);
  font-family: var(--font); font-size: .8125rem;
}
.mod-search input:focus { outline: none; border-color: var(--border-strong); box-shadow: var(--ring-blue); }
.mod-search input::-webkit-search-cancel-button { filter: invert(.6); }

.mod-empty {
  text-align: center; padding: 2rem 0;
  color: var(--text-muted); font-size: .875rem;
}
.mod-empty[hidden] { display: none; }

@media (max-width: 640px) {
  .mod-grid { grid-template-columns: 1fr; }
  .mod-search input { width: 100%; }
  .mod-search { width: 100%; margin-top: .5rem; }
}
