# Theming

Every colour, corner radius and font on the generated pages uses a `--pyd-*` custom property. To
retint the pages, set a handful of these properties. Anything the tokens do not cover is a normal
CSS rule, and your rules win by default. See [Overriding rules](#overriding-rules).

To replace a whole renderer, for example to build signature blocks or member tables with your own
Astro component, see [Component overrides](/starlight-pydocs/guides/component-overrides/) instead.

## Loading your stylesheet

The plugin's stylesheet is `starlight-pydocs/styles`. Under Starlight, the plugin appends it to
Starlight's `customCss` for you. Pass `injectStyles: false` if you want to import it yourself.

Add your own CSS to that same list. Register it like any other Starlight custom stylesheet:

```js title="astro.config.mjs" {4}
starlight({
  title: 'My project',
  plugins: [starlightPydocs({ packages: [{ name: 'mypkg', search: ['../src'] }] })],
  customCss: ['./src/styles/custom.css'],
});
```

Both kinds of change go in that file. To retint, set tokens. To override, write a rule:

```css title="src/styles/custom.css" {5-10}
:root {
  --pyd-accent: #b45309;
  --pyd-radius: 0;
}
/* Unlayered, so this rule beats the plugin
   stylesheet with no !important. */
.pyd-signature {
  background: transparent;
  border-inline-start: 3px solid var(--pyd-accent);
}
```

In a plain Astro project nothing is injected. The plugin's built-in page layout imports the
stylesheet itself, so the generated pages are styled either way. Import it yourself when you pass a
layout of your own as `layout`, or in a hand-written page that embeds API docs with
[`<Autodoc>`](/starlight-pydocs/guides/autodoc/):

```astro title="src/layouts/MyLayout.astro"
---
import 'starlight-pydocs/styles';
---
```

## Overriding rules

Every rule in the stylesheet is wrapped in `@layer starlight-pydocs`. An unlayered rule always beats
a layered rule, whatever its specificity. So a plain selector in your own stylesheet wins, with no
specificity fight and no `!important`. You do not need to increase selector specificity to override
the plugin's styles.

Keep your overrides unlayered, and this holds however you load them. If you put your own rules in a
cascade layer, the comparison changes. The plugin appends its stylesheet after your `customCss`
entries, so its layer is declared last, and later-declared layers win.

One exception: inside Starlight's own `.sl-markdown-content`, Starlight's unlayered rules apply too.
There, give your rule enough specificity to beat Starlight's. The plugin's stylesheet does the same
in the few places it must.

Class hooks are stable and prefixed `pyd-`. The ones worth knowing:

| Class                                 | What it wraps                                            |
| ------------------------------------- | -------------------------------------------------------- |
| `.pyd-module`                         | A whole module page's body                               |
| `.pyd-member`                         | One documented object, including its members             |
| `.pyd-heading`, `.pyd-anchor`         | An object's heading and its permalink                    |
| `.pyd-badges`, `.pyd-badge`           | Kind, label and deprecation badges                       |
| `.pyd-signature`                      | A signature block (`--overload`, `--attribute` variants) |
| `.pyd-type`                           | A name inside a signature or annotation (linked or not)  |
| `.pyd-provenance`, `.pyd-source-link` | The line under a heading                                 |
| `.pyd-section`, `.pyd-section-title`  | One docstring section and its heading                    |
| `.pyd-params`, `.pyd-summary-table`   | Parameter tables and member summaries                    |
| `.pyd-aside`                          | An admonition (`--tip`, `--caution`, `--danger`)         |
| `.pyd-inherited`                      | The `<details>` block of members from a base class       |
| `.pyd-search…`                        | The symbol search element and its results                |
| `.pyd-page`, `.pyd-toc`               | The built-in vanilla layout and its table of contents    |

For anything more selective, use the data attributes instead. They use the model's own vocabulary:
`[data-pydocs-kind="class"]`, `[data-pydocs-path="mypkg.Report"]`,
`[data-pydocs-group="methods"]`, `[data-pydocs-section="parameters"]`,
`[data-pydocs-badge="deprecated"]` and `[data-pydocs-aside="note"]`.

## Tokens

Set any of these on `:root`, or on a narrower selector to retint one part of the site. The values
shown are the defaults. Each uses Starlight's token first, then a static fallback for sites without
Starlight.

| Token               | Default                                   | Used for                                         |
| ------------------- | ----------------------------------------- | ------------------------------------------------ |
| `--pyd-text`        | `var(--sl-color-text, #24292f)`           | Body text in signatures and tables               |
| `--pyd-text-muted`  | `var(--sl-color-gray-3, #57606a)`         | Badges, provenance, section titles, source links |
| `--pyd-text-invert` | `var(--sl-color-black, #ffffff)`          | Text on an accent background (the kind badge)    |
| `--pyd-bg`          | `var(--sl-color-bg, #ffffff)`             | Page background in the built-in layout           |
| `--pyd-surface`     | `var(--sl-color-gray-6, #f6f8fa)`         | Signature blocks, badges, search results         |
| `--pyd-border`      | `var(--sl-color-hairline-light, #d8dee4)` | Member rules, table borders, input borders       |
| `--pyd-accent`      | `var(--sl-color-text-accent, #2563eb)`    | Linked types, anchors, the kind badge            |
| `--pyd-accent-low`  | `var(--sl-color-accent-low, #dbeafe)`     | Accent backgrounds                               |
| `--pyd-note`        | `var(--sl-color-blue, #3b82f6)`           | Note asides                                      |
| `--pyd-note-low`    | `var(--sl-color-blue-low, #dbeafe)`       | Note aside background                            |
| `--pyd-tip`         | `var(--sl-color-purple, #8b5cf6)`         | Tip asides                                       |
| `--pyd-tip-low`     | `var(--sl-color-purple-low, #ede9fe)`     | Tip aside background                             |
| `--pyd-caution`     | `var(--sl-color-orange, #ea580c)`         | Caution asides and deprecation badges            |
| `--pyd-caution-low` | `var(--sl-color-orange-low, #ffedd5)`     | Caution aside background                         |
| `--pyd-danger`      | `var(--sl-color-red, #dc2626)`            | Danger asides                                    |
| `--pyd-danger-low`  | `var(--sl-color-red-low, #fee2e2)`        | Danger aside background                          |
| `--pyd-radius`      | `0.375rem`                                | Corner radius on signatures and inputs           |
| `--pyd-mono`        | `var(--sl-font-mono, …)`                  | Every monospaced string                          |

Because the defaults reference Starlight's tokens, changing your site's accent colour also changes
the API pages. To make the API pages differ, retint one token at a time.

:::caution[Starlight's colour tokens are contrast tokens]
`--sl-color-white` is _dark_ in light mode: the token names describe contrast, not a literal colour.
This is why text on an accent background here uses `--pyd-text-invert`, which defaults to
Starlight's `--sl-color-black`. This follows the same pattern as Starlight's own LinkButton. If you
substitute a literal white, light mode breaks.
:::

## Dark mode

Under Starlight, there is nothing to do. The tokens follow `--sl-*`, and Starlight flips those.

Without Starlight, the stylesheet defines a dark palette twice, so both ways of choosing a theme
work: once under `@media (prefers-color-scheme: dark)`, guarded by
`:root:not([data-theme='light'])`, and once under `:root[data-theme='dark']`. Set
`data-theme="dark"` or `data-theme="light"` on `<html>` from your own theme toggle, and the API
pages follow it. Leave it unset, and they follow the operating system.

## Code inside docstrings

The site's own pipeline highlights fenced blocks and doctest transcripts in a docstring, because
docstring prose renders through the Markdown processor your project configured. There is no
separate Shiki configuration here, and nothing to keep in sync.

Under Starlight, that means Expressive Code. A docstring's code block gets the same frame, copy
button and themes as a code block in a hand-written page. In a plain Astro project, it is Shiki's
`.astro-code` output, configured by `markdown.shikiConfig`:

```js title="astro.config.mjs" {4-5}
export default defineConfig({
  markdown: {
    shikiConfig: {
      themes: { light: 'github-light', dark: 'github-dark' },
      defaultColor: false,
    },
  },
});
```

Two themes with `defaultColor: false` emit both palettes as custom properties. This is what lets a
single stylesheet work in both light and dark. This site uses that configuration.

Signatures are not Shiki output, but they are Shiki coloured. The model builds them, so that every
type can be a link, and the tokens are then given the colours of the same themes you configure
above. A cross-reference keeps its own `.pyd-type` colour through that pass, so a linked
`dict[str, Report]` still reads as a link rather than disappearing into the grammar.

The colours are worked out once, at `astro:config:done`, and stored beside the extracted dump.
Shiki cannot resolve a theme from inside the page-rendering bundle, where Astro has tree-shaken the
theme registry away, so nothing is highlighted at render time.

:::note
The theme pair comes from `markdown.shikiConfig`. Starlight sites usually leave that alone, because
Expressive Code renders their prose code blocks, so signatures fall back to `github-light` and
`github-dark`. Set `themes` there to change them.
:::

[Component overrides](/starlight-pydocs/guides/component-overrides/)
  [Vanilla Astro](/starlight-pydocs/guides/vanilla-astro/)