{
  "openapi": "3.1.0",
  "info": {
    "title": "bianca.codes content API",
    "version": "1.0.0",
    "summary": "Read-only markdown, feed, and JSON endpoints for the bianca.codes blog.",
    "description": "A static, unauthenticated content surface for AI agents and crawlers. Every blog post is published both as HTML and as a markdown twin, either at an explicit `.md` URL or via `Accept: text/markdown` content negotiation on the canonical page URL. Start at `/llms.txt` to resolve a topic to a post slug. Content responses are static files: safe to cache and cheap to fetch. A small number of read-only JSON endpoints under `/api/` are also described here. Write endpoints and the chat assistant are deliberately excluded — they are site infrastructure, not a public API.\n\n## Versioning\n\nThis surface is versioned with semver and is currently `1.0.0`. Every response carries the served version in the `X-API-Version` header, so a client can detect drift from the version it integrated against. The major version is also addressable in the URL path: `/v1/...` is a stable alias for the current major, and pinning to it protects you from a future breaking change.\n\n## Deprecation policy\n\nA breaking change to a documented endpoint means a new major version at a new path prefix; the previous major keeps working. Before an endpoint is withdrawn it is marked in two machine-readable ways: a `Deprecation` header (RFC 9745) carrying the date the deprecation was announced, and a `Sunset` header (RFC 8594) carrying the date it will stop responding. Sunset is never less than 180 days after Deprecation. Operations retired in this way are also flagged `deprecated: true` in this document before removal.\n\n## Rate limits\n\nResponses from the `/api/` endpoints carry the RFC 9331 `RateLimit` and `RateLimit-Policy` headers, so a client can self-throttle without probing for the ceiling. A refused request returns `429` with `Retry-After`. The limits are generous, but honour the headers rather than assuming a fixed rate.\n\nThis applies to the `/api/` endpoints only. The static content — markdown twins, `llms.txt`, and the feeds — is served from the CDN, carries no rate-limit headers, and is not rate limited.",
    "license": {
      "name": "Content © Bianca W — all rights reserved",
      "url": "https://bianca.codes/terms/"
    },
    "contact": {
      "name": "Bianca W",
      "url": "https://bianca.codes/contact/"
    }
  },
  "servers": [
    {
      "url": "https://bianca.codes",
      "description": "Production — latest version"
    },
    {
      "url": "https://bianca.codes/v1",
      "description": "Production pinned to major version 1. Prefer this for anything long-lived: it will keep serving the v1 contract after a v2 exists."
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "discovery",
      "description": "Indexes that map topics and slugs to content URLs."
    },
    {
      "name": "content",
      "description": "The posts and pages themselves, as markdown."
    },
    {
      "name": "feeds",
      "description": "Syndication and crawl formats."
    },
    {
      "name": "booking",
      "description": "Read-only view of when a consultation can be booked."
    }
  ],
  "paths": {
    "/llms.txt": {
      "get": {
        "operationId": "getLlmsIndex",
        "tags": [
          "discovery"
        ],
        "summary": "Get the llms.txt site index",
        "description": "The llmstxt.org index: a one-line site summary, when-to-use guidance, the topic taxonomy, and every published post with its excerpt and markdown URL. Fetch this first — it is the cheapest way to map a user question to a specific post slug.",
        "responses": {
          "200": {
            "description": "The llms.txt index. Served as `text/plain`; the body itself is markdown-formatted per the llmstxt.org convention.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "An llmstxt.org index document."
                }
              }
            }
          },
          "404": {
            "description": "No such path. The body lists the sitemap, llms.txt, and blog index so an agent can recover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "tags": [
          "discovery"
        ],
        "summary": "Get this OpenAPI description",
        "description": "Returns this document — the machine-readable description of the content surface, so an agent can discover the available operations without out-of-band knowledge.",
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 document describing this content API.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "An OpenAPI 3.1 document."
                }
              }
            }
          },
          "404": {
            "description": "No such path. The body lists the sitemap, llms.txt, and blog index so an agent can recover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            }
          }
        }
      }
    },
    "/blog/{slug}.md": {
      "parameters": [
        {
          "name": "slug",
          "in": "path",
          "required": true,
          "description": "The post slug, as published in /llms.txt and /sitemap.xml. Lowercase, hyphen-separated, no file extension.",
          "schema": {
            "type": "string",
            "pattern": "^[a-z0-9]+(?:[-.][a-z0-9]+)*$"
          },
          "example": "build-an-audit-of-what-survived-the-copilot-cull"
        }
      ],
      "get": {
        "operationId": "getPostMarkdown",
        "tags": [
          "content"
        ],
        "summary": "Get a blog post as markdown",
        "description": "Returns the full text of one post as CommonMark, led by YAML frontmatter carrying `title`, `date`, optional `updated`, `tags`, and the `canonical` HTML URL. Check `date` before presenting a technique as current. The same document is served from the canonical page URL when the request carries `Accept: text/markdown`.",
        "responses": {
          "200": {
            "description": "The post as markdown, with YAML frontmatter.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "CommonMark document. Post documents lead with YAML frontmatter."
                }
              }
            }
          },
          "404": {
            "description": "No such path. The body lists the sitemap, llms.txt, and blog index so an agent can recover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            }
          }
        }
      }
    },
    "/blog/index.md": {
      "get": {
        "operationId": "getBlogIndexMarkdown",
        "tags": [
          "discovery"
        ],
        "summary": "Get the blog index as markdown",
        "description": "Every published post as a markdown list — title, excerpt, and markdown URL — newest first. Narrower than /llms.txt: posts only, no topic taxonomy or usage guidance.",
        "responses": {
          "200": {
            "description": "The blog index as markdown.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "CommonMark document. Post documents lead with YAML frontmatter."
                }
              }
            }
          },
          "404": {
            "description": "No such path. The body lists the sitemap, llms.txt, and blog index so an agent can recover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            }
          }
        }
      }
    },
    "/docs.md": {
      "get": {
        "operationId": "getDocsMarkdown",
        "tags": [
          "discovery"
        ],
        "summary": "Get the developer and agent documentation as markdown",
        "description": "The full guide to this content surface: both ways to fetch a post as markdown, the discovery endpoints, the JSON error envelope, and what this site is and is not a good source for. The same document is served from /docs/ when the request carries `Accept: text/markdown`.",
        "responses": {
          "200": {
            "description": "The developer documentation as markdown.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "CommonMark document. Post documents lead with YAML frontmatter."
                }
              }
            }
          },
          "404": {
            "description": "No such path. The body lists the sitemap, llms.txt, and blog index so an agent can recover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            }
          }
        }
      }
    },
    "/about.md": {
      "get": {
        "operationId": "getAboutMarkdown",
        "tags": [
          "content"
        ],
        "summary": "Get the About page as markdown",
        "description": "Bianca's background, areas of expertise, and the stack this site covers. Use this to decide whether a question is in scope before searching individual posts.",
        "responses": {
          "200": {
            "description": "The About page as markdown.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "CommonMark document. Post documents lead with YAML frontmatter."
                }
              }
            }
          },
          "404": {
            "description": "No such path. The body lists the sitemap, llms.txt, and blog index so an agent can recover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            }
          }
        }
      }
    },
    "/contact.md": {
      "get": {
        "operationId": "getContactMarkdown",
        "tags": [
          "content"
        ],
        "summary": "Get contact information as markdown",
        "description": "How to reach Bianca. There is no static contact form — /contact/ hosts an interactive chat assistant that answers questions about her background and can take a message.",
        "responses": {
          "200": {
            "description": "Contact information as markdown.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "CommonMark document. Post documents lead with YAML frontmatter."
                }
              }
            }
          },
          "404": {
            "description": "No such path. The body lists the sitemap, llms.txt, and blog index so an agent can recover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            }
          }
        }
      }
    },
    "/index.md": {
      "get": {
        "operationId": "getHomeMarkdown",
        "tags": [
          "content"
        ],
        "summary": "Get the home page as markdown",
        "description": "A short markdown summary of the site with links onward to the blog, About, and Contact. Prefer /llms.txt when the goal is to enumerate content.",
        "responses": {
          "200": {
            "description": "The home page summary as markdown.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "CommonMark document. Post documents lead with YAML frontmatter."
                }
              }
            }
          },
          "404": {
            "description": "No such path. The body lists the sitemap, llms.txt, and blog index so an agent can recover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            }
          }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "operationId": "getSitemap",
        "tags": [
          "feeds"
        ],
        "summary": "Get the XML sitemap",
        "description": "Sitemaps 0.9 XML listing every canonical URL — static pages, topic hubs, and posts. Only post entries carry a `lastmod` timestamp; static pages and topic hubs are listed with `changefreq` and `priority` only, so `lastmod` detects changes to posts but not to those pages.",
        "responses": {
          "200": {
            "description": "A Sitemaps 0.9 urlset.",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string",
                  "description": "Sitemaps 0.9 XML urlset."
                }
              }
            }
          },
          "404": {
            "description": "No such path. The body lists the sitemap, llms.txt, and blog index so an agent can recover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            }
          }
        }
      }
    },
    "/rss.xml": {
      "get": {
        "operationId": "getRssFeed",
        "tags": [
          "feeds"
        ],
        "summary": "Get the RSS 2.0 feed",
        "description": "RSS 2.0 feed of recent posts with titles, links, publication dates, and descriptions. Use it to poll for new content; use /sitemap.xml for the complete URL set.",
        "responses": {
          "200": {
            "description": "An RSS 2.0 feed document.",
            "content": {
              "application/rss+xml": {
                "schema": {
                  "type": "string",
                  "description": "RSS 2.0 XML."
                }
              }
            }
          },
          "404": {
            "description": "No such path. The body lists the sitemap, llms.txt, and blog index so an agent can recover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            }
          }
        }
      }
    },
    "/robots.txt": {
      "get": {
        "operationId": "getRobotsTxt",
        "tags": [
          "discovery"
        ],
        "summary": "Get the robots.txt crawl policy",
        "description": "The crawl policy for this site. All user agents are allowed, and the sitemap location is advertised here.",
        "responses": {
          "200": {
            "description": "The robots.txt policy.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "A robots.txt policy document."
                }
              }
            }
          },
          "404": {
            "description": "No such path. The body lists the sitemap, llms.txt, and blog index so an agent can recover.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            }
          }
        }
      }
    },
    "/api/availability": {
      "get": {
        "operationId": "getAvailability",
        "tags": [
          "booking"
        ],
        "summary": "Get bookable consultation windows",
        "description": "Free/busy windows for booking a consultation, as UTC instants. `consult` lists the windows open for booking and `busy` lists the times already taken; a candidate slot is bookable when it falls inside a `consult` range, outside every `busy` range, respects `bufferMinutes` either side, and starts at least `shortNoticeHours` from `now`. Returns 503 when booking is switched off, which is a normal state rather than a fault.",
        "responses": {
          "200": {
            "description": "The current availability window.",
            "headers": {
              "RateLimit": {
                "description": "Remaining quota and seconds until reset, per RFC 9331 — for example `\"public\";r=59;t=60`. Read `r` to decide whether to slow down.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "The quota policy in force, per RFC 9331 — for example `\"public\";q=60;w=60`, meaning 60 requests per 60-second window.",
                "schema": {
                  "type": "string"
                }
              },
              "X-API-Version": {
                "description": "The semver version of the API surface that served this response. Compare it with the version you integrated against to detect drift.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Availability"
                }
              }
            }
          },
          "405": {
            "description": "The request used a method other than GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            },
            "headers": {
              "RateLimit": {
                "description": "Remaining quota and seconds until reset, per RFC 9331 — for example `\"public\";r=59;t=60`. Read `r` to decide whether to slow down.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "The quota policy in force, per RFC 9331 — for example `\"public\";q=60;w=60`, meaning 60 requests per 60-second window.",
                "schema": {
                  "type": "string"
                }
              },
              "X-API-Version": {
                "description": "The semver version of the API surface that served this response. Compare it with the version you integrated against to detect drift.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Availability could not be computed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            },
            "headers": {
              "RateLimit": {
                "description": "Remaining quota and seconds until reset, per RFC 9331 — for example `\"public\";r=59;t=60`. Read `r` to decide whether to slow down.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "The quota policy in force, per RFC 9331 — for example `\"public\";q=60;w=60`, meaning 60 requests per 60-second window.",
                "schema": {
                  "type": "string"
                }
              },
              "X-API-Version": {
                "description": "The semver version of the API surface that served this response. Compare it with the version you integrated against to detect drift.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "502": {
            "description": "The upstream calendar service failed. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            },
            "headers": {
              "RateLimit": {
                "description": "Remaining quota and seconds until reset, per RFC 9331 — for example `\"public\";r=59;t=60`. Read `r` to decide whether to slow down.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "The quota policy in force, per RFC 9331 — for example `\"public\";q=60;w=60`, meaning 60 requests per 60-second window.",
                "schema": {
                  "type": "string"
                }
              },
              "X-API-Version": {
                "description": "The semver version of the API surface that served this response. Compare it with the version you integrated against to detect drift.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "Booking is switched off, or the calendar connection needs re-authorisation. Read `error` to tell the two apart.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            },
            "headers": {
              "RateLimit": {
                "description": "Remaining quota and seconds until reset, per RFC 9331 — for example `\"public\";r=59;t=60`. Read `r` to decide whether to slow down.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "The quota policy in force, per RFC 9331 — for example `\"public\";q=60;w=60`, meaning 60 requests per 60-second window.",
                "schema": {
                  "type": "string"
                }
              },
              "X-API-Version": {
                "description": "The semver version of the API surface that served this response. Compare it with the version you integrated against to detect drift.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/canned-questions": {
      "get": {
        "operationId": "getCannedQuestions",
        "tags": [
          "content"
        ],
        "summary": "Get the pre-answered questions about Bianca",
        "description": "The question-and-answer pairs backing the chat assistant on /contact/. Read these to answer common questions about background, experience, and availability without invoking the assistant itself.",
        "responses": {
          "200": {
            "description": "The full set of pre-answered questions.",
            "headers": {
              "RateLimit": {
                "description": "Remaining quota and seconds until reset, per RFC 9331 — for example `\"public\";r=59;t=60`. Read `r` to decide whether to slow down.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "The quota policy in force, per RFC 9331 — for example `\"public\";q=60;w=60`, meaning 60 requests per 60-second window.",
                "schema": {
                  "type": "string"
                }
              },
              "X-API-Version": {
                "description": "The semver version of the API surface that served this response. Compare it with the version you integrated against to detect drift.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CannedQuestions"
                }
              }
            }
          },
          "405": {
            "description": "The request used a method other than GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            },
            "headers": {
              "RateLimit": {
                "description": "Remaining quota and seconds until reset, per RFC 9331 — for example `\"public\";r=59;t=60`. Read `r` to decide whether to slow down.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "The quota policy in force, per RFC 9331 — for example `\"public\";q=60;w=60`, meaning 60 requests per 60-second window.",
                "schema": {
                  "type": "string"
                }
              },
              "X-API-Version": {
                "description": "The semver version of the API surface that served this response. Compare it with the version you integrated against to detect drift.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "The server is misconfigured and cannot read the content store.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            },
            "headers": {
              "RateLimit": {
                "description": "Remaining quota and seconds until reset, per RFC 9331 — for example `\"public\";r=59;t=60`. Read `r` to decide whether to slow down.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "The quota policy in force, per RFC 9331 — for example `\"public\";q=60;w=60`, meaning 60 requests per 60-second window.",
                "schema": {
                  "type": "string"
                }
              },
              "X-API-Version": {
                "description": "The semver version of the API surface that served this response. Compare it with the version you integrated against to detect drift.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "502": {
            "description": "The upstream content store failed. Retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Human- and agent-readable recovery guidance."
                }
              }
            },
            "headers": {
              "RateLimit": {
                "description": "Remaining quota and seconds until reset, per RFC 9331 — for example `\"public\";r=59;t=60`. Read `r` to decide whether to slow down.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "The quota policy in force, per RFC 9331 — for example `\"public\";q=60;w=60`, meaning 60 requests per 60-second window.",
                "schema": {
                  "type": "string"
                }
              },
              "X-API-Version": {
                "description": "The semver version of the API surface that served this response. Compare it with the version you integrated against to detect drift.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "TimeRange": {
        "type": "object",
        "title": "TimeRange",
        "description": "A half-open interval [start, end) expressed as UTC instants.",
        "required": [
          "start",
          "end"
        ],
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time",
            "description": "Inclusive start of the range, as an ISO 8601 UTC instant."
          },
          "end": {
            "type": "string",
            "format": "date-time",
            "description": "Exclusive end of the range, as an ISO 8601 UTC instant."
          }
        }
      },
      "Availability": {
        "type": "object",
        "title": "Availability",
        "description": "Everything needed to decide whether a proposed meeting time is bookable, without a second round trip.",
        "required": [
          "now",
          "horizonEnd",
          "shortNoticeHours",
          "bufferMinutes",
          "busy",
          "consult"
        ],
        "properties": {
          "now": {
            "type": "string",
            "format": "date-time",
            "description": "The server's current time. Compare candidate slots against this rather than against the local clock, which may be skewed or in another zone."
          },
          "horizonEnd": {
            "type": "string",
            "format": "date-time",
            "description": "The end of the bookable window. A slot after this is rejected even if it looks free, because availability is not known that far out."
          },
          "shortNoticeHours": {
            "type": "integer",
            "description": "Minimum hours between `now` and the start of a booking. Slots sooner than this are refused as short notice."
          },
          "bufferMinutes": {
            "type": "integer",
            "description": "Minutes of clearance required either side of a booking. A slot abutting a busy range by less than this is not bookable."
          },
          "busy": {
            "type": "array",
            "description": "Ranges already taken. A bookable slot overlaps none of these.",
            "items": {
              "$ref": "#/components/schemas/TimeRange"
            }
          },
          "consult": {
            "type": "array",
            "description": "Ranges open for consultations. A bookable slot falls entirely within one of these.",
            "items": {
              "$ref": "#/components/schemas/TimeRange"
            }
          }
        }
      },
      "CannedQuestion": {
        "type": "object",
        "title": "CannedQuestion",
        "description": "One pre-answered question about Bianca.",
        "required": [
          "slug",
          "question",
          "answer"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "Stable identifier for the question. Use it to deduplicate."
          },
          "question": {
            "type": "string",
            "description": "The question as a reader would ask it."
          },
          "answer": {
            "type": "string",
            "description": "The prepared answer, in prose. Safe to quote directly — it is written to be read aloud."
          }
        }
      },
      "CannedQuestions": {
        "type": "object",
        "title": "CannedQuestions",
        "description": "A successful envelope wrapping the full question set.",
        "required": [
          "ok",
          "result"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success. The error envelope sets this false, so branching on it distinguishes the two without inspecting the status code."
          },
          "result": {
            "type": "array",
            "description": "Every published question, in display order.",
            "items": {
              "$ref": "#/components/schemas/CannedQuestion"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "title": "Error",
        "description": "Structured error body. Returned as JSON when the client accepts JSON, and as markdown prose otherwise. `ok`, `error`, `message`, and `docs` are always present; `hint` appears when the failure has an actionable remedy. Some endpoints add further context fields alongside these — a 409 from the booking endpoint carries refreshed `availability`, for instance.",
        "required": [
          "ok",
          "error",
          "message",
          "docs"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": false,
            "description": "Always false on an error response. Branch on this to detect failure."
          },
          "error": {
            "type": "string",
            "description": "Stable, machine-readable error code in snake_case.",
            "examples": [
              "not_found",
              "method_not_allowed"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation of what went wrong. Show this to a person — never the `error` code."
          },
          "hint": {
            "type": "string",
            "description": "How to resolve the error, when a remedy exists. Absent when there is no useful action for the caller to take.",
            "examples": [
              "Resend the request with the POST method."
            ]
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "URL of documentation explaining how to resolve this error."
          }
        }
      }
    }
  }
}