Repository: BurntSushi/jiff (language: rust)

Issue:
Inconsistency: Are days 24 hours or not?
There seems to be some inconsistency in the treatment of days in spans. In particular, `TryFrom<Span> for Duration` assumes that days are 24 hours, whereas `Timestamp::saturating_add` rejects days as units that can't be resolved without a specific timezone.

Personaly I think the assumption that days are 24 hours is dangerous and shouldn't be made (because it is readily violated by DST), but more than that I think all functions should at least agree on whether days are assumed to always be 24 hours or not.
provide a way to enforce the use of the bundled tzdb
I was talking with someone on the Polars team, and they mentioned that they would actually prefer to use a bundled copy of tzdb even if a system zoneinfo was available. This is to ensure that all time zone transition calculations produce the same value across deployments, regardless of the system copy of tzdb.

Jiff can kinda do this today in two ways, but both have costs:

1. You can enable `tzdb-bundle-always` and disable `tzdb-zoneinfo`. But because of feature unification, if another dependency enables `tzdb-zoneinfo`, you'll wind up using the system copy of the zoneinfo database without meaning to.
2. You can create a bundled `TimeZoneDatabase` (the API for this is new and will be part of `jiff 0.2`) and then use it explicitly. This means not using `Timestamp::in_tz` or any other convenient parsing functions for `Zoned` since they all rely the implicit global tzdb. There isn't anything in Jiff that _requires_ the implicit global tzdb, but it makes a lot of APIs much more convenient.

I think (2) is a defensible choice, but I also think it's important to be able to make changes to the global tzdb and its configuration.

I think the most flexible thing to do is to provide a way to _set_ the global tzdb. However, this raises the possibility of some other dependency setting the global tzdb as well. It's kinda like feature unification, but I'd consider it a major faux pas for a _library_ using Jiff to be setting the global tzdb. It should be an application-only kind of thing.

That does kind of make me wonder about the wisdom of being able to set a global tzdb. Today, nobody can set it other than Jiff. That gives one some assurances. So maybe the right solution here is to indeed suggest path (2) above. And I think that while this is more cumbersome than the convenience APIs in Jiff, I think it should be about on par with what you can do with `chrono-tz`.


Discussion / hints:
It is true that sometimes days are treated as _always_ 24 hours and sometimes days are treated as more or less than 24 hours, depending on time zone transitions. In contexts where the meaning of a day is ambiguous, it's banned, as in the case of `Timestamp::checked_add` (and `Timestamp::saturating_add`). In cases where it could be either, like in the `Span` APIs, whether a day is 24 hours or not is determined by whether 1) a relative date is provided and 2) whether that relative date is a `Zoned` or not.

The shape of when days are allowed or not, and when they are 24 hours or otherwise, was taken straight from Temporal. For example, see its [`Instant.add`](https://tc39.es/proposal-temporal/docs/instant.html#add) API.

I think it would be better to be very concrete here. If you're working with a `Zoned`, for example, it is impossible to accidentally treat "1 day" as "always 24 hours." The APIs won't get it wrong precisely because a `Zoned` is involved. But when you're working with civil time, you _want_ to be able to operate on days, yet civil time is unaware of time zones. So days are always 24 hours.

While I agree that this can all seem very inconsistent and arbitrary upon initial inspection, the thing I really care about is whether you're committing bugs in your code because of it. Do you have any code you've written where you got bit by days being 24 hours when you didn't want them to be?

> Personaly I think the assumption that days are 24 hours is dangerous and shouldn't be made (because it is readily violated by DST)

You really can't blanket reject the assumption. Because sometimes you want it. And if you sometimes you want it, you really can't have all functions agree on whether days are uniform or not. Like, if all functions agreed that days were non-uniform, then what would adding 1 day to `civil::DateTime` do? It doesn't have a time zone, so it can't choose anything other than "1 day is 24 hours." So in that context, it is definitionally uniform. There is no other possible interpretation that I can see. And once you have that, it flows through to other APIs, like `Span`.
I guess I could have been clearer. I specifically have a problem with `TryFrom<Span> for Duration` and `Span::total` making that assumption. I think the behavior of `civil::DateTime` is fine, because if you use it you more-or-less explicitly choose to ignore DST and other timezone oddities. However, the conversion to `Duration` and getting the total duration of a `Span` in absolute units are general APIs and I think making the assumption that days are 24 hours there lends itself to misuse when combined with zoned datetimes.
> I specifically have a problem with `TryFrom<Span>` for Duration making that assumption.

Ah I see. That follows from the behavior of `Span::round`, `Span::total`, `Span::compare`, `Span::checked_add` and `Span::checked_sub`. And that's all consistent with the [corresponding `Temporal.Duration` APIs](https://tc39.es/proposal-temporal/docs/duration.html) as well. Namely, days are always treated as 24 hours unless a relative time zone aware datetime is given.

An alternative design is that these APIs return an error when non-zero day units are given _and_ no relative date is given. But if a relative date is given, then whether it is civil or time zone aware would determine the length of the day.

I think [this Temporal issue](https://github.com/tc39/proposal-temporal/issues/2768#issuecomment-1936819469) sketches out the shape of why Temporal went with this design decision. There is even more background [here](https://github.com/tc39/proposal-temporal/issues/686) and [here](https://github.com/tc39/proposal-temporal/issues/759), but it gets very dense very quickly.

I am not opposed to diverging from Temporal. I specifically did not set a goal that Jiff must match Temporal. Jiff is merely heavily inspired by Temporal. With that said, I've generally deferred to Temporal on these kinds of decisions, because they quite literally reflect the result of multiple experts investing person years of effort into the design. It doesn't mean they're always right, but there's got to be something compelling IMO to push me towards reversing one of their decisions.

> and I think making the assumption that days are 24 hours there lends itself to misuse when combined with zoned datetimes.

Can you give a concrete example?
Currently not near my computer, but will give an example when I get back (probably tomorrow).
The first problematic example that I stumbled upon is this (I actually didn't realize this code was incorrect until you mentioned that Span::round assumes 24 hour days):

I wanted to enforce that some input spans should only contain absolute units and I figured that a good way to do this would be to normalize the span to contain seconds as the largest unit. My assumption was that any ambiguity about the absolute duration of the span would produce an error:
```rust
let normalized_span = input_span.round(SpanRound::new().largest(Unit::Second)).unwrap();
```
This works fine in most cases, but will unexpectedly change the effective duration of the span when the span contains days and DST is involved, whereas I expected it to produce an error:
```rust
let input_span = 2.days();
let normalized_span = input_span.round(SpanRound::new().largest(Unit::Second)).unwrap();

let datetime = date(2024, 11, 3).at(0, 0, 0, 0).intz("America/New_York").unwrap();
println!("{}", &datetime + input_span);     // 2024-11-05T00:00:00-05:00[America/New_York]
println!("{}", &datetime + normalized_span) // 2024-11-04T23:00:00-05:00[America/New_York]
```
But where did you get the input Span from? Getting a Span from two zoned datetimes will already return a Span with units no bigger than 1 hour. You'll only get bigger units if you explicitly ask for them.
The span comes from external data (specifically some data dumps that store durations with the ISO 8601 format).
The other problem with your suggestion is that, as of today, all of the Span APIs can be used correctly, without a relative date, using any Span returned by subtracting two datetimes in the default configuration. Your suggestion would make that not the case for spans from the difference between two DateTimes or two Dates.
FWIW, I do find your suggestion compelling.
> The other problem with your suggestion is that, as of today, all of the Span APIs can be used correctly, without a relative date, using any Span returned by subtracting two datetimes in the default configuration. Your suggestion would make that not the case for spans from the difference between two DateTimes or two Dates.

Based on my (admittedly relatively surface level) understanding, I would propose this:
The difference between two DateTimes could use hours as the largest unit, which would match Zoned.
The difference between two Dates could be made to work by adding a special case that allows adding or subtracting two spans as long as they both contain only one unit and it is the same unit.

This would almost certainly be an ergonomic hit in some cases that I haven't thought of, but my biased opinion is that the extra strictness and correctness is worth it.
PS: There's probably a good chance that this proposal breaks some part of the API that I haven't though of yet.

Also the proposed special case for spans is problematic in that it breaks the invariant that you can always add absolute units to a span. That tradeoff may or may not be worth it.
> Also the proposed special case for spans is problematic in that it breaks the invariant that you can always add absolute units to a span.

I believe this is not true today. If you're adding two spans and _either_ of those spans have non-uniform units bigger than days, then you need a relative date. Because the lower units could add up and overflow into bigger units. I think the key thing _today_ is that, under the default configuration for computing spans between any two datetimes, yes, you can always add absolute units to the span returned. But the change to avoiding implicitly assuming days are always 24 hours would indeed break that for `DateTime` and `Date`. The change to hours for `DateTime` would restore it there, but yes, I don't see a way to fix it for `Date`, which is unfortunate.
Honestly, I don't know. Like you said, the discussion gets very dense very quickly and the tradeoffs are hard to evaluate. I would keep the issue open for now, in case someone else wants to weigh in with more practical experience or proposals.
@FeldrinH Can you say more about your higher level use case here? Like, what is the higher level problem you're trying to solve? This is what I understand so far:

* You have some ISO 8601 durations in serialized form somewhere.
* You want to parse them and convert them into durations with units no bigger than seconds. Or more generally, a duration with uniform units.
* You want an error to appear if there is any ambiguity about what the units might mean.
* You demonstrate the problem by comparing the duration before and after rounding with a time zone aware datetime.

I don't mean to ask for these details as a way to dismiss your concern, but I do think it's important to connect the threads a bit more here. In particular, I am wondering about the connection here between the time zone aware datetime after the fact instead of providing it to the rounding routine. Or perhaps that is part of the problem. I could see, for example, not realizing that you _need_ to provide a relative datetime to `round`, trying it and then relying on it to produce an error if you're doing something wrong. Since it's silent about days, you assume it's okay and go on to do something with those durations and time zone aware datetimes. But had `round` given you an error, I think your remedy would have been to use a zoned relative datetime with `round` and thereby get correct spans. Is that right?

If so, yeah, I can see how the failure mode is effectively a way of pushing you towards using the APIs correctly and preventing misuse.
Basically I am parsing and processing some data dumps from some event logs. The documentation on the data format is limited, so I want to validate everything as strictly as possible. As part of that I want to enforce that there must be no ambiguity about the duration of spans. If there ever was ambiguity (e.g. if the event logs contained a span with days) then I would have to figure out what that actually means. The zoned datetimes only come into play because I later summarize the data in my local timezone (which has DST).

> But had round given you an error, I think your remedy would have been to use a zoned relative datetime with round and thereby get correct spans. Is that right?

Not quite. If there is ambiguity about the duration of the spans then something is wrong with the input data and I will need to dig deeper into what that means. I am assuming that the durations will never contain ambiguous units like days or months, but I don't know for sure, so my goal with rounding without providing a zoned datetime was to enforce that if my assumption turns out to be wrong I will get an error.
> I could see, for example, not realizing that you need to provide a relative datetime to round, trying it and then relying on it to produce an error if you're doing something wrong. Since it's silent about days, you assume it's okay and go on to do something with those durations and time zone aware datetimes.

That is definitely an issue that I could see happening, though my specific situation was a little different.
One suggestion I got from the Temporal folks is to provide a "marker" that makes it okay to assume days are always 24 hours. So, for example, there would be a new `SpanDaysAre24Hours` marker type (or similarly named) that would have trait impls like `impl From<SpanDaysAre24Hours> for SpanRelativeTo`. But if that marker isn't given, or a relative date isn't given, then using units of days would result in an error. So in your case, your `round` call would have errored.

The advantage of a marker is that it absolves users of Jiff from providing a "dummy" relative date in the case that all days are 24 hours.

I'm still going to noodle on this. And I think this is probably something that is a breaking change as well. In particular, this will result in an ergonomic hit when using some of the `Span` APIs. With that said, I think this is _somewhat_ mitigate by the fact that all of datetime APIs that produce spans, like `Zoned::until`, also expose the rounding interface. And they set the relative datetime for you.
I really appreciate the API design of Jiff making the correct things easy to do.

On the other hand, I feel like implementing this proposal as the default may violate the principal of least surprise and make me hesitant to just "throw stuff" at Jiff and expect it to work.

While I appreciate Jiff's strong error-driven api, I actually don't want it to be the case that I'm getting random errors due to "valid" data. This isn't in opposition to an error driven api, but rather in favor of *somewhat* flexible input behavior by default.

As mentioned, this gets very dense very fast, and I'd be concerned for general usability if this is the default and somewhat realistic to hit.

On the other hand, I like the idea of "strict modes" where you can layer additional assertions on input data. This wouldn't require that the internal modeling or types change - instead you can harden the consistency of the data at the edge when you receive it, maybe with some parse_unambiguous method or something similar.
@johnpyp Sorry, I'm having a hard time following your comment. Could you say more concretely what you'd like? Are you saying the status quo is desirable? Because there are plenty of error cases in the status quo too. This issue is just talking about one of them for a few APIs on `Span`.

Also, does something like [this](https://github.com/BurntSushi/jiff/issues/21#issuecomment-2253722166) help? Which basically lets you opt out of `Span` and lets you use something more like `std::time::Duration`.


Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8492a479..df72e94b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,111 @@
 # CHANGELOG
 
+0.2.0 (TBD)
+===========
+
+**BREAKING CHANGES**:
+
+* [#28](https://github.com/BurntSushi/jiff/issues/28):
+The deprecated `intz` routines on `Zoned`, `Timestamp`, `civil::DateTime` and
+`civil::Date` have been removed. You can use `in_tz` instead. This change was
+made because many found the name `intz` to be unclear.
+* [#32](https://github.com/BurntSushi/jiff/issues/32):
+The `PartialEq` and `Eq` trait implementations on `Span` have been removed.
+Ideally these shouldn't have been used, but if you do need them, please use
+`Span::fieldwise` to create a `SpanFieldwise`, which does have the `PartialEq`
+and `Eq` traits implemented. These were removed on `Span` itself because they
+made it very easy to commit subtle bugs.
+* [#36](https://github.com/BurntSushi/jiff/issues/36):
+Turn panics during `Timestamp::saturing_add` into errors. Callers adding
+spans that are known to contain units of hours or smaller are guaranteed that
+this will not panic.
+* [#48](https://github.com/BurntSushi/jiff/issues/48):
+On `Span` APIs, days are no longer silently assumed to always be 24 hours when
+a relative datetime is not provided. Instead, to perform operations on units
+of days or bigger, callers must either provide a relative date or opt into
+invariant 24-hour days with `SpanRelativeTo::days_are_24_hours`. Shortcuts have
+been added to the span builders. For example, `SpanTotal::days_are_24_hours`.
+* [#147](https://github.com/BurntSushi/jiff/issues/147):
+Change the behavior of the deprecated `%V` conversion specifier in
+`jiff::fmt::strtime` from formatting an IANA time zone identifier to formatting
+an ISO 8601 week number. To format an IANA time zone identifier, use `%Q` or
+`%:Q` (which were introduced in `jiff 0.1`).
+* [#212](https://github.com/BurntSushi/jiff/issues/212):
+When parsing into a `Zoned` with a civil time corresponding to a gap, we treat
+all offsets as invalid and return an error. Previously, we would accept the
+offset as given. This brings us into line with Temporal's behavior. For
+example, previously Jiff accepted `2006-04-02T02:30-05[America/Indiana/Vevay]`
+but will now return an error. This is desirable for cases where a datetime in
+the future is serialized before a change in the daylight saving time rules.
+For more examples, see `jiff::fmt::temporal::DateTimeParser::offset_conflict`
+for details on how to change Jiff's default behavior. This behavior change also
+applies to `tz::OffsetConflict::PreferOffset`.
+* [#213](https://github.com/BurntSushi/jiff/issues/213):
+Tweak the semantics of `tz::TimeZoneDatabase` so that it only initializes one
+internal tzdb instead of trying to find as many as possible. It is unlikely
+that you'll be impacted by this change, but it's meant to make the semantics
+be a bit more sensible. (In `jiff 0.1`, it was in theory possible for one tz
+lookup to succeed in the system zoneinfo and then another tz lookup to fail
+in zoneinfo but succeed automatically via the bundled copy. But this seems
+confusing and possibly not desirable. Hence the change.)
+* [#218](https://github.com/BurntSushi/jiff/issues/218):
+In order to make naming a little more consistent between `Zoned`
+and `civil::Date`, the `civil::Date::to_iso_week_date` and
+`civil::ISOWeekDate::to_date` APIs were renamed to `civil::Date::iso_week_date`
+and `civil::ISOWeekDate::date`.
+* [#220](https://github.com/BurntSushi/jiff/issues/220):
+Remove `Span::to_duration` for converting a `Span` to a `std::time::Duration`
+and rename `Span::to_jiff_duration` to `Span::to_duration`. This prioritizes
+`SignedDuration` as the "primary" non-calendar duration type in Jiff. And makes
+it more consistent with APIs like `Zoned::duration_since`. For non-calendar
+spans, the `TryFrom<Span> for std::time::Duration` still exists. For calendar
+durations, use `Span::to_duration` and then convert the `SignedDuration` to
+`std::time::Duration`. Additionally, `Timestamp::from_jiff_duration` and
+`Timestamp::as_jiff_duration` were renamed to `Timestamp::from_duration` and
+`Timestamp::as_duration`, respectively. The old deprecated routines on the
+unsigned `std::time::Duration` have been removed.
+* [#221](https://github.com/BurntSushi/jiff/issues/221):
+Change the type of the value yielded by the `jiff::tz::TimeZoneNameIter`
+iterator from `String` to `jiff::tz::TimeZoneName`. This opaque type is more
+API evolution friendly. To access the string, either use `TimeZoneName`'s
+`Display` trait implementation, or its `as_str` method.
+* [#222](https://github.com/BurntSushi/jiff/issues/222):
+Split `TimeZone::to_offset` into two methods. One that just returns the
+offset, and another, `TimeZone::to_offset_info`, which includes the offset,
+DST status and time zone abbreviation. The extra info is rarely needed and
+is sometimes more costly to compute. Also, make the lifetime of the time
+zone abbreviation returned by `TimeZoneTransition::abbreviation` tied to
+the transition instead of the time zone (for future API flexibility, likely
+in core-only environments). This change was overall motivated by wanting to
+do less work in the common case (where we only need the offset), and for
+reducing the size of a `TimeZone` considerably in core-only environments.
+Callers previously using `TimeZone::to_offset` to get DST status and time zone
+abbreviation should now use `TimeZone::to_offset_info`.
+* [#230](https://github.com/BurntSushi/jiff/issues/230):
+When `TimeZone::system()` cannot find a system configured time zone, `jiff
+0.1` would automatically fall back to `TimeZone::UTC` (with a WARN-level log
+message). In `jiff 0.2`, the fall back is now to `TimeZone::unknown()`, which
+has a special `Etc/Unknown` identifier (as specified by Unicode and reserved by
+the IANA time zone database). The fallback otherwise still behaves as if it
+were `TimeZone::UTC`. This helps surface error conditions related to finding
+the system time zone without causing unrecoverable failure.
+
+Enhancements:
+
+* [#136](https://github.com/BurntSushi/jiff/issues/136):
+When the special `SpanRelativeTo::days_are_24_hours()` marker is used, weeks
+will also be treated as invariant. That is, 7 24-hour days. In all cases,
+working with years and months still requires a relative date.
+* [#228](https://github.com/BurntSushi/jiff/issues/228):
+It is now possible to forcefully use a bundled copy of the IANA time zone
+database without relying on disabling crate features. This can be done by
+enabling the `tzdb-bundle-always` crate feature and explicitly creating a
+`jiff::tz::TimeZoneDatabase::bundled()` database. Once in hand, you must use
+APIs like `TimeZoneDatabase::get` to create a `TimeZone` and avoid any APIs
+that implicitly use the global time zone database (like `Timestamp::in_tz` or
+even `Zoned::from_str`).
+
+
 0.1.29 (2025-02-02)
 ===================
 This release includes a few small enhancements and a bug fix. In particular,
diff --git a/COMPARE.md b/COMPARE.md
index ebc50a54..daf9876a 100644
--- a/COMPARE.md
+++ b/COMPARE.md
@@ -603,15 +603,18 @@ them into one, and Chrono does not provide operations on both at the same time.
 ### Jiff supports zone-aware re-balancing of durations
 
 If you have a span of `1.day()` and want to convert it to hours, then that
-calculation depends on how long the day is. If you don't provide any reference
-datetime, then Jiff assumes the day is always 24 hours long:
+calculation depends on how long the day is. If you provide a civil date as
+a relative reference point, then Jiff assumes the day is always 24 hours long:
 
 ```rust
-use jiff::{SpanRound, ToSpan, Unit};
+use jiff::{civil, SpanRound, ToSpan, Unit};
 
 fn main() -> anyhow::Result<()> {
+    let relative = civil::date(2024, 4, 1);
     let span1 = 1.day();
-    let span2 = span1.round(SpanRound::new().largest(Unit::Hour))?;
+    let span2 = span1.round(
+        SpanRound::new().largest(Unit::Hour).relative(relative),
+    )?;
     assert_eq!(span2, 24.hours().fieldwise());
 
     Ok(())
@@ -1093,7 +1096,7 @@ use jiff::{Timestamp, ToSpan};
 fn main() -> anyhow::Result<()> {
     let ts = Timestamp::MAX;
     assert!(ts.checked_add(1.day()).is_err());
-    assert_eq!(ts.saturating_add(1.hour()), ts);
+    assert_eq!(ts.saturating_add(1.hour())?, ts);
 
     Ok(())
 }
diff --git a/bench/src/bench.rs b/bench/src/bench.rs
index 655f8bad..6b60a0f5 100644
--- a/bench/src/bench.rs
+++ b/bench/src/bench.rs
@@ -673,7 +673,7 @@ fn parse_friendly(c: &mut Criterion) {
         c.bench_function(&format!("jiff-span/{NAME}/{kind}"), |b| {
             b.iter(|| {
                 let got: jiff::Span = input.parse().unwrap();
-                assert_eq!(got, expected);
+                assert_eq!(got.fieldwise(), expected.fieldwise());
             })
         });
     }
diff --git a/src/civil/date.rs b/src/civil/date.rs
index 7ec96947..19f3cbe1 100644
--- a/src/civil/date.rs
+++ b/src/civil/date.rs
@@ -307,7 +307,7 @@ impl Date {
     /// precisely to the minimum and maximum values of a `Date`. Therefore,
     /// converting between them is lossless and infallible.
     ///
-    /// This routine is equivalent to [`ISOWeekDate::to_date`]. It is also
+    /// This routine is equivalent to [`ISOWeekDate::date`]. It is also
     /// available via a `From<ISOWeekDate>` trait implementation for `Date`.
     ///
     /// [ISO 8601 week date]: https://en.wikipedia.org/wiki/ISO_week_date
@@ -1808,7 +1808,8 @@ impl Date {
     /// units like hours, this can only be done for uniform units. (Uniform
     /// units are units for which each individual unit always corresponds to
     /// the same elapsed time regardless of the datetime it is relative to.)
-    /// This can't be done for units like years or months.
+    /// This can't be done for units like years, months or days without a
+    /// relative date.
     ///
     /// ```
     /// use jiff::{civil::date, SignedDuration, Span, SpanRound, ToSpan, Unit};
@@ -2055,30 +2056,6 @@ impl Date {
     }
 }
 
-/// Deprecated APIs.
-impl Date {
-    /// A deprecated equivalent to [`Date::iso_week_date`].
-    ///
-    /// This method will be removed in `jiff 0.2`. This was done to make naming
-    /// more consistent throughout the crate.
-    #[deprecated(since = "0.1.23", note = "use Date::iso_week_date instead")]
-    #[inline]
-    pub fn to_iso_week_date(self) -> ISOWeekDate {
-        self.iso_week_date()
-    }
-
-    /// A deprecated equivalent to [`Date::in_tz`].
-    ///
-    /// This will be removed in `jiff 0.2`. The method was renamed to make
-    /// it clearer that the name stood for "in time zone."
-    #[deprecated(since = "0.1.25", note = "use Date::in_tz instead")]
-    #[inline]
-    pub fn intz(self, time_zone_name: &str) -> Result<Zoned, Error> {
-        let tz = crate::tz::db().get(time_zone_name)?;
-        self.to_zoned(tz)
-    }
-}
-
 // Constants used for converting between Gregorian calendar dates and Unix
 // epoch days.
 //
diff --git a/src/civil/datetime.rs b/src/civil/datetime.rs
index 1bcc6e93..8ac23a97 100644
--- a/src/civil/datetime.rs
+++ b/src/civil/datetime.rs
@@ -2371,20 +2371,6 @@ impl DateTime {
     }
 }
 
-/// Deprecated APIs.
-impl DateTime {
-    /// A deprecated equivalent to [`DateTime::in_tz`].
-    ///
-    /// This will be removed in `jiff 0.2`. The method was renamed to make
-    /// it clearer that the name stood for "in time zone."
-    #[deprecated(since = "0.1.25", note = "use DateTime::in_tz instead")]
-    #[inline]
-    pub fn intz(self, time_zone_name: &str) -> Result<Zoned, Error> {
-        let tz = crate::tz::db().get(time_zone_name)?;
-        self.to_zoned(tz)
-    }
-}
-
 impl Default for DateTime {
     #[inline]
     fn default() -> DateTime {
diff --git a/src/civil/iso_week_date.rs b/src/civil/iso_week_date.rs
index e20efb00..34000c13 100644
--- a/src/civil/iso_week_date.rs
+++ b/src/civil/iso_week_date.rs
@@ -50,11 +50,11 @@ use crate::{
 ///
 /// let d = date(2019, 12, 30);
 /// let weekdate = ISOWeekDate::new(2020, 1, Weekday::Monday).unwrap();
-/// assert_eq!(d.to_iso_week_date(), weekdate);
+/// assert_eq!(d.iso_week_date(), weekdate);
 ///
 /// let d = date(2024, 3, 9);
 /// let weekdate = ISOWeekDate::new(2024, 10, Weekday::Saturday).unwrap();
-/// assert_eq!(d.to_iso_week_date(), weekdate);
+/// assert_eq!(d.iso_week_date(), weekdate);
 /// ```
 ///
 /// # Example: overlapping leap and long years
@@ -69,7 +69,7 @@ use crate::{
 /// let mut overlapping = vec![];
 /// for year in 1900..=1999 {
 ///     let date = date(year, 1, 1);
-///     if date.in_leap_year() && date.to_iso_week_date().in_long_year() {
+///     if date.in_leap_year() && date.iso_week_date().in_long_year() {
 ///         overlapping.push(year);
 ///     }
 /// }
@@ -206,8 +206,8 @@ impl ISOWeekDate {
     /// set based on the minimum and maximum values of a `Date`. Therefore,
     /// converting to and from `Date` values is non-lossy and infallible.
     ///
-    /// This routine is equivalent to [`Date::to_iso_week_date`]. This
-    /// routine is also available via a `From<Date>` trait implementation for
+    /// This routine is equivalent to [`Date::iso_week_date`]. This routine
+    /// is also available via a `From<Date>` trait implementation for
     /// `ISOWeekDate`.
     ///
     /// # Example
@@ -239,7 +239,7 @@ impl ISOWeekDate {
     /// ```
     /// use jiff::civil::date;
     ///
-    /// let weekdate = date(2019, 12, 30).to_iso_week_date();
+    /// let weekdate = date(2019, 12, 30).iso_week_date();
     /// assert_eq!(weekdate.year(), 2020);
     /// ```
     #[inline]
@@ -259,11 +259,11 @@ impl ISOWeekDate {
     /// ```
     /// use jiff::civil::date;
     ///
-    /// let weekdate = date(2019, 12, 30).to_iso_week_date();
+    /// let weekdate = date(2019, 12, 30).iso_week_date();
     /// assert_eq!(weekdate.year(), 2020);
     /// assert_eq!(weekdate.week(), 1);
     ///
-    /// let weekdate = date(1948, 12, 31).to_iso_week_date();
+    /// let weekdate = date(1948, 12, 31).iso_week_date();
     /// assert_eq!(weekdate.year(), 1948);
     /// assert_eq!(weekdate.week(), 53);
     /// ```
@@ -285,7 +285,7 @@ impl ISOWeekDate {
     /// ```
     /// use jiff::civil::{date, Weekday};
     ///
-    /// let weekdate = date(1948, 12, 31).to_iso_week_date();
+    /// let weekdate = date(1948, 12, 31).iso_week_date();
     /// assert_eq!(weekdate.year(), 1948);
     /// assert_eq!(weekdate.week(), 53);
     /// assert_eq!(weekdate.weekday(), Weekday::Friday);
@@ -638,19 +638,6 @@ impl ISOWeekDate {
     }
 }
 
-/// Deprecated APIs.
-impl ISOWeekDate {
-    /// A deprecated equivalent to [`ISOWeekDate::date`].
-    ///
-    /// This method will be removed in `jiff 0.2`. This was done to make naming
-    /// more consistent throughout the crate.
-    #[deprecated(since = "0.1.26", note = "use ISOWeekDate::date instead")]
-    #[inline]
-    pub fn to_date(self) -> Date {
-        Date::from_iso_week_date(self)
-    }
-}
-
 impl ISOWeekDate {
     /// Creates a new ISO week date from ranged values.
     ///
diff --git a/src/fmt/friendly/mod.rs b/src/fmt/friendly/mod.rs
index e7b082ae..f15256fa 100644
--- a/src/fmt/friendly/mod.rs
+++ b/src/fmt/friendly/mod.rs
@@ -427,7 +427,7 @@ use jiff::{civil, Span};
 
 let span: Span = "1 month".parse()?;
 // converts to signed duration
-let sdur = span.to_jiff_duration(civil::date(2024, 5, 1))?;
+let sdur = span.to_duration(civil::date(2024, 5, 1))?;
 // converts to standard library unsigned duration
 let dur = Duration::try_from(sdur)?;
 // exactly 31 days where each day is 24 hours long.
@@ -435,7 +435,7 @@ assert_eq!(dur, Duration::from_secs(31 * 24 * 60 * 60));
 
 // Now change the reference date and notice that the
 // resulting duration is changed but still correct.
-let sdur = span.to_jiff_duration(civil::date(2024, 6, 1))?;
+let sdur = span.to_duration(civil::date(2024, 6, 1))?;
 let dur = Duration::try_from(sdur)?;
 // exactly 30 days where each day is 24 hours long.
 assert_eq!(dur, Duration::from_secs(30 * 24 * 60 * 60));
diff --git a/src/fmt/friendly/printer.rs b/src/fmt/friendly/printer.rs
index 330ddfc1..5b25fc66 100644
--- a/src/fmt/friendly/printer.rs
+++ b/src/fmt/friendly/printer.rs
@@ -1187,7 +1187,7 @@ impl SpanPrinter {
         self.print_span_designators_non_fraction(&non_fractional, wtr)?;
         wtr.write_fractional_duration(
             unit,
-            &fractional.to_jiff_duration_invariant(),
+            &fractional.to_duration_invariant(),
         )?;
         Ok(())
     }
@@ -1615,7 +1615,7 @@ impl FractionalPrinter {
         fmtfraction: FractionalFormatter,
     ) -> FractionalPrinter {
         debug_assert!(span.largest_unit() <= Unit::from(unit));
-        let dur = span.to_jiff_duration_invariant();
+        let dur = span.to_duration_invariant();
         FractionalPrinter::from_duration(&dur, unit, fmtint, fmtfraction)
     }
 
diff --git a/src/fmt/rfc9557.rs b/src/fmt/rfc9557.rs
index 09afad66..b7793d2e 100644
--- a/src/fmt/rfc9557.rs
+++ b/src/fmt/rfc9557.rs
@@ -1021,6 +1021,12 @@ mod tests {
     #[cfg(feature = "std")]
     #[test]
     fn err_time_zone_db_lookup() {
+        // The error message snapshotted below can vary based on tzdb
+        // config, so only run this when we know we've got a real tzdb.
+        if crate::tz::db().is_definitively_empty() {
+            return;
+        }
+
         let p = |input| {
             Parser::new()
                 .parse(input)
diff --git a/src/fmt/serde.rs b/src/fmt/serde.rs
index b3064f04..9a34f114 100644
--- a/src/fmt/serde.rs
+++ b/src/fmt/serde.rs
@@ -1119,6 +1119,55 @@ pub mod timestamp {
 ///
 /// # Ok::<(), Box<dyn std::error::Error>>(())
 /// ```
+///
+/// # Example: serializing an unknown `TimeZone` works
+///
+/// For example, when a time zone was created from
+/// [`TimeZone::system`](crate::tz::TimeZone::system) and a system configured
+/// time zone could not be found. One can artifically create this situation
+/// with [`TimeZone::unknown`](crate::tz::TimeZone::unknown):
+///
+/// ```
+/// use jiff::tz::TimeZone;
+///
+/// #[derive(Debug, serde::Deserialize, serde::Serialize)]
+/// struct Record {
+///     #[serde(with = "jiff::fmt::serde::tz::required")]
+///     tz: TimeZone,
+/// }
+///
+/// let record = Record { tz: TimeZone::unknown() };
+/// assert_eq!(
+///     serde_json::to_string(&record)?,
+///     r#"{"tz":"Etc/Unknown"}"#,
+/// );
+///
+/// # Ok::<(), Box<dyn std::error::Error>>(())
+/// ```
+///
+/// And it deserializes as well:
+///
+/// ```
+/// use jiff::tz::TimeZone;
+///
+/// #[derive(Debug, serde::Deserialize, serde::Serialize)]
+/// struct Record {
+///     #[serde(with = "jiff::fmt::serde::tz::required")]
+///     tz: TimeZone,
+/// }
+///
+/// let json = r#"{"tz":"Etc/Unknown"}"#;
+/// let got: Record = serde_json::from_str(&json)?;
+/// assert!(got.tz.is_unknown());
+///
+/// # Ok::<(), Box<dyn std::error::Error>>(())
+/// ```
+///
+/// An unknown time zone is "allowed" to percolate through Jiff because it's
+/// usually not desirable to return an error and completely fail if a system
+/// time zone could not be detected. On the other hand, by using a special
+/// `Etc/Unknown` identifier for this case, it still surfaces the fact that
+/// something has gone wrong.
 pub mod tz {
     use serde::de;
 
@@ -1229,7 +1278,7 @@ pub mod tz {
                      fixed offsets or a POSIX time zone can't be serialized \
                      (this typically occurs when this is a system time zone \
                       derived from `/etc/localtime` on Unix systems that \
-                      isn't symlinked to an entry in `/usr/share/zoneinfo`)",
+                      isn't symlinked to an entry in `/usr/share/zoneinfo)",
                 ));
             }
             se.collect_str(&super::TemporalTimeZone(tz))
diff --git a/src/fmt/strtime/format.rs b/src/fmt/strtime/format.rs
index cc22ef09..bec324ee 100644
--- a/src/fmt/strtime/format.rs
+++ b/src/fmt/strtime/format.rs
@@ -65,7 +65,7 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
                 b'n' => self.fmt_literal("\n").context("%n failed")?,
                 b'P' => self.fmt_ampm_lower(ext).context("%P failed")?,
                 b'p' => self.fmt_ampm_upper(ext).context("%p failed")?,
-                b'Q' => self.fmt_iana_nocolon(b'Q').context("%Q failed")?,
+                b'Q' => self.fmt_iana_nocolon().context("%Q failed")?,
                 b'R' => self.fmt_clock_nosecs(ext).context("%R failed")?,
                 b'S' => self.fmt_second(ext).context("%S failed")?,
                 b's' => self.fmt_timestamp(ext).context("%s failed")?,
@@ -73,7 +73,7 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
                 b't' => self.fmt_literal("\t").context("%t failed")?,
                 b'U' => self.fmt_week_sun(ext).context("%U failed")?,
                 b'u' => self.fmt_weekday_mon(ext).context("%u failed")?,
-                b'V' => self.fmt_iana_nocolon(b'V').context("%V failed")?,
+                b'V' => self.fmt_week_iso(ext).context("%V failed")?,
                 b'W' => self.fmt_week_mon(ext).context("%W failed")?,
                 b'w' => self.fmt_weekday_sun(ext).context("%w failed")?,
                 b'Y' => self.fmt_year(ext).context("%Y failed")?,
@@ -88,12 +88,7 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
                         ));
                     }
                     match self.f() {
-                        b'Q' => {
-                            self.fmt_iana_colon(b'Q').context("%:Q failed")?
-                        }
-                        b'V' => {
-                            self.fmt_iana_colon(b'V').context("%:V failed")?
-                        }
+                        b'Q' => self.fmt_iana_colon().context("%:Q failed")?,
                         b'z' => {
                             self.fmt_offset_colon().context("%:z failed")?
                         }
@@ -286,6 +281,7 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
         let day = self
             .tm
             .day
+            .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
             .ok_or_else(|| err!("requires date to format day"))?
             .get();
         ext.write_int(b'0', Some(2), day, self.wtr)
@@ -296,6 +292,7 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
         let day = self
             .tm
             .day
+            .or_else(|| self.tm.to_date().ok().map(|d| d.day_ranged()))
             .ok_or_else(|| err!("requires date to format day"))?
             .get();
         ext.write_int(b' ', Some(2), day, self.wtr)
@@ -376,6 +373,7 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
         let month = self
             .tm
             .month
+            .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
             .ok_or_else(|| err!("requires date to format month"))?
             .get();
         ext.write_int(b'0', Some(2), month, self.wtr)
@@ -386,6 +384,7 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
         let month = self
             .tm
             .month
+            .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
             .ok_or_else(|| err!("requires date to format month"))?;
         ext.write_str(Case::AsIs, month_name_full(month), self.wtr)
     }
@@ -395,18 +394,13 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
         let month = self
             .tm
             .month
+            .or_else(|| self.tm.to_date().ok().map(|d| d.month_ranged()))
             .ok_or_else(|| err!("requires date to format month"))?;
         ext.write_str(Case::AsIs, month_name_abbrev(month), self.wtr)
     }
 
     /// %Q
-    fn fmt_iana_nocolon(&mut self, which: u8) -> Result<(), Error> {
-        if which == b'V' {
-            warn!(
-                "`%V` is DEPRECATED and will emit an ISO 8601 week \
-                 number in `jiff 0.2`, use `%Q` instead",
-            );
-        }
+    fn fmt_iana_nocolon(&mut self) -> Result<(), Error> {
         let Some(iana) = self.tm.iana_time_zone() else {
             let offset = self.tm.offset.ok_or_else(|| {
                 err!(
@@ -421,13 +415,7 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
     }
 
     /// %:Q
-    fn fmt_iana_colon(&mut self, which: u8) -> Result<(), Error> {
-        if which == b'V' {
-            warn!(
-                "`%:V` is DEPRECATED and will result in an error \
-                 in `jiff 0.2`, use `%:Q` instead",
-            );
-        }
+    fn fmt_iana_colon(&mut self) -> Result<(), Error> {
         let Some(iana) = self.tm.iana_time_zone() else {
             let offset = self.tm.offset.ok_or_else(|| {
                 err!(
@@ -590,6 +578,20 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
         ext.write_int(b'0', Some(2), weeknum, self.wtr)
     }
 
+    /// %V
+    fn fmt_week_iso(&mut self, ext: Extension) -> Result<(), Error> {
+        let weeknum = self
+            .tm
+            .iso_week
+            .or_else(|| {
+                self.tm.to_date().ok().map(|d| d.iso_week_date().week_ranged())
+            })
+            .ok_or_else(|| {
+                err!("requires date to format ISO 8601 week number")
+            })?;
+        ext.write_int(b'0', Some(2), weeknum, self.wtr)
+    }
+
     /// %W
     fn fmt_week_mon(&mut self, ext: Extension) -> Result<(), Error> {
         // Short circuit if the week number was explicitly set.
@@ -626,6 +628,7 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
         let year = self
             .tm
             .year
+            .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
             .ok_or_else(|| err!("requires date to format year"))?
             .get();
         ext.write_int(b'0', Some(4), year, self.wtr)
@@ -636,6 +639,7 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
         let year = self
             .tm
             .year
+            .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
             .ok_or_else(|| err!("requires date to format year (2-digit)"))?
             .get();
         if !(1969 <= year && year <= 2068) {
@@ -653,6 +657,7 @@ impl<'f, 't, 'w, W: Write> Formatter<'f, 't, 'w, W> {
         let year = self
             .tm
             .year
+            .or_else(|| self.tm.to_date().ok().map(|d| d.year_ranged()))
             .ok_or_else(|| err!("requires date to format century (2-digit)"))?
             .get();
         let century = year / 100;
diff --git a/src/fmt/strtime/mod.rs b/src/fmt/strtime/mod.rs
index f8f704ff..dcf5c788 100644
--- a/src/fmt/strtime/mod.rs
+++ b/src/fmt/strtime/mod.rs
@@ -188,6 +188,7 @@ strings, the strings are matched without regard to ASCII case.
 | `%t` | `\t` | Formats as a tab character. Parses arbitrary whitespace. |
 | `%U` | `03` | Week number. Week 1 is the first week starting with a Sunday. Zero padded. |
 | `%u` | `7` | The day of the week beginning with Monday at `1`. |
+| `%V` | `05` | Week number in the [ISO 8601 week-based] calendar. Zero padded. |
 | `%W` | `03` | Week number. Week 1 is the first week starting with a Monday. Zero padded. |
 | `%w` | `0` | The day of the week beginning with Sunday at `0`. |
 | `%Y` | `2024` | A full year, including century. Zero padded to 4 digits. |
@@ -196,17 +197,6 @@ strings, the strings are matched without regard to ASCII case.
 | `%z` | `+0530` | A time zone offset in the format `[+-]HHMM[SS]`. |
 | `%:z` | `+05:30` | A time zone offset in the format `[+-]HH:MM[:SS]`. |
 
-The following specifiers are deprecated. Specifically, in `jiff 0.2`, `%V` will
-parse or print the ISO 8601 week number and `%:V` will no longer be recognized.
-To emit an IANA time zone identifier (which is what `%V` does in `jiff 0.1`)
-in a forward compatible way, please use the `%Q` or `%:Q` specifier (as listed
-above).
-
-| Specifier | Example | Description |
-| --------- | ------- | ----------- |
-| `%V` | `America/New_York`, `+0530` | An IANA time zone identifier, or `%z` if one doesn't exist. |
-| `%:V` | `America/New_York`, `+05:30` | An IANA time zone identifier, or `%:z` if one doesn't exist. |
-
 When formatting, the following flags can be inserted immediately after the `%`
 and before the directive:
 
@@ -257,8 +247,6 @@ is variable width data. If you have a use case for this, please
 The following things are currently unsupported:
 
 * Parsing or formatting fractional seconds in the time time zone offset.
-* A conversion specifier for an ISO 8601 week number. It is planned to support
-  this, via `%V`, in `jiff 0.2`.
 * Locale oriented conversion specifiers, such as `%c`, `%r` and `%+`, are not
   supported by Jiff. For locale oriented datetime formatting, please use the
   [`icu`] crate.
@@ -270,7 +258,7 @@ The following things are currently unsupported:
 */
 
 use crate::{
-    civil::{Date, DateTime, Time, Weekday},
+    civil::{Date, DateTime, ISOWeekDate, Time, Weekday},
     error::{err, ErrorContext},
     fmt::{
         strtime::{format::Formatter, parse::Parser},
@@ -488,6 +476,7 @@ pub struct BrokenDownTime {
     day: Option<t::Day>,
     day_of_year: Option<t::DayOfYear>,
     iso_week_year: Option<t::ISOYear>,
+    iso_week: Option<t::ISOWeek>,
     week_sun: Option<t::WeekNum>,
     week_mon: Option<t::WeekNum>,
     hour: Option<t::Hour>,
@@ -1035,9 +1024,18 @@ impl BrokenDownTime {
     #[inline]
     pub fn to_date(&self) -> Result<Date, Error> {
         let Some(year) = self.year else {
+            // The Gregorian year and ISO week year may be parsed separately.
+            // That is, they are two different fields. So if the Gregorian year
+            // is absent, we might still have an ISO 8601 week date.
+            if let Some(date) = self.to_date_from_iso()? {
+                return Ok(date);
+            }
             return Err(err!("missing year, date cannot be created"));
         };
         let mut date = self.to_date_from_gregorian(year)?;
+        if date.is_none() {
+            date = self.to_date_from_iso()?;
+        }
         if date.is_none() {
             date = self.to_date_from_day_of_year(year)?;
         }
@@ -1093,6 +1091,18 @@ impl BrokenDownTime {
         }))
     }
 
+    #[inline]
+    fn to_date_from_iso(&self) -> Result<Option<Date>, Error> {
+        let (Some(y), Some(w), Some(d)) =
+            (self.iso_week_year, self.iso_week, self.weekday)
+        else {
+            return Ok(None);
+        };
+        let wd = ISOWeekDate::new_ranged(y, w, d)
+            .context("invalid ISO 8601 week date")?;
+        Ok(Some(wd.date()))
+    }
+
     #[inline]
     fn to_date_from_week_sun(
         &self,
@@ -1540,6 +1550,32 @@ impl BrokenDownTime {
         self.iso_week_year.map(|x| x.get())
     }
 
+    /// Returns the parsed ISO 8601 week-based number, if available.
+    ///
+    /// The week number is guaranteed to be in the range `1..53`. Week `1` is
+    /// the first week of the year to contain 4 days.
+    ///
+    ///
+    /// # Example
+    ///
+    /// This shows how to parse just an ISO 8601 week-based dates:
+    ///
+    /// ```
+    /// use jiff::{civil::{Weekday, date}, fmt::strtime::BrokenDownTime};
+    ///
+    /// let tm = BrokenDownTime::parse("%G-W%V-%w", "2020-W01-1")?;
+    /// assert_eq!(tm.iso_week_year(), Some(2020));
+    /// assert_eq!(tm.iso_week(), Some(1));
+    /// assert_eq!(tm.weekday(), Some(Weekday::Monday));
+    /// assert_eq!(tm.to_date()?, date(2019, 12, 30));
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    #[inline]
+    pub fn iso_week(&self) -> Option<i8> {
+        self.iso_week.map(|x| x.get())
+    }
+
     /// Returns the Sunday based week number.
     ///
     /// The week number returned is always in the range `0..=53`. Week `1`
@@ -2016,6 +2052,46 @@ impl BrokenDownTime {
         Ok(())
     }
 
+    /// Set the ISO 8601 week-based number on this broken down time.
+    ///
+    /// The week number must be in the range `1..53`. Week `1` is
+    /// the first week of the year to contain 4 days.
+    ///
+    /// # Errors
+    ///
+    /// This returns an error if the given week number is out of range.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use jiff::{civil::Weekday, fmt::strtime::BrokenDownTime};
+    ///
+    /// let mut tm = BrokenDownTime::default();
+    /// // out of range
+    /// assert!(tm.set_iso_week(Some(0)).is_err());
+    /// // out of range
+    /// assert!(tm.set_iso_week(Some(54)).is_err());
+    ///
+    /// tm.set_iso_week_year(Some(2020))?;
+    /// tm.set_iso_week(Some(1))?;
+    /// tm.set_weekday(Some(Weekday::Monday));
+    /// assert_eq!(tm.to_string("%G-W%V-%w")?, "2020-W01-1");
+    /// assert_eq!(tm.to_string("%F")?, "2019-12-30");
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    #[inline]
+    pub fn set_iso_week(
+        &mut self,
+        week_number: Option<i8>,
+    ) -> Result<(), Error> {
+        self.iso_week = match week_number {
+            None => None,
+            Some(wk) => Some(t::ISOWeek::try_new("week-number", wk)?),
+        };
+        Ok(())
+    }
+
     /// Set the Sunday based week number.
     ///
     /// The week number returned is always in the range `0..=53`. Week `1`
@@ -2379,7 +2455,7 @@ impl BrokenDownTime {
 
 impl<'a> From<&'a Zoned> for BrokenDownTime {
     fn from(zdt: &'a Zoned) -> BrokenDownTime {
-        let (_, _, tzabbrev) = zdt.time_zone().to_offset(zdt.timestamp());
+        let offset_info = zdt.time_zone().to_offset_info(zdt.timestamp());
         #[cfg(feature = "alloc")]
         let iana = {
             use alloc::string::ToString;
@@ -2390,7 +2466,7 @@ impl<'a> From<&'a Zoned> for BrokenDownTime {
             // In theory, this could fail, but I've never seen a time zone
             // abbreviation longer than a few bytes. Please file an issue if
             // this is a problem for you.
-            tzabbrev: Abbreviation::new(tzabbrev),
+            tzabbrev: Abbreviation::new(offset_info.abbreviation()),
             #[cfg(feature = "alloc")]
             iana,
             ..BrokenDownTime::from(zdt.datetime())
@@ -2436,6 +2512,17 @@ impl From<Date> for BrokenDownTime {
     }
 }
 
+impl From<ISOWeekDate> for BrokenDownTime {
+    fn from(wd: ISOWeekDate) -> BrokenDownTime {
+        BrokenDownTime {
+            iso_week_year: Some(wd.year_ranged()),
+            iso_week: Some(wd.week_ranged()),
+            weekday: Some(wd.weekday()),
+            ..BrokenDownTime::default()
+        }
+    }
+}
+
 impl From<Time> for BrokenDownTime {
     fn from(t: Time) -> BrokenDownTime {
         BrokenDownTime {
diff --git a/src/fmt/strtime/parse.rs b/src/fmt/strtime/parse.rs
index 6463db1f..6a97dca4 100644
--- a/src/fmt/strtime/parse.rs
+++ b/src/fmt/strtime/parse.rs
@@ -75,7 +75,7 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> {
                 b'n' => self.parse_whitespace().context("%n failed")?,
                 b'P' => self.parse_ampm().context("%P failed")?,
                 b'p' => self.parse_ampm().context("%p failed")?,
-                b'Q' => self.parse_iana_nocolon(b'Q').context("%Q failed")?,
+                b'Q' => self.parse_iana_nocolon().context("%Q failed")?,
                 b'R' => self.parse_clock_nosecs().context("%R failed")?,
                 b'S' => self.parse_second(ext).context("%S failed")?,
                 b's' => self.parse_timestamp(ext).context("%s failed")?,
@@ -83,7 +83,7 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> {
                 b't' => self.parse_whitespace().context("%t failed")?,
                 b'U' => self.parse_week_sun(ext).context("%U failed")?,
                 b'u' => self.parse_weekday_mon(ext).context("%u failed")?,
-                b'V' => self.parse_iana_nocolon(b'V').context("%V failed")?,
+                b'V' => self.parse_week_iso(ext).context("%V failed")?,
                 b'W' => self.parse_week_mon(ext).context("%W failed")?,
                 b'w' => self.parse_weekday_sun(ext).context("%w failed")?,
                 b'Y' => self.parse_year(ext).context("%Y failed")?,
@@ -97,12 +97,9 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> {
                         ));
                     }
                     match self.f() {
-                        b'Q' => self
-                            .parse_iana_colon(b'Q')
-                            .context("%:Q failed")?,
-                        b'V' => self
-                            .parse_iana_colon(b'V')
-                            .context("%:V failed")?,
+                        b'Q' => {
+                            self.parse_iana_colon().context("%:Q failed")?
+                        }
                         b'z' => {
                             self.parse_offset_colon().context("%:z failed")?
                         }
@@ -394,7 +391,7 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> {
 
     /// Parse `%Q`, which is the IANA time zone identifier or an offset without
     /// colons.
-    fn parse_iana_nocolon(&mut self, which: u8) -> Result<(), Error> {
+    fn parse_iana_nocolon(&mut self) -> Result<(), Error> {
         #[cfg(not(feature = "alloc"))]
         {
             Err(err!(
@@ -405,12 +402,6 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> {
         {
             use alloc::string::ToString;
 
-            if which == b'V' {
-                warn!(
-                    "`%V` is DEPRECATED and will parse an ISO 8601 week \
-                     number in `jiff 0.2`, use `%Q` instead",
-                );
-            }
             if !self.inp.is_empty() && matches!(self.inp[0], b'+' | b'-') {
                 return self.parse_offset_nocolon();
             }
@@ -424,7 +415,7 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> {
 
     /// Parse `%:Q`, which is the IANA time zone identifier or an offset with
     /// colons.
-    fn parse_iana_colon(&mut self, which: u8) -> Result<(), Error> {
+    fn parse_iana_colon(&mut self) -> Result<(), Error> {
         #[cfg(not(feature = "alloc"))]
         {
             Err(err!(
@@ -435,12 +426,6 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> {
         {
             use alloc::string::ToString;
 
-            if which == b'V' {
-                warn!(
-                    "`%:V` is DEPRECATED and will result in an error \
-                     in `jiff 0.2`, use `%:Q` instead",
-                );
-            }
             if !self.inp.is_empty() && matches!(self.inp[0], b'+' | b'-') {
                 return self.parse_offset_colon();
             }
@@ -857,6 +842,20 @@ impl<'f, 'i, 't> Parser<'f, 'i, 't> {
         Ok(())
     }
 
+    /// Parse `%V`, which is an ISO 8601 week number.
+    fn parse_week_iso(&mut self, ext: Extension) -> Result<(), Error> {
+        let (week, inp) = ext
+            .parse_number(2, Flag::PadZero, self.inp)
+            .context("failed to parse ISO 8601 week number")?;
+        self.inp = inp;
+
+        let week = t::ISOWeek::try_new("week", week)
+            .context("ISO 8601 week number is invalid")?;
+        self.tm.iso_week = Some(week);
+        self.bump_fmt();
+        Ok(())
+    }
+
     /// Parse `%W`, which is a week number with Monday being the first day
     /// in the first week numbered `01`.
     fn parse_week_mon(&mut self, ext: Extension) -> Result<(), Error> {
diff --git a/src/fmt/temporal/mod.rs b/src/fmt/temporal/mod.rs
index b873d2c6..52c157ce 100644
--- a/src/fmt/temporal/mod.rs
+++ b/src/fmt/temporal/mod.rs
@@ -278,7 +278,7 @@ impl DateTimeParser {
     /// whenever parsing a datetime with an offset that is inconsistent with
     /// the time zone.
     ///
-    /// # Example
+    /// # Example: respecting offsets even when they're invalid
     ///
     /// ```
     /// use jiff::{civil::date, fmt::temporal::DateTimeParser, tz};
@@ -297,6 +297,128 @@ impl DateTimeParser {
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
+    ///
+    /// # Example: all offsets are invalid for gaps in civil time by default
+    ///
+    /// When parsing a datetime with an offset for a gap in civil time, the
+    /// offset is treated as invalid. This results in parsing failing. For
+    /// example, some parts of Indiana in the US didn't start using daylight
+    /// saving time until 2006. If a datetime for 2006 were serialized before
+    /// the updated daylight saving time rules were known, then this parse
+    /// error will prevent you from silently changing the originally intended
+    /// time:
+    ///
+    /// ```
+    /// use jiff::{fmt::temporal::DateTimeParser};
+    ///
+    /// static PARSER: DateTimeParser = DateTimeParser::new();
+    ///
+    /// // DST in Indiana/Vevay began at 2006-04-02T02:00 local time.
+    /// // The last time Indiana/Vevay observed DST was in 1972.
+    /// let result = PARSER.parse_zoned(
+    ///     "2006-04-02T02:30-05[America/Indiana/Vevay]",
+    /// );
+    /// assert_eq!(
+    ///     result.unwrap_err().to_string(),
+    ///     "parsing \"2006-04-02T02:30-05[America/Indiana/Vevay]\" failed: \
+    ///      datetime 2006-04-02T02:30:00 could not resolve to timestamp \
+    ///      since 'reject' conflict resolution was chosen, and because \
+    ///      datetime has offset -05, but the time zone America/Indiana/Vevay \
+    ///      for the given datetime falls in a gap \
+    ///      (between offsets -05 and -04), \
+    ///      and all offsets for a gap are regarded as invalid",
+    /// );
+    /// ```
+    ///
+    /// If one doesn't want an error here, then you can either prioritize the
+    /// instant in time by respecting the offset:
+    ///
+    /// ```
+    /// use jiff::{fmt::temporal::DateTimeParser, tz};
+    ///
+    /// static PARSER: DateTimeParser = DateTimeParser::new()
+    ///     .offset_conflict(tz::OffsetConflict::AlwaysOffset);
+    ///
+    /// let zdt = PARSER.parse_zoned(
+    ///     "2006-04-02T02:30-05[America/Indiana/Vevay]",
+    /// )?;
+    /// assert_eq!(
+    ///     zdt.to_string(),
+    ///     "2006-04-02T03:30:00-04:00[America/Indiana/Vevay]",
+    /// );
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    ///
+    /// or you can force your own disambiguation rules, e.g., by taking the
+    /// earlier time:
+    ///
+    /// ```
+    /// use jiff::{fmt::temporal::DateTimeParser, tz};
+    ///
+    /// static PARSER: DateTimeParser = DateTimeParser::new()
+    ///     .disambiguation(tz::Disambiguation::Earlier)
+    ///     .offset_conflict(tz::OffsetConflict::AlwaysTimeZone);
+    ///
+    /// let zdt = PARSER.parse_zoned(
+    ///     "2006-04-02T02:30-05[America/Indiana/Vevay]",
+    /// )?;
+    /// assert_eq!(
+    ///     zdt.to_string(),
+    ///     "2006-04-02T01:30:00-05:00[America/Indiana/Vevay]",
+    /// );
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    ///
+    /// # Example: a `Z` never results in an offset conflict
+    ///
+    /// [RFC 9557] specifies that `Z` indicates that the offset from UTC to
+    /// get local time is unknown. Since it doesn't prescribe a particular
+    /// offset, when a `Z` is parsed with a time zone annotation, the
+    /// `OffsetConflict::ALwaysOffset` strategy is used regardless of what
+    /// is set here. For example:
+    ///
+    /// ```
+    /// use jiff::fmt::temporal::DateTimeParser;
+    ///
+    /// // NOTE: The default is reject.
+    /// static PARSER: DateTimeParser = DateTimeParser::new();
+    ///
+    /// let zdt = PARSER.parse_zoned(
+    ///     "2025-06-20T17:30Z[America/New_York]",
+    /// )?;
+    /// assert_eq!(
+    ///     zdt.to_string(),
+    ///     "2025-06-20T13:30:00-04:00[America/New_York]",
+    /// );
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    ///
+    /// Conversely, if the `+00:00` offset was used, then an error would
+    /// occur because of the offset conflict:
+    ///
+    /// ```
+    /// use jiff::fmt::temporal::DateTimeParser;
+    ///
+    /// // NOTE: The default is reject.
+    /// static PARSER: DateTimeParser = DateTimeParser::new();
+    ///
+    /// let result = PARSER.parse_zoned(
+    ///     "2025-06-20T17:30+00[America/New_York]",
+    /// );
+    /// assert_eq!(
+    ///     result.unwrap_err().to_string(),
+    ///     "parsing \"2025-06-20T17:30+00[America/New_York]\" failed: \
+    ///      datetime 2025-06-20T17:30:00 could not resolve to a timestamp \
+    ///      since 'reject' conflict resolution was chosen, and because \
+    ///      datetime has offset +00, but the time zone America/New_York \
+    ///      for the given datetime unambiguously has offset -04",
+    /// );
+    /// ```
+    ///
+    /// [RFC 9557]: https://datatracker.ietf.org/doc/rfc9557/
     #[inline]
     pub const fn offset_conflict(
         self,
@@ -447,6 +569,11 @@ impl DateTimeParser {
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
+    ///
+    /// If you _really_ need to parse something like `2024-06-08T07:00-04`
+    /// into a `Zoned` with a fixed offset of `-04:00` as its `TimeZone`,
+    /// then you'll need to use lower level parsing routines. See the
+    /// documentation on [`Pieces`] for a case study of how to achieve this.
     pub fn parse_zoned<I: AsRef<[u8]>>(
         &self,
         input: I,
diff --git a/src/fmt/temporal/parser.rs b/src/fmt/temporal/parser.rs
index 7464ad01..a0bbf68e 100644
--- a/src/fmt/temporal/parser.rs
+++ b/src/fmt/temporal/parser.rs
@@ -91,6 +91,22 @@ impl<'i> ParsedDateTime<'i> {
         let Some(ref parsed_offset) = self.offset else {
             return Ok(tz.into_ambiguous_zoned(dt));
         };
+        if parsed_offset.is_zulu() {
+            // When `Z` is used, that means the offset to local time is not
+            // known. In this case, there really can't be a conflict because
+            // there is an explicit acknowledgment that the offset could be
+            // anything. So we just always accept `Z` as if it were `UTC` and
+            // respect that. If we didn't have this special check, we'd fall
+            // below and the `Z` would just be treated as `+00:00`, which would
+            // likely result in `OffsetConflict::Reject` raising an error.
+            // (Unless the actual correct offset at the time is `+00:00` for
+            // the time zone parsed.)
+            return OffsetConflict::AlwaysOffset
+                .resolve(dt, Offset::UTC, tz)
+                .with_context(|| {
+                    err!("parsing {input:?} failed", input = self.input)
+                });
+        }
         let offset = parsed_offset.to_offset()?;
         let is_equal = |parsed: Offset, candidate: Offset| {
             // If they're equal down to the second, then no amount of rounding
diff --git a/src/fmt/temporal/printer.rs b/src/fmt/temporal/printer.rs
index 16ca57d3..7652b603 100644
--- a/src/fmt/temporal/printer.rs
+++ b/src/fmt/temporal/printer.rs
@@ -53,11 +53,15 @@ impl DateTimePrinter {
     ) -> Result<(), Error> {
         let timestamp = zdt.timestamp();
         let tz = zdt.time_zone();
-        let (offset, _, _) = tz.to_offset(timestamp);
+        let offset = tz.to_offset(timestamp);
         let dt = offset.to_datetime(timestamp);
         self.print_datetime(&dt, &mut wtr)?;
-        self.print_offset_rounded(&offset, &mut wtr)?;
-        self.print_time_zone_annotation(&tz, &offset, &mut wtr)?;
+        if tz.is_unknown() {
+            wtr.write_str("Z[Etc/Unknown]")?;
+        } else {
+            self.print_offset_rounded(&offset, &mut wtr)?;
+            self.print_time_zone_annotation(&tz, &offset, &mut wtr)?;
+        }
         Ok(())
     }
 
@@ -153,6 +157,9 @@ impl DateTimePrinter {
         if let Some(iana_name) = tz.iana_name() {
             return wtr.write_str(iana_name);
         }
+        if tz.is_unknown() {
+            return wtr.write_str("Etc/Unknown");
+        }
         if let Ok(offset) = tz.to_fixed_offset() {
             return self.print_offset_full_precision(&offset, wtr);
         }
diff --git a/src/signed_duration.rs b/src/signed_duration.rs
index 1d472cc5..e43601bf 100644
--- a/src/signed_duration.rs
+++ b/src/signed_duration.rs
@@ -72,7 +72,7 @@ use crate::util::libm::Float;
 ///
 /// let span: Span = "P1d".parse()?;
 /// let relative = date(2024, 11, 3).in_tz("US/Eastern")?;
-/// let duration = span.to_jiff_duration(&relative)?;
+/// let duration = span.to_duration(&relative)?;
 /// // This example also motivates *why* a relative date
 /// // is required. Not all days are the same length!
 /// assert_eq!(duration.to_string(), "PT25H");
@@ -1791,7 +1791,7 @@ impl SignedDuration {
     ) -> SignedDuration {
         // OK because all the difference between any two timestamp values can
         // fit into a signed duration.
-        timestamp2.as_jiff_duration() - timestamp1.as_jiff_duration()
+        timestamp2.as_duration() - timestamp1.as_duration()
     }
 
     pub(crate) fn datetime_until(
@@ -2075,6 +2075,13 @@ impl TryFrom<SignedDuration> for Duration {
     type Error = Error;
 
     fn try_from(sd: SignedDuration) -> Result<Duration, Error> {
+        // This isn't needed, but improves error messages.
+        if sd.is_negative() {
+            return Err(err!(
+                "cannot convert negative duration `{sd:?}` to \
+                 unsigned `std::time::Duration`",
+            ));
+        }
         let secs = u64::try_from(sd.as_secs()).map_err(|_| {
             err!("seconds in signed duration {sd:?} overflowed u64")
         })?;
diff --git a/src/span.rs b/src/span.rs
index 08e43f9a..f87ab047 100644
--- a/src/span.rs
+++ b/src/span.rs
@@ -294,24 +294,22 @@ pub(crate) use span_eq;
 ///
 /// # Comparisons
 ///
-/// While a `Span` currently implements the `PartialEq` and `Eq` traits, these
-/// implementations are deprecated and will be removed in `jiff 0.2`. The trait
-/// implementations only do comparisons based on the fields in the `Span` and
-/// do not take total duration into account. These were deprecated because
-/// it made it too easy to introduce bugs. For example, `120.minutes()` and
-/// `2.hours()` always correspond to the same total duration, but they have
-/// different representations in memory.
+/// A `Span` does not implement the `PartialEq` or `Eq` traits. These traits
+/// were implemented in an earlier version of Jiff, but they made it too
+/// easy to introduce bugs. For example, `120.minutes()` and `2.hours()`
+/// always correspond to the same total duration, but they have different
+/// representations in memory and so didn't compare equivalent.
 ///
 /// The reason why the `PartialEq` and `Eq` trait implementations do not do
 /// comparisons with total duration is because it is fundamentally impossible
 /// to do such comparisons without a reference date in all cases.
 ///
-/// However, it is undeniably occasionally useful to do comparisons based on
-/// the component fields, so long as such use cases can tolerate two different
-/// spans comparing unequal even when their total durations are equivalent. For
-/// example, many of the tests in Jiff (including the tests in the documentation)
-/// work by comparing a `Span` to an expected result. This is a good demonstration
-/// of when fieldwise comparisons are appropriate.
+/// However, it is undeniably occasionally useful to do comparisons based
+/// on the component fields, so long as such use cases can tolerate two
+/// different spans comparing unequal even when their total durations are
+/// equivalent. For example, many of the tests in Jiff (including the tests in
+/// the documentation) work by comparing a `Span` to an expected result. This
+/// is a good demonstration of when fieldwise comparisons are appropriate.
 ///
 /// To do fieldwise comparisons with a span, use the [`Span::fieldwise`]
 /// method. This method creates a [`SpanFieldwise`], which is just a `Span`
@@ -636,12 +634,15 @@ pub(crate) use span_eq;
 /// ```
 /// use std::time::Duration;
 ///
-/// use jiff::{Span, ToSpan};
+/// use jiff::ToSpan;
 ///
 /// let span = 2.months().hours(10);
 /// assert_eq!(
 ///     Duration::try_from(span).unwrap_err().to_string(),
-///     "cannot convert span with non-zero months, must use Span::to_duration with a relative date instead",
+///     "failed to convert span to duration without relative datetime \
+///      (must use `Span::to_duration` instead): using unit 'month' in a \
+///      span or configuration requires that a relative reference time \
+///      be given, but none was provided",
 /// );
 ///
 /// # Ok::<(), Box<dyn std::error::Error>>(())
@@ -650,7 +651,7 @@ pub(crate) use span_eq;
 /// Similar code can be written for `SignedDuration` as well.
 ///
 /// If you need to convert such spans, then as the error suggests, you'll need
-/// to use [`Span::to_jiff_duration`] with a relative date.
+/// to use [`Span::to_duration`] with a relative date.
 ///
 /// And note that since a `Span` is signed and a `std::time::Duration` is unsigned,
 /// converting a negative `Span` to `std::time::Duration` will always fail. One can use
@@ -1407,11 +1408,6 @@ impl Span {
     /// field values. For example, Jiff itself makes heavy use of fieldwise
     /// comparisons for tests.
     ///
-    /// **NOTE:** In `jiff 0.1`, the `Span` type itself also implements the
-    /// `Eq` and `PartialEq` traits. This was considered a bug in the API and
-    /// these trait implementations have been deprecated and will be removed
-    /// in `jiff 0.2`.
-    ///
     /// # Example: the difference between `SpanFieldwise` and `Span::compare`
     ///
     /// In short, `SpanFieldwise` considers `2 hours` and `120 minutes` to be
@@ -1543,7 +1539,7 @@ impl Span {
 
     /// Adds a span to this one and returns the sum as a new span.
     ///
-    /// When adding a span with units greater than days, callers must provide
+    /// When adding a span with units greater than hours, callers must provide
     /// a relative datetime to anchor the spans.
     ///
     /// Arithmetic proceeds as specified in [RFC 5545]. Bigger units are
@@ -1585,14 +1581,13 @@ impl Span {
     ///
     /// This returns an error when adding the two spans would overflow any
     /// individual field of a span. This will also return an error if either
-    /// of the spans have non-zero units of weeks or greater and no relative
+    /// of the spans have non-zero units of days or greater and no relative
     /// reference time is provided.
     ///
-    /// Note that in `jiff 0.2`, this routine will return an error if no
-    /// relative reference time is given and either of the spans have non-zero
-    /// units of _days_ or greater. Callers may write forward compatible code
-    /// that assumes days are an invariant 24 hours in length by providing
-    /// [`SpanRelativeTo::days_are_24_hours`].
+    /// Callers may use [`SpanArithmetic::days_are_24_hours`] as a special
+    /// marker instead of providing a relative civil date to indicate that
+    /// all days should be 24 hours long. This also results in treating all
+    /// weeks as seven 24 hour days (168 hours).
     ///
     /// # Example
     ///
@@ -1615,10 +1610,28 @@ impl Span {
     /// ```
     /// use jiff::ToSpan;
     ///
+    /// let span1 = 2.hours().minutes(59);
+    /// let span2 = 2.minutes();
+    /// assert_eq!(span1.checked_add(span2)?, 3.hours().minutes(1).fieldwise());
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    ///
+    /// # Example: days are not assumed to be 24 hours by default
+    ///
+    /// When dealing with units involving days or weeks, one must either
+    /// provide a relative datetime (shown in the following examples) or opt
+    /// into invariant 24 hour days:
+    ///
+    /// ```
+    /// use jiff::{SpanRelativeTo, ToSpan};
+    ///
     /// let span1 = 2.days().hours(23);
     /// let span2 = 2.hours();
-    /// // When no relative datetime is given, days are always 24 hours long.
-    /// assert_eq!(span1.checked_add(span2)?, 3.days().hours(1).fieldwise());
+    /// assert_eq!(
+    ///     span1.checked_add((span2, SpanRelativeTo::days_are_24_hours()))?,
+    ///     3.days().hours(1).fieldwise(),
+    /// );
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
@@ -1738,8 +1751,6 @@ impl Span {
             },
             None => {
                 requires_relative_date_err(unit)?;
-                // TODO(0.2): turn this warning into an error.
-                deprecation_warn_invariant_days(unit);
                 return span1.checked_add_invariant(unit, &span2);
             }
         };
@@ -1765,8 +1776,6 @@ impl Span {
             },
             None => {
                 requires_relative_date_err(unit)?;
-                // TODO(0.2): turn this warning into an error.
-                deprecation_warn_invariant_days(unit);
                 return span1.checked_add_invariant_duration(unit, dur2);
             }
         };
@@ -1776,14 +1785,15 @@ impl Span {
     }
 
     /// Like `checked_add`, but only applies for invariant units. That is,
-    /// when *both* spans whose non-zero units are all days or smaller.
+    /// when *both* spans whose non-zero units are all hours or smaller
+    /// (or weeks or smaller with the "days are 24 hours" marker).
     #[inline]
     fn checked_add_invariant(
         &self,
         unit: Unit,
         span: &Span,
     ) -> Result<Span, Error> {
-        assert!(unit <= Unit::Day);
+        assert!(unit <= Unit::Week);
         let nanos1 = self.to_invariant_nanoseconds();
         let nanos2 = span.to_invariant_nanoseconds();
         let sum = nanos1 + nanos2;
@@ -1797,7 +1807,7 @@ impl Span {
         unit: Unit,
         duration: SignedDuration,
     ) -> Result<Span, Error> {
-        assert!(unit <= Unit::Day);
+        assert!(unit <= Unit::Week);
         let nanos1 = self.to_invariant_nanoseconds();
         let nanos2 = t::NoUnits96::new_unchecked(duration.as_nanos());
         let sum = nanos1 + nanos2;
@@ -1866,17 +1876,16 @@ impl Span {
     /// # Errors
     ///
     /// If either of the spans being compared have a non-zero calendar unit
-    /// (units bigger than days), then this routine requires a relative
+    /// (units bigger than hours), then this routine requires a relative
     /// datetime. If one is not provided, then an error is returned.
     ///
     /// An error can also occur when adding either span to the relative
     /// datetime given results in overflow.
     ///
-    /// Note that in `jiff 0.2`, this routine will return an error if no
-    /// relative reference time is given and either of the spans have non-zero
-    /// units of _days_ or greater. Callers may write forward compatible code
-    /// that assumes days are an invariant 24 hours in length by providing
-    /// [`SpanRelativeTo::days_are_24_hours`].
+    /// Callers may use [`SpanArithmetic::days_are_24_hours`] as a special
+    /// marker instead of providing a relative civil date to indicate that
+    /// all days should be 24 hours long. This also results in treating all
+    /// weeks as seven 24 hour days (168 hours).
     ///
     /// # Example
     ///
@@ -1910,7 +1919,7 @@ impl Span {
     /// account when comparing spans:
     ///
     /// ```
-    /// use jiff::{ToSpan, Zoned};
+    /// use jiff::{civil, ToSpan, Zoned};
     ///
     /// let span1 = 79.hours().minutes(10);
     /// let span2 = 3.days().hours(7).seconds(630);
@@ -1925,9 +1934,9 @@ impl Span {
     /// );
     ///
     /// // Compare with the result of sorting without taking DST into account.
-    /// // We can do that here since days are considered 24 hours long in all
-    /// // cases when no relative datetime is provided:
-    /// spans.sort_by(|s1, s2| s1.compare(s2).unwrap());
+    /// // We can that by providing a relative civil date:
+    /// let relative = civil::date(2020, 11, 1);
+    /// spans.sort_by(|s1, s2| s1.compare((s2, relative)).unwrap());
     /// assert_eq!(
     ///     spans.map(|sp| sp.fieldwise()),
     ///     [span3.fieldwise(), span1.fieldwise(), span2.fieldwise()],
@@ -1963,18 +1972,17 @@ impl Span {
     ///
     /// # Errors
     ///
-    /// If this span has any non-zero calendar unit (units bigger than days),
+    /// If this span has any non-zero calendar unit (units bigger than hours),
     /// then this routine requires a relative datetime. If one is not provided,
     /// then an error is returned.
     ///
     /// An error can also occur when adding the span to the relative
     /// datetime given results in overflow.
     ///
-    /// Note that in `jiff 0.2`, this routine will return an error if no
-    /// relative reference time is given and either of the spans have non-zero
-    /// units of _days_ or greater. Callers may write forward compatible code
-    /// that assumes days are an invariant 24 hours in length by providing
-    /// [`SpanRelativeTo::days_are_24_hours`].
+    /// Callers may use [`SpanArithmetic::days_are_24_hours`] as a special
+    /// marker instead of providing a relative civil date to indicate that
+    /// all days should be 24 hours long. This also results in treating all
+    /// weeks as seven 24 hour days (168 hours).
     ///
     /// # Example
     ///
@@ -1996,10 +2004,13 @@ impl Span {
     /// `123,456,789` seconds.
     ///
     /// ```
-    /// use jiff::{ToSpan, Unit};
+    /// use jiff::{SpanTotal, ToSpan, Unit};
     ///
     /// let span = 123_456_789.seconds();
-    /// assert_eq!(span.total(Unit::Day)?, 1428.8980208333332);
+    /// assert_eq!(
+    ///     span.total(SpanTotal::from(Unit::Day).days_are_24_hours())?,
+    ///     1428.8980208333332,
+    /// );
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
@@ -2041,12 +2052,12 @@ impl Span {
     /// its `sort_by(..)` call because `Span::compare` may fail and there
     /// is no "fallible" sorting routine in Rust's standard library (as of
     /// 2024-07-07). While the ways in which `Span::compare` can fail for
-    /// a valid configuration limited to overflow for "extreme" values, it
+    /// a valid configuration are limited to overflow for "extreme" values, it
     /// is possible to sort spans infallibly by computing floating point
     /// representations for each span up-front:
     ///
     /// ```
-    /// use jiff::{ToSpan, Unit, Zoned};
+    /// use jiff::{civil::Date, ToSpan, Unit, Zoned};
     ///
     /// let span1 = 79.hours().minutes(10);
     /// let span2 = 3.days().hours(7).seconds(630);
@@ -2065,12 +2076,12 @@ impl Span {
     /// );
     ///
     /// // Compare with the result of sorting without taking DST into account.
-    /// // We can do that here since days are considered 24 hours long in all
-    /// // cases when no relative datetime is provided:
+    /// // We do that here by providing a relative civil date.
+    /// let relative: Date = "2020-11-01".parse()?;
     /// let mut spans = [
-    ///     (span1, span1.total(Unit::Day)?),
-    ///     (span2, span2.total(Unit::Day)?),
-    ///     (span3, span3.total(Unit::Day)?),
+    ///     (span1, span1.total((Unit::Day, relative))?),
+    ///     (span2, span2.total((Unit::Day, relative))?),
+    ///     (span3, span3.total((Unit::Day, relative))?),
     /// ];
     /// spans.sort_by(|&(_, total1), &(_, total2)| total1.total_cmp(&total2));
     /// assert_eq!(
@@ -2146,28 +2157,46 @@ impl Span {
     /// without a relative datetime, or when overflow occurs. Overflow can
     /// occur when the span, added to the relative datetime if given, would
     /// exceed the minimum or maximum datetime values. Overflow can also occur
-    /// if the span is too big to fit into the request unit configuration. For
-    /// example, a span like `19_998.years()` cannot be represented with a
+    /// if the span is too big to fit into the requested unit configuration.
+    /// For example, a span like `19_998.years()` cannot be represented with a
     /// 64-bit integer number of nanoseconds.
     ///
-    /// Note that in `jiff 0.2`, this routine will return an error if no
-    /// relative reference time is given and either of the spans have non-zero
-    /// units of _days_ or greater. Callers may write forward compatible code
-    /// that assumes days are an invariant 24 hours in length by providing
-    /// [`SpanRelativeTo::days_are_24_hours`].
+    /// Callers may use [`SpanArithmetic::days_are_24_hours`] as a special
+    /// marker instead of providing a relative civil date to indicate that
+    /// all days should be 24 hours long. This also results in treating all
+    /// weeks as seven 24 hour days (168 hours).
     ///
     /// # Example: balancing
     ///
     /// This example demonstrates balancing, not rounding. And in particular,
-    /// this example shows how to balance a span as much as possible without
-    /// needing to specify a relative datetime:
+    /// this example shows how to balance a span as much as possible (i.e.,
+    /// with units of hours or smaller) without needing to specify a relative
+    /// datetime:
     ///
     /// ```
     /// use jiff::{SpanRound, ToSpan, Unit};
     ///
     /// let span = 123_456_789_123_456_789i64.nanoseconds();
     /// assert_eq!(
-    ///     span.round(SpanRound::new().largest(Unit::Day))?.fieldwise(),
+    ///     span.round(SpanRound::new().largest(Unit::Hour))?.fieldwise(),
+    ///     34_293.hours().minutes(33).seconds(9)
+    ///         .milliseconds(123).microseconds(456).nanoseconds(789),
+    /// );
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    ///
+    /// Or you can opt into invariant 24-hour days (and 7-day weeks) without a
+    /// relative date with [`SpanRound::days_are_24_hours`]:
+    ///
+    /// ```
+    /// use jiff::{SpanRound, ToSpan, Unit};
+    ///
+    /// let span = 123_456_789_123_456_789i64.nanoseconds();
+    /// assert_eq!(
+    ///     span.round(
+    ///         SpanRound::new().largest(Unit::Day).days_are_24_hours(),
+    ///     )?.fieldwise(),
     ///     1_428.days()
     ///         .hours(21).minutes(33).seconds(9)
     ///         .milliseconds(123).microseconds(456).nanoseconds(789),
@@ -2186,21 +2215,21 @@ impl Span {
     ///
     /// let span = 123_456_789_123_456_789i64.nanoseconds();
     /// assert_eq!(
-    ///     span.round(SpanRound::new().largest(Unit::Day).smallest(Unit::Second))?,
-    ///     1_428.days().hours(21).minutes(33).seconds(9).fieldwise(),
+    ///     span.round(SpanRound::new().largest(Unit::Hour).smallest(Unit::Second))?,
+    ///     34_293.hours().minutes(33).seconds(9).fieldwise(),
     /// );
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     ///
-    /// Or, just rounding to the nearest day can make use of the
+    /// Or, just rounding to the nearest hour can make use of the
     /// `From<Unit> for SpanRound` trait implementation:
     ///
     /// ```
-    /// use jiff::{SpanRound, ToSpan, Unit};
+    /// use jiff::{ToSpan, Unit};
     ///
     /// let span = 123_456_789_123_456_789i64.nanoseconds();
-    /// assert_eq!(span.round(Unit::Day)?, 1_429.days().fieldwise());
+    /// assert_eq!(span.round(Unit::Hour)?, 34_294.hours().fieldwise());
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
@@ -2265,10 +2294,7 @@ impl Span {
     /// Converts a `Span` to a [`SignedDuration`] relative to the date given.
     ///
     /// In most cases, it is unlikely that you'll need to use this routine to
-    /// convert a `Span` to a `SignedDuration`. Namely, every Jiff routine for
-    /// computing a `Span` between datetimes ([`Zoned`], [`Timestamp`],
-    /// [`DateTime`], etc.) will return spans with uniform units by default.
-    /// That is, _by default_:
+    /// convert a `Span` to a `SignedDuration`. Namely, by default:
     ///
     /// * [`Zoned::until`] guarantees that the biggest non-zero unit is hours.
     /// * [`Timestamp::until`] guarantees that the biggest non-zero unit is
@@ -2278,29 +2304,20 @@ impl Span {
     /// * [`Date::until`] guarantees that the biggest non-zero unit is days.
     /// * [`Time::until`] guarantees that the biggest non-zero unit is hours.
     ///
-    /// Of course, this can be changed by asking, for example, `Zoned::until`
-    /// to return units up to years. But by default, in every case above,
-    /// converting the resulting `Span` to a `SignedDuration` can be done
-    /// _correctly_ without providing a relative date. This conversion is done
-    /// with the `TryFrom<Span> for SignedDuration` trait implementation.
-    /// (Which will error when given a span with non-zero units bigger than
-    /// days.)
-    ///
-    /// # Planned breaking change
+    /// In the above, only [`DateTime::until`] and [`Date::until`] return
+    /// calendar units by default. In which case, one may pass
+    /// [`SpanRelativeTo::days_are_24_hours`] or an actual relative date to
+    /// resolve the length of a day.
     ///
-    /// It is planned to rename this routine to `Span::to_duration` in `jiff
-    /// 0.2`. The current `Span::to_duration` routine, which returns a
-    /// `std::time::Duration`, will be removed. If callers need to convert
-    /// a `Span` to a `std::time::Duration`, then they should first convert
-    /// it to a `SignedDuration`, and then to a `std::time::Duration` via
-    /// `TryFrom<SignedDuration> for Duration`.
+    /// Of course, any of the above can be changed by asking, for example,
+    /// `Zoned::until` to return units up to years.
     ///
     /// # Errors
     ///
     /// This returns an error if adding this span to the date given results in
     /// overflow. This can also return an error if one uses
     /// [`SpanRelativeTo::days_are_24_hours`] with a `Span` that has non-zero
-    /// units greater than days.
+    /// units greater than weeks.
     ///
     /// # Example: converting a span with calendar units to a `SignedDuration`
     ///
@@ -2308,175 +2325,40 @@ impl Span {
     /// year:
     ///
     /// ```
-    /// use jiff::{civil::date, SignedDuration, Span, ToSpan};
-    ///
-    /// let span = 1.year();
-    ///
-    /// let duration = span.to_jiff_duration(date(2024, 1, 1))?;
-    /// assert_eq!(duration, SignedDuration::from_secs(31_622_400));
-    /// let duration = span.to_jiff_duration(date(2023, 1, 1))?;
-    /// assert_eq!(duration, SignedDuration::from_secs(31_536_000));
-    ///
-    /// # Ok::<(), Box<dyn std::error::Error>>(())
-    /// ```
-    #[inline]
-    pub fn to_jiff_duration<'a, R: Into<SpanRelativeTo<'a>>>(
-        &self,
-        relative: R,
-    ) -> Result<SignedDuration, Error> {
-        let max_unit = self.largest_unit();
-        let relative: SpanRelativeTo<'a> = relative.into();
-        let Some(result) = relative.to_relative(max_unit).transpose() else {
-            return Ok(self.to_jiff_duration_invariant());
-        };
-        let relspan = result
-            .and_then(|r| r.into_relative_span(Unit::Second, *self))
-            .with_context(|| {
-                err!(
-                    "could not compute normalized relative span \
-                     from datetime {relative} and span {self}",
-                    relative = relative.kind,
-                )
-            })?;
-        debug_assert!(relspan.span.largest_unit() <= Unit::Second);
-        Ok(relspan.span.to_jiff_duration_invariant())
-    }
-
-    /// Converts an entirely invariant span to a `SignedDuration`.
-    ///
-    /// Callers must ensure that this span has no units greater than days. If
-    /// it does have non-zero units of days, then every day is considered 24
-    /// hours.
-    #[inline]
-    pub(crate) fn to_jiff_duration_invariant(&self) -> SignedDuration {
-        // This guarantees, at compile time, that a maximal invariant Span
-        // (that is, all units are days or lower and all units are set to their
-        // maximum values) will still balance out to a number of seconds that
-        // fits into a `i64`. This in turn implies that a `SignedDuration` can
-        // represent all possible invariant positive spans.
-        const _FITS_IN_U64: () = {
-            debug_assert!(
-                i64::MAX as i128
-                    > ((t::SpanDays::MAX * t::SECONDS_PER_CIVIL_DAY.bound())
-                        + (t::SpanHours::MAX * t::SECONDS_PER_HOUR.bound())
-                        + (t::SpanMinutes::MAX
-                            * t::SECONDS_PER_MINUTE.bound())
-                        + t::SpanSeconds::MAX
-                        + (t::SpanMilliseconds::MAX
-                            / t::MILLIS_PER_SECOND.bound())
-                        + (t::SpanMicroseconds::MAX
-                            / t::MICROS_PER_SECOND.bound())
-                        + (t::SpanNanoseconds::MAX
-                            / t::NANOS_PER_SECOND.bound())),
-            );
-            ()
-        };
-
-        let nanos = self.to_invariant_nanoseconds();
-        debug_assert!(
-            self.largest_unit() <= Unit::Day,
-            "units must be days or lower"
-        );
-
-        let seconds = nanos / t::NANOS_PER_SECOND;
-        let seconds = i64::from(seconds);
-        let subsec_nanos = nanos % t::NANOS_PER_SECOND;
-        // OK because % 1_000_000_000 above guarantees that the result fits
-        // in a i32.
-        let subsec_nanos = i32::try_from(subsec_nanos).unwrap();
-
-        // SignedDuration::new can panic if |subsec_nanos| >= 1_000_000_000
-        // and seconds == {i64::MIN,i64::MAX}. But this can never happen
-        // because we guaranteed by construction above that |subsec_nanos| <
-        // 1_000_000_000.
-        SignedDuration::new(seconds, subsec_nanos)
-    }
-}
-
-/// Deprecated APIs on `Span`.
-impl Span {
-    /// Converts a non-negative `Span` to an unsigned [`std::time::Duration`]
-    /// relative to the date given.
-    ///
-    /// In most cases, it is unlikely that you'll need to use this routine to
-    /// convert a `Span` to a `Duration`. Namely, every Jiff routine for
-    /// computing a `Span` between datetimes ([`Zoned`], [`Timestamp`],
-    /// [`DateTime`], etc.) will return spans with uniform units by default.
-    /// That is, _by default_:
-    ///
-    /// * [`Zoned::until`] guarantees that the biggest non-zero unit is hours.
-    /// * [`Timestamp::until`] guarantees that the biggest non-zero unit is
-    /// seconds.
-    /// * [`DateTime::until`] guarantees that the biggest non-zero unit is
-    /// days.
-    /// * [`Date::until`] guarantees that the biggest non-zero unit is days.
-    /// * [`Time::until`] guarantees that the biggest non-zero unit is hours.
-    ///
-    /// Of course, this can be changed by asking, for example, `Zoned::until`
-    /// to return units up to years. But by default, in every case above,
-    /// converting the resulting `Span` to a `std::time::Duration` can be done
-    /// _correctly_ without providing a relative date. This conversion is done
-    /// with the `TryFrom<Span> for Duration` trait implementation. (Which will
-    /// error when given a span with non-zero units bigger than days.)
-    ///
-    /// # Errors
-    ///
-    /// This returns an error if this span is negative or if adding this span
-    /// to the date given results in overflow.
-    ///
-    /// # Example: converting a span with calendar units to a `Duration`
-    ///
-    /// This compares the number of seconds in a non-leap year with a leap
-    /// year:
-    ///
-    /// ```
-    /// use std::time::Duration;
-    ///
-    /// use jiff::{civil::date, Span, ToSpan};
+    /// use jiff::{civil::date, SignedDuration, ToSpan};
     ///
     /// let span = 1.year();
     ///
     /// let duration = span.to_duration(date(2024, 1, 1))?;
-    /// assert_eq!(duration, Duration::from_secs(31_622_400));
+    /// assert_eq!(duration, SignedDuration::from_secs(31_622_400));
     /// let duration = span.to_duration(date(2023, 1, 1))?;
-    /// assert_eq!(duration, Duration::from_secs(31_536_000));
+    /// assert_eq!(duration, SignedDuration::from_secs(31_536_000));
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     ///
-    /// # Example: converting a negative span
+    /// # Example: converting a span without a relative datetime
     ///
-    /// Since a `Span` is signed and a `Duration` is unsigned, converting
-    /// a negative `Span` to `Duration` will always fail. One can use
-    /// [`Span::signum`] to get the sign of the span and [`Span::abs`] to make
-    /// the span positive before converting it to a `Duration`:
+    /// If for some reason it doesn't make sense to include a
+    /// relative datetime, you can use this routine to convert a
+    /// `Span` with units up to weeks to a `SignedDuration` via the
+    /// [`SpanRelativeTo::days_are_24_hours`] marker:
     ///
     /// ```
-    /// use std::time::Duration;
+    /// use jiff::{civil::date, SignedDuration, SpanRelativeTo, ToSpan};
     ///
-    /// use jiff::{civil::date, Span, ToSpan};
+    /// let span = 1.week().days(1);
     ///
-    /// let span = -1.year();
-    /// let (sign, duration) = (
-    ///     span.signum(),
-    ///     span.abs().to_duration(date(2024, 1, 1))?,
-    /// );
-    /// assert_eq!((sign, duration), (-1, Duration::from_secs(31_622_400)));
+    /// let duration = span.to_duration(SpanRelativeTo::days_are_24_hours())?;
+    /// assert_eq!(duration, SignedDuration::from_hours(192));
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
-    #[deprecated(since = "0.1.5", note = "use Span::to_jiff_duration instead")]
     #[inline]
     pub fn to_duration<'a, R: Into<SpanRelativeTo<'a>>>(
         &self,
         relative: R,
-    ) -> Result<UnsignedDuration, Error> {
-        if self.is_negative() {
-            return Err(err!(
-                "cannot convert negative span {self:?} \
-                 to unsigned std::time::Duration",
-            ));
-        }
+    ) -> Result<SignedDuration, Error> {
         let max_unit = self.largest_unit();
         let relative: SpanRelativeTo<'a> = relative.into();
         let Some(result) = relative.to_relative(max_unit).transpose() else {
@@ -2495,22 +2377,28 @@ impl Span {
         Ok(relspan.span.to_duration_invariant())
     }
 
-    /// Converts a positive and entirely invariant span to a std Duration.
+    /// Converts an entirely invariant span to a `SignedDuration`.
     ///
-    /// Callers must ensure that this span is positive and that it has no units
-    /// greater than days. If it does have non-zero units of days, then every
-    /// day is considered 24 hours.
+    /// Callers must ensure that this span has no units greater than weeks.
+    /// If it does have non-zero units of days or weeks, then every day is
+    /// considered 24 hours and every week 7 days. Generally speaking, callers
+    /// should also ensure that if this span does have non-zero day/week units,
+    /// then callers have either provided a civil relative date or the special
+    /// `SpanRelativeTo::days_are_24_hours()` marker.
     #[inline]
-    fn to_duration_invariant(&self) -> UnsignedDuration {
+    pub(crate) fn to_duration_invariant(&self) -> SignedDuration {
         // This guarantees, at compile time, that a maximal invariant Span
         // (that is, all units are days or lower and all units are set to their
         // maximum values) will still balance out to a number of seconds that
-        // fits into a `u64`. This in turn implies that a `Duration` can
+        // fits into a `i64`. This in turn implies that a `SignedDuration` can
         // represent all possible invariant positive spans.
         const _FITS_IN_U64: () = {
             debug_assert!(
-                u64::MAX as u128
-                    > ((t::SpanDays::MAX * t::SECONDS_PER_CIVIL_DAY.bound())
+                i64::MAX as i128
+                    > ((t::SpanWeeks::MAX
+                        * t::SECONDS_PER_CIVIL_WEEK.bound())
+                        + (t::SpanDays::MAX
+                            * t::SECONDS_PER_CIVIL_DAY.bound())
                         + (t::SpanHours::MAX * t::SECONDS_PER_HOUR.bound())
                         + (t::SpanMinutes::MAX
                             * t::SECONDS_PER_MINUTE.bound())
@@ -2520,37 +2408,29 @@ impl Span {
                         + (t::SpanMicroseconds::MAX
                             / t::MICROS_PER_SECOND.bound())
                         + (t::SpanNanoseconds::MAX
-                            / t::NANOS_PER_SECOND.bound()))
-                        as u128,
+                            / t::NANOS_PER_SECOND.bound())),
             );
             ()
         };
 
         let nanos = self.to_invariant_nanoseconds();
-        debug_assert!(nanos >= 0, "span must be positive");
         debug_assert!(
-            self.largest_unit() <= Unit::Day,
-            "units must be days or lower"
+            self.largest_unit() <= Unit::Week,
+            "units must be weeks or lower"
         );
 
         let seconds = nanos / t::NANOS_PER_SECOND;
-        // These are OK because even if we have maximal units for days, hours,
-        // minutes, seconds, milliseconds, microseconds and nanoseconds, it
-        // still doesn't combine to more than u64::MAX seconds.
         let seconds = i64::from(seconds);
-        let seconds = u64::try_from(seconds).unwrap();
-
         let subsec_nanos = nanos % t::NANOS_PER_SECOND;
-        // These are okay because we assume nanos is positive and
-        // because % 1_000_000_000 above guarantees that the result
-        // fits into a 32-bit integer, signed or unsigned.
+        // OK because % 1_000_000_000 above guarantees that the result fits
+        // in a i32.
         let subsec_nanos = i32::try_from(subsec_nanos).unwrap();
-        let subsec_nanos = u32::try_from(subsec_nanos).unwrap();
 
-        // Duration::new can panic if subsec_nanos >= 1_000_000_000 and seconds
-        // == u64::MAX. But this can never happen because we guaranteed by
-        // construction above that subsec_nanos < 1_000_000_000.
-        UnsignedDuration::new(seconds, subsec_nanos)
+        // SignedDuration::new can panic if |subsec_nanos| >= 1_000_000_000
+        // and seconds == {i64::MIN,i64::MAX}. But this can never happen
+        // because we guaranteed by construction above that |subsec_nanos| <
+        // 1_000_000_000.
+        SignedDuration::new(seconds, subsec_nanos)
     }
 }
 
@@ -2842,8 +2722,11 @@ impl Span {
     /// Converts the given number of nanoseconds to a `Span` whose units do not
     /// exceed `largest`.
     ///
-    /// Note that `largest` is capped at `Unit::Day`. That is, if any unit
-    /// greater than `Unit::Day` is given, then it is treated as `Unit::Day`.
+    /// Note that `largest` is capped at `Unit::Week`. Note though that if
+    /// any unit greater than `Unit::Week` is given, then it is treated as
+    /// `Unit::Day`. The only way to get weeks in the `Span` returned is to
+    /// specifically request `Unit::Week`.
+    ///
     /// And also note that days in this context are civil days. That is, they
     /// are always 24 hours long. Callers needing to deal with variable length
     /// days should do so outside of this routine and should not provide a
@@ -2855,7 +2738,38 @@ impl Span {
         let nanos = nanos.rinto();
         let mut span = Span::new();
         match largest {
-            Unit::Year | Unit::Month | Unit::Week | Unit::Day => {
+            Unit::Week => {
+                let micros = nanos.div_ceil(t::NANOS_PER_MICRO);
+                span = span.try_nanoseconds_ranged(
+                    nanos.rem_ceil(t::NANOS_PER_MICRO),
+                )?;
+                let millis = micros.div_ceil(t::MICROS_PER_MILLI);
+                span = span.try_microseconds_ranged(
+                    micros.rem_ceil(t::MICROS_PER_MILLI),
+                )?;
+                let secs = millis.div_ceil(t::MILLIS_PER_SECOND);
+                span = span.try_milliseconds_ranged(
+                    millis.rem_ceil(t::MILLIS_PER_SECOND),
+                )?;
+                let mins = secs.div_ceil(t::SECONDS_PER_MINUTE);
+                span = span.try_seconds_ranged(
+                    secs.rem_ceil(t::SECONDS_PER_MINUTE),
+                )?;
+                let hours = mins.div_ceil(t::MINUTES_PER_HOUR);
+                span = span
+                    .try_minutes_ranged(mins.rem_ceil(t::MINUTES_PER_HOUR))?;
+                let days = hours.div_ceil(t::HOURS_PER_CIVIL_DAY);
+                span = span.try_hours_ranged(
+                    hours.rem_ceil(t::HOURS_PER_CIVIL_DAY),
+                )?;
+                let weeks = days.div_ceil(t::DAYS_PER_CIVIL_WEEK);
+                span = span
+                    .try_days_ranged(days.rem_ceil(t::DAYS_PER_CIVIL_WEEK))?;
+                span = span.try_weeks_ranged(weeks)?;
+                Ok(span)
+            }
+            Unit::Year | Unit::Month | Unit::Day => {
+                // Unit::Year | Unit::Month | Unit::Week | Unit::Day => {
                 let micros = nanos.div_ceil(t::NANOS_PER_MICRO);
                 span = span.try_nanoseconds_ranged(
                     nanos.rem_ceil(t::NANOS_PER_MICRO),
@@ -2970,11 +2884,11 @@ impl Span {
     /// Converts the non-variable units of this `Span` to a total number of
     /// nanoseconds.
     ///
-    /// This includes days, even though they can be of irregular length during
-    /// time zone transitions. If this applies, then callers should set the
-    /// days to `0` before calling this routine.
+    /// This includes days and weeks, even though they can be of irregular
+    /// length during time zone transitions. If this applies, then callers
+    /// should set the days and weeks to `0` before calling this routine.
     ///
-    /// All units above days are always ignored.
+    /// All units above weeks are always ignored.
     #[inline]
     pub(crate) fn to_invariant_nanoseconds(&self) -> NoUnits128 {
         let mut nanos = NoUnits128::rfrom(self.get_nanoseconds_ranged());
@@ -2990,6 +2904,8 @@ impl Span {
             NoUnits128::rfrom(self.get_hours_ranged()) * t::NANOS_PER_HOUR;
         nanos +=
             NoUnits128::rfrom(self.get_days_ranged()) * t::NANOS_PER_CIVIL_DAY;
+        nanos += NoUnits128::rfrom(self.get_weeks_ranged())
+            * t::NANOS_PER_CIVIL_WEEK;
         nanos
     }
 
@@ -3001,11 +2917,11 @@ impl Span {
     /// it's faster to only deal with seconds. And in particular, acknowledges
     /// that nanosecond precision durations are somewhat rare.
     ///
-    /// This includes days, even though they can be of irregular length during
-    /// time zone transitions. If this applies, then callers should set the
-    /// days to `0` before calling this routine.
+    /// This includes days and weeks, even though they can be of irregular
+    /// length during time zone transitions. If this applies, then callers
+    /// should set the days and weeks to `0` before calling this routine.
     ///
-    /// All units above days are always ignored.
+    /// All units above weeks are always ignored.
     #[inline]
     pub(crate) fn to_invariant_seconds(&self) -> Option<NoUnits> {
         if self.has_fractional_seconds() {
@@ -3018,6 +2934,8 @@ impl Span {
             NoUnits::rfrom(self.get_hours_ranged()) * t::SECONDS_PER_HOUR;
         seconds +=
             NoUnits::rfrom(self.get_days_ranged()) * t::SECONDS_PER_CIVIL_DAY;
+        seconds += NoUnits::rfrom(self.get_weeks_ranged())
+            * t::SECONDS_PER_CIVIL_WEEK;
         Some(seconds)
     }
 
@@ -3233,7 +3151,7 @@ impl Span {
     ///
     /// This is useful for debugging. Normally, this would be the "alternate"
     /// debug impl (perhaps), but that's what insta uses and I preferred having
-    /// the friendly format used there since is much more terse.
+    /// the friendly format used there since it is much more terse.
     #[cfg(feature = "alloc")]
     #[allow(dead_code)]
     fn debug(&self) -> alloc::string::String {
@@ -3330,26 +3248,6 @@ impl Default for Span {
     }
 }
 
-/// DEPRECATED since `jiff 0.1.25`.
-///
-/// Please use [`Span::fieldwise`] to create a span that can be compared by its
-/// fields.
-///
-/// This trait implementation will be removed in `jiff 0.2`.
-impl Eq for Span {}
-
-/// DEPRECATED since `jiff 0.1.25`.
-///
-/// Please use [`Span::fieldwise`] to create a span that can be compared by its
-/// fields.
-///
-/// This trait implementation will be removed in `jiff 0.2`.
-impl PartialEq for Span {
-    fn eq(&self, rhs: &Span) -> bool {
-        self.fieldwise() == rhs.fieldwise()
-    }
-}
-
 impl core::fmt::Debug for Span {
     #[inline]
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
@@ -3432,20 +3330,20 @@ impl core::ops::Mul<Span> for i64 {
 ///
 /// * The span is negative. This is an error because a `std::time::Duration` is
 ///   unsigned.)
-/// * The span has any non-zero units greater than days. This is an error
+/// * The span has any non-zero units greater than hours. This is an error
 ///   because it's impossible to determine the length of, e.g., a month without
 ///   a reference date.
 ///
 /// This can never result in overflow because a `Duration` can represent a
-/// bigger span of time than `Span` when limited to units of days or lower.
+/// bigger span of time than `Span` when limited to units of hours or lower.
 ///
-/// If you need to convert a `Span` to a `Duration` that has non-zero units
-/// bigger than days (or a `Span` with days of non-uniform length), then please
-/// use [`Span::to_duration`] with a corresponding relative date.
+/// If you need to convert a `Span` to a `Duration` that has non-zero
+/// units bigger than hours, then please use [`Span::to_duration`] with a
+/// corresponding relative date.
 ///
 /// # Example: maximal span
 ///
-/// This example shows the maximum possible span using units of days or
+/// This example shows the maximum possible span using units of hours or
 /// smaller, and the corresponding `Duration` value:
 ///
 /// ```
@@ -3454,7 +3352,6 @@ impl core::ops::Mul<Span> for i64 {
 /// use jiff::Span;
 ///
 /// let sp = Span::new()
-///     .days(7_304_484)
 ///     .hours(175_307_616)
 ///     .minutes(10_518_456_960i64)
 ///     .seconds(631_107_417_600i64)
@@ -3462,7 +3359,7 @@ impl core::ops::Mul<Span> for i64 {
 ///     .microseconds(631_107_417_600_000_000i64)
 ///     .nanoseconds(9_223_372_036_854_775_807i64);
 /// let duration = Duration::try_from(sp)?;
-/// assert_eq!(duration, Duration::new(3_795_867_877_636, 854_775_807));
+/// assert_eq!(duration, Duration::new(3_164_760_460_036, 854_775_807));
 ///
 /// # Ok::<(), Box<dyn std::error::Error>>(())
 /// ```
@@ -3490,21 +3387,14 @@ impl TryFrom<Span> for UnsignedDuration {
 
     #[inline]
     fn try_from(sp: Span) -> Result<UnsignedDuration, Error> {
+        // This isn't needed, but improves error messages.
         if sp.is_negative() {
             return Err(err!(
                 "cannot convert negative span {sp:?} \
                  to unsigned std::time::Duration",
             ));
         }
-        if sp.largest_unit() > Unit::Day {
-            return Err(err!(
-                "cannot convert span with non-zero {unit}, \
-                 must use Span::to_duration with a relative date \
-                 instead",
-                unit = sp.largest_unit().plural(),
-            ));
-        }
-        Ok(sp.to_duration_invariant())
+        SignedDuration::try_from(sp).and_then(UnsignedDuration::try_from)
     }
 }
 
@@ -3599,31 +3489,25 @@ impl TryFrom<UnsignedDuration> for Span {
 /// # Errors
 ///
 /// This can fail for only when the span has any non-zero units greater than
-/// days. This is an error because it's impossible to determine the length of,
+/// hours. This is an error because it's impossible to determine the length of,
 /// e.g., a month without a reference date.
 ///
 /// This can never result in overflow because a `SignedDuration` can represent
-/// a bigger span of time than `Span` when limited to units of days or lower.
+/// a bigger span of time than `Span` when limited to units of hours or lower.
 ///
 /// If you need to convert a `Span` to a `SignedDuration` that has non-zero
-/// units bigger than days (or a `Span` with days of non-uniform length), then
-/// please use [`Span::to_jiff_duration`] with a corresponding relative date.
-///
-/// Note that in `jiff 0.2`, this routine will return an error the span has
-/// non-zero units of _days_ or greater. Callers may write forward compatible
-/// code that assumes days are an invariant 24 hours in length by providing
-/// [`SpanRelativeTo::days_are_24_hours`] to [`Span::to_jiff_duration`].
+/// units bigger than hours, then please use [`Span::to_duration`] with a
+/// corresponding relative date.
 ///
 /// # Example: maximal span
 ///
-/// This example shows the maximum possible span using units of days or
+/// This example shows the maximum possible span using units of hours or
 /// smaller, and the corresponding `SignedDuration` value:
 ///
 /// ```
 /// use jiff::{SignedDuration, Span};
 ///
 /// let sp = Span::new()
-///     .days(7_304_484)
 ///     .hours(175_307_616)
 ///     .minutes(10_518_456_960i64)
 ///     .seconds(631_107_417_600i64)
@@ -3631,7 +3515,7 @@ impl TryFrom<UnsignedDuration> for Span {
 ///     .microseconds(631_107_417_600_000_000i64)
 ///     .nanoseconds(9_223_372_036_854_775_807i64);
 /// let duration = SignedDuration::try_from(sp)?;
-/// assert_eq!(duration, SignedDuration::new(3_795_867_877_636, 854_775_807));
+/// assert_eq!(duration, SignedDuration::new(3_164_760_460_036, 854_775_807));
 ///
 /// # Ok::<(), Box<dyn std::error::Error>>(())
 /// ```
@@ -3640,17 +3524,11 @@ impl TryFrom<Span> for SignedDuration {
 
     #[inline]
     fn try_from(sp: Span) -> Result<SignedDuration, Error> {
-        let largest = sp.largest_unit();
-        if largest > Unit::Day {
-            return Err(err!(
-                "cannot convert span with non-zero {unit}, \
-                 must use Span::to_duration with a relative date \
-                 instead",
-                unit = largest.plural(),
-            ));
-        }
-        deprecation_warn_invariant_days(largest);
-        Ok(sp.to_jiff_duration_invariant())
+        requires_relative_date_err(sp.largest_unit()).context(
+            "failed to convert span to duration without relative datetime \
+             (must use `Span::to_duration` instead)",
+        )?;
+        Ok(sp.to_duration_invariant())
     }
 }
 
@@ -3857,11 +3735,6 @@ impl quickcheck::Arbitrary for Span {
 /// values. For example, Jiff itself makes heavy use of fieldwise comparisons
 /// for tests.
 ///
-/// **NOTE:** In `jiff 0.1`, the `Span` type itself also implements the
-/// `Eq` and `PartialEq` traits. This was considered a bug in the API and
-/// these trait implementations have been deprecated and will be removed
-/// in `jiff 0.2`.
-///
 /// # Construction
 ///
 /// While callers may use `SpanFieldwise(span)` (where `span` has type [`Span`])
@@ -4309,8 +4182,7 @@ impl Unit {
     ///
     /// # Panics
     ///
-    /// When this unit is definitively variable. That is, years, months or
-    /// weeks.
+    /// When this unit is always variable. That is, years or months.
     pub(crate) fn nanoseconds(self) -> NoUnits128 {
         match self {
             Unit::Nanosecond => Constant(1),
@@ -4320,6 +4192,7 @@ impl Unit {
             Unit::Minute => t::NANOS_PER_MINUTE,
             Unit::Hour => t::NANOS_PER_HOUR,
             Unit::Day => t::NANOS_PER_CIVIL_DAY,
+            Unit::Week => t::NANOS_PER_CIVIL_WEEK,
             unit => unreachable!("{unit:?} has no definitive time interval"),
         }
         .rinto()
@@ -4331,12 +4204,8 @@ impl Unit {
     /// can vary in time depending on its reference point. A 'day' can as well,
     /// but we sorta special case 'day' to mean '24 hours' for cases where
     /// the user is dealing with civil time.
-    fn is_definitively_variable(self) -> bool {
-        // TODO(0.2): Add `Unit::Day` to this, effectively forcing folks to use
-        // either a relative datetime or `SpanRelativeTo::days_are_24_hours` in
-        // order to avoid to do arithmetic with spans containing non-zero units
-        // of days.
-        matches!(self, Unit::Year | Unit::Month | Unit::Week)
+    fn is_variable(self) -> bool {
+        matches!(self, Unit::Year | Unit::Month | Unit::Week | Unit::Day)
     }
 
     /// A human readable singular description of this unit of time.
@@ -4411,9 +4280,10 @@ impl quickcheck::Arbitrary for Unit {
 /// This type provides a way to ergonomically add two spans with an optional
 /// relative datetime. Namely, a relative datetime is only needed when at least
 /// one of the two spans being added (or subtracted) has a non-zero calendar
-/// unit (years, months or weeks). Otherwise, an error will be returned.
+/// unit (years, months, weeks or days). Otherwise, an error will be returned.
 ///
-/// When no relative datetime is provided, days are always 24 hours long.
+/// Callers may use [`SpanArithmetic::days_are_24_hours`] to opt into 24-hour
+/// invariant days (and 7-day weeks) without providing a relative datetime.
 ///
 /// The main way to construct values of this type is with its `From` trait
 /// implementations:
@@ -4443,6 +4313,42 @@ pub struct SpanArithmetic<'a> {
     relative: Option<SpanRelativeTo<'a>>,
 }
 
+impl<'a> SpanArithmetic<'a> {
+    /// This is a convenience function for setting the relative option on
+    /// this configuration to [`SpanRelativeTo::days_are_24_hours`].
+    ///
+    /// # Example
+    ///
+    /// When doing arithmetic on spans involving days, either a relative
+    /// datetime must be provided, or a special assertion opting into 24-hour
+    /// days is required. Otherwise, you get an error.
+    ///
+    /// ```
+    /// use jiff::{SpanArithmetic, ToSpan};
+    ///
+    /// let span1 = 2.days().hours(12);
+    /// let span2 = 12.hours();
+    /// // No relative date provided, which results in an error.
+    /// assert_eq!(
+    ///     span1.checked_add(span2).unwrap_err().to_string(),
+    ///     "using unit 'day' in a span or configuration requires that \
+    ///      either a relative reference time be given or \
+    ///      `SpanRelativeTo::days_are_24_hours()` is used to indicate \
+    ///      invariant 24-hour days, but neither were provided",
+    /// );
+    /// let sum = span1.checked_add(
+    ///     SpanArithmetic::from(span2).days_are_24_hours(),
+    /// )?;
+    /// assert_eq!(sum, 3.days().fieldwise());
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    #[inline]
+    pub fn days_are_24_hours(self) -> SpanArithmetic<'a> {
+        self.relative(SpanRelativeTo::days_are_24_hours())
+    }
+}
+
 impl<'a> SpanArithmetic<'a> {
     #[inline]
     fn relative<R: Into<SpanRelativeTo<'a>>>(
@@ -4614,9 +4520,10 @@ impl<'a> From<(UnsignedDuration, &'a Zoned)> for SpanArithmetic<'a> {
 /// This type provides a way to ergonomically compare two spans with an
 /// optional relative datetime. Namely, a relative datetime is only needed when
 /// at least one of the two spans being compared has a non-zero calendar unit
-/// (years, months or weeks). Otherwise, an error will be returned.
+/// (years, months, weeks or days). Otherwise, an error will be returned.
 ///
-/// When no relative datetime is provided, days are always 24 hours long.
+/// Callers may use [`SpanCompare::days_are_24_hours`] to opt into 24-hour
+/// invariant days (and 7-day weeks) without providing a relative datetime.
 ///
 /// The main way to construct values of this type is with its `From` trait
 /// implementations:
@@ -4645,6 +4552,42 @@ pub struct SpanCompare<'a> {
     relative: Option<SpanRelativeTo<'a>>,
 }
 
+impl<'a> SpanCompare<'a> {
+    /// This is a convenience function for setting the relative option on
+    /// this configuration to [`SpanRelativeTo::days_are_24_hours`].
+    ///
+    /// # Example
+    ///
+    /// When comparing spans involving days, either a relative datetime must be
+    /// provided, or a special assertion opting into 24-hour days is
+    /// required. Otherwise, you get an error.
+    ///
+    /// ```
+    /// use jiff::{SpanCompare, ToSpan, Unit};
+    ///
+    /// let span1 = 2.days().hours(12);
+    /// let span2 = 60.hours();
+    /// // No relative date provided, which results in an error.
+    /// assert_eq!(
+    ///     span1.compare(span2).unwrap_err().to_string(),
+    ///     "using unit 'day' in a span or configuration requires that \
+    ///      either a relative reference time be given or \
+    ///      `SpanRelativeTo::days_are_24_hours()` is used to indicate \
+    ///      invariant 24-hour days, but neither were provided",
+    /// );
+    /// let ordering = span1.compare(
+    ///     SpanCompare::from(span2).days_are_24_hours(),
+    /// )?;
+    /// assert_eq!(ordering, std::cmp::Ordering::Equal);
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    #[inline]
+    pub fn days_are_24_hours(self) -> SpanCompare<'a> {
+        self.relative(SpanRelativeTo::days_are_24_hours())
+    }
+}
+
 impl<'a> SpanCompare<'a> {
     #[inline]
     fn new(span: Span) -> SpanCompare<'static> {
@@ -4673,8 +4616,6 @@ impl<'a> SpanCompare<'a> {
             },
             None => {
                 requires_relative_date_err(unit)?;
-                // TODO(0.2): turn this warning into an error.
-                deprecation_warn_invariant_days(unit);
                 let nanos1 = span1.to_invariant_nanoseconds();
                 let nanos2 = span2.to_invariant_nanoseconds();
                 return Ok(nanos1.cmp(&nanos2));
@@ -4759,12 +4700,13 @@ impl<'a, 'b> From<(&'a Span, SpanRelativeTo<'b>)> for SpanCompare<'b> {
 /// Options for [`Span::total`].
 ///
 /// This type provides a way to ergonomically determine the number of a
-/// particular unit in a span, with a potentially fractional component, with an
-/// optional relative datetime. Namely, a relative datetime is only needed when
-/// the span has a non-zero calendar unit (years, months or weeks). Otherwise,
-/// an error will be returned.
+/// particular unit in a span, with a potentially fractional component, with
+/// an optional relative datetime. Namely, a relative datetime is only needed
+/// when the span has a non-zero calendar unit (years, months, weeks or days).
+/// Otherwise, an error will be returned.
 ///
-/// When no relative datetime is provided, days are always 24 hours long.
+/// Callers may use [`SpanTotal::days_are_24_hours`] to opt into 24-hour
+/// invariant days (and 7-day weeks) without providing a relative datetime.
 ///
 /// The main way to construct values of this type is with its `From` trait
 /// implementations:
@@ -4795,10 +4737,13 @@ impl<'a, 'b> From<(&'a Span, SpanRelativeTo<'b>)> for SpanCompare<'b> {
 /// seconds.
 ///
 /// ```
-/// use jiff::{ToSpan, Unit};
+/// use jiff::{SpanTotal, ToSpan, Unit};
 ///
 /// let span = 123_456_789.seconds();
-/// assert_eq!(span.total(Unit::Day)?, 1428.8980208333332);
+/// assert_eq!(
+///     span.total(SpanTotal::from(Unit::Day).days_are_24_hours())?,
+///     1428.8980208333332,
+/// );
 ///
 /// # Ok::<(), Box<dyn std::error::Error>>(())
 /// ```
@@ -4838,6 +4783,48 @@ pub struct SpanTotal<'a> {
     relative: Option<SpanRelativeTo<'a>>,
 }
 
+impl<'a> SpanTotal<'a> {
+    /// This is a convenience function for setting the relative option on
+    /// this configuration to [`SpanRelativeTo::days_are_24_hours`].
+    ///
+    /// # Example
+    ///
+    /// When computing the total duration for spans involving days, either a
+    /// relative datetime must be provided, or a special assertion opting into
+    /// 24-hour days is required. Otherwise, you get an error.
+    ///
+    /// ```
+    /// use jiff::{civil::date, SpanTotal, ToSpan, Unit};
+    ///
+    /// let span = 2.days().hours(12);
+    ///
+    /// // No relative date provided, which results in an error.
+    /// assert_eq!(
+    ///     span.total(Unit::Hour).unwrap_err().to_string(),
+    ///     "using unit 'day' in a span or configuration requires that either \
+    ///      a relative reference time be given or \
+    ///      `SpanRelativeTo::days_are_24_hours()` is used to indicate \
+    ///      invariant 24-hour days, but neither were provided",
+    /// );
+    ///
+    /// // If we can assume all days are 24 hours, then we can assert it:
+    /// let total = span.total(
+    ///     SpanTotal::from(Unit::Hour).days_are_24_hours(),
+    /// )?;
+    /// assert_eq!(total, 60.0);
+    ///
+    /// // Or provide a relative datetime, which is preferred if possible:
+    /// let total = span.total((Unit::Hour, date(2025, 1, 26)))?;
+    /// assert_eq!(total, 60.0);
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    #[inline]
+    pub fn days_are_24_hours(self) -> SpanTotal<'a> {
+        self.relative(SpanRelativeTo::days_are_24_hours())
+    }
+}
+
 impl<'a> SpanTotal<'a> {
     #[inline]
     fn new(unit: Unit) -> SpanTotal<'static> {
@@ -4863,13 +4850,11 @@ impl<'a> SpanTotal<'a> {
             },
             None => {
                 requires_relative_date_err(max_unit)?;
-                // TODO(0.2): turn this warning into an error.
-                deprecation_warn_invariant_days(max_unit);
                 return Ok(self.total_invariant(span));
             }
         };
         let relspan = relative.into_relative_span(self.unit, span)?;
-        if !relspan.kind.is_variable(self.unit) {
+        if !self.unit.is_variable() {
             return Ok(self.total_invariant(relspan.span));
         }
 
@@ -4901,7 +4886,7 @@ impl<'a> SpanTotal<'a> {
 
     #[inline]
     fn total_invariant(&self, span: Span) -> f64 {
-        assert!(self.unit <= Unit::Day);
+        assert!(self.unit <= Unit::Week);
         let nanos = span.to_invariant_nanoseconds();
         (nanos.get() as f64) / (self.unit.nanoseconds().get() as f64)
     }
@@ -5042,7 +5027,7 @@ impl<'a> SpanRound<'a> {
     ///
     /// When a largest unit is not specified, then it defaults to the largest
     /// non-zero unit that is at least as big as the configured smallest
-    /// unit. For example, given a span of 2 months 17 hours, the default
+    /// unit. For example, given a span of `2 months 17 hours`, the default
     /// largest unit would be `Unit::Month`. The default implies that a span's
     /// units do not get "bigger" than what was given.
     ///
@@ -5050,9 +5035,10 @@ impl<'a> SpanRound<'a> {
     /// configuration back to using the "automatic" default. Instead, callers
     /// must create a new configuration.
     ///
-    /// If a largest unit is set and no other options are set, then the rounding
-    /// operation can be said to be a "re-balancing." That is, the span won't
-    /// lose precision, but the way in which it is expressed may change.
+    /// If a largest unit is set and no other options are set, then the
+    /// rounding operation can be said to be a "re-balancing." That is, the
+    /// span won't lose precision, but the way in which it is expressed may
+    /// change.
     ///
     /// # Errors
     ///
@@ -5073,14 +5059,14 @@ impl<'a> SpanRound<'a> {
     ///
     /// let span = 86_401_123_456_789i64.nanoseconds();
     /// assert_eq!(
-    ///     span.round(SpanRound::new().largest(Unit::Day))?.fieldwise(),
-    ///     1.day().seconds(1).milliseconds(123).microseconds(456).nanoseconds(789),
+    ///     span.round(SpanRound::new().largest(Unit::Hour))?.fieldwise(),
+    ///     24.hours().seconds(1).milliseconds(123).microseconds(456).nanoseconds(789),
     /// );
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     ///
-    /// If you need to use a largest unit bigger than days, then you must
+    /// If you need to use a largest unit bigger than hours, then you must
     /// provide a relative datetime as a reference point (otherwise an error
     /// will occur):
     ///
@@ -5099,6 +5085,23 @@ impl<'a> SpanRound<'a> {
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     ///
+    /// As a special case for days, one can instead opt into invariant 24-hour
+    /// days (and 7-day weeks) without providing an explicit relative date:
+    ///
+    /// ```
+    /// use jiff::{SpanRound, ToSpan, Unit};
+    ///
+    /// let span = 86_401_123_456_789i64.nanoseconds();
+    /// assert_eq!(
+    ///     span.round(
+    ///         SpanRound::new().largest(Unit::Day).days_are_24_hours(),
+    ///     )?.fieldwise(),
+    ///     1.day().seconds(1).milliseconds(123).microseconds(456).nanoseconds(789),
+    /// );
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    ///
     /// # Example: re-balancing while taking DST into account
     ///
     /// When given a zone aware relative datetime, rounding will even take
@@ -5248,7 +5251,7 @@ impl<'a> SpanRound<'a> {
     ///
     /// # Errors
     ///
-    /// If rounding involves a calendar unit (units bigger than days) and no
+    /// If rounding involves a calendar unit (units bigger than hours) and no
     /// relative datetime is provided, then this configuration will lead to
     /// an error when used with [`Span::round`].
     ///
@@ -5290,6 +5293,39 @@ impl<'a> SpanRound<'a> {
         SpanRound { relative: Some(relative.into()), ..self }
     }
 
+    /// This is a convenience function for setting the relative option on
+    /// this configuration to [`SpanRelativeTo::days_are_24_hours`].
+    ///
+    /// # Example
+    ///
+    /// When rounding spans involving days, either a relative datetime must be
+    /// provided, or a special assertion opting into 24-hour days is
+    /// required. Otherwise, you get an error.
+    ///
+    /// ```
+    /// use jiff::{SpanRound, ToSpan, Unit};
+    ///
+    /// let span = 2.days().hours(12);
+    /// // No relative date provided, which results in an error.
+    /// assert_eq!(
+    ///     span.round(Unit::Day).unwrap_err().to_string(),
+    ///     "error with `smallest` rounding option: using unit 'day' in a \
+    ///      span or configuration requires that either a relative reference \
+    ///      time be given or `SpanRelativeTo::days_are_24_hours()` is used \
+    ///      to indicate invariant 24-hour days, but neither were provided",
+    /// );
+    /// let rounded = span.round(
+    ///     SpanRound::new().smallest(Unit::Day).days_are_24_hours(),
+    /// )?;
+    /// assert_eq!(rounded, 3.days().fieldwise());
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    #[inline]
+    pub fn days_are_24_hours(self) -> SpanRound<'a> {
+        self.relative(SpanRelativeTo::days_are_24_hours())
+    }
+
     /// Returns the configured smallest unit on this round configuration.
     #[inline]
     pub(crate) fn get_smallest(&self) -> Unit {
@@ -5354,41 +5390,20 @@ impl<'a> SpanRound<'a> {
                 }
             }
             None => {
-                deprecation_warn_invariant_days(smallest);
-                deprecation_warn_invariant_days(existing_largest);
                 // This is only okay if none of our units are above 'day'.
                 // That is, a reference point is only necessary when there is
                 // no reasonable invariant interpretation of the span. And this
                 // is only true when everything is less than 'day'.
-                if smallest.is_definitively_variable() {
-                    return Err(err!(
-                        "using unit '{unit}' in round option 'smallest' \
-                         requires that a relative reference time be given, \
-                         but none was provided",
-                        unit = smallest.singular(),
-                    ));
-                }
+                requires_relative_date_err(smallest)
+                    .context("error with `smallest` rounding option")?;
                 if let Some(largest) = self.largest {
-                    deprecation_warn_invariant_days(largest);
-                    if largest.is_definitively_variable() {
-                        return Err(err!(
-                            "using unit '{unit}' in round option 'largest' \
-                             requires that a relative reference time be \
-                             given, but none was provided",
-                            unit = largest.singular(),
-                        ));
-                    }
-                }
-                if existing_largest.is_definitively_variable() {
-                    return Err(err!(
-                        "using largest unit (which is '{unit}') in given span \
-                         requires that a relative reference time be given, \
-                         but none was provided",
-                        unit = existing_largest.singular(),
-                    ));
+                    requires_relative_date_err(largest)
+                        .context("error with `largest` rounding option")?;
                 }
-                assert!(max <= Unit::Day);
-                // TODO(0.2): turn this warning into an error.
+                requires_relative_date_err(existing_largest).context(
+                    "error with largest unit in span to be rounded",
+                )?;
+                assert!(max <= Unit::Week);
                 return Ok(round_span_invariant(
                     span, smallest, largest, increment, mode,
                 )?);
@@ -5483,18 +5498,21 @@ impl<'a> SpanRelativeTo<'a> {
     /// * [`Span::compare`]
     /// * [`Span::total`]
     /// * [`Span::round`]
-    /// * [`Span::to_jiff_duration`]
+    /// * [`Span::to_duration`]
     ///
-    /// Specifically, in `jiff 0.1`, the above APIs permitted _silently_
-    /// assuming that days are always 24 hours when a relative reference date
-    /// wasn't provided. This functionality has been deprecated, and in `jiff
-    /// 0.2`, this will turn into an error.
+    /// Specifically, in a previous version of Jiff, the above APIs permitted
+    /// _silently_ assuming that days are always 24 hours when a relative
+    /// reference date wasn't provided. In the current version of Jiff, this
+    /// silent interpretation no longer happens and instead an error will
+    /// occur.
     ///
     /// If you need to use these APIs with spans that contain non-zero units
-    /// of days but without a relative reference time, then you may use
-    /// this routine to create a special marker for `SpanRelativeTo` that
+    /// of days or weeks but without a relative reference date, then you may
+    /// use this routine to create a special marker for `SpanRelativeTo` that
     /// permits the APIs above to assume days are always 24 hours.
     ///
+    /// # Motivation
+    ///
     /// The purpose of the marker is two-fold:
     ///
     /// * Requiring the marker is important for improving the consistency of
@@ -5505,20 +5523,21 @@ impl<'a> SpanRelativeTo<'a> {
     /// 24 hours if no relative reference time was given and either span had
     /// non-zero units of days. With this marker, APIs _never_ assume days are
     /// always 24 hours automatically.
-    /// * When it _is_ appropriate to assume all days are hours (for example,
-    /// when only dealing with spans derived from [`civil`](crate::civil)
-    /// datetimes) and where providing a relative reference datetime doesn't
-    /// make sense. In this case, one _could_ provide a "dummy" reference date
-    /// since the precise date in civil time doesn't impact the length of a
-    /// day. But a marker like the one returned here is more explicit for the
-    /// purpose of assuming days are always 24 hours.
+    /// * When it _is_ appropriate to assume all days are 24 hours
+    /// (for example, when only dealing with spans derived from
+    /// [`civil`](crate::civil) datetimes) and where providing a relative
+    /// reference datetime doesn't make sense. In this case, one _could_
+    /// provide a "dummy" reference date since the precise date in civil time
+    /// doesn't impact the length of a day. But a marker like the one returned
+    /// here is more explicit for the purpose of assuming days are always 24
+    /// hours.
     ///
     /// With that said, ideally, callers should provide a relative reference
     /// datetime if possible.
     ///
     /// See [Issue #48] for more discussion on this topic.
     ///
-    /// # Example
+    /// # Example: different interpretations of "1 day"
     ///
     /// This example shows how "1 day" can be interpreted differently via the
     /// [`Span::total`] API:
@@ -5528,10 +5547,15 @@ impl<'a> SpanRelativeTo<'a> {
     ///
     /// let span = 1.day();
     ///
-    /// // In `jiff 0.2`, this will return an error.
-    /// let hours = span.total(Unit::Hour)?;
-    /// assert_eq!(hours, 24.0);
-    /// // In `jiff 0.2`, this will continue to work.
+    /// // An error because days aren't always 24 hours:
+    /// assert_eq!(
+    ///     span.total(Unit::Hour).unwrap_err().to_string(),
+    ///     "using unit 'day' in a span or configuration requires that either \
+    ///      a relative reference time be given or \
+    ///      `SpanRelativeTo::days_are_24_hours()` is used to indicate \
+    ///      invariant 24-hour days, but neither were provided",
+    /// );
+    /// // Opt into invariant 24 hour days without a relative date:
     /// let marker = SpanRelativeTo::days_are_24_hours();
     /// let hours = span.total((Unit::Hour, marker))?;
     /// assert_eq!(hours, 24.0);
@@ -5549,31 +5573,47 @@ impl<'a> SpanRelativeTo<'a> {
     ///
     /// Similar behavior applies to the other APIs listed above.
     ///
-    /// # Example: working with [`civil::Date`](crate::civil::Date)
+    /// # Example: different interpretations of "1 week"
     ///
-    /// A `Span` returned by computing the difference in time between two
-    /// [`civil::Date`](crate::civil::Date)s will have a non-zero number of
-    /// days. Previously, if one wanted to add spans returned by these APIs,
-    /// you could do so without futzing with relative dates:
+    /// This example shows how "1 week" can be interpreted differently via the
+    /// [`Span::total`] API:
     ///
     /// ```
-    /// use jiff::{civil::date, ToSpan};
+    /// use jiff::{SpanRelativeTo, ToSpan, Unit, Zoned};
     ///
-    /// let d1 = date(2025, 1, 18);
-    /// let d2 = date(2025, 1, 26);
-    /// let d3 = date(2025, 2, 14);
+    /// let span = 1.week();
     ///
-    /// let span1 = d2 - d1;
-    /// let span2 = d3 - d2;
-    /// // This will return an error in `jiff 0.2`:
-    /// let total = span1.checked_add(span2)?;
-    /// assert_eq!(total, 27.days().fieldwise());
+    /// // An error because days aren't always 24 hours:
+    /// assert_eq!(
+    ///     span.total(Unit::Hour).unwrap_err().to_string(),
+    ///     "using unit 'week' in a span or configuration requires that either \
+    ///      a relative reference time be given or \
+    ///      `SpanRelativeTo::days_are_24_hours()` is used to indicate \
+    ///      invariant 24-hour days, but neither were provided",
+    /// );
+    /// // Opt into invariant 24 hour days without a relative date:
+    /// let marker = SpanRelativeTo::days_are_24_hours();
+    /// let hours = span.total((Unit::Hour, marker))?;
+    /// assert_eq!(hours, 168.0);
+    /// // Weeks can be shorter than 24*7 hours:
+    /// let zdt: Zoned = "2024-03-10[America/New_York]".parse()?;
+    /// let hours = span.total((Unit::Hour, &zdt))?;
+    /// assert_eq!(hours, 167.0);
+    /// // Weeks can be longer than 24*7 hours:
+    /// let zdt: Zoned = "2024-11-03[America/New_York]".parse()?;
+    /// let hours = span.total((Unit::Hour, &zdt))?;
+    /// assert_eq!(hours, 169.0);
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     ///
-    /// But the above will stop working in `jiff 0.2`. The arithmetic call will
-    /// return an error. Instead, you can either provide a relative date:
+    /// # Example: working with [`civil::Date`](crate::civil::Date)
+    ///
+    /// A `Span` returned by computing the difference in time between two
+    /// [`civil::Date`](crate::civil::Date)s will have a non-zero number of
+    /// days. In older versions of Jiff, if one wanted to add spans returned by
+    /// these APIs, you could do so without futzing with relative dates. But
+    /// now you either need to provide a relative date:
     ///
     /// ```
     /// use jiff::{civil::date, ToSpan};
@@ -5584,7 +5624,6 @@ impl<'a> SpanRelativeTo<'a> {
     ///
     /// let span1 = d2 - d1;
     /// let span2 = d3 - d2;
-    /// // This will continue to work in `jiff 0.2`:
     /// let total = span1.checked_add((span2, d1))?;
     /// assert_eq!(total, 27.days().fieldwise());
     ///
@@ -5604,7 +5643,6 @@ impl<'a> SpanRelativeTo<'a> {
     ///
     /// let span1 = d2 - d1;
     /// let span2 = d3 - d2;
-    /// // This will continue to work in `jiff 0.2`:
     /// let total = span1.checked_add(
     ///     (span2, SpanRelativeTo::days_are_24_hours()),
     /// )?;
@@ -5615,28 +5653,11 @@ impl<'a> SpanRelativeTo<'a> {
     ///
     /// [Issue #48]: https://github.com/BurntSushi/jiff/issues/48
     #[inline]
-    pub fn days_are_24_hours() -> SpanRelativeTo<'static> {
+    pub const fn days_are_24_hours() -> SpanRelativeTo<'static> {
         let kind = SpanRelativeToKind::DaysAre24Hours;
         SpanRelativeTo { kind }
     }
 
-    /// Returns true when the given unit is variable relative to this reference
-    /// point.
-    ///
-    /// In effect, days and greater are variable when the reference point has
-    /// a time zone. Otherwise, only weeks and greater are variable.
-    ///
-    /// TODO(0.2): I believe this routine should go away completely in lieu
-    /// of just using `Unit::is_definitively_variable`. And this case analysis
-    /// should shift to `SpanRelativeTo::to_relative` I think.
-    fn is_variable(&self, unit: Unit) -> bool {
-        if unit.is_definitively_variable() {
-            return true;
-        }
-        unit == Unit::Day
-            && matches!(self.kind, SpanRelativeToKind::Zoned { .. })
-    }
-
     /// Converts this public API relative datetime into a more versatile
     /// internal representation of the same concept.
     ///
@@ -5649,7 +5670,8 @@ impl<'a> SpanRelativeTo<'a> {
     ///
     /// This can return `None` if this `SpanRelativeTo` isn't actually a
     /// datetime but a "marker" indicating some unit (like days) should be
-    /// treated as invariant.
+    /// treated as invariant. Or `None` is returned when the given unit is
+    /// always invariant (hours or smaller).
     ///
     /// # Errors
     ///
@@ -5657,7 +5679,7 @@ impl<'a> SpanRelativeTo<'a> {
     /// returned. In practice, this only occurs for a civil datetime near the
     /// civil datetime minimum and maximum values.
     fn to_relative(&self, unit: Unit) -> Result<Option<Relative<'a>>, Error> {
-        if !self.is_variable(unit) {
+        if !unit.is_variable() {
             return Ok(None);
         }
         match self.kind {
@@ -5670,13 +5692,13 @@ impl<'a> SpanRelativeTo<'a> {
                 })))
             }
             SpanRelativeToKind::DaysAre24Hours => {
-                if matches!(unit, Unit::Year | Unit::Month | Unit::Week) {
+                if matches!(unit, Unit::Year | Unit::Month) {
                     return Err(err!(
                         "using unit '{unit}' in span or configuration \
                          requires that a relative reference time be given \
-                         (`Span::days_are_24_hours()` was given but this \
-                         only permits using days without a relative reference \
-                         time)",
+                         (`SpanRelativeTo::days_are_24_hours()` was given \
+                         but this only permits using days and weeks \
+                         without a relative reference time)",
                         unit = unit.singular(),
                     ));
                 }
@@ -5989,18 +6011,6 @@ enum RelativeSpanKind<'a> {
 }
 
 impl<'a> RelativeSpanKind<'a> {
-    /// Returns true when the given unit is variable relative to this reference
-    /// span.
-    ///
-    /// In effect, days and greater are variable when the reference point has
-    /// a time zone. Otherwise, only weeks and greater are variable.
-    fn is_variable(&self, unit: Unit) -> bool {
-        if unit.is_definitively_variable() {
-            return true;
-        }
-        unit == Unit::Day && matches!(*self, RelativeSpanKind::Zoned { .. })
-    }
-
     /// Create a balanced `RelativeSpan` from this range of time.
     ///
     /// # Errors
@@ -6284,7 +6294,7 @@ impl Nudge {
         mode: RoundMode,
     ) -> Result<Nudge, Error> {
         // Ensures this is only called when rounding invariant units.
-        assert!(smallest <= Unit::Day);
+        assert!(smallest <= Unit::Week);
 
         let sign = balanced.get_sign_ranged();
         let balanced_nanos = balanced.to_invariant_nanoseconds();
@@ -6518,8 +6528,8 @@ fn round_span_invariant(
     increment: NoUnits128,
     mode: RoundMode,
 ) -> Result<Span, Error> {
-    assert!(smallest <= Unit::Day);
-    assert!(largest <= Unit::Day);
+    assert!(smallest <= Unit::Week);
+    assert!(largest <= Unit::Week);
     let nanos = span.to_invariant_nanoseconds();
     let rounded =
         mode.round_by_unit_in_nanoseconds(nanos, smallest, increment);
@@ -6619,33 +6629,25 @@ fn parse_iso_or_friendly(bytes: &[u8]) -> Result<Span, Error> {
     }
 }
 
-#[inline(always)]
-fn deprecation_warn_invariant_days(_unit: Unit) {
-    #[cfg(feature = "logging")]
-    {
-        if _unit == Unit::Day {
-            warn!(
-                "Operations on `Span` that silently assume days are always \
-                 24 hours are DEPRECATED and will turn into a hard error \
-                 in `jiff 0.2`. Please use \
-                 `SpanRelativeTo::days_are_24_hours()` \
-                 or provide a relative datetime instead.",
-            );
-        }
-    }
-}
-
 fn requires_relative_date_err(unit: Unit) -> Result<(), Error> {
-    if unit.is_definitively_variable() {
-        // TODO(0.2): When the largest unit is days, we should modify this
-        // to suggest `SpanRelativeTo::days_are_24_hours` if it's undesirable
-        // to provide a relative date.
-        return Err(err!(
-            "using unit '{unit}' in a span or configuration \
-             requires that a relative reference time be given, \
-             but none was provided",
-            unit = unit.singular(),
-        ));
+    if unit.is_variable() {
+        return Err(if matches!(unit, Unit::Week | Unit::Day) {
+            err!(
+                "using unit '{unit}' in a span or configuration \
+                 requires that either a relative reference time be given \
+                 or `SpanRelativeTo::days_are_24_hours()` is used to \
+                 indicate invariant 24-hour days, \
+                 but neither were provided",
+                unit = unit.singular(),
+            )
+        } else {
+            err!(
+                "using unit '{unit}' in a span or configuration \
+                 requires that a relative reference time be given, \
+                 but none was provided",
+                unit = unit.singular(),
+            )
+        });
     }
     Ok(())
 }
@@ -6671,7 +6673,9 @@ mod tests {
         assert_eq!(total, 469200.0);
 
         let span = 123456789.seconds();
-        let total = span.total(Unit::Day).unwrap();
+        let total = span
+            .total(SpanTotal::from(Unit::Day).days_are_24_hours())
+            .unwrap();
         assert_eq!(total, 1428.8980208333332);
 
         let span = 2756.hours();
@@ -6689,11 +6693,19 @@ mod tests {
             return;
         }
 
+        let span1 = 79.hours().minutes(10);
+        let span2 = 79.hours().seconds(630);
+        let span3 = 78.hours().minutes(50);
+        let mut array = [span1, span2, span3];
+        array.sort_by(|sp1, sp2| sp1.compare(sp2).unwrap());
+        assert_eq!(array, [span3, span1, span2].map(SpanFieldwise));
+
+        let day24 = SpanRelativeTo::days_are_24_hours();
         let span1 = 79.hours().minutes(10);
         let span2 = 3.days().hours(7).seconds(630);
         let span3 = 3.days().hours(6).minutes(50);
         let mut array = [span1, span2, span3];
-        array.sort_by(|sp1, sp2| sp1.compare(sp2).unwrap());
+        array.sort_by(|sp1, sp2| sp1.compare((sp2, day24)).unwrap());
         assert_eq!(array, [span3, span1, span2].map(SpanFieldwise));
 
         let dt = date(2020, 11, 1).at(0, 0, 0, 0);
@@ -6768,7 +6780,9 @@ mod tests {
         span_eq!(rounded, 8_000_000.microseconds());
 
         let span = 130.minutes();
-        let rounded = span.round(SpanRound::new().largest(Unit::Day)).unwrap();
+        let rounded = span
+            .round(SpanRound::new().largest(Unit::Day).days_are_24_hours())
+            .unwrap();
         span_eq!(rounded, 2.hours().minutes(10));
 
         let span = 10.minutes().seconds(52);
diff --git a/src/timestamp.rs b/src/timestamp.rs
index 0f297652..611529c2 100644
--- a/src/timestamp.rs
+++ b/src/timestamp.rs
@@ -658,15 +658,6 @@ impl Timestamp {
     /// Positive durations result in a timestamp after the Unix epoch. Negative
     /// durations result in a timestamp before the Unix epoch.
     ///
-    /// # Planned breaking change
-    ///
-    /// It is planned to rename this routine to `Timestamp::from_duration` in
-    /// `jiff 0.2`. The current `Timestamp::from_duration` routine, which
-    /// accepts a `std::time::Duration`, will be removed. If callers need to
-    /// build a `Timestamp` from a `std::time::Duration`, then they should
-    /// first convert it to a `SignedDuration` via
-    /// `TryFrom<Duration> for SignedDuration`.
-    ///
     /// # Errors
     ///
     /// This returns an error if the given duration corresponds to a timestamp
@@ -683,7 +674,7 @@ impl Timestamp {
     /// let unix_epoch = SystemTime::UNIX_EPOCH;
     /// let now = SystemTime::now();
     /// let duration = SignedDuration::system_until(unix_epoch, now)?;
-    /// let ts = Timestamp::from_jiff_duration(duration)?;
+    /// let ts = Timestamp::from_duration(duration)?;
     /// assert!(ts > Timestamp::UNIX_EPOCH);
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
@@ -693,7 +684,7 @@ impl Timestamp {
     /// instead. Indeed, the above example is copied almost exactly from the
     /// `TryFrom` implementation.
     #[inline]
-    pub fn from_jiff_duration(
+    pub fn from_duration(
         duration: SignedDuration,
     ) -> Result<Timestamp, Error> {
         Timestamp::new(duration.as_secs(), duration.subsec_nanos())
@@ -908,37 +899,28 @@ impl Timestamp {
 
     /// Returns this timestamp as a [`SignedDuration`] since the Unix epoch.
     ///
-    /// # Planned breaking change
-    ///
-    /// It is planned to rename this routine to `Timestamp::as_duration` in
-    /// `jiff 0.2`. The current `Timestamp::as_duration` routine, which returns
-    /// a `std::time::Duration`, will be removed. If callers need a
-    /// `std::time::Duration` from a `Timestamp`, then they should call this
-    /// routine and then use `TryFrom<SignedDuration> for Duration` to convert
-    /// the result.
-    ///
     /// # Example
     ///
     /// ```
     /// use jiff::{SignedDuration, Timestamp};
     ///
     /// assert_eq!(
-    ///     Timestamp::UNIX_EPOCH.as_jiff_duration(),
+    ///     Timestamp::UNIX_EPOCH.as_duration(),
     ///     SignedDuration::ZERO,
     /// );
     /// assert_eq!(
-    ///     Timestamp::new(5, 123_456_789)?.as_jiff_duration(),
+    ///     Timestamp::new(5, 123_456_789)?.as_duration(),
     ///     SignedDuration::new(5, 123_456_789),
     /// );
     /// assert_eq!(
-    ///     Timestamp::new(-5, -123_456_789)?.as_jiff_duration(),
+    ///     Timestamp::new(-5, -123_456_789)?.as_duration(),
     ///     SignedDuration::new(-5, -123_456_789),
     /// );
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     #[inline]
-    pub fn as_jiff_duration(self) -> SignedDuration {
+    pub fn as_duration(self) -> SignedDuration {
         SignedDuration::from_timestamp(self)
     }
 
@@ -1284,11 +1266,11 @@ impl Timestamp {
         self,
         duration: SignedDuration,
     ) -> Result<Timestamp, Error> {
-        let start = self.as_jiff_duration();
+        let start = self.as_duration();
         let end = start.checked_add(duration).ok_or_else(|| {
             err!("overflow when adding {duration:?} to {self}")
         })?;
-        Timestamp::from_jiff_duration(end)
+        Timestamp::from_duration(end)
     }
 
     /// This routine is identical to [`Timestamp::checked_add`] with the
@@ -1352,12 +1334,10 @@ impl Timestamp {
     /// result saturates on overflow. That is, instead of overflow, either
     /// [`Timestamp::MIN`] or [`Timestamp::MAX`] is returned.
     ///
-    /// # Panics
+    /// # Errors
     ///
-    /// This panics if the given `Span` contains any non-zero units greater
-    /// than hours. In `jiff 0.2`, this panic will be changed to an error. It
-    /// panics in `jiff 0.1` to avoid giving incorrect results. (It was an
-    /// oversight to make this method infallible.)
+    /// This returns an error if the given `Span` contains any non-zero units
+    /// greater than hours.
     ///
     /// # Example
     ///
@@ -1368,51 +1348,45 @@ impl Timestamp {
     ///
     /// assert_eq!(
     ///     Timestamp::MAX,
-    ///     Timestamp::MAX.saturating_add(1.nanosecond()),
+    ///     Timestamp::MAX.saturating_add(1.nanosecond())?,
     /// );
     /// assert_eq!(
     ///     Timestamp::MIN,
-    ///     Timestamp::MIN.saturating_add(-1.nanosecond()),
+    ///     Timestamp::MIN.saturating_add(-1.nanosecond())?,
     /// );
     /// assert_eq!(
     ///     Timestamp::MAX,
-    ///     Timestamp::UNIX_EPOCH.saturating_add(SignedDuration::MAX),
+    ///     Timestamp::UNIX_EPOCH.saturating_add(SignedDuration::MAX)?,
     /// );
     /// assert_eq!(
     ///     Timestamp::MIN,
-    ///     Timestamp::UNIX_EPOCH.saturating_add(SignedDuration::MIN),
+    ///     Timestamp::UNIX_EPOCH.saturating_add(SignedDuration::MIN)?,
     /// );
     /// assert_eq!(
     ///     Timestamp::MAX,
-    ///     Timestamp::UNIX_EPOCH.saturating_add(std::time::Duration::MAX),
+    ///     Timestamp::UNIX_EPOCH.saturating_add(std::time::Duration::MAX)?,
     /// );
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     #[inline]
     pub fn saturating_add<A: Into<TimestampArithmetic>>(
         self,
         duration: A,
-    ) -> Timestamp {
+    ) -> Result<Timestamp, Error> {
         let duration: TimestampArithmetic = duration.into();
-        match duration.saturating_add(self) {
-            Ok(ts) => ts,
-            Err(err) => {
-                // FIXME: This should be an actual error.
-                // We'll do that in jiff 0.2.
-                panic!(
-                    "saturating Timestamp arithmetic \
-                     requires only time units: {err}"
-                )
-            }
-        }
+        duration.saturating_add(self).context(
+            "saturating `Timestamp` arithmetic requires only time units",
+        )
     }
 
     /// This routine is identical to [`Timestamp::saturating_add`] with the
     /// span parameter negated.
     ///
-    /// # Panics
+    /// # Errors
     ///
-    /// This routine panics under the same conditions as
-    /// [`Timestamp::saturating_add`].
+    /// This returns an error if the given `Span` contains any non-zero units
+    /// greater than hours.
     ///
     /// # Example
     ///
@@ -1423,33 +1397,35 @@ impl Timestamp {
     ///
     /// assert_eq!(
     ///     Timestamp::MIN,
-    ///     Timestamp::MIN.saturating_sub(1.nanosecond()),
+    ///     Timestamp::MIN.saturating_sub(1.nanosecond())?,
     /// );
     /// assert_eq!(
     ///     Timestamp::MAX,
-    ///     Timestamp::MAX.saturating_sub(-1.nanosecond()),
+    ///     Timestamp::MAX.saturating_sub(-1.nanosecond())?,
     /// );
     /// assert_eq!(
     ///     Timestamp::MIN,
-    ///     Timestamp::UNIX_EPOCH.saturating_sub(SignedDuration::MAX),
+    ///     Timestamp::UNIX_EPOCH.saturating_sub(SignedDuration::MAX)?,
     /// );
     /// assert_eq!(
     ///     Timestamp::MAX,
-    ///     Timestamp::UNIX_EPOCH.saturating_sub(SignedDuration::MIN),
+    ///     Timestamp::UNIX_EPOCH.saturating_sub(SignedDuration::MIN)?,
     /// );
     /// assert_eq!(
     ///     Timestamp::MIN,
-    ///     Timestamp::UNIX_EPOCH.saturating_sub(std::time::Duration::MAX),
+    ///     Timestamp::UNIX_EPOCH.saturating_sub(std::time::Duration::MAX)?,
     /// );
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     #[inline]
     pub fn saturating_sub<A: Into<TimestampArithmetic>>(
         self,
         duration: A,
-    ) -> Timestamp {
+    ) -> Result<Timestamp, Error> {
         let duration: TimestampArithmetic = duration.into();
         let Ok(duration) = duration.checked_neg() else {
-            return Timestamp::MIN;
+            return Ok(Timestamp::MIN);
         };
         self.saturating_add(duration)
     }
@@ -2001,172 +1977,6 @@ impl Timestamp {
     }
 }
 
-/// Deprecated APIs on `Timestamp`.
-impl Timestamp {
-    /// Creates a new instant from a `Duration` since the Unix epoch.
-    ///
-    /// A `Duration` is always positive. If you need to construct
-    /// a timestamp before the Unix epoch with a `Duration`, use
-    /// [`Timestamp::from_signed_duration`].
-    ///
-    /// # Errors
-    ///
-    /// This returns an error if the given duration corresponds to a timestamp
-    /// greater than [`Timestamp::MAX`].
-    ///
-    /// # Example
-    ///
-    /// How one might construct a `Timestamp` from a `SystemTime` if one can
-    /// assume the time is after the Unix epoch:
-    ///
-    /// ```
-    /// use std::time::SystemTime;
-    /// use jiff::Timestamp;
-    ///
-    /// let elapsed = SystemTime::UNIX_EPOCH.elapsed()?;
-    /// assert!(Timestamp::from_duration(elapsed).is_ok());
-    ///
-    /// # Ok::<(), Box<dyn std::error::Error>>(())
-    /// ```
-    ///
-    /// Of course, one should just use [`Timestamp::try_from`] for this
-    /// instead.
-    #[deprecated(
-        since = "0.1.5",
-        note = "use Timestamp::from_jiff_duration instead"
-    )]
-    #[inline]
-    pub fn from_duration(
-        duration: core::time::Duration,
-    ) -> Result<Timestamp, Error> {
-        #[allow(deprecated)]
-        Timestamp::from_signed_duration(1, duration)
-    }
-
-    /// Creates a new timestamp from a `Duration` with the given sign since the
-    /// Unix epoch.
-    ///
-    /// Positive durations result in a timestamp after the Unix epoch. Negative
-    /// durations result in a timestamp before the Unix epoch.
-    ///
-    /// # Errors
-    ///
-    /// This returns an error if the given duration corresponds to a timestamp
-    /// outside of the [`Timestamp::MIN`] and [`Timestamp::MAX`] boundaries.
-    ///
-    /// # Example
-    ///
-    /// How one might construct a `Timestamp` from a `SystemTime`:
-    ///
-    /// ```
-    /// use std::time::SystemTime;
-    /// use jiff::Timestamp;
-    ///
-    /// let unix_epoch = SystemTime::UNIX_EPOCH;
-    /// let now = SystemTime::now();
-    /// let (duration, sign) = match now.duration_since(unix_epoch) {
-    ///     Ok(duration) => (duration, 1),
-    ///     Err(err) => (err.duration(), -1),
-    /// };
-    ///
-    /// let ts = Timestamp::from_signed_duration(sign, duration)?;
-    /// assert!(ts > Timestamp::UNIX_EPOCH);
-    ///
-    /// # Ok::<(), Box<dyn std::error::Error>>(())
-    /// ```
-    ///
-    /// # Example: a sign of 0 always results in `Timestamp::UNIX_EPOCH`
-    ///
-    /// ```
-    /// use jiff::Timestamp;
-    ///
-    /// let duration = std::time::Duration::new(5, 123_456_789);
-    /// let ts = Timestamp::from_signed_duration(0, duration)?;
-    /// assert_eq!(ts, Timestamp::UNIX_EPOCH);
-    ///
-    /// # Ok::<(), Box<dyn std::error::Error>>(())
-    /// ```
-    #[deprecated(
-        since = "0.1.5",
-        note = "use Timestamp::from_jiff_duration instead"
-    )]
-    #[inline]
-    pub fn from_signed_duration(
-        sign: i8,
-        duration: core::time::Duration,
-    ) -> Result<Timestamp, Error> {
-        let sign = sign.signum();
-        let seconds = i64::try_from(duration.as_secs()).map_err(|_| {
-            err!(
-                "could not convert unsigned `Duration` of `{} seconds` \
-                 to signed 64-bit integer",
-                duration.as_secs(),
-            )
-        })?;
-        let nanos = i32::try_from(duration.subsec_nanos())
-            .expect("nanoseconds in duration are less than 1,000,000,000");
-        // NOTE: Can multiplication actually fail here? I think if `seconds` is
-        // `i64::MIN`? But, no, I don't think so. Since `duration` is always
-        // positive.
-        Timestamp::new(seconds * i64::from(sign), nanos * i32::from(sign))
-    }
-
-    /// Returns this timestamp as a standard library
-    /// [`Duration`](std::time::Duration) since the Unix epoch.
-    ///
-    /// Since a `Duration` is unsigned and a `Timestamp` is signed, this
-    /// also returns the sign of this timestamp (`-1`, `0` or `1`) along with
-    /// the unsigned `Duration`. A negative sign means the duration should be
-    /// subtracted from the Unix epoch. A positive sign means the duration
-    /// should be added to the Unix epoch. A zero sign means the duration is
-    /// the same precise instant as the Unix epoch.
-    ///
-    /// # Example
-    ///
-    /// ```
-    /// use std::time::Duration;
-    /// use jiff::Timestamp;
-    ///
-    /// assert_eq!(
-    ///     Timestamp::UNIX_EPOCH.as_duration(),
-    ///     (0, Duration::ZERO),
-    /// );
-    /// assert_eq!(
-    ///     Timestamp::new(5, 123_456_789)?.as_duration(),
-    ///     (1, Duration::new(5, 123_456_789)),
-    /// );
-    /// assert_eq!(
-    ///     Timestamp::new(-5, -123_456_789)?.as_duration(),
-    ///     (-1, Duration::new(5, 123_456_789)),
-    /// );
-    ///
-    /// # Ok::<(), Box<dyn std::error::Error>>(())
-    /// ```
-    #[deprecated(
-        since = "0.1.5",
-        note = "use Timestamp::as_jiff_duration instead"
-    )]
-    #[inline]
-    pub fn as_duration(self) -> (i8, core::time::Duration) {
-        let second = u64::try_from(self.as_second().abs())
-            .expect("absolute value of seconds fits in u64");
-        let nanosecond = u32::try_from(self.subsec_nanosecond().abs())
-            .expect("nanosecond always fit in a u32");
-        (self.signum(), core::time::Duration::new(second, nanosecond))
-    }
-
-    /// A deprecated equivalent to [`Timestamp::in_tz`].
-    ///
-    /// This will be removed in `jiff 0.2`. The method was renamed to make
-    /// it clearer that the name stood for "in time zone."
-    #[deprecated(since = "0.1.25", note = "use Timestamp::in_tz instead")]
-    #[inline]
-    pub fn intz(self, time_zone_name: &str) -> Result<Zoned, Error> {
-        let tz = crate::tz::db().get(time_zone_name)?;
-        Ok(self.to_zoned(tz))
-    }
-}
-
 /// Internal APIs using Jiff ranged integers.
 impl Timestamp {
     #[inline]
@@ -2682,7 +2492,7 @@ impl From<Timestamp> for std::time::SystemTime {
     #[inline]
     fn from(time: Timestamp) -> std::time::SystemTime {
         let unix_epoch = std::time::SystemTime::UNIX_EPOCH;
-        let sdur = time.as_jiff_duration();
+        let sdur = time.as_duration();
         let dur = sdur.unsigned_abs();
         // These are guaranteed to succeed because we assume that SystemTime
         // uses at least 64 bits for the time, and our durations are capped via
@@ -2705,7 +2515,7 @@ impl TryFrom<std::time::SystemTime> for Timestamp {
     ) -> Result<Timestamp, Error> {
         let unix_epoch = std::time::SystemTime::UNIX_EPOCH;
         let dur = SignedDuration::system_until(unix_epoch, system_time)?;
-        Timestamp::from_jiff_duration(dur)
+        Timestamp::from_duration(dur)
     }
 }
 
@@ -3701,15 +3511,19 @@ mod tests {
     }
 
     #[test]
-    #[should_panic]
     fn timestamp_saturating_add() {
-        Timestamp::MIN.saturating_add(Span::new().days(1));
+        insta::assert_snapshot!(
+            Timestamp::MIN.saturating_add(Span::new().days(1)).unwrap_err(),
+            @"saturating `Timestamp` arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero day units (operations on `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `Span`)",
+        )
     }
 
     #[test]
-    #[should_panic]
     fn timestamp_saturating_sub() {
-        Timestamp::MAX.saturating_sub(Span::new().days(1));
+        insta::assert_snapshot!(
+            Timestamp::MAX.saturating_sub(Span::new().days(1)).unwrap_err(),
+            @"saturating `Timestamp` arithmetic requires only time units: operation can only be performed with units of hours or smaller, but found non-zero day units (operations on `Timestamp`, `tz::Offset` and `civil::Time` don't support calendar units in a `Span`)",
+        )
     }
 
     quickcheck::quickcheck! {
diff --git a/src/tz/concatenated.rs b/src/tz/concatenated.rs
index 98d6ed5d..358fc375 100644
--- a/src/tz/concatenated.rs
+++ b/src/tz/concatenated.rs
@@ -1096,17 +1096,19 @@ mod tests {
             {
                 let (year, month, day, hour, min, sec, nano) = datetime;
                 let timestamp = Timestamp::new(unix_sec, unix_nano).unwrap();
-                let (got_offset, _, got_abbrev) = tz.to_offset(timestamp);
+                let info = tz.to_offset_info(timestamp);
                 assert_eq!(
-                    got_offset, offset,
+                    info.offset(),
+                    offset,
                     "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
                 );
                 assert_eq!(
-                    got_abbrev, abbrev,
+                    info.abbreviation(),
+                    abbrev,
                     "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
                 );
                 assert_eq!(
-                    got_offset.to_datetime(timestamp),
+                    info.offset().to_datetime(timestamp),
                     date(year, month, day).at(hour, min, sec, nano),
                     "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
                 );
diff --git a/src/tz/db/bundled/disabled.rs b/src/tz/db/bundled/disabled.rs
index 6432f330..7b0256a0 100644
--- a/src/tz/db/bundled/disabled.rs
+++ b/src/tz/db/bundled/disabled.rs
@@ -1,11 +1,11 @@
-use crate::tz::TimeZone;
+use crate::tz::{TimeZone, TimeZoneNameIter};
 
 #[derive(Clone)]
-pub(crate) struct BundledZoneInfo;
+pub(crate) struct Database;
 
-impl BundledZoneInfo {
-    pub(crate) fn new() -> BundledZoneInfo {
-        BundledZoneInfo
+impl Database {
+    pub(crate) fn new() -> Database {
+        Database
     }
 
     pub(crate) fn reset(&self) {}
@@ -14,9 +14,8 @@ impl BundledZoneInfo {
         None
     }
 
-    #[cfg(feature = "alloc")]
-    pub(crate) fn available(&self) -> alloc::vec::Vec<alloc::string::String> {
-        alloc::vec::Vec::new()
+    pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
+        TimeZoneNameIter::empty()
     }
 
     pub(crate) fn is_definitively_empty(&self) -> bool {
@@ -24,7 +23,7 @@ impl BundledZoneInfo {
     }
 }
 
-impl core::fmt::Debug for BundledZoneInfo {
+impl core::fmt::Debug for Database {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
         write!(f, "Bundled(unavailable)")
     }
diff --git a/src/tz/db/bundled/enabled.rs b/src/tz/db/bundled/enabled.rs
index 33e5daaa..8718a4a3 100644
--- a/src/tz/db/bundled/enabled.rs
+++ b/src/tz/db/bundled/enabled.rs
@@ -1,12 +1,10 @@
-use alloc::{string::String, vec::Vec};
+use crate::tz::{TimeZone, TimeZoneNameIter};
 
-use crate::tz::TimeZone;
+pub(crate) struct Database;
 
-pub(crate) struct BundledZoneInfo;
-
-impl BundledZoneInfo {
-    pub(crate) fn new() -> BundledZoneInfo {
-        BundledZoneInfo
+impl Database {
+    pub(crate) fn new() -> Database {
+        Database
     }
 
     pub(crate) fn reset(&self) {
@@ -19,6 +17,11 @@ impl BundledZoneInfo {
         if let Some(tz) = self::global::get(name) {
             return Some(tz);
         }
+        // Check for the special `Etc/Unknown` value, which isn't in the
+        // IANA time zone database.
+        if name == "Etc/Unknown" {
+            return Some(TimeZone::unknown());
+        }
         let (canonical_name, tzif) = lookup(name)?;
         let tz = match TimeZone::tzif(canonical_name, tzif) {
             Ok(tz) => tz,
@@ -36,8 +39,8 @@ impl BundledZoneInfo {
         Some(tz)
     }
 
-    pub(crate) fn available(&self) -> Vec<String> {
-        available().into_iter().map(String::from).collect()
+    pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
+        TimeZoneNameIter::from_iter(available())
     }
 
     pub(crate) fn is_definitively_empty(&self) -> bool {
@@ -45,7 +48,7 @@ impl BundledZoneInfo {
     }
 }
 
-impl core::fmt::Debug for BundledZoneInfo {
+impl core::fmt::Debug for Database {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
         write!(f, "Bundled(available)")
     }
diff --git a/src/tz/db/concatenated/disabled.rs b/src/tz/db/concatenated/disabled.rs
index 3ff18330..4da2274b 100644
--- a/src/tz/db/concatenated/disabled.rs
+++ b/src/tz/db/concatenated/disabled.rs
@@ -1,17 +1,17 @@
-use crate::tz::TimeZone;
+use crate::tz::{TimeZone, TimeZoneNameIter};
 
 #[derive(Clone)]
-pub(crate) struct Concatenated;
+pub(crate) struct Database;
 
-impl Concatenated {
-    pub(crate) fn from_env() -> Concatenated {
-        Concatenated
+impl Database {
+    pub(crate) fn from_env() -> Database {
+        Database
     }
 
     #[cfg(feature = "std")]
     pub(crate) fn from_path(
         path: &std::path::Path,
-    ) -> Result<Concatenated, crate::Error> {
+    ) -> Result<Database, crate::Error> {
         Err(crate::error::err!(
             "system concatenated tzdb unavailable: \
              crate feature `tzdb-concatenated` is disabled, \
@@ -20,8 +20,8 @@ impl Concatenated {
         ))
     }
 
-    pub(crate) fn none() -> Concatenated {
-        Concatenated
+    pub(crate) fn none() -> Database {
+        Database
     }
 
     pub(crate) fn reset(&self) {}
@@ -30,9 +30,8 @@ impl Concatenated {
         None
     }
 
-    #[cfg(feature = "alloc")]
-    pub(crate) fn available(&self) -> alloc::vec::Vec<alloc::string::String> {
-        alloc::vec::Vec::new()
+    pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
+        TimeZoneNameIter::empty()
     }
 
     pub(crate) fn is_definitively_empty(&self) -> bool {
@@ -40,7 +39,7 @@ impl Concatenated {
     }
 }
 
-impl core::fmt::Debug for Concatenated {
+impl core::fmt::Debug for Database {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
         write!(f, "Concatenated(unavailable)")
     }
diff --git a/src/tz/db/concatenated/enabled.rs b/src/tz/db/concatenated/enabled.rs
index 2e726898..ba21246e 100644
--- a/src/tz/db/concatenated/enabled.rs
+++ b/src/tz/db/concatenated/enabled.rs
@@ -15,7 +15,7 @@ use std::{
 use crate::{
     error::{err, Error},
     timestamp::Timestamp,
-    tz::{concatenated::ConcatenatedTzif, TimeZone},
+    tz::{concatenated::ConcatenatedTzif, TimeZone, TimeZoneNameIter},
     util::{self, array_str::ArrayStr, cache::Expiration, utf8},
 };
 
@@ -35,14 +35,14 @@ static TZDATA_LOCATIONS: &[TzdataLocation] = &[
     },
 ];
 
-pub(crate) struct Concatenated {
+pub(crate) struct Database {
     path: Option<PathBuf>,
     names: Option<Names>,
     zones: RwLock<CachedZones>,
 }
 
-impl Concatenated {
-    pub(crate) fn from_env() -> Concatenated {
+impl Database {
+    pub(crate) fn from_env() -> Database {
         let mut attempted = vec![];
         for loc in TZDATA_LOCATIONS {
             let path = loc.to_path_buf();
@@ -50,7 +50,7 @@ impl Concatenated {
                 "opening concatenated tzdata database at {}",
                 path.display()
             );
-            match Concatenated::from_path(&path) {
+            match Database::from_path(&path) {
                 Ok(db) => return db,
                 Err(_err) => {
                     trace!("failed opening {}: {_err}", path.display());
@@ -63,21 +63,21 @@ impl Concatenated {
              following paths: {}",
             attempted.join(", "),
         );
-        Concatenated::none()
+        Database::none()
     }
 
-    pub(crate) fn from_path(path: &Path) -> Result<Concatenated, Error> {
+    pub(crate) fn from_path(path: &Path) -> Result<Database, Error> {
         let names = Some(Names::new(path)?);
         let zones = RwLock::new(CachedZones::new());
-        Ok(Concatenated { path: Some(path.to_path_buf()), names, zones })
+        Ok(Database { path: Some(path.to_path_buf()), names, zones })
     }
 
     /// Creates a "dummy" zoneinfo database in which all lookups fail.
-    pub(crate) fn none() -> Concatenated {
+    pub(crate) fn none() -> Database {
         let path = None;
         let names = None;
         let zones = RwLock::new(CachedZones::new());
-        Concatenated { path, names, zones }
+        Database { path, names, zones }
     }
 
     pub(crate) fn reset(&self) {
@@ -94,6 +94,10 @@ impl Concatenated {
         if query == "UTC" {
             return Some(TimeZone::UTC);
         }
+        // Similarly for the special `Etc/Unknown` value.
+        if query == "Etc/Unknown" {
+            return Some(TimeZone::unknown());
+        }
         let path = self.path.as_ref()?;
         // The fast path is when the query matches a pre-existing unexpired
         // time zone.
@@ -185,10 +189,14 @@ impl Concatenated {
         }
     }
 
-    pub(crate) fn available(&self) -> Vec<String> {
-        let Some(path) = self.path.as_ref() else { return vec![] };
-        let Some(names) = self.names.as_ref() else { return vec![] };
-        names.available(path)
+    pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
+        let Some(path) = self.path.as_ref() else {
+            return TimeZoneNameIter::empty();
+        };
+        let Some(names) = self.names.as_ref() else {
+            return TimeZoneNameIter::empty();
+        };
+        TimeZoneNameIter::from_iter(names.available(path).into_iter())
     }
 
     pub(crate) fn is_definitively_empty(&self) -> bool {
@@ -196,7 +204,7 @@ impl Concatenated {
     }
 }
 
-impl core::fmt::Debug for Concatenated {
+impl core::fmt::Debug for Database {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
         write!(f, "Concatenated(")?;
         if let Some(ref path) = self.path {
@@ -561,7 +569,7 @@ mod tests {
         const ENV: &str = "JIFF_DEBUG_CONCATENATED_TZDATA";
         let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
         let path = PathBuf::from(val);
-        let db = Concatenated::from_path(&path)?;
+        let db = Database::from_path(&path)?;
         for name in db.available() {
             std::eprintln!("{name}");
         }
diff --git a/src/tz/db/mod.rs b/src/tz/db/mod.rs
index 23f313c6..3886b90d 100644
--- a/src/tz/db/mod.rs
+++ b/src/tz/db/mod.rs
@@ -4,10 +4,6 @@ use crate::{
     util::sync::Arc,
 };
 
-use self::{
-    bundled::BundledZoneInfo, concatenated::Concatenated, zoneinfo::ZoneInfo,
-};
-
 mod bundled;
 mod concatenated;
 mod zoneinfo;
@@ -81,6 +77,11 @@ pub fn db() -> &'static TimeZoneDatabase {
 /// feature is enabled, then the `jiff-tzdb` crate will be used to embed the
 /// entire Time Zone Database into the compiled artifact.
 ///
+/// On Android systems, and when the `tzdb-concatenated` crate feature is
+/// enabled (which it is by default), Jiff will attempt to read a concatenated
+/// zoneinfo database using the `ANDROID_DATA` or `ANDROID_ROOT` environment
+/// variables.
+///
 /// In general, using `/usr/share/zoneinfo` (or an equivalent) is heavily
 /// preferred in lieu of embedding the database into your compiled artifact.
 /// The reason is because your system copy of the Time Zone Database may be
@@ -185,16 +186,16 @@ pub fn db() -> &'static TimeZoneDatabase {
 /// ```
 #[derive(Clone)]
 pub struct TimeZoneDatabase {
-    inner: Option<Arc<TimeZoneDatabaseInner>>,
+    inner: Option<Arc<Kind>>,
 }
 
 #[derive(Debug)]
 // Needed for core-only "dumb" `Arc`.
 #[cfg_attr(not(feature = "alloc"), derive(Clone))]
-struct TimeZoneDatabaseInner {
-    zoneinfo: ZoneInfo,
-    concatenated: Concatenated,
-    bundled: BundledZoneInfo,
+enum Kind {
+    ZoneInfo(zoneinfo::Database),
+    Concatenated(concatenated::Database),
+    Bundled(bundled::Database),
 }
 
 impl TimeZoneDatabase {
@@ -245,18 +246,46 @@ impl TimeZoneDatabase {
     /// return an embedded copy of the Time Zone Database since Windows does
     /// not have a canonical installation of the Time Zone Database.
     pub fn from_env() -> TimeZoneDatabase {
-        let zoneinfo = ZoneInfo::from_env();
-        let concatenated = Concatenated::from_env();
-        let bundled = BundledZoneInfo::new();
-        let inner = TimeZoneDatabaseInner { zoneinfo, concatenated, bundled };
-        let db = TimeZoneDatabase { inner: Some(Arc::new(inner)) };
-        if db.is_definitively_empty() {
-            warn!(
-                "could not find zoneinfo, concatenated tzdata or \
-                 bundled time zone database",
-            );
+        // On Android, try the concatenated database first, since that's
+        // typically what is used.
+        //
+        // Overall this logic might be sub-optimal. Like, does it really make
+        // sense to check for the zoneinfo or concatenated database on non-Unix
+        // platforms? Probably not to be honest. But these should only be
+        // executed ~once generally, so it doesn't seem like a big deal to try.
+        // And trying makes things a little more flexible I think.
+        if cfg!(target_os = "android") {
+            let db = concatenated::Database::from_env();
+            if !db.is_definitively_empty() {
+                return TimeZoneDatabase::new(Kind::Concatenated(db));
+            }
+
+            let db = zoneinfo::Database::from_env();
+            if !db.is_definitively_empty() {
+                return TimeZoneDatabase::new(Kind::ZoneInfo(db));
+            }
+        } else {
+            let db = zoneinfo::Database::from_env();
+            if !db.is_definitively_empty() {
+                return TimeZoneDatabase::new(Kind::ZoneInfo(db));
+            }
+
+            let db = concatenated::Database::from_env();
+            if !db.is_definitively_empty() {
+                return TimeZoneDatabase::new(Kind::Concatenated(db));
+            }
+        }
+
+        let db = bundled::Database::new();
+        if !db.is_definitively_empty() {
+            return TimeZoneDatabase::new(Kind::Bundled(db));
         }
-        db
+
+        warn!(
+            "could not find zoneinfo, concatenated tzdata or \
+             bundled time zone database",
+        );
+        TimeZoneDatabase::none()
     }
 
     /// Returns a time zone database initialized from the given directory.
@@ -279,19 +308,14 @@ impl TimeZoneDatabase {
         path: P,
     ) -> Result<TimeZoneDatabase, Error> {
         let path = path.as_ref();
-        let zoneinfo = ZoneInfo::from_dir(path)?;
-        let concatenated = Concatenated::none();
-        let bundled = BundledZoneInfo::new();
-        let inner = TimeZoneDatabaseInner { zoneinfo, concatenated, bundled };
-        let db = TimeZoneDatabase { inner: Some(Arc::new(inner)) };
+        let db = zoneinfo::Database::from_dir(path)?;
         if db.is_definitively_empty() {
             warn!(
-                "could not find zoneinfo data at directory {path} \
-                 (and there is no bundled time zone database)",
+                "could not find zoneinfo data at directory {path}",
                 path = path.display(),
             );
         }
-        Ok(db)
+        Ok(TimeZoneDatabase::new(Kind::ZoneInfo(db)))
     }
 
     /// Returns a time zone database initialized from a path pointing to a
@@ -322,19 +346,43 @@ impl TimeZoneDatabase {
         path: P,
     ) -> Result<TimeZoneDatabase, Error> {
         let path = path.as_ref();
-        let zoneinfo = ZoneInfo::none();
-        let concatenated = Concatenated::from_path(path)?;
-        let bundled = BundledZoneInfo::new();
-        let inner = TimeZoneDatabaseInner { zoneinfo, concatenated, bundled };
-        let db = TimeZoneDatabase { inner: Some(Arc::new(inner)) };
+        let db = concatenated::Database::from_path(path)?;
         if db.is_definitively_empty() {
             warn!(
-                "could not find concatenated tzdata in file {path} \
-                 (and there is no bundled time zone database)",
+                "could not find concatenated tzdata in file {path}",
                 path = path.display(),
             );
         }
-        Ok(db)
+        Ok(TimeZoneDatabase::new(Kind::Concatenated(db)))
+    }
+
+    /// Returns a time zone database initialized from the bundled copy of
+    /// the [IANA Time Zone Database].
+    ///
+    /// While this API is always available, in order to get a non-empty
+    /// database back, this requires that one of the crate features
+    /// `tzdb-bundle-always` or `tzdb-bundle-platform` is enabled. In the
+    /// latter case, the bundled database is only available on platforms known
+    /// to lack a system copy of the IANA Time Zone Database (i.e., non-Unix
+    /// systems).
+    ///
+    /// This routine is infallible, but it may return a database
+    /// that is definitively empty if the bundled data is not
+    /// available. To query whether the data is empty or not, use
+    /// [`TimeZoneDatabase::is_definitively_empty`].
+    ///
+    /// [IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
+    pub fn bundled() -> TimeZoneDatabase {
+        let db = bundled::Database::new();
+        if db.is_definitively_empty() {
+            warn!("could not find embedded/bundled zoneinfo");
+        }
+        TimeZoneDatabase::new(Kind::Bundled(db))
+    }
+
+    /// Creates a new DB from the internal kind.
+    fn new(kind: Kind) -> TimeZoneDatabase {
+        TimeZoneDatabase { inner: Some(Arc::new(kind)) }
     }
 
     /// Returns a [`TimeZone`] corresponding to the IANA time zone identifier
@@ -372,17 +420,25 @@ impl TimeZoneDatabase {
                 )
             }
         })?;
-        if let Some(tz) = inner.zoneinfo.get(name) {
-            trace!("found time zone `{name}` in {:?}", inner.zoneinfo);
-            return Ok(tz);
-        }
-        if let Some(tz) = inner.concatenated.get(name) {
-            trace!("found time zone `{name}` in {:?}", inner.concatenated);
-            return Ok(tz);
-        }
-        if let Some(tz) = inner.bundled.get(name) {
-            trace!("found time zone `{name}` in {:?}", inner.bundled);
-            return Ok(tz);
+        match *inner {
+            Kind::ZoneInfo(ref db) => {
+                if let Some(tz) = db.get(name) {
+                    trace!("found time zone `{name}` in {db:?}", db = self);
+                    return Ok(tz);
+                }
+            }
+            Kind::Concatenated(ref db) => {
+                if let Some(tz) = db.get(name) {
+                    trace!("found time zone `{name}` in {db:?}", db = self);
+                    return Ok(tz);
+                }
+            }
+            Kind::Bundled(ref db) => {
+                if let Some(tz) = db.get(name) {
+                    trace!("found time zone `{name}` in {db:?}", db = self);
+                    return Ok(tz);
+                }
+            }
         }
         Err(err!("failed to find time zone `{name}` in time zone database"))
     }
@@ -405,19 +461,15 @@ impl TimeZoneDatabase {
     ///     println!("{tzid}");
     /// }
     /// ```
-    #[cfg(feature = "alloc")]
-    pub fn available(&self) -> TimeZoneNameIter {
-        let Some(ref inner) = self.inner else {
-            return TimeZoneNameIter {
-                it: alloc::vec::Vec::new().into_iter(),
-            };
+    pub fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
+        let Some(inner) = self.inner.as_deref() else {
+            return TimeZoneNameIter::empty();
         };
-        let mut all = inner.zoneinfo.available();
-        all.extend(inner.concatenated.available());
-        all.extend(inner.bundled.available());
-        all.sort();
-        all.dedup();
-        TimeZoneNameIter { it: all.into_iter() }
+        match *inner {
+            Kind::ZoneInfo(ref db) => db.available(),
+            Kind::Concatenated(ref db) => db.available(),
+            Kind::Bundled(ref db) => db.available(),
+        }
     }
 
     /// Resets the internal cache of this database.
@@ -431,9 +483,11 @@ impl TimeZoneDatabase {
     /// invalidation heuristics to kick in.
     pub fn reset(&self) {
         let Some(inner) = self.inner.as_deref() else { return };
-        inner.zoneinfo.reset();
-        inner.concatenated.reset();
-        inner.bundled.reset();
+        match *inner {
+            Kind::ZoneInfo(ref db) => db.reset(),
+            Kind::Concatenated(ref db) => db.reset(),
+            Kind::Bundled(ref db) => db.reset(),
+        }
     }
 
     /// Returns true if it is known that this time zone database is empty.
@@ -455,9 +509,11 @@ impl TimeZoneDatabase {
     /// ```
     pub fn is_definitively_empty(&self) -> bool {
         let Some(inner) = self.inner.as_deref() else { return true };
-        inner.zoneinfo.is_definitively_empty()
-            && inner.concatenated.is_definitively_empty()
-            && inner.bundled.is_definitively_empty()
+        match *inner {
+            Kind::ZoneInfo(ref db) => db.is_definitively_empty(),
+            Kind::Concatenated(ref db) => db.is_definitively_empty(),
+            Kind::Bundled(ref db) => db.is_definitively_empty(),
+        }
     }
 }
 
@@ -467,12 +523,12 @@ impl core::fmt::Debug for TimeZoneDatabase {
         let Some(inner) = self.inner.as_deref() else {
             return write!(f, "unavailable)");
         };
-        write!(
-            f,
-            "{:?}, {:?}, {:?}",
-            inner.zoneinfo, inner.concatenated, inner.bundled
-        )?;
-        Ok(())
+        match *inner {
+            Kind::ZoneInfo(ref db) => write!(f, "{db:?}")?,
+            Kind::Concatenated(ref db) => write!(f, "{db:?}")?,
+            Kind::Bundled(ref db) => write!(f, "{db:?}")?,
+        }
+        write!(f, ")")
     }
 }
 
@@ -482,21 +538,106 @@ impl core::fmt::Debug for TimeZoneDatabase {
 ///
 /// There are no guarantees about the order in which this iterator yields
 /// time zone identifiers.
-#[cfg(feature = "alloc")]
+///
+/// The lifetime parameter corresponds to the lifetime of the
+/// `TimeZoneDatabase` from which this iterator was created.
 #[derive(Clone, Debug)]
-pub struct TimeZoneNameIter {
-    it: alloc::vec::IntoIter<alloc::string::String>,
+pub struct TimeZoneNameIter<'d> {
+    #[cfg(feature = "alloc")]
+    it: alloc::vec::IntoIter<TimeZoneName<'d>>,
+    #[cfg(not(feature = "alloc"))]
+    it: core::iter::Empty<TimeZoneName<'d>>,
+}
+
+impl<'d> TimeZoneNameIter<'d> {
+    /// Creates a time zone name iterator that never yields any elements.
+    fn empty() -> TimeZoneNameIter<'d> {
+        #[cfg(feature = "alloc")]
+        {
+            TimeZoneNameIter { it: alloc::vec::Vec::new().into_iter() }
+        }
+        #[cfg(not(feature = "alloc"))]
+        {
+            TimeZoneNameIter { it: core::iter::empty() }
+        }
+    }
+
+    /// Creates a time zone name iterator that yields the elements from the
+    /// iterator given. (They are collected into a `Vec`.)
+    #[cfg(feature = "alloc")]
+    fn from_iter(
+        it: impl Iterator<Item = impl Into<alloc::string::String>>,
+    ) -> TimeZoneNameIter<'d> {
+        let names: alloc::vec::Vec<TimeZoneName<'d>> =
+            it.map(|name| TimeZoneName::new(name.into())).collect();
+        TimeZoneNameIter { it: names.into_iter() }
+    }
 }
 
-#[cfg(feature = "alloc")]
-impl Iterator for TimeZoneNameIter {
-    type Item = alloc::string::String;
+impl<'d> Iterator for TimeZoneNameIter<'d> {
+    type Item = TimeZoneName<'d>;
 
-    fn next(&mut self) -> Option<alloc::string::String> {
+    fn next(&mut self) -> Option<TimeZoneName<'d>> {
         self.it.next()
     }
 }
 
+/// A name for a time zone yield by the [`TimeZoneNameIter`] iterator.
+///
+/// The iterator is created by [`TimeZoneDatabase::available`].
+///
+/// The lifetime parameter corresponds to the lifetime of the
+/// `TimeZoneDatabase` from which this name was created.
+#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
+pub struct TimeZoneName<'d> {
+    /// The lifetime of the tzdb.
+    ///
+    /// We don't currently use this, but it could be quite useful if we ever
+    /// adopt a "compile time" tzdb like what `chrono-tz` has. Then we could
+    /// return strings directly from the embedded data. Or perhaps a "compile
+    /// time" TZif or some such.
+    lifetime: core::marker::PhantomData<&'d str>,
+    #[cfg(feature = "alloc")]
+    name: alloc::string::String,
+    #[cfg(not(feature = "alloc"))]
+    name: core::convert::Infallible,
+}
+
+impl<'d> TimeZoneName<'d> {
+    /// Returns a new time zone name from the string given.
+    ///
+    /// The lifetime returned is inferred according to the caller's context.
+    #[cfg(feature = "alloc")]
+    fn new(name: alloc::string::String) -> TimeZoneName<'d> {
+        TimeZoneName { lifetime: core::marker::PhantomData, name }
+    }
+
+    /// Returns this time zone name as a borrowed string.
+    ///
+    /// Note that the lifetime of the string returned is tied to `self`,
+    /// which may be shorter than the lifetime `'d` of the originating
+    /// `TimeZoneDatabase`.
+    #[inline]
+    pub fn as_str<'a>(&'a self) -> &'a str {
+        #[cfg(feature = "alloc")]
+        {
+            self.name.as_str()
+        }
+        #[cfg(not(feature = "alloc"))]
+        {
+            // Can never be reached because `TimeZoneName` cannot currently
+            // be constructed in core-only environments.
+            unreachable!()
+        }
+    }
+}
+
+impl<'d> core::fmt::Display for TimeZoneName<'d> {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        write!(f, "{}", self.as_str())
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/tz/db/zoneinfo/disabled.rs b/src/tz/db/zoneinfo/disabled.rs
index 79a70013..a9c79d45 100644
--- a/src/tz/db/zoneinfo/disabled.rs
+++ b/src/tz/db/zoneinfo/disabled.rs
@@ -1,17 +1,17 @@
-use crate::tz::TimeZone;
+use crate::tz::{TimeZone, TimeZoneNameIter};
 
 #[derive(Clone)]
-pub(crate) struct ZoneInfo;
+pub(crate) struct Database;
 
-impl ZoneInfo {
-    pub(crate) fn from_env() -> ZoneInfo {
-        ZoneInfo
+impl Database {
+    pub(crate) fn from_env() -> Database {
+        Database
     }
 
     #[cfg(feature = "std")]
     pub(crate) fn from_dir(
         dir: &std::path::Path,
-    ) -> Result<ZoneInfo, crate::Error> {
+    ) -> Result<Database, crate::Error> {
         Err(crate::error::err!(
             "system tzdb unavailable: \
              crate feature `tzdb-zoneinfo` is disabled, \
@@ -20,8 +20,8 @@ impl ZoneInfo {
         ))
     }
 
-    pub(crate) fn none() -> ZoneInfo {
-        ZoneInfo
+    pub(crate) fn none() -> Database {
+        Database
     }
 
     pub(crate) fn reset(&self) {}
@@ -30,9 +30,8 @@ impl ZoneInfo {
         None
     }
 
-    #[cfg(feature = "alloc")]
-    pub(crate) fn available(&self) -> alloc::vec::Vec<alloc::string::String> {
-        alloc::vec::Vec::new()
+    pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
+        TimeZoneNameIter::empty()
     }
 
     pub(crate) fn is_definitively_empty(&self) -> bool {
@@ -40,7 +39,7 @@ impl ZoneInfo {
     }
 }
 
-impl core::fmt::Debug for ZoneInfo {
+impl core::fmt::Debug for Database {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
         write!(f, "ZoneInfo(unavailable)")
     }
diff --git a/src/tz/db/zoneinfo/enabled.rs b/src/tz/db/zoneinfo/enabled.rs
index 7ef52fd1..aa21396a 100644
--- a/src/tz/db/zoneinfo/enabled.rs
+++ b/src/tz/db/zoneinfo/enabled.rs
@@ -15,7 +15,7 @@ use std::{
 use crate::{
     error::{err, Error},
     timestamp::Timestamp,
-    tz::{tzif::is_possibly_tzif, TimeZone},
+    tz::{tzif::is_possibly_tzif, TimeZone, TimeZoneNameIter},
     util::{self, cache::Expiration, parse, utf8},
 };
 
@@ -24,18 +24,18 @@ const DEFAULT_TTL: Duration = Duration::new(5 * 60, 0);
 static ZONEINFO_DIRECTORIES: &[&str] =
     &["/usr/share/zoneinfo", "/etc/zoneinfo"];
 
-pub(crate) struct ZoneInfo {
+pub(crate) struct Database {
     dir: Option<PathBuf>,
     names: Option<ZoneInfoNames>,
     zones: RwLock<CachedZones>,
 }
 
-impl ZoneInfo {
-    pub(crate) fn from_env() -> ZoneInfo {
+impl Database {
+    pub(crate) fn from_env() -> Database {
         if let Some(tzdir) = std::env::var_os("TZDIR") {
             let tzdir = PathBuf::from(tzdir);
             trace!("opening zoneinfo database at TZDIR={}", tzdir.display());
-            match ZoneInfo::from_dir(&tzdir) {
+            match Database::from_dir(&tzdir) {
                 Ok(db) => return db,
                 Err(_err) => {
                     // This is a WARN because it represents a failure to
@@ -49,7 +49,7 @@ impl ZoneInfo {
         for dir in ZONEINFO_DIRECTORIES {
             let tzdir = Path::new(dir);
             trace!("opening zoneinfo database at {}", tzdir.display());
-            match ZoneInfo::from_dir(&tzdir) {
+            match Database::from_dir(&tzdir) {
                 Ok(db) => return db,
                 Err(_err) => {
                     trace!("failed opening {}: {_err}", tzdir.display());
@@ -61,21 +61,21 @@ impl ZoneInfo {
              paths: {}",
             ZONEINFO_DIRECTORIES.join(", "),
         );
-        ZoneInfo::none()
+        Database::none()
     }
 
-    pub(crate) fn from_dir(dir: &Path) -> Result<ZoneInfo, Error> {
+    pub(crate) fn from_dir(dir: &Path) -> Result<Database, Error> {
         let names = Some(ZoneInfoNames::new(dir)?);
         let zones = RwLock::new(CachedZones::new());
-        Ok(ZoneInfo { dir: Some(dir.to_path_buf()), names, zones })
+        Ok(Database { dir: Some(dir.to_path_buf()), names, zones })
     }
 
     /// Creates a "dummy" zoneinfo database in which all lookups fail.
-    pub(crate) fn none() -> ZoneInfo {
+    pub(crate) fn none() -> Database {
         let dir = None;
         let names = None;
         let zones = RwLock::new(CachedZones::new());
-        ZoneInfo { dir, names, zones }
+        Database { dir, names, zones }
     }
 
     pub(crate) fn reset(&self) {
@@ -92,6 +92,10 @@ impl ZoneInfo {
         if query == "UTC" {
             return Some(TimeZone::UTC);
         }
+        // Similarly for the special `Etc/Unknown` value.
+        if query == "Etc/Unknown" {
+            return Some(TimeZone::unknown());
+        }
         // If we couldn't build any time zone names, then every lookup will
         // fail. So just bail now.
         let names = self.names.as_ref()?;
@@ -176,9 +180,11 @@ impl ZoneInfo {
         }
     }
 
-    pub(crate) fn available(&self) -> Vec<String> {
-        let Some(names) = self.names.as_ref() else { return vec![] };
-        names.available()
+    pub(crate) fn available<'d>(&'d self) -> TimeZoneNameIter<'d> {
+        let Some(names) = self.names.as_ref() else {
+            return TimeZoneNameIter::empty();
+        };
+        TimeZoneNameIter::from_iter(names.available().into_iter())
     }
 
     pub(crate) fn is_definitively_empty(&self) -> bool {
@@ -186,7 +192,7 @@ impl ZoneInfo {
     }
 }
 
-impl core::fmt::Debug for ZoneInfo {
+impl core::fmt::Debug for Database {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
         write!(f, "ZoneInfo(")?;
         if let Some(ref dir) = self.dir {
diff --git a/src/tz/mod.rs b/src/tz/mod.rs
index 00c95bb9..1256fe85 100644
--- a/src/tz/mod.rs
+++ b/src/tz/mod.rs
@@ -93,10 +93,8 @@ use crate::{
 #[cfg(feature = "alloc")]
 use self::posix::ReasonablePosixTimeZone;
 
-#[cfg(feature = "alloc")]
-pub use self::db::TimeZoneNameIter;
 pub use self::{
-    db::{db, TimeZoneDatabase},
+    db::{db, TimeZoneDatabase, TimeZoneName, TimeZoneNameIter},
     offset::{Dst, Offset, OffsetArithmetic, OffsetConflict, OffsetRound},
 };
 
@@ -210,6 +208,14 @@ mod zic;
 /// `TimeZone` from a `TimeZoneDatabase`. (Or, in the case of the system time
 /// zone, by calling `TimeZone::system`.)
 ///
+/// # A `TimeZone` is cheap to clone
+///
+/// A `TimeZone` can be cheaply cloned. It uses automic reference counting
+/// internally. When `alloc` is disabled, cloning a `TimeZone` is still cheap
+/// because POSIX time zones and TZif time zones are unsupported. Therefore,
+/// cloning a time zone does a deep copy (since automic reference counting is
+/// not available), but the data being copied is small.
+///
 /// # Time zone equality
 ///
 /// `TimeZone` provides an imperfect notion of equality. That is, when two time
@@ -353,20 +359,29 @@ impl TimeZone {
 
     /// Returns the system configured time zone, if available.
     ///
-    /// If the system's default time zone could not be determined, or if
-    /// the `tz-system` crate feature is not enabled, then this returns
-    /// [`TimeZone::UTC`]. A `WARN` level log will also be emitted with a
-    /// message explaining why time zone detection failed. The fallback
-    /// to UTC is a practical trade-off, is what most other systems tend
-    /// to do and is also recommended by [relevant standards such as
-    /// freedesktop.org][freedesktop-org-localtime].
-    ///
     /// Detection of a system's default time zone is generally heuristic
     /// based and platform specific.
     ///
     /// If callers need to know whether discovery of the system time zone
     /// failed, then use [`TimeZone::try_system`].
     ///
+    /// # Fallback behavior
+    ///
+    /// If the system's default time zone could not be determined, or if
+    /// the `tz-system` crate feature is not enabled, then this returns
+    /// [`TimeZone::unknown`]. A `WARN` level log will also be emitted with
+    /// a message explaining why time zone detection failed. The fallback to
+    /// an unknown time zone is a practical trade-off, is what most other
+    /// systems tend to do and is also recommended by [relevant standards such
+    /// as freedesktop.org][freedesktop-org-localtime].
+    ///
+    /// An unknown time zone _behaves_ like [`TimeZone::UTC`], but will
+    /// print as `Etc/Unknown` when converting a `Zoned` to a string.
+    ///
+    /// If you would instead like to fall back to UTC instead
+    /// of the special "unknown" time zone, then you can do
+    /// `TimeZone::try_system().unwrap_or(TimeZone::UTC)`.
+    ///
     /// # Platform behavior
     ///
     /// This section is a "best effort" explanation of how the time zone is
@@ -386,11 +401,13 @@ impl TimeZone {
     ///
     /// Otherwise, when `TZ` isn't set, then:
     ///
-    /// On Unix systems, this inspects `/etc/localtime`. If it's a symbolic
-    /// link to an entry in `/usr/share/zoneinfo`, then the suffix is
-    /// considered an IANA Time Zone Database identifier. Otherwise,
+    /// On Unix non-Android systems, this inspects `/etc/localtime`. If it's
+    /// a symbolic link to an entry in `/usr/share/zoneinfo`, then the suffix
+    /// is considered an IANA Time Zone Database identifier. Otherwise,
     /// `/etc/localtime` is read as a TZif file directly.
     ///
+    /// On Android systems, this inspects the `persist.sys.timezone` property.
+    ///
     /// On Windows, the system time zone is determined via
     /// [`GetDynamicTimeZoneInformation`]. The result is then mapped to an
     /// IANA Time Zone Database identifier via Unicode's
@@ -407,9 +424,10 @@ impl TimeZone {
             Err(_err) => {
                 warn!(
                     "failed to get system time zone, \
-                     falling back to UTC: {_err}",
+                     falling back to `Etc/Unknown` \
+                     (which behaves like UTC): {_err}",
                 );
-                TimeZone::UTC
+                TimeZone::unknown()
             }
         }
     }
@@ -619,6 +637,48 @@ impl TimeZone {
         Ok(TimeZone { kind: Some(Arc::new(kind)) })
     }
 
+    /// Returns a `TimeZone` that is specifially marked as "unknown."
+    ///
+    /// This corresponds to the Unicode CLDR identifier `Etc/Unknown`, which
+    /// is guaranteed to never be a valid IANA time zone identifier (as of
+    /// the `2025a` release of tzdb).
+    ///
+    /// This type of `TimeZone` is used in circumstances where one wants to
+    /// signal that discovering a time zone failed for some reason, but that
+    /// execution can reasonably continue. For example, [`TimeZone::system`]
+    /// returns this type of time zone when the system time zone could not be
+    /// discovered.
+    ///
+    /// # Example
+    ///
+    /// Jiff permits an "unknown" time zone to losslessly be transmitted
+    /// through serialization:
+    ///
+    /// ```
+    /// use jiff::{civil::date, tz::TimeZone, Zoned};
+    ///
+    /// let tz = TimeZone::unknown();
+    /// let zdt = date(2025, 2, 1).at(17, 0, 0, 0).to_zoned(tz)?;
+    /// assert_eq!(zdt.to_string(), "2025-02-01T17:00:00Z[Etc/Unknown]");
+    /// let got: Zoned = "2025-02-01T17:00:00Z[Etc/Unknown]".parse()?;
+    /// assert_eq!(got, zdt);
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    ///
+    /// Note that not all systems support this. Some systems will reject
+    /// `Etc/Unknown` because it is not a valid IANA time zone identifier and
+    /// does not have an entry in the IANA time zone database. However, Jiff
+    /// takes this approach because it surfaces an error condition in detecting
+    /// the end user's time zone. Callers not wanting an "unknown" time zone
+    /// can use `TimeZone::try_system().unwrap_or(TimeZone::UTC)` instead of
+    /// `TimeZone::system`. (Where the latter falls back to the "unknown" time
+    /// zone when a system configured time zone could not be found.)
+    pub fn unknown() -> TimeZone {
+        let kind = TimeZoneKind::Unknown;
+        TimeZone { kind: Some(Arc::new(kind)) }
+    }
+
     /// This creates an unnamed TZif-backed `TimeZone`.
     ///
     /// At present, the only way for an unnamed TZif-backed `TimeZone` to be
@@ -665,12 +725,14 @@ impl TimeZone {
     ///
     /// Basically, this is only `false` when this `TimeZone` was created from
     /// a `/etc/localtime` for which a valid IANA time zone identifier could
-    /// not be extracted.
+    /// not be extracted. It is also `false` when [`TimeZone::is_unknown`]
+    /// is `true`.
     #[cfg(feature = "serde")]
     #[inline]
     pub(crate) fn has_succinct_serialization(&self) -> bool {
         let Some(ref kind) = self.kind else { return true };
         match **kind {
+            TimeZoneKind::Unknown => true,
             TimeZoneKind::Fixed(_) => true,
             #[cfg(feature = "alloc")]
             TimeZoneKind::Posix(_) => true,
@@ -698,10 +760,42 @@ impl TimeZone {
         match **kind {
             #[cfg(feature = "alloc")]
             TimeZoneKind::Tzif(ref tz) => tz.name(),
+            // Note that while `Etc/Unknown` looks like an IANA time zone
+            // identifier, it is specifically and explicitly NOT an IANA time
+            // zone identifier. So we do not return it here if we have an
+            // unknown time zone identifier.
             _ => None,
         }
     }
 
+    /// Returns true if and only if this time zone is unknown.
+    ///
+    /// This has the special internal identifier of `Etc/Unknown`, and this
+    /// is what will be used when converting a `Zoned` to a string.
+    ///
+    /// Note that while `Etc/Unknown` looks like an IANA time zone identifier,
+    /// it is specifically and explicitly not one. It is reserved and is
+    /// guaranteed to never be an IANA time zone identifier.
+    ///
+    /// An unknown time zone can be created via [`TimeZone::unknown`]. It is
+    /// also returned by [`TimeZone::system`] when a system configured time
+    /// zone could not be found.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use jiff::tz::TimeZone;
+    ///
+    /// let tz = TimeZone::unknown();
+    /// assert_eq!(tz.iana_name(), None);
+    /// assert!(tz.is_unknown());
+    /// ```
+    #[inline]
+    pub fn is_unknown(&self) -> bool {
+        let Some(ref kind) = self.kind else { return false };
+        matches!(**kind, TimeZoneKind::Unknown)
+    }
+
     /// When this time zone is a POSIX time zone, return it.
     ///
     /// This doesn't attempt to convert other time zones that are representable
@@ -756,8 +850,7 @@ impl TimeZone {
     /// ```
     #[inline]
     pub fn to_datetime(&self, timestamp: Timestamp) -> DateTime {
-        let (offset, _, _) = self.to_offset(timestamp);
-        offset.to_datetime(timestamp)
+        self.to_offset(timestamp).to_datetime(timestamp)
     }
 
     /// Returns the offset corresponding to the given timestamp in this time
@@ -783,29 +876,26 @@ impl TimeZone {
     ///
     /// // A timestamp in DST in New York.
     /// let ts = Timestamp::from_second(1_720_493_204)?;
-    /// let (offset, dst, abbrev) = tz.to_offset(ts);
+    /// let offset = tz.to_offset(ts);
     /// assert_eq!(offset, tz::offset(-4));
-    /// assert_eq!(dst, Dst::Yes);
-    /// assert_eq!(abbrev, "EDT");
     /// assert_eq!(offset.to_datetime(ts).to_string(), "2024-07-08T22:46:44");
     ///
     /// // A timestamp *not* in DST in New York.
     /// let ts = Timestamp::from_second(1_704_941_204)?;
-    /// let (offset, dst, abbrev) = tz.to_offset(ts);
+    /// let offset = tz.to_offset(ts);
     /// assert_eq!(offset, tz::offset(-5));
-    /// assert_eq!(dst, Dst::No);
-    /// assert_eq!(abbrev, "EST");
     /// assert_eq!(offset.to_datetime(ts).to_string(), "2024-01-10T21:46:44");
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     #[inline]
-    pub fn to_offset(&self, _timestamp: Timestamp) -> (Offset, Dst, &str) {
+    pub fn to_offset(&self, _timestamp: Timestamp) -> Offset {
         let Some(ref kind) = self.kind else {
-            return (Offset::UTC, Dst::No, "UTC");
+            return Offset::UTC;
         };
         match **kind {
-            TimeZoneKind::Fixed(ref tz) => (tz.offset(), Dst::No, tz.name()),
+            TimeZoneKind::Unknown => Offset::UTC,
+            TimeZoneKind::Fixed(ref tz) => tz.to_offset(),
             #[cfg(feature = "alloc")]
             TimeZoneKind::Posix(ref tz) => tz.to_offset(_timestamp),
             #[cfg(feature = "alloc")]
@@ -813,6 +903,77 @@ impl TimeZone {
         }
     }
 
+    /// Returns the offset information corresponding to the given timestamp in
+    /// this time zone. This includes the offset along with daylight saving
+    /// time status and a time zone abbreviation.
+    ///
+    /// This is like [`TimeZone::to_offset`], but returns the aforementioned
+    /// extra data in addition to the offset. This data may, in some cases, be
+    /// more expensive to compute.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use jiff::{tz::{self, Dst, TimeZone}, Timestamp};
+    ///
+    /// let tz = TimeZone::get("America/New_York")?;
+    ///
+    /// // A timestamp in DST in New York.
+    /// let ts = Timestamp::from_second(1_720_493_204)?;
+    /// let info = tz.to_offset_info(ts);
+    /// assert_eq!(info.offset(), tz::offset(-4));
+    /// assert_eq!(info.dst(), Dst::Yes);
+    /// assert_eq!(info.abbreviation(), "EDT");
+    /// assert_eq!(
+    ///     info.offset().to_datetime(ts).to_string(),
+    ///     "2024-07-08T22:46:44",
+    /// );
+    ///
+    /// // A timestamp *not* in DST in New York.
+    /// let ts = Timestamp::from_second(1_704_941_204)?;
+    /// let info = tz.to_offset_info(ts);
+    /// assert_eq!(info.offset(), tz::offset(-5));
+    /// assert_eq!(info.dst(), Dst::No);
+    /// assert_eq!(info.abbreviation(), "EST");
+    /// assert_eq!(
+    ///     info.offset().to_datetime(ts).to_string(),
+    ///     "2024-01-10T21:46:44",
+    /// );
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    #[inline]
+    pub fn to_offset_info<'t>(
+        &'t self,
+        _timestamp: Timestamp,
+    ) -> TimeZoneOffsetInfo<'t> {
+        let Some(ref kind) = self.kind else {
+            return TimeZoneOffsetInfo {
+                offset: Offset::UTC,
+                dst: Dst::No,
+                abbreviation: TimeZoneAbbreviation::Borrowed("UTC"),
+            };
+        };
+        match **kind {
+            TimeZoneKind::Unknown => {
+                TimeZoneOffsetInfo {
+                    offset: Offset::UTC,
+                    dst: Dst::No,
+                    // It'd be kinda nice if this were just `ERR` to
+                    // indicate an error, but I can't find any precedent
+                    // for that. And CLDR says `Etc/Unknown` should behave
+                    // like UTC, so... I guess we use UTC here.
+                    abbreviation: TimeZoneAbbreviation::Borrowed("UTC"),
+                }
+            }
+            TimeZoneKind::Fixed(ref tz) => tz.to_offset_info(),
+            #[cfg(feature = "alloc")]
+            TimeZoneKind::Posix(ref tz) => tz.to_offset_info(_timestamp),
+            #[cfg(feature = "alloc")]
+            TimeZoneKind::Tzif(ref tz) => tz.to_offset_info(_timestamp),
+        }
+    }
+
     /// If this time zone is a fixed offset, then this returns the offset.
     /// If this time zone is not a fixed offset, then an error is returned.
     ///
@@ -860,15 +1021,16 @@ impl TimeZone {
     pub fn to_fixed_offset(&self) -> Result<Offset, Error> {
         let Some(ref kind) = self.kind else { return Ok(Offset::UTC) };
         #[allow(irrefutable_let_patterns)]
-        let TimeZoneKind::Fixed(ref tz) = **kind
-        else {
-            return Err(err!(
+        match **kind {
+            TimeZoneKind::Unknown => Ok(Offset::UTC),
+            TimeZoneKind::Fixed(ref tz) => Ok(tz.to_offset()),
+            #[cfg(feature = "alloc")]
+            _ => Err(err!(
                 "cannot convert non-fixed {kind} time zone to offset \
                  without timestamp or civil datetime",
                 kind = self.kind_description(),
-            ));
-        };
-        Ok(tz.offset())
+            )),
+        }
     }
 
     /// Converts a civil datetime to a [`Zoned`] in this time zone.
@@ -1103,8 +1265,11 @@ impl TimeZone {
         let ambiguous_kind = match self.kind {
             None => AmbiguousOffset::Unambiguous { offset: Offset::UTC },
             Some(ref kind) => match **kind {
+                TimeZoneKind::Unknown => {
+                    AmbiguousOffset::Unambiguous { offset: Offset::UTC }
+                }
                 TimeZoneKind::Fixed(ref tz) => {
-                    AmbiguousOffset::Unambiguous { offset: tz.offset() }
+                    AmbiguousOffset::Unambiguous { offset: tz.to_offset() }
                 }
                 #[cfg(feature = "alloc")]
                 TimeZoneKind::Posix(ref tz) => tz.to_ambiguous_kind(dt),
@@ -1174,15 +1339,15 @@ impl TimeZone {
     ///     .map(|t| (
     ///         t.timestamp().to_zoned(now.time_zone().clone()),
     ///         t.offset(),
-    ///         t.abbreviation(),
+    ///         t.abbreviation().to_string(),
     ///     ))
     ///     .collect::<Vec<_>>();
     /// assert_eq!(transitions, vec![
-    ///     ("2024-11-03 01:00-05[US/Eastern]".parse()?, offset(-5), "EST"),
-    ///     ("2024-03-10 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT"),
-    ///     ("2023-11-05 01:00-05[US/Eastern]".parse()?, offset(-5), "EST"),
-    ///     ("2023-03-12 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT"),
-    ///     ("2022-11-06 01:00-05[US/Eastern]".parse()?, offset(-5), "EST"),
+    ///     ("2024-11-03 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
+    ///     ("2024-03-10 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
+    ///     ("2023-11-05 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
+    ///     ("2023-03-12 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
+    ///     ("2022-11-06 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
     /// ]);
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
@@ -1254,15 +1419,15 @@ impl TimeZone {
     ///     .map(|t| (
     ///         t.timestamp().to_zoned(now.time_zone().clone()),
     ///         t.offset(),
-    ///         t.abbreviation(),
+    ///         t.abbreviation().to_string(),
     ///     ))
     ///     .collect::<Vec<_>>();
     /// assert_eq!(transitions, vec![
-    ///     ("2025-03-09 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT"),
-    ///     ("2025-11-02 01:00-05[US/Eastern]".parse()?, offset(-5), "EST"),
-    ///     ("2026-03-08 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT"),
-    ///     ("2026-11-01 01:00-05[US/Eastern]".parse()?, offset(-5), "EST"),
-    ///     ("2027-03-14 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT"),
+    ///     ("2025-03-09 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
+    ///     ("2025-11-02 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
+    ///     ("2026-03-08 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
+    ///     ("2026-11-01 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
+    ///     ("2027-03-14 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
     /// ]);
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
@@ -1282,6 +1447,7 @@ impl TimeZone {
         _timestamp: Timestamp,
     ) -> Option<TimeZoneTransition> {
         match **self.kind.as_ref()? {
+            TimeZoneKind::Unknown => None,
             TimeZoneKind::Fixed(_) => None,
             #[cfg(feature = "alloc")]
             TimeZoneKind::Posix(ref tz) => tz.previous_transition(_timestamp),
@@ -1297,6 +1463,7 @@ impl TimeZone {
         _timestamp: Timestamp,
     ) -> Option<TimeZoneTransition> {
         match **self.kind.as_ref()? {
+            TimeZoneKind::Unknown => None,
             TimeZoneKind::Fixed(_) => None,
             #[cfg(feature = "alloc")]
             TimeZoneKind::Posix(ref tz) => tz.next_transition(_timestamp),
@@ -1313,6 +1480,7 @@ impl TimeZone {
             return "UTC";
         };
         match **kind {
+            TimeZoneKind::Unknown => "Etc/Unknown",
             TimeZoneKind::Fixed(_) => "fixed",
             #[cfg(feature = "alloc")]
             TimeZoneKind::Posix(_) => "POSIX",
@@ -1328,6 +1496,7 @@ impl core::fmt::Debug for TimeZone {
         let field: &dyn core::fmt::Debug = match self.kind {
             None => &"UTC",
             Some(ref kind) => match &**kind {
+                TimeZoneKind::Unknown => &"Etc/Unknown",
                 TimeZoneKind::Fixed(ref tz) => tz,
                 #[cfg(feature = "alloc")]
                 TimeZoneKind::Posix(ref tz) => tz,
@@ -1342,6 +1511,14 @@ impl core::fmt::Debug for TimeZone {
 #[derive(Debug, Eq, PartialEq)]
 #[cfg_attr(not(feature = "alloc"), derive(Clone))]
 enum TimeZoneKind {
+    // It would be nice if we could represent this similarly to
+    // `TimeZone::UTC`. That is, without putting it behind an `Arc`. But I
+    // didn't see an easy way to do that while retaining the single-word size
+    // of `TimeZone` without pointer tagging, since `Arc` only gives the
+    // compiler a single niche value. Plus, it should be exceptionally rare
+    // for a unknown time zone to be used anyway. It's generally an error
+    // condition.
+    Unknown,
     Fixed(TimeZoneFixed),
     #[cfg(feature = "alloc")]
     Posix(TimeZonePosix),
@@ -1352,38 +1529,38 @@ enum TimeZoneKind {
 #[derive(Clone)]
 struct TimeZoneFixed {
     offset: Offset,
-    name: ArrayStr<9>,
 }
 
 impl TimeZoneFixed {
     #[inline]
     fn new(offset: Offset) -> TimeZoneFixed {
-        let name = offset.to_array_str();
-        TimeZoneFixed { offset, name }
+        TimeZoneFixed { offset }
     }
 
     #[inline]
-    fn name(&self) -> &str {
-        self.name.as_str()
+    fn to_offset(&self) -> Offset {
+        self.offset
     }
 
     #[inline]
-    fn offset(&self) -> Offset {
-        self.offset
+    fn to_offset_info(&self) -> TimeZoneOffsetInfo<'_> {
+        let abbreviation =
+            TimeZoneAbbreviation::Owned(self.offset.to_array_str());
+        TimeZoneOffsetInfo { offset: self.offset, dst: Dst::No, abbreviation }
     }
 }
 
 impl core::fmt::Debug for TimeZoneFixed {
     #[inline]
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        f.debug_tuple("Fixed").field(&self.offset()).finish()
+        f.debug_tuple("Fixed").field(&self.to_offset()).finish()
     }
 }
 
 impl core::fmt::Display for TimeZoneFixed {
     #[inline]
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        core::fmt::Display::fmt(&self.name, f)
+        core::fmt::Display::fmt(&self.offset, f)
     }
 }
 
@@ -1392,7 +1569,7 @@ impl Eq for TimeZoneFixed {}
 impl PartialEq for TimeZoneFixed {
     #[inline]
     fn eq(&self, rhs: &TimeZoneFixed) -> bool {
-        self.offset() == rhs.offset()
+        self.to_offset() == rhs.to_offset()
     }
 }
 
@@ -1405,10 +1582,15 @@ struct TimeZonePosix {
 #[cfg(feature = "alloc")]
 impl TimeZonePosix {
     #[inline]
-    fn to_offset(&self, timestamp: Timestamp) -> (Offset, Dst, &str) {
+    fn to_offset(&self, timestamp: Timestamp) -> Offset {
         self.posix.to_offset(timestamp)
     }
 
+    #[inline]
+    fn to_offset_info(&self, timestamp: Timestamp) -> TimeZoneOffsetInfo<'_> {
+        self.posix.to_offset_info(timestamp)
+    }
+
     #[inline]
     fn to_ambiguous_kind(&self, dt: DateTime) -> AmbiguousOffset {
         self.posix.to_ambiguous_kind(dt)
@@ -1472,10 +1654,15 @@ impl TimeZoneTzif {
     }
 
     #[inline]
-    fn to_offset(&self, timestamp: Timestamp) -> (Offset, Dst, &str) {
+    fn to_offset(&self, timestamp: Timestamp) -> Offset {
         self.tzif.to_offset(timestamp)
     }
 
+    #[inline]
+    fn to_offset_info(&self, timestamp: Timestamp) -> TimeZoneOffsetInfo<'_> {
+        self.tzif.to_offset_info(timestamp)
+    }
+
     #[inline]
     fn to_ambiguous_kind(&self, dt: DateTime) -> AmbiguousOffset {
         self.tzif.to_ambiguous_kind(dt)
@@ -1629,6 +1816,10 @@ impl<'t> TimeZoneTransition<'t> {
     /// abbreviation `CST` can be used for the time zones `Asia/Shanghai`,
     /// `America/Chicago` and `America/Havana`.
     ///
+    /// The lifetime of the string returned is tied to this
+    /// `TimeZoneTransition`, which may be shorter than `'t` (the lifetime of
+    /// the time zone this transition was created from).
+    ///
     /// # Example
     ///
     /// ```
@@ -1647,7 +1838,7 @@ impl<'t> TimeZoneTransition<'t> {
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     #[inline]
-    pub fn abbreviation(&self) -> &'t str {
+    pub fn abbreviation<'a>(&'a self) -> &'a str {
         self.abbrev
     }
 
@@ -1702,15 +1893,15 @@ impl<'t> TimeZoneTransition<'t> {
 ///     .map(|t| (
 ///         t.timestamp().to_zoned(now.time_zone().clone()),
 ///         t.offset(),
-///         t.abbreviation(),
+///         t.abbreviation().to_string(),
 ///     ))
 ///     .collect::<Vec<_>>();
 /// assert_eq!(transitions, vec![
-///     ("2024-11-03 01:00-05[US/Eastern]".parse()?, offset(-5), "EST"),
-///     ("2024-03-10 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT"),
-///     ("2023-11-05 01:00-05[US/Eastern]".parse()?, offset(-5), "EST"),
-///     ("2023-03-12 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT"),
-///     ("2022-11-06 01:00-05[US/Eastern]".parse()?, offset(-5), "EST"),
+///     ("2024-11-03 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
+///     ("2024-03-10 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
+///     ("2023-11-05 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
+///     ("2023-03-12 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
+///     ("2022-11-06 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
 /// ]);
 ///
 /// # Ok::<(), Box<dyn std::error::Error>>(())
@@ -1753,15 +1944,15 @@ impl<'t> core::iter::FusedIterator for TimeZonePrecedingTransitions<'t> {}
 ///     .map(|t| (
 ///         t.timestamp().to_zoned(now.time_zone().clone()),
 ///         t.offset(),
-///         t.abbreviation(),
+///         t.abbreviation().to_string(),
 ///     ))
 ///     .collect::<Vec<_>>();
 /// assert_eq!(transitions, vec![
-///     ("2025-03-09 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT"),
-///     ("2025-11-02 01:00-05[US/Eastern]".parse()?, offset(-5), "EST"),
-///     ("2026-03-08 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT"),
-///     ("2026-11-01 01:00-05[US/Eastern]".parse()?, offset(-5), "EST"),
-///     ("2027-03-14 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT"),
+///     ("2025-03-09 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
+///     ("2025-11-02 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
+///     ("2026-03-08 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
+///     ("2026-11-01 01:00-05[US/Eastern]".parse()?, offset(-5), "EST".to_string()),
+///     ("2027-03-14 03:00-04[US/Eastern]".parse()?, offset(-4), "EDT".to_string()),
 /// ]);
 ///
 /// # Ok::<(), Box<dyn std::error::Error>>(())
@@ -1798,6 +1989,7 @@ impl<'a> core::fmt::Display for DiagnosticName<'a> {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
         let Some(ref kind) = self.0.kind else { return write!(f, "UTC") };
         match **kind {
+            TimeZoneKind::Unknown => write!(f, "Etc/Unknown"),
             TimeZoneKind::Fixed(ref tz) => write!(f, "{tz}"),
             #[cfg(feature = "alloc")]
             TimeZoneKind::Posix(ref tz) => write!(f, "{tz}"),
@@ -3072,6 +3264,193 @@ impl AmbiguousZoned {
     }
 }
 
+/// An offset along with DST status and a time zone abbreviation.
+///
+/// This information can be computed from a [`TimeZone`] given a [`Timestamp`]
+/// via [`TimeZone::to_offset_info`].
+///
+/// Generally, the extra information associated with the offset is not commonly
+/// needed. And indeed, inspecting the daylight saving time status of a
+/// particular instant in a time zone _usually_ leads to bugs. For example, not
+/// all time zone transitions are the result of daylight saving time. Some are
+/// the result of permanent changes to the standard UTC offset of a region.
+///
+/// This information is available via an API distinct from
+/// [`TimeZone::to_offset`] because it is not commonly needed and because it
+/// can sometimes be more expensive to compute.
+///
+/// The main use case for daylight saving time status or time zone
+/// abbreviations is for formatting datetimes in an end user's locale. If you
+/// want this, consider using the [`icu`] crate.
+///
+/// The lifetime parameter `'t` corresponds to the lifetime of the `TimeZone`
+/// that this info was extracted from.
+///
+/// # Example
+///
+/// ```
+/// use jiff::{tz::{self, Dst, TimeZone}, Timestamp};
+///
+/// let tz = TimeZone::get("America/New_York")?;
+///
+/// // A timestamp in DST in New York.
+/// let ts = Timestamp::from_second(1_720_493_204)?;
+/// let info = tz.to_offset_info(ts);
+/// assert_eq!(info.offset(), tz::offset(-4));
+/// assert_eq!(info.dst(), Dst::Yes);
+/// assert_eq!(info.abbreviation(), "EDT");
+/// assert_eq!(
+///     info.offset().to_datetime(ts).to_string(),
+///     "2024-07-08T22:46:44",
+/// );
+///
+/// // A timestamp *not* in DST in New York.
+/// let ts = Timestamp::from_second(1_704_941_204)?;
+/// let info = tz.to_offset_info(ts);
+/// assert_eq!(info.offset(), tz::offset(-5));
+/// assert_eq!(info.dst(), Dst::No);
+/// assert_eq!(info.abbreviation(), "EST");
+/// assert_eq!(
+///     info.offset().to_datetime(ts).to_string(),
+///     "2024-01-10T21:46:44",
+/// );
+///
+/// # Ok::<(), Box<dyn std::error::Error>>(())
+/// ```
+///
+/// [`icu`]: https://docs.rs/icu
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
+pub struct TimeZoneOffsetInfo<'t> {
+    offset: Offset,
+    dst: Dst,
+    abbreviation: TimeZoneAbbreviation<'t>,
+}
+
+impl<'t> TimeZoneOffsetInfo<'t> {
+    /// Returns the offset.
+    ///
+    /// The offset is duration, from UTC, that should be used to offset the
+    /// civil time in a particular location.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use jiff::{civil, tz::{TimeZone, offset}};
+    ///
+    /// let tz = TimeZone::get("US/Eastern")?;
+    /// // Get the offset for 2023-03-10 00:00:00.
+    /// let start = civil::date(2024, 3, 10).to_zoned(tz.clone())?.timestamp();
+    /// let info = tz.to_offset_info(start);
+    /// assert_eq!(info.offset(), offset(-5));
+    /// // Go forward a day and notice the offset changes due to DST!
+    /// let start = civil::date(2024, 3, 11).to_zoned(tz.clone())?.timestamp();
+    /// let info = tz.to_offset_info(start);
+    /// assert_eq!(info.offset(), offset(-4));
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    #[inline]
+    pub fn offset(&self) -> Offset {
+        self.offset
+    }
+
+    /// Returns the time zone abbreviation corresponding to this offset info.
+    ///
+    /// Note that abbreviations can to be ambiguous. For example, the
+    /// abbreviation `CST` can be used for the time zones `Asia/Shanghai`,
+    /// `America/Chicago` and `America/Havana`.
+    ///
+    /// The lifetime of the string returned is tied to this
+    /// `TimeZoneOffsetInfo`, which may be shorter than `'t` (the lifetime of
+    /// the time zone this transition was created from).
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use jiff::{civil, tz::TimeZone};
+    ///
+    /// let tz = TimeZone::get("US/Eastern")?;
+    /// // Get the time zone abbreviation for 2023-03-10 00:00:00.
+    /// let start = civil::date(2024, 3, 10).to_zoned(tz.clone())?.timestamp();
+    /// let info = tz.to_offset_info(start);
+    /// assert_eq!(info.abbreviation(), "EST");
+    /// // Go forward a day and notice the abbreviation changes due to DST!
+    /// let start = civil::date(2024, 3, 11).to_zoned(tz.clone())?.timestamp();
+    /// let info = tz.to_offset_info(start);
+    /// assert_eq!(info.abbreviation(), "EDT");
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    #[inline]
+    pub fn abbreviation(&self) -> &str {
+        self.abbreviation.as_str()
+    }
+
+    /// Returns whether daylight saving time is enabled for this offset
+    /// info.
+    ///
+    /// Callers should generally treat this as informational only. In
+    /// particular, not all time zone transitions are related to daylight
+    /// saving time. For example, some transitions are a result of a region
+    /// permanently changing their offset from UTC.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use jiff::{civil, tz::{Dst, TimeZone}};
+    ///
+    /// let tz = TimeZone::get("US/Eastern")?;
+    /// // Get the DST status of 2023-03-11 00:00:00.
+    /// let start = civil::date(2024, 3, 11).to_zoned(tz.clone())?.timestamp();
+    /// let info = tz.to_offset_info(start);
+    /// assert_eq!(info.dst(), Dst::Yes);
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    #[inline]
+    pub fn dst(&self) -> Dst {
+        self.dst
+    }
+}
+
+/// A light abstraction over different representations of a time zone
+/// abbreviation.
+///
+/// The lifetime parameter `'t` corresponds to the lifetime of the time zone
+/// that produced this abbreviation.
+#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
+enum TimeZoneAbbreviation<'t> {
+    /// For when the abbreviation is borrowed directly from other data. For
+    /// example, from TZif or from POSIX TZ strings.
+    Borrowed(&'t str),
+    /// For when the abbreviation has to be derived from other data. For
+    /// example, from a fixed offset.
+    ///
+    /// The idea here is that a `TimeZone` shouldn't need to store the
+    /// string representation of a fixed offset. Particularly in core-only
+    /// environments, this is quite wasteful. So we make the string on-demand
+    /// only when it's requested.
+    ///
+    /// An alternative design is to just implement `Display` and reuse
+    /// `Offset`'s `Display` impl, but then we couldn't offer a `-> &str` API.
+    /// I feel like that's just a bit overkill, and really just comes from the
+    /// core-only straight-jacket.
+    Owned(ArrayStr<9>),
+}
+
+impl<'t> TimeZoneAbbreviation<'t> {
+    /// Returns this abbreviation as a string borrowed from `self`.
+    ///
+    /// Notice that, like `Cow`, the lifetime of the string returned is
+    /// tied to `self` and thus may be shorter than `'t`.
+    fn as_str<'a>(&'a self) -> &'a str {
+        match *self {
+            TimeZoneAbbreviation::Borrowed(s) => s,
+            TimeZoneAbbreviation::Owned(ref s) => s.as_str(),
+        }
+    }
+}
+
 /// Creates a new time zone offset in a `const` context from a given number
 /// of hours.
 ///
@@ -3726,17 +4105,19 @@ mod tests {
             {
                 let (year, month, day, hour, min, sec, nano) = datetime;
                 let timestamp = Timestamp::new(unix_sec, unix_nano).unwrap();
-                let (got_offset, _, got_abbrev) = tz.to_offset(timestamp);
+                let info = tz.to_offset_info(timestamp);
                 assert_eq!(
-                    got_offset, offset,
+                    info.offset(),
+                    offset,
                     "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
                 );
                 assert_eq!(
-                    got_abbrev, abbrev,
+                    info.abbreviation(),
+                    abbrev,
                     "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
                 );
                 assert_eq!(
-                    got_offset.to_datetime(timestamp),
+                    info.offset().to_datetime(timestamp),
                     date(year, month, day).at(hour, min, sec, nano),
                     "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
                 );
@@ -4004,7 +4385,7 @@ mod tests {
                 let (year, month, day, hour, min, sec, nano) = datetime;
                 let timestamp = Timestamp::new(unix_sec, unix_nano).unwrap();
                 assert_eq!(
-                    tz.to_offset(timestamp).0,
+                    tz.to_offset(timestamp),
                     offset,
                     "\ntimestamp({unix_sec}, {unix_nano})",
                 );
@@ -4385,11 +4766,17 @@ mod tests {
         {
             #[cfg(debug_assertions)]
             {
-                assert_eq!(28, core::mem::size_of::<TimeZone>());
+                assert_eq!(16, core::mem::size_of::<TimeZone>());
             }
             #[cfg(not(debug_assertions))]
             {
-                assert_eq!(20, core::mem::size_of::<TimeZone>());
+                // This asserts the same value as the alloc value above, but
+                // it wasn't always this way, which is why it's written out
+                // separately. Moreover, in theory, I'd be open to regressing
+                // this value if it led to an improvement in alloc-mode. But
+                // more likely, it would be nice to decrease this size in
+                // non-alloc modes.
+                assert_eq!(8, core::mem::size_of::<TimeZone>());
             }
         }
     }
@@ -4401,34 +4788,34 @@ mod tests {
         let ts = Timestamp::from_second(123456789).unwrap();
 
         let tz = TimeZone::fixed(offset(-5));
-        let (off, dst, label) = tz.to_offset(ts);
-        assert_eq!(off, offset(-5));
-        assert_eq!(dst, Dst::No);
-        assert_eq!(label, "-05");
+        let info = tz.to_offset_info(ts);
+        assert_eq!(info.offset(), offset(-5));
+        assert_eq!(info.dst(), Dst::No);
+        assert_eq!(info.abbreviation(), "-05");
 
         let tz = TimeZone::fixed(offset(5));
-        let (off, dst, label) = tz.to_offset(ts);
-        assert_eq!(off, offset(5));
-        assert_eq!(dst, Dst::No);
-        assert_eq!(label, "+05");
+        let info = tz.to_offset_info(ts);
+        assert_eq!(info.offset(), offset(5));
+        assert_eq!(info.dst(), Dst::No);
+        assert_eq!(info.abbreviation(), "+05");
 
         let tz = TimeZone::fixed(offset(-12));
-        let (off, dst, label) = tz.to_offset(ts);
-        assert_eq!(off, offset(-12));
-        assert_eq!(dst, Dst::No);
-        assert_eq!(label, "-12");
+        let info = tz.to_offset_info(ts);
+        assert_eq!(info.offset(), offset(-12));
+        assert_eq!(info.dst(), Dst::No);
+        assert_eq!(info.abbreviation(), "-12");
 
         let tz = TimeZone::fixed(offset(12));
-        let (off, dst, label) = tz.to_offset(ts);
-        assert_eq!(off, offset(12));
-        assert_eq!(dst, Dst::No);
-        assert_eq!(label, "+12");
+        let info = tz.to_offset_info(ts);
+        assert_eq!(info.offset(), offset(12));
+        assert_eq!(info.dst(), Dst::No);
+        assert_eq!(info.abbreviation(), "+12");
 
         let tz = TimeZone::fixed(offset(0));
-        let (off, dst, label) = tz.to_offset(ts);
-        assert_eq!(off, offset(0));
-        assert_eq!(dst, Dst::No);
-        assert_eq!(label, "UTC");
+        let info = tz.to_offset_info(ts);
+        assert_eq!(info.offset(), offset(0));
+        assert_eq!(info.dst(), Dst::No);
+        assert_eq!(info.abbreviation(), "UTC");
     }
 
     /// This tests a few other cases for `TimeZone::to_fixed_offset` that
diff --git a/src/tz/offset.rs b/src/tz/offset.rs
index 8aeaaaa6..5a72dfc0 100644
--- a/src/tz/offset.rs
+++ b/src/tz/offset.rs
@@ -1934,45 +1934,10 @@ impl OffsetConflict {
 
         let amb = tz.to_ambiguous_timestamp(dt);
         match amb.offset() {
-            // Basically, if the offset parsed matches one of our ambiguous
-            // offsets, then the offset is used to resolve the ambiguity and
-            // yields an unambiguous result. But in every other case, the
-            // offset parsed is completely ignored. (And this may result in
-            // returning an ambiguous instant.)
-            //
-            // Note that gaps are a little weird here. We always pick the
-            // "before" offset even when it doesn't match what's `given`.
-            // This matches what Temporal does, and I think makes more
-            // intuitive sense. For example, if we have
-            //
-            //   2024-03-10T01:30:00-05:00[US/Eastern]
-            //
-            // And set the hour to `2`, then it makes sense to result in a
-            // clock time of `03:30` in offset `-04`. Moreover, if we always
-            // took what was given, then starting with
-            //
-            //   2024-03-31T12:00:00+00:00[Atlantic/Azores]
-            //
-            // And setting the hour to `0` would result in
-            //
-            //   2024-03-30T23:00:00-01:00[Atlantic/Azores]
-            //
-            // which is highly unintuitive. In the other direction, we might
-            // start with
-            //
-            //   2024-03-10T03:30:00-04:00[US/Eastern]
-            //
-            // And setting the hour to `2` still results in the clock time
-            // being set to `03:30`, i.e., not changing in this instance.
-            //
-            // See also: https://github.com/tc39/proposal-temporal/issues/3078
-            // See also: https://github.com/BurntSushi/jiff/issues/211
-            Gap { before, after }
-                if is_equal(given, before) || is_equal(given, after) =>
-            {
-                let kind = Unambiguous { offset: before };
-                AmbiguousTimestamp::new(dt, kind)
-            }
+            // We only look for folds because we consider all offsets for gaps
+            // to be invalid. Which is consistent with how they're treated as
+            // `OffsetConflict::Reject`. Thus, like any other invalid offset,
+            // we fallback to disambiguation (which is handled by the caller).
             Fold { before, after }
                 if is_equal(given, before) || is_equal(given, after) =>
             {
@@ -2014,34 +1979,15 @@ impl OffsetConflict {
                 tzname = tz.diagnostic_name(),
             )),
             Unambiguous { .. } => Ok(amb.into_ambiguous_zoned(tz)),
-            Gap { before, after }
-                if !is_equal(given, before) && !is_equal(given, after) =>
-            {
-                // Temporal actually seems to report an error whenever a gap
-                // is found, even if the parsed offset matches one of the two
-                // offsets in the gap. I think the reasoning is because the
-                // parsed offset and offsets from the gap will flip-flop, thus
-                // resulting in a datetime with an offset distinct from the one
-                // given. Here's an example. Consider this datetime:
-                //
-                //     2024-03-10T02:30-04:00[America/New_York]
-                //
-                // This occurs in the EST->EDT transition gap, such that
-                // `02:30` never actually appears on a clock for folks in
-                // `America/New_York`. The `-04` offset means that the
-                // timestamp this corresponds to is unambiguous. Namely, it is:
-                //
-                //     2024-03-10T06:30Z
-                //
-                // This instant, when converted to `America/New_York`, is:
-                //
-                //     2024-03-10T01:30-05:00[America/New_York]
-                //
-                // As you can see, the offset flip-flopped. The same flip-flop
-                // happens the other way if you use `02:30-05:00`. Presumably,
-                // Temporal errors here because the offset changes. But the
-                // instant in time is the same *and* it is unambiguous. So it's
-                // not clear to me why we ought to error in this case.
+            Gap { before, after } => {
+                // In `jiff 0.1`, we reported an error when we found a gap
+                // where neither offset matched what was given. But now we
+                // report an error whenever we find a gap, as we consider
+                // all offsets to be invalid for the gap. This now matches
+                // Temporal's behavior which I think is more consistent. And in
+                // particular, this makes it more consistent with the behavior
+                // of `PreferOffset` when a gap is found (which was also
+                // changed to treat all offsets in a gap as invalid).
                 //
                 // Ref: https://github.com/tc39/proposal-temporal/issues/2892
                 Err(err!(
@@ -2049,8 +1995,8 @@ impl OffsetConflict {
                      since 'reject' conflict resolution was chosen, and \
                      because datetime has offset {given}, but the time \
                      zone {tzname} for the given datetime falls in a gap \
-                     between offsets {before} and {after}, neither of which \
-                     match the offset",
+                     (between offsets {before} and {after}), and all \
+                     offsets for a gap are regarded as invalid",
                     tzname = tz.diagnostic_name(),
                 ))
             }
@@ -2067,7 +2013,7 @@ impl OffsetConflict {
                     tzname = tz.diagnostic_name(),
                 ))
             }
-            Gap { .. } | Fold { .. } => {
+            Fold { .. } => {
                 let kind = Unambiguous { offset: given };
                 Ok(AmbiguousTimestamp::new(dt, kind).into_ambiguous_zoned(tz))
             }
diff --git a/src/tz/posix.rs b/src/tz/posix.rs
index e817fce1..5aa19872 100644
--- a/src/tz/posix.rs
+++ b/src/tz/posix.rs
@@ -82,7 +82,10 @@ use crate::{
     civil::{Date, DateTime, Time, Weekday},
     error::{err, Error, ErrorContext},
     timestamp::Timestamp,
-    tz::{AmbiguousOffset, Dst, Offset, TimeZoneTransition},
+    tz::{
+        AmbiguousOffset, Dst, Offset, TimeZoneAbbreviation,
+        TimeZoneOffsetInfo, TimeZoneTransition,
+    },
     util::{
         array_str::Abbreviation,
         escape::{Byte, Bytes},
@@ -257,22 +260,59 @@ impl ReasonablePosixTimeZone {
     /// This also includes whether the offset returned should be considered
     /// to be "DST" or not, along with the time zone abbreviation (e.g., EST
     /// for standard time in New York, and EDT for DST in New York).
-    pub(crate) fn to_offset(
+    pub(crate) fn to_offset(&self, timestamp: Timestamp) -> Offset {
+        if self.dst.is_none() {
+            return self.std_offset();
+        }
+
+        let dt = Offset::UTC.to_datetime(timestamp);
+        self.dst_info_utc(dt.date().year_ranged())
+            .filter(|dst_info| dst_info.in_dst(dt))
+            .map(|dst_info| dst_info.offset)
+            .unwrap_or_else(|| self.std_offset())
+    }
+
+    /// Returns the appropriate time zone offset to use for the given
+    /// timestamp.
+    ///
+    /// This also includes whether the offset returned should be considered
+    /// to be "DST" or not, along with the time zone abbreviation (e.g., EST
+    /// for standard time in New York, and EDT for DST in New York).
+    pub(crate) fn to_offset_info(
         &self,
         timestamp: Timestamp,
-    ) -> (Offset, Dst, &str) {
+    ) -> TimeZoneOffsetInfo<'_> {
         if self.dst.is_none() {
-            return (self.std_offset(), Dst::No, self.std_abbrev.as_str());
+            let abbreviation =
+                TimeZoneAbbreviation::Borrowed(self.std_abbrev.as_str());
+            return TimeZoneOffsetInfo {
+                offset: self.std_offset(),
+                dst: Dst::No,
+                abbreviation,
+            };
         }
 
         let dt = Offset::UTC.to_datetime(timestamp);
         self.dst_info_utc(dt.date().year_ranged())
             .filter(|dst_info| dst_info.in_dst(dt))
             .map(|dst_info| {
-                (dst_info.offset, Dst::Yes, dst_info.dst.abbrev.as_str())
+                let abbreviation = TimeZoneAbbreviation::Borrowed(
+                    dst_info.dst.abbrev.as_str(),
+                );
+                TimeZoneOffsetInfo {
+                    offset: dst_info.offset,
+                    dst: Dst::Yes,
+                    abbreviation,
+                }
             })
             .unwrap_or_else(|| {
-                (self.std_offset(), Dst::No, self.std_abbrev.as_str())
+                let abbreviation =
+                    TimeZoneAbbreviation::Borrowed(self.std_abbrev.as_str());
+                TimeZoneOffsetInfo {
+                    offset: self.std_offset(),
+                    dst: Dst::No,
+                    abbreviation,
+                }
             })
     }
 
diff --git a/src/tz/tzif.rs b/src/tz/tzif.rs
index ea631549..f0308a88 100644
--- a/src/tz/tzif.rs
+++ b/src/tz/tzif.rs
@@ -18,7 +18,8 @@ use crate::{
     timestamp::Timestamp,
     tz::{
         posix::{IanaTz, ReasonablePosixTimeZone},
-        AmbiguousOffset, Dst, Offset, TimeZoneTransition,
+        AmbiguousOffset, Dst, Offset, TimeZoneAbbreviation,
+        TimeZoneOffsetInfo, TimeZoneTransition,
     },
     util::{
         crc32,
@@ -98,16 +99,46 @@ impl Tzif {
         self.name.as_deref()
     }
 
+    /// Returns the appropriate time zone offset to use for the given
+    /// timestamp.
+    pub(crate) fn to_offset(&self, timestamp: Timestamp) -> Offset {
+        match self.to_local_time_type(timestamp) {
+            Ok(typ) => typ.offset,
+            Err(tz) => tz.to_offset(timestamp),
+        }
+    }
+
     /// Returns the appropriate time zone offset to use for the given
     /// timestamp.
     ///
     /// This also includes whether the offset returned should be considered to
     /// be DST or not, along with the time zone abbreviation (e.g., EST for
     /// standard time in New York, and EDT for DST in New York).
-    pub(crate) fn to_offset(
+    pub(crate) fn to_offset_info(
         &self,
         timestamp: Timestamp,
-    ) -> (Offset, Dst, &str) {
+    ) -> TimeZoneOffsetInfo<'_> {
+        let typ = match self.to_local_time_type(timestamp) {
+            Ok(typ) => typ,
+            Err(tz) => return tz.to_offset_info(timestamp),
+        };
+        let abbreviation =
+            TimeZoneAbbreviation::Borrowed(self.designation(typ));
+        TimeZoneOffsetInfo {
+            offset: typ.offset,
+            dst: typ.is_dst,
+            abbreviation,
+        }
+    }
+
+    /// Returns the local time type for the timestamp given.
+    ///
+    /// If one could not be found, then this implies that the caller should
+    /// use the POSIX time zone returned in the error variant.
+    fn to_local_time_type(
+        &self,
+        timestamp: Timestamp,
+    ) -> Result<&LocalTimeType, &ReasonablePosixTimeZone> {
         // This is guaranteed because we always push at least one transition.
         // This isn't guaranteed by TZif since it might have 0 transitions,
         // but we always add a "dummy" first transition with our minimum
@@ -163,15 +194,14 @@ impl Tzif {
                 // This is likely why some things break with the "slim"
                 // version: they don't support POSIX TZ strings (or don't
                 // support them correctly).
-                Some(tz) => return tz.to_offset(timestamp),
+                Some(tz) => return Err(tz),
                 // This case is technically unspecified, but I think the
                 // typical thing to do is to just use the last transition.
                 // I'm not 100% sure on this one.
                 None => &self.transitions[index],
             }
         };
-        let typ = self.local_time_type(t);
-        self.local_time_type_to_offset(typ)
+        Ok(self.local_time_type(t))
     }
 
     /// Returns a possibly ambiguous timestamp for the given civil datetime.
@@ -338,13 +368,6 @@ impl Tzif {
         })
     }
 
-    fn local_time_type_to_offset(
-        &self,
-        typ: &LocalTimeType,
-    ) -> (Offset, Dst, &str) {
-        (typ.offset, typ.is_dst, self.designation(typ))
-    }
-
     fn designation(&self, typ: &LocalTimeType) -> &str {
         // OK because we verify that the designation range on every local
         // time type is a valid range into `self.designations`.
@@ -440,32 +463,34 @@ impl Tzif {
             if let Some(ref tz) = tzif.posix_tz {
                 let last = tzif.transitions.last().expect("last transition");
                 let typ = tzif.local_time_type(last);
-                let (offset, dst, abbrev) = tz.to_offset(last.timestamp);
-                if offset != typ.offset {
+                let info = tz.to_offset_info(last.timestamp);
+                if info.offset() != typ.offset {
                     return Err(err!(
                         "expected last transition to have DST offset \
-                         of {}, but got {offset} according to POSIX TZ \
+                         of {}, but got {} according to POSIX TZ \
                          string {}",
                         typ.offset,
+                        info.offset(),
                         tz,
                     ));
                 }
-                if dst != typ.is_dst {
+                if info.dst() != typ.is_dst {
                     return Err(err!(
                         "expected last transition to have is_dst={}, \
                          but got is_dst={} according to POSIX TZ \
                          string {}",
                         typ.is_dst.is_dst(),
-                        dst.is_dst(),
+                        info.dst().is_dst(),
                         tz,
                     ));
                 }
-                if abbrev != tzif.designation(&typ) {
+                if info.abbreviation() != tzif.designation(&typ) {
                     return Err(err!(
                         "expected last transition to have \
-                         designation={abbrev}, \
+                         designation={}, \
                          but got designation={} according to POSIX TZ \
                          string {}",
+                        info.abbreviation(),
                         tzif.designation(&typ),
                         tz,
                     ));
diff --git a/src/util/array_str.rs b/src/util/array_str.rs
index 22a10de3..fab7be6a 100644
--- a/src/util/array_str.rs
+++ b/src/util/array_str.rs
@@ -5,7 +5,7 @@
 ///
 /// Also, since it isn't needed and it lets us save on storage requirements,
 /// `N` must be less than `256` (so that the length can fit in a `u8`).
-#[derive(Clone, Copy, Eq, PartialEq)]
+#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
 pub(crate) struct ArrayStr<const N: usize> {
     /// The UTF-8 bytes that make up the string.
     ///
diff --git a/src/util/t.rs b/src/util/t.rs
index 763ff7d9..a4fe36b5 100644
--- a/src/util/t.rs
+++ b/src/util/t.rs
@@ -418,6 +418,9 @@ pub(crate) type SpanZoneOffsetSeconds = ri8<-59, 59>;
 /// The number of months in a year.
 pub(crate) const MONTHS_PER_YEAR: Constant = Constant(12);
 
+/// The number of days in a week.
+pub(crate) const DAYS_PER_CIVIL_WEEK: Constant = Constant(7);
+
 /// The number of whole hours in one day.
 pub(crate) const HOURS_PER_CIVIL_DAY: Constant = Constant(24);
 
@@ -428,6 +431,17 @@ pub(crate) const MINUTES_PER_CIVIL_DAY: Constant =
 /// The number of minutes in an hour.
 pub(crate) const MINUTES_PER_HOUR: Constant = Constant(60);
 
+/// The number of seconds in a civil week.
+///
+/// Some weeks will have more or less seconds because of DST transitions. But
+/// such things are ignored when dealing with civil time, and so this constant
+/// is still useful.
+pub(crate) const SECONDS_PER_CIVIL_WEEK: Constant = Constant(
+    DAYS_PER_CIVIL_WEEK.value()
+        * HOURS_PER_CIVIL_DAY.value()
+        * SECONDS_PER_HOUR.value(),
+);
+
 /// The number of seconds in a civil day.
 ///
 /// Some days will have more or less seconds because of DST transitions. But
@@ -460,6 +474,14 @@ pub(crate) const MICROS_PER_SECOND: Constant = Constant(1_000_000);
 /// The number of microseconds in a single millisecond.
 pub(crate) const MICROS_PER_MILLI: Constant = Constant(1_000);
 
+/// The number of nanoseconds in a civil week.
+///
+/// Some weeks will have more or less seconds because of DST transitions. But
+/// such things are ignored when dealing with civil time, and so this constant
+/// is still useful.
+pub(crate) const NANOS_PER_CIVIL_WEEK: Constant =
+    Constant(SECONDS_PER_CIVIL_WEEK.value() * NANOS_PER_SECOND.value());
+
 /// The number of nanoseconds in a civil day.
 ///
 /// Some days will have more or less seconds because of DST transitions. But
diff --git a/src/zoned.rs b/src/zoned.rs
index 4249f158..f36a7964 100644
--- a/src/zoned.rs
+++ b/src/zoned.rs
@@ -490,7 +490,7 @@ impl Zoned {
     /// ```
     #[inline]
     pub fn new(timestamp: Timestamp, time_zone: TimeZone) -> Zoned {
-        let (offset, _, _) = time_zone.to_offset(timestamp);
+        let offset = time_zone.to_offset(timestamp);
         let datetime = offset.to_datetime(timestamp);
         let inner = ZonedInner { timestamp, datetime, offset, time_zone };
         Zoned { inner }
@@ -3058,20 +3058,6 @@ impl Zoned {
     }
 }
 
-/// Deprecated APIs.
-impl Zoned {
-    /// A deprecated equivalent to [`Zoned::in_tz`].
-    ///
-    /// This will be removed in `jiff 0.2`. The method was renamed to make
-    /// it clearer that the name stood for "in time zone."
-    #[deprecated(since = "0.1.25", note = "use Zoned::in_tz instead")]
-    #[inline]
-    pub fn intz(&self, name: &str) -> Result<Zoned, Error> {
-        let tz = crate::tz::db().get(name)?;
-        Ok(self.with_time_zone(tz))
-    }
-}
-
 impl Default for Zoned {
     #[inline]
     fn default() -> Zoned {
@@ -5180,10 +5166,9 @@ impl ZonedWith {
     ///
     /// # Example: offset conflict resolution and disambiguation
     ///
-    /// This example shows how to set the disambiguation configuration can
-    /// interact with the default offset conflict resolution strategy in
-    /// unintuitive ways. For example, here, we request the "earlier" datetime
-    /// whenever there's an ambiguity:
+    /// This example shows how the disambiguation configuration can
+    /// interact with the default offset conflict resolution strategy of
+    /// [`OffsetConflict::PreferOffset`]:
     ///
     /// ```
     /// use jiff::{civil::date, tz, Zoned};
@@ -5197,19 +5182,42 @@ impl ZonedWith {
     ///     .disambiguation(tz::Disambiguation::Earlier)
     ///     .day(10)
     ///     .build()?;
+    /// assert_eq!(zdt2.datetime(), date(2024, 3, 10).at(1, 5, 0, 0));
+    /// assert_eq!(zdt2.offset(), tz::offset(-5));
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    ///
+    /// Namely, while we started with an offset of `-04`, it (along with all
+    /// other offsets) are considered invalid during civil time gaps due to
+    /// time zone transitions (such as the beginning of daylight saving time in
+    /// most locations).
+    ///
+    /// The default disambiguation strategy is
+    /// [`Disambiguation::Compatible`], which in the case of gaps, chooses the
+    /// time after the gap:
+    ///
+    /// ```
+    /// use jiff::{civil::date, tz, Zoned};
+    ///
+    /// // This datetime is unambiguous.
+    /// let zdt1 = "2024-03-11T02:05[America/New_York]".parse::<Zoned>()?;
+    /// assert_eq!(zdt1.offset(), tz::offset(-4));
+    /// // But the same time on March 10 is ambiguous because there is a gap!
+    /// let zdt2 = zdt1
+    ///     .with()
+    ///     .day(10)
+    ///     .build()?;
     /// assert_eq!(zdt2.datetime(), date(2024, 3, 10).at(3, 5, 0, 0));
     /// assert_eq!(zdt2.offset(), tz::offset(-4));
     ///
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     ///
-    /// Yet instead, we get the later time, after the gap. This is because
-    /// the default offset conflict resolution strategy is
-    /// [`OffsetConflict::PreferOffset`], and therefore, the offset in the
-    /// current zoned datetime is prioritized. In the example above, the
-    /// offset of `zdt1` is `-04`, since it comes after DST takes effect in
-    /// `America/New_York`. One can override the offset conflict resolution
-    /// to force disambiguation in cases like this:
+    /// Alternatively, one can choose to always respect the offset, and thus
+    /// civil time for the provided time zone will be adjusted to match the
+    /// instant prescribed by the offset. In this case, no disambiguation is
+    /// performed:
     ///
     /// ```
     /// use jiff::{civil::date, tz, Zoned};
@@ -5220,11 +5228,12 @@ impl ZonedWith {
     /// // But the same time on March 10 is ambiguous because there is a gap!
     /// let zdt2 = zdt1
     ///     .with()
-    ///     .disambiguation(tz::Disambiguation::Earlier)
-    ///     // ignore any offset given
-    ///     .offset_conflict(tz::OffsetConflict::AlwaysTimeZone)
+    ///     .offset_conflict(tz::OffsetConflict::AlwaysOffset)
     ///     .day(10)
     ///     .build()?;
+    /// // Why do we get this result? Because `2024-03-10T02:05-04` is
+    /// // `2024-03-10T06:05Z`. And in `America/New_York`, the civil time
+    /// // for that timestamp is `2024-03-10T01:05-05`.
     /// assert_eq!(zdt2.datetime(), date(2024, 3, 10).at(1, 5, 0, 0));
     /// assert_eq!(zdt2.offset(), tz::offset(-5));
     ///
@@ -5372,7 +5381,7 @@ mod tests {
             }
             #[cfg(all(target_pointer_width = "64", not(feature = "alloc")))]
             {
-                assert_eq!(120, core::mem::size_of::<Zoned>());
+                assert_eq!(104, core::mem::size_of::<Zoned>());
             }
         }
         #[cfg(not(debug_assertions))]
@@ -5383,7 +5392,13 @@ mod tests {
             }
             #[cfg(all(target_pointer_width = "64", not(feature = "alloc")))]
             {
-                assert_eq!(56, core::mem::size_of::<Zoned>());
+                // This asserts the same value as the alloc value above, but
+                // it wasn't always this way, which is why it's written out
+                // separately. Moreover, in theory, I'd be open to regressing
+                // this value if it led to an improvement in alloc-mode. But
+                // more likely, it would be nice to decrease this size in
+                // non-alloc modes.
+                assert_eq!(40, core::mem::size_of::<Zoned>());
             }
         }
     }
@@ -5468,8 +5483,9 @@ mod tests {
         let zdt2 = zdt1.with().hour(2).build().unwrap();
         assert_eq!(zdt2.to_string(), "2024-03-10T03:30:00-04:00[US/Eastern]");
 
-        // This is a current difference from Temporal. Temporal ignores the
-        // disambiguation setting (and the bad offset).
+        // I originally thought that this was difference from Temporal. Namely,
+        // I thought that Temporal ignored the disambiguation setting (and the
+        // bad offset). But it doesn't. I was holding it wrong.
         //
         // See: https://github.com/tc39/proposal-temporal/issues/3078
         let zdt1: Zoned = "2024-03-10T01:30[US/Eastern]".parse().unwrap();
@@ -5482,6 +5498,20 @@ mod tests {
             .build()
             .unwrap();
         assert_eq!(zdt2.to_string(), "2024-03-10T01:30:00-05:00[US/Eastern]");
+
+        // This should also respect the disambiguation setting even without
+        // explicitly specifying an invalid offset. This is becaue `02:30-05`
+        // is regarded as invalid since `02:30` isn't a valid civil time on
+        // this date in this time zone.
+        let zdt1: Zoned = "2024-03-10T01:30[US/Eastern]".parse().unwrap();
+        assert_eq!(zdt1.to_string(), "2024-03-10T01:30:00-05:00[US/Eastern]");
+        let zdt2 = zdt1
+            .with()
+            .hour(2)
+            .disambiguation(Disambiguation::Earlier)
+            .build()
+            .unwrap();
+        assert_eq!(zdt2.to_string(), "2024-03-10T01:30:00-05:00[US/Eastern]");
     }
 
     #[test]


Test patch:
diff --git a/tests/tc39_262/span/add.rs b/tests/tc39_262/span/add.rs
index 71c3a8dc..187895f1 100644
--- a/tests/tc39_262/span/add.rs
+++ b/tests/tc39_262/span/add.rs
@@ -4,13 +4,14 @@ use crate::tc39_262::Result;
 
 const MAX_SPAN_SECONDS: i64 = 631_107_417_600;
 const MAX_SPAN_NANOSECONDS: i64 = 9_223_372_036_854_775_807;
+const DAY24: jiff::SpanRelativeTo = jiff::SpanRelativeTo::days_are_24_hours();
 
 /// Source: https://github.com/tc39/test262/blob/29c6f7028a683b8259140e7d6352ae0ca6448a85/test/built-ins/Temporal/Duration/prototype/add/balance-negative-result.js
 #[test]
 fn balance_negative_result() -> Result {
     let sp1 = -60.hours();
     let sp2 = -1.day();
-    let result = sp1.checked_add(sp2)?;
+    let result = sp1.checked_add((sp2, DAY24))?;
     span_eq!(result, -3.days().hours(12));
 
     Ok(())
@@ -99,21 +100,21 @@ fn balance_negative_time_units() -> Result {
 #[test]
 fn basic() -> Result {
     let sp = 1.day().minutes(5);
-    let result = sp.checked_add(2.days().minutes(5))?;
+    let result = sp.checked_add((2.days().minutes(5), DAY24))?;
     span_eq!(result, 3.days().minutes(10));
-    let result = sp.checked_add(12.hours().seconds(30))?;
+    let result = sp.checked_add((12.hours().seconds(30), DAY24))?;
     span_eq!(result, 1.day().hours(12).minutes(5).seconds(30));
 
     let sp = 3.days().minutes(10);
-    let result = sp.checked_add(-2.days().minutes(5))?;
+    let result = sp.checked_add((-2.days().minutes(5), DAY24))?;
     span_eq!(result, 1.day().minutes(5));
 
     let sp = 1.day().hours(12).minutes(5).seconds(30);
-    let result = sp.checked_add(-12.hours().seconds(30))?;
+    let result = sp.checked_add((-12.hours().seconds(30), DAY24))?;
     span_eq!(result, 1.day().minutes(5));
 
     let sp = "P50DT50H50M50.500500500S".parse::<Span>()?;
-    let result = sp.checked_add(sp)?;
+    let result = sp.checked_add((sp, DAY24))?;
     span_eq!(
         result,
         104.days()
@@ -140,7 +141,7 @@ fn basic() -> Result {
 fn nanoseconds_is_number_max_safe_integer() -> Result {
     let sp1 = MAX_SPAN_NANOSECONDS.nanoseconds();
     let sp2 = 1.day().nanoseconds(2);
-    let result = sp1.checked_add(sp2)?;
+    let result = sp1.checked_add((sp2, DAY24))?;
 
     let nanos = i128::from(MAX_SPAN_NANOSECONDS) + 2;
     let expected = Span::new()
@@ -170,7 +171,7 @@ fn no_calendar_units() -> Result {
     );
     insta::assert_snapshot!(
         1.week().checked_add(blank).unwrap_err(),
-        @"using unit 'week' in a span or configuration requires that a relative reference time be given, but none was provided",
+        @"using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
     );
 
     let ok = 1.day();
diff --git a/tests/tc39_262/span/round.rs b/tests/tc39_262/span/round.rs
index 52c658a6..8df890d5 100644
--- a/tests/tc39_262/span/round.rs
+++ b/tests/tc39_262/span/round.rs
@@ -6,6 +6,7 @@ use jiff::{
 use crate::tc39_262::Result;
 
 const MAX_SPAN_SECONDS: i64 = 631_107_417_600;
+const DAY24: jiff::SpanRelativeTo = jiff::SpanRelativeTo::days_are_24_hours();
 
 fn mk<const N: usize>(units: [i64; N]) -> Span {
     assert!(N <= 10);
@@ -26,7 +27,8 @@ fn mk<const N: usize>(units: [i64; N]) -> Span {
 #[test]
 fn balance_negative_result() -> Result {
     let span1 = -60.hours();
-    let span2 = span1.round(SpanRound::new().largest(Unit::Day))?;
+    let span2 = span1
+        .round(SpanRound::new().largest(Unit::Day).days_are_24_hours())?;
     span_eq!(span2, -2.days().hours(12));
     Ok(())
 }
@@ -65,7 +67,7 @@ fn calendar_possibly_required() -> Result {
     let sp = 1.year();
     insta::assert_snapshot!(
         sp.round(Unit::Hour).unwrap_err(),
-        @"using largest unit (which is 'year') in given span requires that a relative reference time be given, but none was provided",
+        @"error with largest unit in span to be rounded: using unit 'year' in a span or configuration requires that a relative reference time be given, but none was provided",
     );
     span_eq!(sp.round(relative_years)?, 1.year());
     span_eq!(sp.round(relative_months)?, 12.months());
@@ -75,7 +77,7 @@ fn calendar_possibly_required() -> Result {
     let sp = 12.months();
     insta::assert_snapshot!(
         sp.round(Unit::Hour).unwrap_err(),
-        @"using largest unit (which is 'month') in given span requires that a relative reference time be given, but none was provided",
+        @"error with largest unit in span to be rounded: using unit 'month' in a span or configuration requires that a relative reference time be given, but none was provided",
     );
     span_eq!(sp.round(relative_years)?, 1.year());
     span_eq!(sp.round(relative_months)?, 12.months());
@@ -85,7 +87,7 @@ fn calendar_possibly_required() -> Result {
     let sp = 5.weeks();
     insta::assert_snapshot!(
         sp.round(Unit::Hour).unwrap_err(),
-        @"using largest unit (which is 'week') in given span requires that a relative reference time be given, but none was provided",
+        @"error with largest unit in span to be rounded: using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
     );
     span_eq!(sp.round(relative_years)?, 1.month().days(4));
     span_eq!(sp.round(relative_months)?, 1.month().days(4));
@@ -93,13 +95,19 @@ fn calendar_possibly_required() -> Result {
     span_eq!(sp.round(relative_days)?, 35.days());
 
     let sp = 42.days();
+    // We differ from Temporal in that we require opt-in for 24-hour days.
+    insta::assert_snapshot!(
+        sp.round(Unit::Hour).unwrap_err(),
+        @"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
+    );
     span_eq!(sp.round(relative_years)?, 1.month().days(11));
     span_eq!(sp.round(relative_months)?, 1.month().days(11));
     span_eq!(sp.round(relative_weeks)?, 6.weeks());
     span_eq!(sp.round(relative_days)?, 42.days());
-    // We don't need a relative datetime for rounding days.
-    // In this case, we assume civil day (always 24 hours).
-    span_eq!(sp.round(Unit::Hour)?, 42.days());
+    span_eq!(
+        sp.round(SpanRound::new().largest(Unit::Day).relative(DAY24))?,
+        42.days()
+    );
 
     Ok(())
 }
@@ -506,6 +514,89 @@ fn largestunit_smallestunit_combinations() -> Result {
         ),
     ];
 
+    for &(largest, entry) in exact.iter() {
+        for &(smallest, expected) in entry.iter() {
+            let result = sp.round(
+                SpanRound::new()
+                    .largest(largest)
+                    .smallest(smallest)
+                    .relative(DAY24),
+            )?;
+            span_eq!(
+                result,
+                expected,
+                "largest unit {largest:?}, smallest unit {smallest:?}",
+            );
+        }
+    }
+
+    Ok(())
+}
+
+#[test]
+fn largestunit_smallestunit_combinations_without_day() -> Result {
+    let sp = mk([0, 0, 0, 0, 125, 5, 5, 5, 5, 5]);
+    let exact: &[(Unit, &[(Unit, Span)])] = &[
+        (
+            Unit::Hour,
+            &[
+                (Unit::Hour, mk([0, 0, 0, 0, 125])),
+                (Unit::Minute, mk([0, 0, 0, 0, 125, 5])),
+                (Unit::Second, mk([0, 0, 0, 0, 125, 5, 5])),
+                (Unit::Millisecond, mk([0, 0, 0, 0, 125, 5, 5, 5])),
+                (Unit::Microsecond, mk([0, 0, 0, 0, 125, 5, 5, 5, 5])),
+                (Unit::Nanosecond, mk([0, 0, 0, 0, 125, 5, 5, 5, 5, 5])),
+            ],
+        ),
+        (
+            Unit::Minute,
+            &[
+                (Unit::Minute, mk([0, 0, 0, 0, 0, 7_505])),
+                (Unit::Second, mk([0, 0, 0, 0, 0, 7_505, 5])),
+                (Unit::Millisecond, mk([0, 0, 0, 0, 0, 7_505, 5, 5])),
+                (Unit::Microsecond, mk([0, 0, 0, 0, 0, 7_505, 5, 5, 5])),
+                (Unit::Nanosecond, mk([0, 0, 0, 0, 0, 7_505, 5, 5, 5, 5])),
+            ],
+        ),
+        (
+            Unit::Second,
+            &[
+                (Unit::Second, mk([0, 0, 0, 0, 0, 0, 450_305])),
+                (Unit::Millisecond, mk([0, 0, 0, 0, 0, 0, 450_305, 5])),
+                (Unit::Microsecond, mk([0, 0, 0, 0, 0, 0, 450_305, 5, 5])),
+                (Unit::Nanosecond, mk([0, 0, 0, 0, 0, 0, 450_305, 5, 5, 5])),
+            ],
+        ),
+        (
+            Unit::Millisecond,
+            &[
+                (Unit::Millisecond, mk([0, 0, 0, 0, 0, 0, 0, 450305005])),
+                (Unit::Microsecond, mk([0, 0, 0, 0, 0, 0, 0, 450305005, 5])),
+                (Unit::Nanosecond, mk([0, 0, 0, 0, 0, 0, 0, 450305005, 5, 5])),
+            ],
+        ),
+        (
+            Unit::Microsecond,
+            &[
+                (
+                    Unit::Microsecond,
+                    mk([0, 0, 0, 0, 0, 0, 0, 0, 450305005005]),
+                ),
+                (
+                    Unit::Nanosecond,
+                    mk([0, 0, 0, 0, 0, 0, 0, 0, 450305005005, 5]),
+                ),
+            ],
+        ),
+        (
+            Unit::Nanosecond,
+            &[(
+                Unit::Nanosecond,
+                mk([0, 0, 0, 0, 0, 0, 0, 0, 0, 450305005005005]),
+            )],
+        ),
+    ];
+
     for &(largest, entry) in exact.iter() {
         for &(smallest, expected) in entry.iter() {
             let result = sp
@@ -538,8 +629,17 @@ fn largestunit_smallestunit_default() -> Result {
     let options = SpanRound::new().smallest(Unit::Week).relative(d);
     span_eq!(almost_week.round(options)?, 1.week());
 
+    let almost_week = 6.days();
+    let options = SpanRound::new().smallest(Unit::Week).days_are_24_hours();
+    span_eq!(almost_week.round(options)?, 1.week());
+
     let almost_day = 86_399.seconds();
-    span_eq!(almost_day.round(Unit::Day)?, 1.day());
+    let options = SpanRound::new().smallest(Unit::Day).relative(d);
+    span_eq!(almost_day.round(options)?, 1.day());
+
+    let almost_day = 86_399.seconds();
+    let options = SpanRound::new().smallest(Unit::Day).days_are_24_hours();
+    span_eq!(almost_day.round(options)?, 1.day());
 
     let almost_hour = 3599.seconds();
     span_eq!(almost_hour.round(Unit::Hour)?, 1.hour());
@@ -643,8 +743,12 @@ fn precision_exact_in_round_duration() -> Result {
     span_eq!(result, 100_001.hours());
 
     let sp = 1_000.days().nanoseconds(5);
-    let result =
-        sp.round(SpanRound::new().smallest(Unit::Day).mode(RoundMode::Ceil))?;
+    let result = sp.round(
+        SpanRound::new()
+            .smallest(Unit::Day)
+            .mode(RoundMode::Ceil)
+            .days_are_24_hours(),
+    )?;
     span_eq!(result, 1001.days());
 
     Ok(())
@@ -653,31 +757,68 @@ fn precision_exact_in_round_duration() -> Result {
 /// Source: https://github.com/tc39/test262/blob/29c6f7028a683b8259140e7d6352ae0ca6448a85/test/built-ins/Temporal/Duration/prototype/round/relativeto-undefined-throw-on-calendar-units.js
 #[test]
 fn relativeto_undefined_throw_on_calendar_units() -> Result {
-    span_eq!(1.day().round(SpanRound::new().largest(Unit::Day))?, 1.day());
+    insta::assert_snapshot!(
+        1.day().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(),
+        @"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
+    );
+    insta::assert_snapshot!(
+        1.day().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(),
+        @"error with largest unit in span to be rounded: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
+    );
+    insta::assert_snapshot!(
+        1.week().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(),
+        @"error with largest unit in span to be rounded: using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
+    );
+    insta::assert_snapshot!(
+        1.month().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(),
+        @"error with largest unit in span to be rounded: using unit 'month' in a span or configuration requires that a relative reference time be given, but none was provided",
+    );
+    insta::assert_snapshot!(
+        1.year().round(SpanRound::new().largest(Unit::Hour)).unwrap_err(),
+        @"error with largest unit in span to be rounded: using unit 'year' in a span or configuration requires that a relative reference time be given, but none was provided",
+    );
+
+    insta::assert_snapshot!(
+        1.month().round(SpanRound::new().largest(Unit::Hour).days_are_24_hours()).unwrap_err(),
+        @"using unit 'month' in span or configuration requires that a relative reference time be given (`SpanRelativeTo::days_are_24_hours()` was given but this only permits using days and weeks without a relative reference time)",
+    );
+    insta::assert_snapshot!(
+        1.year().round(SpanRound::new().largest(Unit::Hour).days_are_24_hours()).unwrap_err(),
+        @"using unit 'year' in span or configuration requires that a relative reference time be given (`SpanRelativeTo::days_are_24_hours()` was given but this only permits using days and weeks without a relative reference time)",
+    );
+
+    insta::assert_snapshot!(
+        1.hour().round(SpanRound::new().largest(Unit::Day)).unwrap_err(),
+        @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
+    );
+    insta::assert_snapshot!(
+        1.day().round(SpanRound::new().largest(Unit::Day)).unwrap_err(),
+        @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
+    );
     insta::assert_snapshot!(
         1.week().round(SpanRound::new().largest(Unit::Day)).unwrap_err(),
-        @"using largest unit (which is 'week') in given span requires that a relative reference time be given, but none was provided",
+        @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
     );
     insta::assert_snapshot!(
         1.month().round(SpanRound::new().largest(Unit::Day)).unwrap_err(),
-        @"using largest unit (which is 'month') in given span requires that a relative reference time be given, but none was provided",
+        @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
     );
     insta::assert_snapshot!(
         1.year().round(SpanRound::new().largest(Unit::Day)).unwrap_err(),
-        @"using largest unit (which is 'year') in given span requires that a relative reference time be given, but none was provided",
+        @"error with `largest` rounding option: using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
     );
 
     insta::assert_snapshot!(
         1.day().round(SpanRound::new().largest(Unit::Week)).unwrap_err(),
-        @"using unit 'week' in round option 'largest' requires that a relative reference time be given, but none was provided",
+        @"error with `largest` rounding option: using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
     );
     insta::assert_snapshot!(
         1.day().round(SpanRound::new().largest(Unit::Month)).unwrap_err(),
-        @"using unit 'month' in round option 'largest' requires that a relative reference time be given, but none was provided",
+        @"error with `largest` rounding option: using unit 'month' in a span or configuration requires that a relative reference time be given, but none was provided",
     );
     insta::assert_snapshot!(
         1.day().round(SpanRound::new().largest(Unit::Year)).unwrap_err(),
-        @"using unit 'year' in round option 'largest' requires that a relative reference time be given, but none was provided",
+        @"error with `largest` rounding option: using unit 'year' in a span or configuration requires that a relative reference time be given, but none was provided",
     );
 
     Ok(())
@@ -756,7 +897,7 @@ fn round_cross_unit_bondary() -> Result {
 #[test]
 fn round_negative_result() -> Result {
     let sp = -60.hours();
-    let result = sp.round(Unit::Day)?;
+    let result = sp.round(SpanRound::from(Unit::Day).days_are_24_hours())?;
     span_eq!(result, -3.days());
 
     Ok(())
@@ -1314,6 +1455,44 @@ fn smallestunit() -> Result {
                 .nanoseconds(7),
         ),
     ];
+    for &(smallest, expected) in tests {
+        span_eq!(
+            sp.round(SpanRound::from(smallest).days_are_24_hours())?,
+            expected,
+            "rounding {sp} to {smallest:?}"
+        );
+    }
+    Ok(())
+}
+
+#[test]
+fn smallestunit_no_days() -> Result {
+    let sp = 2
+        .hours()
+        .minutes(3)
+        .seconds(4)
+        .milliseconds(5)
+        .microseconds(6)
+        .nanoseconds(7);
+    let tests: &[(Unit, Span)] = &[
+        (Unit::Hour, 2.hours()),
+        (Unit::Minute, 2.hours().minutes(3)),
+        (Unit::Second, 2.hours().minutes(3).seconds(4)),
+        (Unit::Millisecond, 2.hours().minutes(3).seconds(4).milliseconds(5)),
+        (
+            Unit::Microsecond,
+            2.hours().minutes(3).seconds(4).milliseconds(5).microseconds(6),
+        ),
+        (
+            Unit::Nanosecond,
+            2.hours()
+                .minutes(3)
+                .seconds(4)
+                .milliseconds(5)
+                .microseconds(6)
+                .nanoseconds(7),
+        ),
+    ];
     for &(smallest, expected) in tests {
         span_eq!(
             sp.round(smallest)?,
diff --git a/tests/tc39_262/span/total.rs b/tests/tc39_262/span/total.rs
index ac712dfd..a3968aee 100644
--- a/tests/tc39_262/span/total.rs
+++ b/tests/tc39_262/span/total.rs
@@ -2,11 +2,13 @@ use jiff::{civil::date, tz, Timestamp, ToSpan, Unit};
 
 use crate::tc39_262::Result;
 
+const DAY24: jiff::SpanRelativeTo = jiff::SpanRelativeTo::days_are_24_hours();
+
 /// Source: https://github.com/tc39/test262/blob/29c6f7028a683b8259140e7d6352ae0ca6448a85/test/built-ins/Temporal/Duration/prototype/total/balance-negative-result.js
 #[test]
 fn balance_negative_result() -> Result {
     let sp = -60.hours();
-    let result = sp.total(Unit::Day)?;
+    let result = sp.total((Unit::Day, DAY24))?;
     assert_eq!(result, -2.5);
 
     Ok(())
@@ -52,12 +54,17 @@ fn calendar_possibly_required() -> Result {
 
     insta::assert_snapshot!(
         week.total(Unit::Day).unwrap_err(),
-        @"using unit 'week' in a span or configuration requires that a relative reference time be given, but none was provided",
+        @"using unit 'week' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
     );
     let result = week.total((Unit::Day, d))?;
     assert_eq!(result, 7.0);
 
-    let result = day.total(Unit::Day)?;
+    // Differs from Temporal. We require explicit opt-in for 24-hour days.
+    insta::assert_snapshot!(
+        day.total(Unit::Day).unwrap_err(),
+        @"using unit 'day' in a span or configuration requires that either a relative reference time be given or `SpanRelativeTo::days_are_24_hours()` is used to indicate invariant 24-hour days, but neither were provided",
+    );
+    let result = day.total((Unit::Day, DAY24))?;
     assert_eq!(result, 42.0);
     let result = day.total((Unit::Day, d))?;
     assert_eq!(result, 42.0);
@@ -198,8 +205,9 @@ fn dst_rounding_result() -> Result {
 /// Source: https://github.com/tc39/test262/blob/29c6f7028a683b8259140e7d6352ae0ca6448a85/test/built-ins/Temporal/Duration/prototype/total/no-dst-day-length.js
 #[test]
 fn no_dst_day_length() -> Result {
-    assert_eq!(1.day().total(Unit::Hour)?, 24.0);
-    assert_eq!(48.hours().total(Unit::Day)?, 2.0);
+    // We differ from Temporal in that we require opt-in for 24-hour days.
+    assert_eq!(1.day().total((Unit::Hour, DAY24))?, 24.0);
+    assert_eq!(48.hours().total((Unit::Day, DAY24))?, 2.0);
 
     let d = date(2017, 1, 1);
     assert_eq!(1.day().total((Unit::Hour, d))?, 24.0);